Files
invoiceninja-cli/src/body.rs
T
awkawbandClaude Sonnet 5 dde10e522a Initial commit: clients resource scaffold over invoiceninja-sdk
A command-line client for the Invoice Ninja v5 API, built on the
invoiceninja-sdk crate (consumed from crates.io, 0.2.0). Only the
clients resource is wired up so far, as the template the rest follow
- see CLAUDE.md and GOALS.md for the reasoning behind the shape of
things (the update fetch-merge-write pattern, why there's no Cargo
workspace with the SDK, the local-checkout override for
co-development, and the Nix flake packaging via packages.default and
overlays.default).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-26 11:47:15 -04:00

51 lines
1.9 KiB
Rust

use std::fs;
use std::io::Read;
use serde_json::Value;
use crate::cli::BodyArgs;
/// Reads the JSON body a `create`/`update` command was given: `--data`
/// inline, `--file` from disk (`-` for stdin), or stdin if neither flag was
/// passed. Errors if the result isn't a JSON object — every create/update
/// endpoint here takes a record, not a bare array or scalar.
pub fn load(body: &BodyArgs) -> Result<Value, Box<dyn std::error::Error>> {
let text = match (&body.data, &body.file) {
(Some(data), None) => data.clone(),
(None, Some(path)) if path.as_os_str() == "-" => read_stdin()?,
(None, Some(path)) => fs::read_to_string(path)?,
(None, None) => read_stdin()?,
// clap's `conflicts_with` on both fields rules this out before we get here.
(Some(_), Some(_)) => unreachable!("--data and --file are mutually exclusive"),
};
let value: Value = serde_json::from_str(&text)?;
if !value.is_object() {
return Err(format!("expected a JSON object, got {value}").into());
}
Ok(value)
}
fn read_stdin() -> Result<String, Box<dyn std::error::Error>> {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
/// Overlays `patch`'s keys onto `base`, one level deep.
///
/// `update` uses this so a caller can send just the fields they're changing
/// instead of retyping the whole record. That matters because Invoice
/// Ninja's `PUT` is a full replace — there's no PATCH route — and the SDK's
/// models default any field the caller omits to `""` / `0` / `false` rather
/// than leaving it alone. Sending the patch by itself would blank out every
/// field it didn't mention; merging onto a freshly-fetched copy keeps them.
pub fn merge_shallow(mut base: Value, patch: Value) -> Value {
if let (Some(base_map), Value::Object(patch_map)) = (base.as_object_mut(), patch) {
for (k, v) in patch_map {
base_map.insert(k, v);
}
}
base
}