fabro/lib/crates/fabro-cli/src/server_client.rs
Bryan Helmkamp c60d859c08 Default settings output to dense resolved view
Make `fabro settings` render dense resolved settings by default for local
inspection, add a resolved view to the server settings endpoint with an
explicit compatibility marker, and update tests plus generated API clients
to lock the new behavior.
2026-04-12 19:06:07 -04:00

905 lines
29 KiB
Rust

use std::collections::VecDeque;
use std::num::NonZeroU64;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail};
use bytes::Bytes;
use fabro_api::types;
use fabro_http::header::{CONTENT_LENGTH, CONTENT_TYPE};
use fabro_http::multipart::{Form, Part};
use fabro_server::bind::Bind;
use fabro_store::{EventEnvelope, RunSummary, StageId};
use fabro_types::settings::SettingsLayer;
use fabro_types::{RunBlobId, RunEvent, RunId};
use fabro_workflow::artifact_snapshot::CapturedArtifactInfo;
use futures::StreamExt;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::fs::File;
use tokio::time::sleep;
use tokio_util::io::ReaderStream;
use crate::args::ServerTargetArgs;
use crate::commands::server::start;
use crate::user_config::cli_http_client_builder;
use crate::{sse, user_config};
#[derive(Clone)]
pub(crate) struct ServerStoreClient {
client: fabro_api::Client,
http_client: fabro_http::HttpClient,
base_url: String,
}
#[derive(Debug, Clone)]
struct LocalServerRuntime {
active_config_path: PathBuf,
storage_dir: PathBuf,
}
pub(crate) struct RunAttachEventStream {
stream: progenitor_client::ByteStream,
pending_bytes: Vec<u8>,
buffered_events: VecDeque<EventEnvelope>,
}
impl RunAttachEventStream {
fn new(stream: progenitor_client::ByteStream) -> Self {
Self {
stream,
pending_bytes: Vec::new(),
buffered_events: VecDeque::new(),
}
}
pub(crate) async fn next_event(&mut self) -> Result<Option<EventEnvelope>> {
loop {
if let Some(event) = self.buffered_events.pop_front() {
return Ok(Some(event));
}
if let Some(chunk) = self.stream.next().await {
let chunk = chunk.map_err(|err| anyhow!("{err}"))?;
self.pending_bytes.extend_from_slice(&chunk);
self.buffer_sse_events(false)?;
} else {
self.buffer_sse_events(true)?;
return Ok(self.buffered_events.pop_front());
}
}
}
fn buffer_sse_events(&mut self, finalize: bool) -> Result<()> {
for payload in sse::drain_sse_payloads(&mut self.pending_bytes, finalize) {
self.buffered_events
.push_back(serde_json::from_str(&payload)?);
}
Ok(())
}
}
pub(crate) use fabro_store::RunProjection;
pub(crate) async fn connect_server(storage_dir: &Path) -> Result<ServerStoreClient> {
connect_api_client_bundle(storage_dir).await
}
pub(crate) async fn connect_server_target_direct(target: &str) -> Result<ServerStoreClient> {
if target.starts_with("http://") || target.starts_with("https://") {
connect_remote_api_client_bundle(target, None)
} else {
let path = Path::new(target);
if !path.is_absolute() {
bail!("server target must be an http(s) URL or absolute Unix socket path");
}
connect_unix_socket_api_client_bundle(path).await
}
}
pub(crate) async fn connect_server_with_settings(
args: &ServerTargetArgs,
settings: &SettingsLayer,
base_config_path: &Path,
) -> Result<ServerStoreClient> {
let target = user_config::resolve_server_target(args, settings)?;
let runtime = LocalServerRuntime {
active_config_path: base_config_path.to_path_buf(),
storage_dir: user_config::storage_dir(settings)?,
};
connect_target_api_client_bundle(&target, &runtime).await
}
async fn connect_api_client_bundle(storage_dir: &Path) -> Result<ServerStoreClient> {
let config_path = user_config::active_settings_path(None);
let bind = start::ensure_server_running_for_storage(storage_dir, &config_path)
.await
.with_context(|| format!("Failed to start fabro server for {}", storage_dir.display()))?;
match bind {
Bind::Unix(path) => connect_unix_socket_api_client_bundle(&path).await,
Bind::Tcp(addr) => Err(anyhow!(
"Unsupported server bind for store client auto-connect: {addr}"
)),
}
}
pub(crate) async fn connect_api_client(storage_dir: &Path) -> Result<fabro_api::Client> {
connect_api_client_bundle(storage_dir)
.await
.map(|client| client.client)
}
async fn connect_target_api_client_bundle(
target: &user_config::ServerTarget,
runtime: &LocalServerRuntime,
) -> Result<ServerStoreClient> {
match target {
user_config::ServerTarget::HttpUrl { api_url, tls } => {
connect_remote_api_client_bundle(api_url, tls.as_ref())
}
user_config::ServerTarget::UnixSocket(path) => {
if let Ok(client) = try_connect_unix_socket_api_client_bundle(path).await {
Ok(client)
} else {
start::ensure_server_running_on_socket(
path,
&runtime.active_config_path,
&runtime.storage_dir,
)
.await
.with_context(|| format!("Failed to start fabro server for {}", path.display()))?;
connect_unix_socket_api_client_bundle(path).await
}
}
}
}
fn connect_remote_api_client_bundle(
api_url: &str,
tls: Option<&user_config::ClientTlsSettings>,
) -> Result<ServerStoreClient> {
let http_client = user_config::build_server_client(tls)?;
let normalized = normalize_remote_server_target(api_url);
let client = fabro_api::Client::new_with_client(&normalized, http_client.clone());
Ok(ServerStoreClient {
client,
http_client,
base_url: normalized,
})
}
fn normalize_remote_server_target(api_url: &str) -> String {
api_url
.trim_end_matches('/')
.strip_suffix("/api/v1")
.unwrap_or(api_url.trim_end_matches('/'))
.to_string()
}
fn build_unix_socket_http_client(path: &Path) -> Result<fabro_http::HttpClient> {
cli_http_client_builder()
.unix_socket(path)
.no_proxy()
.build()
.context("Failed to build Unix-socket HTTP client for fabro server")
}
fn unix_socket_api_client_bundle(http_client: fabro_http::HttpClient) -> ServerStoreClient {
let base_url = "http://fabro".to_string();
let client = fabro_api::Client::new_with_client(&base_url, http_client.clone());
ServerStoreClient {
client,
http_client,
base_url,
}
}
async fn try_connect_unix_socket_api_client_bundle(path: &Path) -> Result<ServerStoreClient> {
let http_client = build_unix_socket_http_client(path)?;
check_server_ready(&http_client).await?;
Ok(unix_socket_api_client_bundle(http_client))
}
async fn connect_unix_socket_api_client_bundle(path: &Path) -> Result<ServerStoreClient> {
let http_client = build_unix_socket_http_client(path)?;
wait_for_server_ready(&http_client).await?;
Ok(unix_socket_api_client_bundle(http_client))
}
async fn check_server_ready(http_client: &fabro_http::HttpClient) -> Result<()> {
match http_client.get("http://fabro/health").send().await {
Ok(response) if response.status().is_success() => Ok(()),
Ok(response) => bail!("server health check returned status {}", response.status()),
Err(err) => Err(anyhow!(err)),
}
}
async fn wait_for_server_ready(http_client: &fabro_http::HttpClient) -> Result<()> {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let mut last_error = None;
while std::time::Instant::now() < deadline {
match check_server_ready(http_client).await {
Ok(()) => return Ok(()),
Err(err) => {
last_error = Some(err);
}
}
sleep(Duration::from_millis(50)).await;
}
Err(last_error.unwrap_or_else(|| anyhow!("server did not become ready in time")))
}
#[derive(Debug, Serialize)]
struct ArtifactBatchUploadManifest {
entries: Vec<ArtifactBatchUploadEntry>,
}
#[derive(Debug, Serialize)]
struct ArtifactBatchUploadEntry {
part: String,
path: String,
#[serde(skip_serializing_if = "Option::is_none")]
sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
expected_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
content_type: Option<String>,
}
impl ServerStoreClient {
/// Build a client for tests that bypasses proxy discovery.
#[cfg(test)]
pub(crate) fn new_no_proxy(base_url: &str) -> Result<Self> {
let http_client = cli_http_client_builder().no_proxy().build()?;
let client = fabro_api::Client::new_with_client(base_url, http_client.clone());
Ok(Self {
client,
http_client,
base_url: base_url.to_string(),
})
}
pub(crate) fn clone_for_reuse(&self) -> Self {
self.clone()
}
pub(crate) fn api(&self) -> &fabro_api::Client {
&self.client
}
#[allow(dead_code)]
pub(crate) fn http_client(&self) -> &fabro_http::HttpClient {
&self.http_client
}
#[allow(dead_code)]
pub(crate) fn base_url(&self) -> &str {
&self.base_url
}
pub(crate) async fn retrieve_resolved_server_settings(&self) -> Result<serde_json::Value> {
let url = format!("{}/api/v1/settings?view=resolved", self.base_url);
let response = self.http_client.get(&url).send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(detail) = value
.get("errors")
.and_then(serde_json::Value::as_array)
.and_then(|errors| errors.first())
.and_then(|entry| entry.get("detail"))
.and_then(serde_json::Value::as_str)
{
bail!("{detail}");
}
}
if body.is_empty() {
bail!("request failed with status {status}");
}
bail!("request failed with status {status}: {body}");
}
let marker = response
.headers()
.get("x-fabro-settings-view")
.and_then(|value| value.to_str().ok());
if marker != Some("resolved") {
bail!(
"server does not support resolved settings view; upgrade the server or use --local"
);
}
response
.json::<serde_json::Value>()
.await
.context("server returned invalid JSON for the resolved settings view")
}
pub(crate) async fn create_run_from_manifest(
&self,
manifest: types::RunManifest,
) -> Result<RunId> {
let response = self
.client
.create_run()
.body(manifest)
.send()
.await
.map_err(map_api_error)?;
let status = response.into_inner();
status
.id
.parse()
.map_err(|err| anyhow!("invalid run ID from server: {err}"))
}
pub(crate) async fn run_preflight(
&self,
manifest: types::RunManifest,
) -> Result<types::PreflightResponse> {
self.client
.run_preflight()
.body(manifest)
.send()
.await
.map(progenitor_client::ResponseValue::into_inner)
.map_err(map_api_error)
}
pub(crate) async fn render_workflow_graph(
&self,
request: types::RenderWorkflowGraphRequest,
) -> Result<Vec<u8>> {
let response = self
.client
.render_workflow_graph()
.body(request)
.send()
.await
.map_err(map_api_error)?;
let mut stream = response.into_inner();
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow!("{err}"))?;
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
pub(crate) async fn start_run(&self, run_id: &RunId, resume: bool) -> Result<()> {
self.client
.start_run()
.id(run_id.to_string())
.body(types::StartRunRequest { resume })
.send()
.await
.map_err(map_api_error)?;
Ok(())
}
pub(crate) async fn cancel_run(&self, run_id: &RunId) -> Result<()> {
self.client
.cancel_run()
.id(run_id.to_string())
.send()
.await
.map_err(map_api_error)?;
Ok(())
}
pub(crate) async fn list_store_runs(&self) -> Result<Vec<RunSummary>> {
let response = self
.client
.list_runs()
.send()
.await
.map_err(map_api_error)?;
response
.into_inner()
.into_iter()
.map(convert_type)
.collect::<Result<Vec<_>>>()
}
pub(crate) async fn get_run_state(&self, run_id: &RunId) -> Result<RunProjection> {
let response = self
.client
.get_run_state()
.id(run_id.to_string())
.send()
.await
.map_err(map_api_error)?;
convert_type(response.into_inner())
}
pub(crate) async fn list_run_events(
&self,
run_id: &RunId,
since_seq: Option<u32>,
limit: Option<usize>,
) -> Result<Vec<EventEnvelope>> {
let mut next_since_seq = since_seq;
let mut all_events = Vec::new();
loop {
let mut request = self.client.list_run_events().id(run_id.to_string());
if let Some(seq) = next_since_seq.and_then(non_zero_u64_from_u32) {
request = request.since_seq(seq);
}
if let Some(limit) = limit.and_then(non_zero_u64_from_usize) {
request = request.limit(limit);
}
let response = request.send().await.map_err(map_api_error)?;
let parsed = response.into_inner();
let page_events = parsed
.data
.into_iter()
.map(convert_type::<_, EventEnvelope>)
.collect::<Result<Vec<EventEnvelope>>>()?;
let next_page_since_seq = page_events.last().map(|event| event.seq.saturating_add(1));
all_events.extend(page_events);
if limit.is_some() || !parsed.meta.has_more || next_page_since_seq.is_none() {
break;
}
next_since_seq = next_page_since_seq;
}
Ok(all_events)
}
pub(crate) async fn attach_run_events(
&self,
run_id: &RunId,
since_seq: Option<u32>,
) -> Result<RunAttachEventStream> {
let mut request = self.client.attach_run_events().id(run_id.to_string());
if let Some(seq) = since_seq.and_then(non_zero_u64_from_u32) {
request = request.since_seq(seq);
}
let response = request.send().await.map_err(map_api_error)?;
Ok(RunAttachEventStream::new(response.into_inner()))
}
pub(crate) async fn list_run_questions(
&self,
run_id: &RunId,
) -> Result<Vec<types::ApiQuestion>> {
let response = self
.client
.list_run_questions()
.id(run_id.to_string())
.page_limit(100)
.page_offset(0)
.send()
.await
.map_err(map_api_error)?;
Ok(response.into_inner().data)
}
pub(crate) async fn submit_run_answer(
&self,
run_id: &RunId,
qid: &str,
value: Option<String>,
selected_option_key: Option<String>,
selected_option_keys: Vec<String>,
) -> Result<()> {
self.client
.submit_run_answer()
.id(run_id.to_string())
.qid(qid)
.body(types::SubmitAnswerRequest {
value,
selected_option_key,
selected_option_keys,
})
.send()
.await
.map_err(map_api_error)?;
Ok(())
}
pub(crate) async fn append_run_event(&self, run_id: &RunId, event: &RunEvent) -> Result<u32> {
let body: types::RunEvent = convert_type(event)?;
let response = self
.client
.append_run_event()
.id(run_id.to_string())
.body(body)
.send()
.await
.map_err(map_api_error)?;
u32::try_from(response.into_inner().seq).context("append_run_event returned invalid seq")
}
pub(crate) async fn write_run_blob(&self, run_id: &RunId, data: &[u8]) -> Result<RunBlobId> {
let response = self
.client
.write_run_blob()
.id(run_id.to_string())
.body(data.to_vec())
.send()
.await
.map_err(map_api_error)?;
response
.into_inner()
.id
.parse()
.context("write_run_blob returned invalid blob id")
}
pub(crate) async fn read_run_blob(
&self,
run_id: &RunId,
blob_id: &RunBlobId,
) -> Result<Option<Bytes>> {
let response = self
.client
.read_run_blob()
.id(run_id.to_string())
.blob_id(blob_id.to_string())
.send()
.await;
match response {
Ok(response) => {
let mut stream = response.into_inner();
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow!("{err}"))?;
bytes.extend_from_slice(&chunk);
}
Ok(Some(Bytes::from(bytes)))
}
Err(err) => {
if is_not_found_error(&err) {
Ok(None)
} else {
Err(map_api_error(err))
}
}
}
}
pub(crate) async fn delete_store_run(&self, run_id: &RunId) -> Result<()> {
self.client
.delete_run()
.id(run_id.to_string())
.send()
.await
.map_err(map_api_error)?;
Ok(())
}
pub(crate) async fn list_run_artifacts(
&self,
run_id: &RunId,
) -> Result<Vec<types::RunArtifactEntry>> {
let response = self
.client
.list_run_artifacts()
.id(run_id.to_string())
.send()
.await
.map_err(map_api_error)?;
Ok(response.into_inner().data)
}
pub(crate) async fn download_stage_artifact(
&self,
run_id: &RunId,
stage_id: &StageId,
filename: &str,
) -> Result<Vec<u8>> {
let response = self
.client
.get_stage_artifact()
.id(run_id.to_string())
.stage_id(stage_id.to_string())
.filename(filename)
.send()
.await
.map_err(map_api_error)?;
let mut stream = response.into_inner();
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow!("{err}"))?;
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
fn stage_artifacts_url(&self, run_id: &RunId, stage_id: &StageId) -> Result<fabro_http::Url> {
let mut url = fabro_http::Url::parse(&self.base_url)
.with_context(|| format!("invalid server base URL {}", self.base_url))?;
url.path_segments_mut()
.map_err(|()| anyhow!("server base URL cannot accept path segments"))?
.extend([
"api",
"v1",
"runs",
&run_id.to_string(),
"stages",
&stage_id.to_string(),
"artifacts",
]);
Ok(url)
}
pub(crate) async fn upload_stage_artifact_file(
&self,
run_id: &RunId,
stage_id: &StageId,
filename: &str,
path: &Path,
bearer_token: &str,
) -> Result<()> {
let mut url = self.stage_artifacts_url(run_id, stage_id)?;
url.query_pairs_mut().append_pair("filename", filename);
let file = File::open(path)
.await
.with_context(|| format!("failed to open artifact {}", path.display()))?;
let content_length = file
.metadata()
.await
.with_context(|| format!("failed to stat artifact {}", path.display()))?
.len();
let body = fabro_http::Body::wrap_stream(ReaderStream::new(file));
let response = self
.http_client
.post(url)
.bearer_auth(bearer_token)
.header(CONTENT_TYPE, "application/octet-stream")
.header(CONTENT_LENGTH, content_length.to_string())
.body(body)
.send()
.await
.with_context(|| format!("failed to upload artifact {}", path.display()))?;
ensure_raw_response_success(response).await
}
pub(crate) async fn upload_stage_artifact_batch(
&self,
run_id: &RunId,
stage_id: &StageId,
artifact_capture_dir: &Path,
artifacts: &[CapturedArtifactInfo],
bearer_token: &str,
) -> Result<()> {
let url = self.stage_artifacts_url(run_id, stage_id)?;
let mut manifest_entries = Vec::with_capacity(artifacts.len());
let mut file_parts = Vec::with_capacity(artifacts.len());
for (index, artifact) in artifacts.iter().enumerate() {
let part_name = format!("file{}", index + 1);
let path = artifact_capture_dir.join(&artifact.path);
let file = File::open(&path)
.await
.with_context(|| format!("failed to open artifact {}", path.display()))?;
let content_length = file
.metadata()
.await
.with_context(|| format!("failed to stat artifact {}", path.display()))?
.len();
manifest_entries.push(ArtifactBatchUploadEntry {
part: part_name.clone(),
path: artifact.path.clone(),
sha256: Some(artifact.content_sha256.clone()),
expected_bytes: Some(artifact.bytes),
content_type: Some(artifact.mime.clone()),
});
file_parts.push((
part_name,
Part::stream_with_length(
fabro_http::Body::wrap_stream(ReaderStream::new(file)),
content_length,
)
.file_name(artifact.path.clone()),
));
}
let manifest = ArtifactBatchUploadManifest {
entries: manifest_entries,
};
let manifest_part =
Part::text(serde_json::to_string(&manifest)?).mime_str("application/json")?;
let mut form = Form::new().part("manifest", manifest_part);
for (part_name, part) in file_parts {
form = form.part(part_name, part);
}
let response = self
.http_client
.post(url)
.bearer_auth(bearer_token)
.multipart(form)
.send()
.await
.context("failed to upload artifact batch")?;
ensure_raw_response_success(response).await
}
pub(crate) async fn generate_preview_url(
&self,
run_id: &RunId,
port: u16,
expires_in_secs: u64,
signed: bool,
) -> Result<types::PreviewUrlResponse> {
let expires_in_secs = NonZeroU64::new(expires_in_secs)
.ok_or_else(|| anyhow!("preview expiry must be greater than zero"))?;
let response = self
.client
.generate_preview_url()
.id(run_id.to_string())
.body(types::PreviewUrlRequest {
expires_in_secs,
port: i64::from(port),
signed,
})
.send()
.await
.map_err(map_api_error)?;
Ok(response.into_inner())
}
pub(crate) async fn create_run_ssh_access(
&self,
run_id: &RunId,
ttl_minutes: f64,
) -> Result<types::SshAccessResponse> {
let response = self
.client
.create_run_ssh_access()
.id(run_id.to_string())
.body(types::SshAccessRequest { ttl_minutes })
.send()
.await
.map_err(map_api_error)?;
Ok(response.into_inner())
}
pub(crate) async fn list_sandbox_files(
&self,
run_id: &RunId,
path: &str,
depth: Option<u32>,
) -> Result<Vec<types::SandboxFileEntry>> {
let mut request = self
.client
.list_sandbox_files()
.id(run_id.to_string())
.path(path);
if let Some(depth) = depth.and_then(non_zero_u64_from_u32) {
request = request.depth(depth);
}
let response = request.send().await.map_err(map_api_error)?;
Ok(response.into_inner().data)
}
pub(crate) async fn get_sandbox_file(&self, run_id: &RunId, path: &str) -> Result<Vec<u8>> {
let response = self
.client
.get_sandbox_file()
.id(run_id.to_string())
.path(path)
.send()
.await
.map_err(map_api_error)?;
let mut stream = response.into_inner();
let mut bytes = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| anyhow!("{err}"))?;
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
pub(crate) async fn put_sandbox_file(
&self,
run_id: &RunId,
path: &str,
bytes: Vec<u8>,
) -> Result<()> {
self.client
.put_sandbox_file()
.id(run_id.to_string())
.path(path)
.body(bytes)
.send()
.await
.map_err(map_api_error)?;
Ok(())
}
}
pub(crate) fn map_api_error<E>(err: progenitor_client::Error<E>) -> anyhow::Error
where
E: serde::Serialize + std::fmt::Debug,
{
match err {
progenitor_client::Error::ErrorResponse(response) => {
let status = response.status();
if let Ok(value) = serde_json::to_value(response.into_inner()) {
if let Some(detail) = value
.get("errors")
.and_then(serde_json::Value::as_array)
.and_then(|errors| errors.first())
.and_then(|entry| entry.get("detail"))
.and_then(serde_json::Value::as_str)
{
return anyhow!("{detail}");
}
}
anyhow!("request failed with status {status}")
}
progenitor_client::Error::UnexpectedResponse(response) => {
anyhow!("request failed with status {}", response.status())
}
other => anyhow!("{other}"),
}
}
async fn ensure_raw_response_success(response: fabro_http::Response) -> Result<()> {
if response.status().is_success() {
return Ok(());
}
let status = response.status();
let body = response.text().await.unwrap_or_default();
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(detail) = value
.get("errors")
.and_then(serde_json::Value::as_array)
.and_then(|errors| errors.first())
.and_then(|entry| entry.get("detail"))
.and_then(serde_json::Value::as_str)
{
bail!("{detail}");
}
}
if body.is_empty() {
bail!("request failed with status {status}");
}
bail!("request failed with status {status}: {body}");
}
fn is_not_found_error<E>(err: &progenitor_client::Error<E>) -> bool
where
E: serde::Serialize + std::fmt::Debug,
{
match err {
progenitor_client::Error::ErrorResponse(response) => {
response.status() == fabro_http::StatusCode::NOT_FOUND
}
progenitor_client::Error::UnexpectedResponse(response) => {
response.status() == fabro_http::StatusCode::NOT_FOUND
}
_ => false,
}
}
fn convert_type<TInput, TOutput>(value: TInput) -> Result<TOutput>
where
TInput: serde::Serialize,
TOutput: DeserializeOwned,
{
serde_json::from_value(serde_json::to_value(value)?).map_err(Into::into)
}
fn non_zero_u64_from_u32(value: u32) -> Option<NonZeroU64> {
NonZeroU64::new(u64::from(value))
}
fn non_zero_u64_from_usize(value: usize) -> Option<NonZeroU64> {
u64::try_from(value).ok().and_then(NonZeroU64::new)
}