diff --git a/CHANGELOG.md b/CHANGELOG.md index 05e964b..26596e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ This project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.4.0] — 2026-10-29 + +### `makegov-tango` + +#### Added + +- **DIBBS `fsc` filter** (Tango API 5.13.0). `ListDibbsRfqsOptions`, `ListDibbsRfpsOptions` and `ListDibbsAwardsOptions` gain `fsc`, the Federal Supply Class (the first four digits of the NSN, e.g. `6515`). Several classes OR together with `|` (`6515|6530`); the API rejects any value that is not exactly four digits with a 400. + ## [0.3.1] — 2026-10-19 ### `makegov-tango` diff --git a/Cargo.lock b/Cargo.lock index b9023dc..eb3a157 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1209,7 +1209,7 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "makegov-tango" -version = "0.3.1" +version = "0.4.0" dependencies = [ "bon", "futures", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "makegov-tango-webhooks" -version = "0.3.1" +version = "0.4.0" dependencies = [ "hex", "hmac", diff --git a/Cargo.toml b/Cargo.toml index 372dd1b..a46644a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ exclude = ["examples"] [workspace.package] -version = "0.3.1" +version = "0.4.0" edition = "2021" rust-version = "1.80" license = "MIT" diff --git a/README.md b/README.md index 89c81a8..dccde24 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Official, async-first Rust SDK for the [Tango API](https://tango.makegov.com) — federal contracts, IDVs, entities, opportunities, grants, vehicles, and more, with dynamic response shaping so you fetch only the fields you need. -> **Pre-1.0 — v0.3.1.** The resource surface now tracks the sibling SDKs endpoint for endpoint, but the public API may still shift before v1.0.0. Pin to a specific version if you depend on this in production. +> **Pre-1.0 — v0.4.0.** The resource surface now tracks the sibling SDKs endpoint for endpoint, but the public API may still shift before v1.0.0. Pin to a specific version if you depend on this in production. The sibling SDKs are `tango-python`, `tango-node` and `tango-go`. This Rust port ships the full transport, error model, retry / rate-limit handling, webhook signing (in the separate `tango-webhooks` crate), and the resource-method surface. Same `0.x → 1.0` graduation logic as Go: the transport and types are stable; the surface stabilizes once it has been verified against the live API alongside the siblings. diff --git a/crates/tango/src/resources/dibbs.rs b/crates/tango/src/resources/dibbs.rs index 9693197..174660f 100644 --- a/crates/tango/src/resources/dibbs.rs +++ b/crates/tango/src/resources/dibbs.rs @@ -41,6 +41,9 @@ pub struct ListDibbsRfqsOptions { /// National Stock Number. Supports OR via `|`. #[builder(into)] pub nsn: Option, + /// Federal Supply Class: the first four digits of the NSN (e.g. `6515`). Each value must be exactly four digits; supports OR via `|`. Any other value is rejected with a 400. + #[builder(into)] + pub fsc: Option, /// Manufacturer part number. Supports OR via `|`. #[builder(into)] pub part_number: Option, @@ -103,6 +106,7 @@ impl ListDibbsRfqsOptions { ); push_opt_bool(&mut q, "open", self.open); push_opt(&mut q, "nsn", self.nsn.as_deref()); + push_opt(&mut q, "fsc", self.fsc.as_deref()); push_opt(&mut q, "part_number", self.part_number.as_deref()); push_opt(&mut q, "solicitation", self.solicitation.as_deref()); push_opt(&mut q, "purchase_request", self.purchase_request.as_deref()); @@ -166,6 +170,9 @@ pub struct ListDibbsRfpsOptions { /// National Stock Number. Supports OR via `|`. #[builder(into)] pub nsn: Option, + /// Federal Supply Class: the first four digits of the NSN (e.g. `6515`). Each value must be exactly four digits; supports OR via `|`. Any other value is rejected with a 400. + #[builder(into)] + pub fsc: Option, /// Manufacturer part number. Supports OR via `|`. #[builder(into)] pub part_number: Option, @@ -216,6 +223,7 @@ impl ListDibbsRfpsOptions { ); push_opt_bool(&mut q, "open", self.open); push_opt(&mut q, "nsn", self.nsn.as_deref()); + push_opt(&mut q, "fsc", self.fsc.as_deref()); push_opt(&mut q, "part_number", self.part_number.as_deref()); push_opt(&mut q, "solicitation", self.solicitation.as_deref()); push_opt(&mut q, "buyer_code", self.buyer_code.as_deref()); @@ -279,6 +287,9 @@ pub struct ListDibbsAwardsOptions { /// National Stock Number. Supports OR via `|`. #[builder(into)] pub nsn: Option, + /// Federal Supply Class: the first four digits of the NSN (e.g. `6515`). Each value must be exactly four digits; supports OR via `|`. Any other value is rejected with a 400. + #[builder(into)] + pub fsc: Option, /// Manufacturer part number. Supports OR via `|`. #[builder(into)] pub part_number: Option, @@ -346,6 +357,7 @@ impl ListDibbsAwardsOptions { self.flat_lists, ); push_opt(&mut q, "nsn", self.nsn.as_deref()); + push_opt(&mut q, "fsc", self.fsc.as_deref()); push_opt(&mut q, "part_number", self.part_number.as_deref()); push_opt(&mut q, "solicitation", self.solicitation.as_deref()); push_opt(&mut q, "award_number", self.award_number.as_deref()); @@ -536,6 +548,7 @@ mod tests { let opts = ListDibbsRfqsOptions::builder() .open(true) .nsn("v-nsn") + .fsc("v-fsc") .part_number("v-part_number") .solicitation("v-solicitation") .purchase_request("v-purchase_request") @@ -554,6 +567,7 @@ mod tests { let q = opts.to_query(); assert_eq!(get_q(&q, "open").as_deref(), Some("true")); assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn")); + assert_eq!(get_q(&q, "fsc").as_deref(), Some("v-fsc")); assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number")); assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation")); assert_eq!( @@ -583,7 +597,7 @@ mod tests { ); assert_eq!(get_q(&q, "search").as_deref(), Some("v-search")); assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering")); - assert_eq!(q.len(), 16); + assert_eq!(q.len(), 17); } #[test] @@ -644,6 +658,7 @@ mod tests { let opts = ListDibbsRfpsOptions::builder() .open(true) .nsn("v-nsn") + .fsc("v-fsc") .part_number("v-part_number") .solicitation("v-solicitation") .buyer_code("v-buyer_code") @@ -658,6 +673,7 @@ mod tests { let q = opts.to_query(); assert_eq!(get_q(&q, "open").as_deref(), Some("true")); assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn")); + assert_eq!(get_q(&q, "fsc").as_deref(), Some("v-fsc")); assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number")); assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation")); assert_eq!(get_q(&q, "buyer_code").as_deref(), Some("v-buyer_code")); @@ -680,7 +696,7 @@ mod tests { ); assert_eq!(get_q(&q, "search").as_deref(), Some("v-search")); assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering")); - assert_eq!(q.len(), 12); + assert_eq!(q.len(), 13); } #[test] @@ -740,6 +756,7 @@ mod tests { fn list_dibbs_awards_all_filters_emit() { let opts = ListDibbsAwardsOptions::builder() .nsn("v-nsn") + .fsc("v-fsc") .part_number("v-part_number") .solicitation("v-solicitation") .award_number("v-award_number") @@ -759,6 +776,7 @@ mod tests { .build(); let q = opts.to_query(); assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn")); + assert_eq!(get_q(&q, "fsc").as_deref(), Some("v-fsc")); assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number")); assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation")); assert_eq!(get_q(&q, "award_number").as_deref(), Some("v-award_number")); @@ -799,7 +817,7 @@ mod tests { ); assert_eq!(get_q(&q, "search").as_deref(), Some("v-search")); assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering")); - assert_eq!(q.len(), 17); + assert_eq!(q.len(), 18); } #[test] diff --git a/crates/tango/tests/routes.rs b/crates/tango/tests/routes.rs index 28b06c1..4790c1b 100644 --- a/crates/tango/tests/routes.rs +++ b/crates/tango/tests/routes.rs @@ -84,7 +84,8 @@ async fn dibbs_routes() { when.method(GET) .path("/api/dibbs/rfqs/") .query_param("open", "false") - .query_param("nsn", "5340-01-123-4567"); + .query_param("nsn", "5340-01-123-4567") + .query_param("fsc", "5340"); then.status(200).json_body(page(json!([{"uuid": "r1"}]))); }) .await; @@ -92,7 +93,8 @@ async fn dibbs_routes() { .mock_async(|when, then| { when.method(GET) .path("/api/dibbs/rfps/") - .query_param("buyer_code", "PB"); + .query_param("buyer_code", "PB") + .query_param("fsc", "6515|6530"); then.status(200).json_body(page(json!([{"uuid": "p1"}]))); }) .await; @@ -100,7 +102,8 @@ async fn dibbs_routes() { .mock_async(|when, then| { when.method(GET) .path("/api/dibbs/awards/") - .query_param("awardee_cage", "1ABC5"); + .query_param("awardee_cage", "1ABC5") + .query_param("fsc", "5340"); then.status(200).json_body(page(json!([{"uuid": "a1"}]))); }) .await; @@ -118,17 +121,24 @@ async fn dibbs_routes() { ListDibbsRfqsOptions::builder() .open(false) .nsn("5340-01-123-4567") + .fsc("5340") .build(), ) .await .expect("rfqs"); assert_eq!(p.results.len(), 1); - c.list_dibbs_rfps(ListDibbsRfpsOptions::builder().buyer_code("PB").build()) - .await - .expect("rfps"); + c.list_dibbs_rfps( + ListDibbsRfpsOptions::builder() + .buyer_code("PB") + .fsc("6515|6530") + .build(), + ) + .await + .expect("rfps"); c.list_dibbs_awards( ListDibbsAwardsOptions::builder() .awardee_cage("1ABC5") + .fsc("5340") .build(), ) .await