Rename the crate from invoiceninjars to invoiceninja-sdk
CI / test (push) Canceled after 0s
CI / Build on the declared MSRV (push) Canceled after 0s
CI / Latest stable (advisory) (push) Canceled after 0s

The package name has never matched the repository it lives in
(gitea.awkawb.cloud/awkawb/invoiceninja-sdk, mirrored to
github.com/awkawb1/invoiceninja-sdk) — both pointed at `invoiceninja-sdk`
while `Cargo.toml` declared `invoiceninjars`. Lines everything up on the
name the repo has always used.

Cargo derives the crate's Rust identifier from the package name by
lowercasing hyphens to underscores, so every `use invoiceninjars::...`
becomes `use invoiceninja_sdk::...` — in doctests across `src/`, the four
examples, all four integration test files, and `README.md`. `Cargo.lock`'s
own self-entry picked up the new name on the next `cargo check`, which is
also when the rename gets its real check: every doctest and every test file
has to resolve the new path to compile at all, not just the ones a grep
happens to catch.

`flake.nix`'s `pname` and description follow, so `nix build` produces
`invoiceninja-sdk-0.1.0.drv` rather than a name that no longer matches
anything. `GOALS.md`'s two mentions — the hypothetical CLI's name and the
`cargo install` example — follow too, for the same reason.

Verified clean on both the dev toolchain and the 1.88 MSRV shell: full test
suite (209 tests), clippy, fmt, and `nix build`/`nix flake check`.

