Add retry with backoff, and narrow the untyped responses
Retry: - `RetryPolicy` (3 retries, full-jitter exponential backoff) applied in a new `execute` dispatch chokepoint, configurable via `with_retry_policy`. `Retry-After` is honoured, and one longer than `max_delay` is surfaced rather than slept through. - What may be replayed depends on the method, not just the status. A 429 means the server refused the request without processing it, so any method is safe. A 5xx or timeout gives no such guarantee, so only idempotent methods are retried: this is a billing API, and replaying a POST the server had already applied would duplicate an invoice, payment, or email. Response types: - `statics`, `search`, and `health_check` return `Statics`, `SearchResults`, and `HealthCheck` instead of being generic. `HealthCheck` covers both deployments — hosted returns a bare message rather than a report — and `SearchResults` covers both the Elasticsearch and non-Elasticsearch bucket sets. - Each keeps the `extra` convention, and `statics_as` / `search_as` / `health_check_as` remain for pinning shapes left untyped. Nested config blobs (gateways, templates, extensions) stay `Value`, same reasoning as `Company::settings`. - `send_email` previously decoded the response as a bare body, but the controller returns `itemResponse(...)` — a `data`-wrapped entity. It now unwraps, so `T` is the entity. Still generic, since the endpoint serves invoices, quotes, credits, and purchase orders. - `chart` stays generic on purpose: the eleven charts share no response shape. - Adds `id_as_string`, since `/statics` returns `payment_types.id` as a string and the other tables return numbers. Adds CHANGELOG.md (Keep a Changelog), and a CLAUDE.md note to keep it current. 17 new tests, 43 in client_requests.rs total. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+22
-4
@@ -242,10 +242,28 @@ written yet:
|
||||
|
||||
### Typed response gaps
|
||||
|
||||
`statics`, `chart`, `search`, `send_email`, and `health_check` are generic
|
||||
over `T: DeserializeOwned` rather than returning concrete structs, because
|
||||
none of them has a stable published schema. Pass `serde_json::Value`, or your
|
||||
own struct to pin the parts you rely on.
|
||||
`statics`, `search`, and `health_check` return concrete types (`Statics`,
|
||||
`SearchResults`, `HealthCheck`). Each keeps the crate's `extra` convention,
|
||||
so fields they don't model stay reachable rather than being dropped, and
|
||||
each has a generic counterpart — `statics_as`, `search_as`,
|
||||
`health_check_as` — for pinning a shape they leave untyped.
|
||||
|
||||
Two remain generic on purpose:
|
||||
|
||||
- **`chart`** — the eleven chart endpoints share no common response shape
|
||||
(`totals_v2` returns per-currency maps, `cashflow_forecast` a date series,
|
||||
`calculated_fields` scalars), so there is nothing to narrow to.
|
||||
- **`send_email`** — the endpoint serves invoices, quotes, credits, and
|
||||
purchase orders, and answers with whichever entity was emailed. The `data`
|
||||
envelope *is* unwrapped, so `T` is the entity itself.
|
||||
|
||||
Within the typed models, a few fields are deliberately left as
|
||||
`serde_json::Value`: `Statics::gateways`, `Statics::templates`,
|
||||
`Statics::bulk_updates`, `HealthCheck::extensions`, and
|
||||
`HealthCheck::queue_data`. These are nested configuration blobs rather than
|
||||
uniform lookup rows, and the same reasoning applies as for
|
||||
`Company::settings` — a wrong concrete type is a hard decode error, whereas
|
||||
`Value` is lossless.
|
||||
|
||||
## Compared with the official PHP SDK
|
||||
|
||||
|
||||
Reference in New Issue
Block a user