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>
157 lines
7.6 KiB
Markdown
157 lines
7.6 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
A command-line client for the [Invoice Ninja](https://www.invoiceninja.com/)
|
|
v5 API, built on [`invoiceninja-sdk`](https://crates.io/crates/invoiceninja-sdk),
|
|
consumed as a normal crates.io dependency (not a path dependency on a sibling
|
|
checkout — see "Development environment" below for how that changed).
|
|
That crate's own `GOALS.md` (at `../invoiceninjars-sdk/GOALS.md`, if you have
|
|
a checkout for reference) explains why it's a separate crate rather than a
|
|
feature of the library — dependencies, MSRV, and versioning, all pre-1.0.
|
|
Read that `GOALS.md` before making a design call here; this project exists
|
|
downstream of decisions made there.
|
|
|
|
**This project's own [`GOALS.md`](GOALS.md) states what it is for, what it
|
|
deliberately is not, and what is still undecided** — read it before making a
|
|
design call, the same way its counterpart in the SDK works. If a change
|
|
would contradict a stated goal or settle an open question, update
|
|
`GOALS.md` in the same change.
|
|
|
|
**Early scaffold.** Only the `clients` resource is wired up, as the template
|
|
for the other ~34. See "Adding a resource" below before writing the next one.
|
|
|
|
## Development environment
|
|
|
|
Nix-managed, with its own `flake.nix` (not shared with the SDK's):
|
|
|
|
```sh
|
|
nix develop
|
|
```
|
|
|
|
The dev toolchain was originally pinned to match `invoiceninjars-sdk`'s own
|
|
(`devVersion` in each `flake.nix`), from back when this crate needed a sibling
|
|
checkout to build at all. That's no longer a hard requirement now that the
|
|
SDK is on crates.io, but the pin hasn't been revisited — if the two drift,
|
|
nothing here forces them back in sync, since the two flakes are independent
|
|
files.
|
|
|
|
**As of 0.2.0 (2026-10-25), the SDK is published on crates.io** and this
|
|
crate depends on it as a normal version requirement
|
|
(`invoiceninja-sdk = "0.2.0"` in `Cargo.toml`), not a path dependency. That
|
|
means:
|
|
|
|
- No `../invoiceninjars-sdk` checkout is needed to build this crate anymore.
|
|
`cargo build` fetches `invoiceninja-sdk` from the registry like any other
|
|
dependency.
|
|
- Picking up a change made in the SDK's working copy now requires that
|
|
change to be published as a new crates.io release first by default — there
|
|
is no more "edit the sibling checkout, rebuild here" loop out of the box.
|
|
For active co-development across both crates, copy
|
|
`.cargo/config.toml.example` to `.cargo/config.toml` (gitignored, so it
|
|
stays per-developer and never affects CI or anyone else's build) to get
|
|
a `paths` override back onto `../invoiceninjars-sdk`. It requires that
|
|
checkout's `[package] version` to match this crate's `invoiceninja-sdk`
|
|
requirement in `Cargo.toml` — bump both together if the SDK's working
|
|
copy moves to an unreleased version. Delete `.cargo/config.toml` to go
|
|
back to the published release.
|
|
- Pre-1.0, a y-bump (`0.2.x` → `0.3.0`) in the SDK is a breaking release per
|
|
that crate's own `GOALS.md`. The default caret requirement already blocks
|
|
picking that up automatically; bumping the version in `Cargo.toml` is a
|
|
deliberate, reviewed step, not something `cargo update` should do on its
|
|
own.
|
|
|
|
**Watch the SDK's own package/crate name.** It has changed at least once
|
|
already (`invoiceninjars` → `invoiceninja-sdk`, crate path `invoiceninja_sdk`)
|
|
without a corresponding change here landing first. If a build fails with "no
|
|
matching package named `invoiceninja-sdk` found" or an unresolved
|
|
`invoiceninja_sdk::` path, check the crates.io listing (or
|
|
`../invoiceninjars-sdk/Cargo.toml`'s `[package] name`, if you have a
|
|
checkout) first.
|
|
|
|
## Common commands
|
|
|
|
```sh
|
|
cargo build
|
|
cargo run -- clients list # needs IN_API_TOKEN, or --token
|
|
cargo fmt --check
|
|
cargo clippy --all-targets
|
|
```
|
|
|
|
No test suite yet — nothing here has behavior worth testing beyond what
|
|
`clap`'s derive and the SDK's own test suite already cover. Add one (clap's
|
|
`Cli::try_parse_from` is the usual way to test argument parsing without
|
|
spawning a process) when a command does more than translate flags into an
|
|
SDK call.
|
|
|
|
## Architecture
|
|
|
|
- `src/cli.rs` — all `clap` derive types. `Cli` holds the global
|
|
options (`--base-url`, `--token`, `--api-password`, `--output`) and the
|
|
top-level `Command` subcommand enum. **`--token` can't be clap-`required`**
|
|
because clap rejects `required` combined with `global` (global is what
|
|
lets `--token` appear after the resource subcommand too, not just before
|
|
it) — the missing case is caught in `config::build_client` instead, with a
|
|
message naming both the flag and the env var. Don't try to make it
|
|
`required` again; the comment on the field explains the panic you'd get.
|
|
- `src/config.rs` — `build_client` turns a parsed `Cli` into an
|
|
`InvoiceNinjaClient`, including the manual token check above.
|
|
- `src/body.rs` — how `create`/`update` get their JSON body (`--data`
|
|
inline, `--file` from disk or `-` for stdin, or stdin by default), plus
|
|
`merge_shallow`, a one-level-deep object overlay.
|
|
- `src/output.rs` — `print_json`, shared by every resource's `--output
|
|
json`. Table rendering is **not** here; it's per-resource (see below),
|
|
since there's no generic notion of which columns matter.
|
|
- `src/commands/clients.rs` — the one resource implemented so far, and the
|
|
template for the rest.
|
|
|
|
### The `update` fetch-merge-write pattern
|
|
|
|
`update` is not a thin wrapper over the SDK's `update_client`. It:
|
|
|
|
1. Fetches the current record with `get_client`.
|
|
2. Serializes it to `serde_json::Value` and shallow-merges the caller's
|
|
`--data`/`--file` JSON on top (`body::merge_shallow`).
|
|
3. Deserializes the merged value back into the model and calls
|
|
`update_client`.
|
|
|
|
This exists because Invoice Ninja's `PUT` is a full replace — there's no
|
|
PATCH route — and every non-`Option` field on an SDK model decodes a missing
|
|
key as that type's default (`""`, `0`, `false`; see
|
|
`invoiceninja_sdk`'s `null_to_default`). Sending the caller's patch alone
|
|
would silently blank out every field they didn't mention. **Keep this
|
|
pattern for every resource's `update`** — it's the CLI-level version of the
|
|
SDK's own "never lose data in a round trip" goal, not a one-off for clients.
|
|
|
|
`create` has no such step: a partial JSON body decodes straight into the
|
|
model, since the same defaulting that makes a bare patch dangerous for
|
|
`update` is exactly what you want when creating a record whose fields you
|
|
haven't all thought about (the API does the equivalent server-side).
|
|
|
|
### Adding a resource
|
|
|
|
Each resource beyond `clients` should follow the same shape:
|
|
|
|
1. Add a variant to `Command` in `src/cli.rs` (e.g. `Invoices(ResourceArgs)`)
|
|
— `ResourceArgs` and `Action` are already generic across resources, reuse
|
|
them as-is.
|
|
2. Add `src/commands/<resource>.rs` mirroring `clients.rs`: a `run` dispatcher
|
|
over `Action`, `list`/`get`/`create`/`update`/`delete` functions calling
|
|
the matching `list_<resource>`/`get_<resource>`/… methods on
|
|
`InvoiceNinjaClient`, and a `print_table` choosing that resource's own key
|
|
columns.
|
|
3. Register the module in `src/commands/mod.rs` and the dispatch arm in
|
|
`main.rs`'s `match cli.command`.
|
|
4. Check `../invoiceninjars-sdk/src/client.rs` for whether the resource has a
|
|
`/bulk` route (most do; a few — `client_gateway_tokens`, `companies`,
|
|
`system_logs` — don't, per that crate's `crud_methods!` vs
|
|
`resource_methods!` split). This CLI doesn't expose bulk actions yet
|
|
either way; that's a separate addition.
|
|
|
|
Resources with entity actions, document uploads, or non-CRUD endpoints
|
|
(reports, charts, search, statics, PDF downloads, …) will need more than
|
|
this template once they're added — the SDK's own `CLAUDE.md` describes which
|
|
resources have which extras.
|