Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
24 changes: 21 additions & 3 deletions crates/tango/src/resources/dibbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub struct ListDibbsRfqsOptions {
/// National Stock Number. Supports OR via `|`.
#[builder(into)]
pub nsn: Option<String>,
/// 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<String>,
/// Manufacturer part number. Supports OR via `|`.
#[builder(into)]
pub part_number: Option<String>,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -166,6 +170,9 @@ pub struct ListDibbsRfpsOptions {
/// National Stock Number. Supports OR via `|`.
#[builder(into)]
pub nsn: Option<String>,
/// 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<String>,
/// Manufacturer part number. Supports OR via `|`.
#[builder(into)]
pub part_number: Option<String>,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -279,6 +287,9 @@ pub struct ListDibbsAwardsOptions {
/// National Stock Number. Supports OR via `|`.
#[builder(into)]
pub nsn: Option<String>,
/// 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<String>,
/// Manufacturer part number. Supports OR via `|`.
#[builder(into)]
pub part_number: Option<String>,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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")
Expand All @@ -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!(
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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")
Expand All @@ -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"));
Expand All @@ -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]
Expand Down Expand Up @@ -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")
Expand All @@ -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"));
Expand Down Expand Up @@ -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]
Expand Down
22 changes: 16 additions & 6 deletions crates/tango/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,26 @@ 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;
let proposal_list = server
.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;
let awards = server
.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;
Expand All @@ -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
Expand Down
Loading