Not yet published to crates.io (checked: the name 404s there), so this
lands before the old name was ever claimed — no one's `Cargo.toml` breaks
today, but it's recorded as a Changed entry since `GOALS.md` asks for
breaking changes to be legible in the changelog rather than discovered at
compile time, and the crate is reachable as a git dependency regardless of
crates.io status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 09:43:05 -04:00
co-authored by Claude Sonnet 5
parent 48244e8390
commit fb670cbb08
17 changed files with 41 additions and 33 deletions
+8
View File
@@ -112,6 +112,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- **The crate is renamed from `invoiceninjars` to `invoiceninja-sdk`**, to
match the repository it has always lived in
(`gitea.awkawb.cloud/awkawb/invoiceninja-sdk`). Depend on it as
`invoiceninja-sdk` in `Cargo.toml`, and import it as `invoiceninja_sdk`
(Cargo lowercases a package name's hyphens to underscores for the crate
identifier) — every `use invoiceninjars::...` becomes
`use invoiceninja_sdk::...`. Not yet published to crates.io, so this
lands before the name was ever claimed there.
- `statics`, `search`, and `health_check` return concrete types instead of - `statics`, `search`, and `health_check` return concrete types instead of
being generic over `T: DeserializeOwned`. being generic over `T: DeserializeOwned`.
- `send_email` now unwraps the `data` envelope, so `T` is the entity - `send_email` now unwraps the `data` envelope, so `T` is the entity
Generated
+1 -1
View File
@@ -528,7 +528,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "invoiceninjars" name = "invoiceninja-sdk"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
+1 -1
View File
@@ -1,5 +1,5 @@
[package] [package]
name = "invoiceninjars" name = "invoiceninja-sdk"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# Set by the dependency graph rather than by this crate's own code: reqwest # Set by the dependency graph rather than by this crate's own code: reqwest
+2 -2
View File
@@ -90,7 +90,7 @@ Invoice Ninja account to contribute.
ask "what does this endpoint actually return", which matters here because ask "what does this endpoint actually return", which matters here because
the e-invoicing and integration methods are typed as the e-invoicing and integration methods are typed as
`serde_json::Value` and were verified against mocks rather than a live `serde_json::Value` and were verified against mocks rather than a live
service. It belongs in a sibling crate (`invoiceninjars-cli`) in a service. It belongs in a sibling crate (`invoiceninja-sdk-cli`) in a
workspace, not in this one, for three reasons: workspace, not in this one, for three reasons:
- *Dependencies.* Every entry in `Cargo.toml` is there because the library - *Dependencies.* Every entry in `Cargo.toml` is there because the library
@@ -106,7 +106,7 @@ Invoice Ninja account to contribute.
- *Versioning.* Pre-1.0 this crate tells callers to pin an exact version - *Versioning.* Pre-1.0 this crate tells callers to pin an exact version
and expect breaking changes in minor releases. A CLI's flags are a and expect breaking changes in minor releases. A CLI's flags are a
compatibility surface too, and one version number would force the two to compatibility surface too, and one version number would force the two to
move in lockstep. `cargo install invoiceninjars` silently needing move in lockstep. `cargo install invoiceninja-sdk` silently needing
`--features cli` is a smaller version of the same problem. `--features cli` is a smaller version of the same problem.
For ad-hoc probing, `examples/` already covers it and costs nothing: For ad-hoc probing, `examples/` already covers it and costs nothing:
+4 -4
View File
@@ -1,4 +1,4 @@
# invoiceninjars # invoiceninja-sdk
An async Rust client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API. An async Rust client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API.
@@ -33,7 +33,7 @@ nix develop .#msrv --command cargo test
## Usage ## Usage
```rust ```rust
use invoiceninjars::{InvoiceNinjaClient, models::ListParams}; use invoiceninja_sdk::{InvoiceNinjaClient, models::ListParams};
// Hosted app: // Hosted app:
let client = InvoiceNinjaClient::hosted(std::env::var("IN_API_TOKEN")?)?; let client = InvoiceNinjaClient::hosted(std::env::var("IN_API_TOKEN")?)?;
@@ -75,7 +75,7 @@ with exponential backoff. Both are configurable:
```rust ```rust
use std::time::Duration; use std::time::Duration;
use invoiceninjars::RetryPolicy; use invoiceninja_sdk::RetryPolicy;
let client = InvoiceNinjaClient::hosted(token)? let client = InvoiceNinjaClient::hosted(token)?
.with_timeout(Duration::from_secs(300)) .with_timeout(Duration::from_secs(300))
@@ -164,7 +164,7 @@ only way the API creates one. Sixteen resources support it, as
`upload_client`, `upload_invoice`, `upload_expense`, and so on: `upload_client`, `upload_invoice`, `upload_expense`, and so on:
```rust ```rust
use invoiceninjars::FileUpload; use invoiceninja_sdk::FileUpload;
let receipt = FileUpload::new("receipt.pdf", std::fs::read("receipt.pdf")?) let receipt = FileUpload::new("receipt.pdf", std::fs::read("receipt.pdf")?)
.mime("application/pdf"); .mime("application/pdf");
+3 -3
View File
@@ -1,8 +1,8 @@
use invoiceninjars::models::ListParams; use invoiceninja_sdk::models::ListParams;
use invoiceninjars::InvoiceNinjaClient; use invoiceninja_sdk::InvoiceNinjaClient;
#[tokio::main] #[tokio::main]
async fn main() -> invoiceninjars::Result<()> { async fn main() -> invoiceninja_sdk::Result<()> {
let base_url = let base_url =
std::env::var("IN_BASE_URL").unwrap_or_else(|_| "https://invoicing.co/api/v1".into()); std::env::var("IN_BASE_URL").unwrap_or_else(|_| "https://invoicing.co/api/v1".into());
let token = std::env::var("IN_API_TOKEN").expect("set IN_API_TOKEN"); let token = std::env::var("IN_API_TOKEN").expect("set IN_API_TOKEN");
+2 -2
View File
@@ -18,7 +18,7 @@
use std::future::Future; use std::future::Future;
use std::process::ExitCode; use std::process::ExitCode;
use invoiceninjars::{Error, InvoiceNinjaClient}; use invoiceninja_sdk::{Error, InvoiceNinjaClient};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::{json, Value}; use serde_json::{json, Value};
@@ -34,7 +34,7 @@ fn describe(error: &Error) -> String {
} }
/// Runs one creation, reporting the outcome and returning the new id. /// Runs one creation, reporting the outcome and returning the new id.
async fn step<T>(name: &str, call: impl Future<Output = invoiceninjars::Result<T>>) -> Option<T> { async fn step<T>(name: &str, call: impl Future<Output = invoiceninja_sdk::Result<T>>) -> Option<T> {
match call.await { match call.await {
Ok(created) => { Ok(created) => {
println!(" created {name}"); println!(" created {name}");
+2 -2
View File
@@ -37,10 +37,10 @@ use std::fmt::Display;
use std::process::ExitCode; use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use invoiceninjars::models::{ use invoiceninja_sdk::models::{
Client, ListParams, Location, PurchaseOrder, RecurringInvoice, Vendor, Webhook, Client, ListParams, Location, PurchaseOrder, RecurringInvoice, Vendor, Webhook,
}; };
use invoiceninjars::{Error, FileUpload, InvoiceNinjaClient}; use invoiceninja_sdk::{Error, FileUpload, InvoiceNinjaClient};
use serde::Serialize; use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
description = "invoiceninjars: a Rust client for the Invoice Ninja v5 API"; description = "invoiceninja-sdk: a Rust client for the Invoice Ninja v5 API";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
@@ -69,7 +69,7 @@
# the "no credentials needed" property in CLAUDE.md) — just not as # the "no credentials needed" property in CLAUDE.md) — just not as
# a source of an installable artifact. # a source of an installable artifact.
packages.default = rustPlatform.buildRustPackage { packages.default = rustPlatform.buildRustPackage {
pname = "invoiceninjars"; pname = "invoiceninja-sdk";
version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version; version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version;
src = self; src = self;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;
+4 -4
View File
@@ -219,8 +219,8 @@ impl InvoiceNinjaClient {
/// It used to be refused locally with a misleading `InvalidToken`. /// It used to be refused locally with a misleading `InvalidToken`.
/// ///
/// ```no_run /// ```no_run
/// # fn main() -> invoiceninjars::Result<()> { /// # fn main() -> invoiceninja_sdk::Result<()> {
/// use invoiceninjars::InvoiceNinjaClient; /// use invoiceninja_sdk::InvoiceNinjaClient;
/// ///
/// let client = InvoiceNinjaClient::hosted(std::env::var("IN_API_TOKEN").unwrap())? /// let client = InvoiceNinjaClient::hosted(std::env::var("IN_API_TOKEN").unwrap())?
/// .with_api_password(std::env::var("IN_API_PASSWORD").unwrap())?; /// .with_api_password(std::env::var("IN_API_PASSWORD").unwrap())?;
@@ -1367,8 +1367,8 @@ impl InvoiceNinjaClient {
/// for — [`Invoice`], [`Quote`], [`Credit`], or [`PurchaseOrder`]: /// for — [`Invoice`], [`Quote`], [`Credit`], or [`PurchaseOrder`]:
/// ///
/// ```no_run /// ```no_run
/// # async fn run(client: &invoiceninjars::InvoiceNinjaClient) -> invoiceninjars::Result<()> { /// # async fn run(client: &invoiceninja_sdk::InvoiceNinjaClient) -> invoiceninja_sdk::Result<()> {
/// use invoiceninjars::models::Invoice; /// use invoiceninja_sdk::models::Invoice;
/// use serde_json::json; /// use serde_json::json;
/// ///
/// let invoice: Invoice = client /// let invoice: Invoice = client
+2 -2
View File
@@ -1,9 +1,9 @@
//! An async Rust client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API. //! An async Rust client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API.
//! //!
//! ```no_run //! ```no_run
//! use invoiceninjars::{InvoiceNinjaClient, models::ListParams}; //! use invoiceninja_sdk::{InvoiceNinjaClient, models::ListParams};
//! //!
//! # async fn run() -> invoiceninjars::Result<()> { //! # async fn run() -> invoiceninja_sdk::Result<()> {
//! let client = InvoiceNinjaClient::new( //! let client = InvoiceNinjaClient::new(
//! "https://invoicing.co/api/v1", //! "https://invoicing.co/api/v1",
//! std::env::var("IN_API_TOKEN").unwrap(), //! std::env::var("IN_API_TOKEN").unwrap(),
+1 -1
View File
@@ -104,7 +104,7 @@ impl ListParams {
/// not cover. /// not cover.
/// ///
/// ``` /// ```
/// use invoiceninjars::models::ListParams; /// use invoiceninja_sdk::models::ListParams;
/// ///
/// let params = ListParams::new() /// let params = ListParams::new()
/// .per_page(100) /// .per_page(100)
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::error::{Error, Result};
/// question in `GOALS.md`. /// question in `GOALS.md`.
/// ///
/// ``` /// ```
/// use invoiceninjars::FileUpload; /// use invoiceninja_sdk::FileUpload;
/// ///
/// let file = FileUpload::new("receipt.pdf", b"%PDF-1.4 ...".to_vec()) /// let file = FileUpload::new("receipt.pdf", b"%PDF-1.4 ...".to_vec())
/// .mime("application/pdf"); /// .mime("application/pdf");
+5 -5
View File
@@ -8,7 +8,7 @@
use std::time::Duration; use std::time::Duration;
use invoiceninjars::{models::ListParams, Error, InvoiceNinjaClient}; use invoiceninja_sdk::{models::ListParams, Error, InvoiceNinjaClient};
use serde_json::json; use serde_json::json;
use wiremock::matchers::{header, method, path, query_param}; use wiremock::matchers::{header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate}; use wiremock::{Mock, MockServer, ResponseTemplate};
@@ -547,7 +547,7 @@ async fn ping_accepts_an_empty_success_body() {
// --- retry and backoff -------------------------------------------------- // --- retry and backoff --------------------------------------------------
use invoiceninjars::RetryPolicy; use invoiceninja_sdk::RetryPolicy;
/// Fast policy so the tests do not actually sleep for seconds. /// Fast policy so the tests do not actually sleep for seconds.
fn quick_retry() -> RetryPolicy { fn quick_retry() -> RetryPolicy {
@@ -751,7 +751,7 @@ async fn retried_requests_keep_their_body_and_headers() {
.mount(&server) .mount(&server)
.await; .await;
let body = invoiceninjars::models::Client { let body = invoiceninja_sdk::models::Client {
name: "Acme".into(), name: "Acme".into(),
..Default::default() ..Default::default()
}; };
@@ -965,7 +965,7 @@ async fn send_email_unwraps_the_data_envelope() {
.mount(&server) .mount(&server)
.await; .await;
let invoice: invoiceninjars::models::Invoice = client(&server) let invoice: invoiceninja_sdk::models::Invoice = client(&server)
.send_email(&json!({ "entity": "invoice", "entity_id": "inv1" })) .send_email(&json!({ "entity": "invoice", "entity_id": "inv1" }))
.await .await
.expect("should return the refreshed entity"); .expect("should return the refreshed entity");
@@ -1179,7 +1179,7 @@ async fn no_budget_means_only_max_retries_bounds_the_call() {
// --- document uploads --------------------------------------------------- // --- document uploads ---------------------------------------------------
use invoiceninjars::FileUpload; use invoiceninja_sdk::FileUpload;
fn pdf(name: &str) -> FileUpload { fn pdf(name: &str) -> FileUpload {
FileUpload::new(name, b"%PDF-1.4 fake".to_vec()).mime("application/pdf") FileUpload::new(name, b"%PDF-1.4 fake".to_vec()).mime("application/pdf")
+1 -1
View File
@@ -11,7 +11,7 @@
//! A wrong *type* is deliberately not covered: that is meant to be a hard //! A wrong *type* is deliberately not covered: that is meant to be a hard
//! error, so it is not something to absorb. //! error, so it is not something to absorb.
use invoiceninjars::models::*; use invoiceninja_sdk::models::*;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value; use serde_json::Value;
+1 -1
View File
@@ -1,4 +1,4 @@
use invoiceninjars::models::{ use invoiceninja_sdk::models::{
BankIntegration, BankTransaction, BankTransactionRule, BulkRequest, Client, ClientGatewayToken, BankIntegration, BankTransaction, BankTransactionRule, BulkRequest, Client, ClientGatewayToken,
Company, CompanyGateway, CompanyToken, Credit, Design, Document, Expense, ExpenseCategory, Company, CompanyGateway, CompanyToken, Credit, Design, Document, Expense, ExpenseCategory,
GroupSetting, HealthCheck, Invoice, ListParams, ListResponse, Location, PaymentTerm, Project, GroupSetting, HealthCheck, Invoice, ListParams, ListResponse, Location, PaymentTerm, Project,
+1 -1
View File
@@ -15,7 +15,7 @@ use std::collections::BTreeMap;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use invoiceninjars::models::*; use invoiceninja_sdk::models::*;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value; use serde_json::Value;