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>
3.7 KiB
3.7 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Wrappers for every one of the API's 34
Route::resourcegroups, plususers— 265 methods. SeeCOVERAGE.mdfor the per-endpoint inventory. COVERAGE.md, a detailed record of what is wrapped, what is not, and why, checked againstroutes/api.phpon thev5-stablebranch.- Retry with full-jitter exponential backoff, configurable through
RetryPolicyandInvoiceNinjaClient::with_retry_policy. Defaults to 3 retries.Retry-Afteris honoured when the server sends it. InvoiceNinjaClient::with_timeoutand aDEFAULT_TIMEOUTof 60 seconds applied to every request.- Typed responses for previously untyped endpoints:
Statics,StaticItem,Currency,DateFormat,SearchResults,SearchHit,HealthCheck, andPhpVersion. statics_as,search_as, andhealth_check_asas generic escape hatches for callers who need to pin a shape the typed models leave inextra.tests/client_requests.rs: 43 tests covering the request plumbing against a loopbackwiremockserver. No network or credentials required.
Changed
statics,search, andhealth_checkreturn concrete types instead of being generic overT: DeserializeOwned.send_emailnow unwraps thedataenvelope, soTis the entity (Invoice,Quote, …) rather than a wrapper the caller had to model. It stays generic because the endpoint serves all four entity types.chartremains generic, deliberately: the eleven chart endpoints share no common response shape, so there is nothing to narrow to.- The base URL is parsed and validated when the client is constructed. A missing scheme used to produce a 404 on every call instead of one error.
list_all_*terminates onmeta.pagination.total_pageswhen the server sends it, falling back to the short-page heuristic only when it does not.
Fixed
list_all_*silently truncated results. The server caps page size atmin(abs(per_page), 5000), so a page shorter than requested did not mean the last page — the walk stopped early and returned a partial list as though it were complete.per_pageis now clamped to the server maximum so the fallback heuristic holds.list_all_*withper_page(0)never terminated: the checkgot < 0has no solution. It now falls back to the default page size, and aMAX_PAGESbackstop raisesError::PaginationLimitrather than quietly truncating.- Requests could hang indefinitely.
reqwestapplies no timeout of its own and none was configured. - Ids were interpolated into request paths unescaped, so a
/,?, or#in an id changed which endpoint was addressed.
Security
- Ids used as URL path segments are escaped, and ids that are empty or
consist only of dots are rejected. Escaping alone is insufficient: the
WHATWG rules the
urlcrate implements decode%2Eback to.before resolving dot segments, so an escaped..still collapsed the path. An empty id would have addressed the collection route, makingdelete_client("")meanDELETE /clients. POSTis never replayed on a 5xx or timeout, since the server may have applied it before the response was lost and a replay would duplicate an invoice, payment, or email. A 429 is the exception — the server states it refused the request, so no work was done.
[0.1.0] - 2026-10-23
Added
- Initial release: async client for the Invoice Ninja v5 API covering clients, invoices, quotes, credits, payments, products, expenses, projects, tasks, and vendors.