Skip to main content

tango/models/
agency.rs

1//! `AgencyRecord` — typed response from `GET /api/agencies/{code}/`.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// A federal agency record.
8///
9/// Returned by [`Client::get_agency`](crate::Client::get_agency). The Tango
10/// API uses CGAC codes as the identifier (e.g. `"9700"` for the Department of
11/// Defense). All fields are optional; unknown server-side fields are captured
12/// in [`extra`](Self::extra) so a schema addition never silently drops data.
13#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
14pub struct AgencyRecord {
15    /// The agency's internal identifier (Tango-specific).
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub agency_id: Option<String>,
18
19    /// Human-readable agency name.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub name: Option<String>,
22
23    /// Common abbreviation (e.g. `"DOD"`).
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub abbreviation: Option<String>,
26
27    /// Code (typically the CGAC code).
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub code: Option<String>,
30
31    /// Parent department metadata (nested object the server returns
32    /// verbatim).
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub department: Option<Value>,
35
36    /// Forward-compatible bucket for any unrecognized fields the server adds.
37    #[serde(flatten)]
38    pub extra: HashMap<String, Value>,
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use serde_json::json;
45
46    #[test]
47    fn decode_minimal() {
48        let body = json!({
49            "agency_id": "97",
50            "name": "Department of Defense",
51            "abbreviation": "DOD",
52            "code": "9700"
53        });
54        let a: AgencyRecord = serde_json::from_value(body).expect("decode");
55        assert_eq!(a.name.as_deref(), Some("Department of Defense"));
56        assert_eq!(a.code.as_deref(), Some("9700"));
57        assert!(a.extra.is_empty());
58    }
59
60    #[test]
61    fn extra_captures_forward_compatible_fields() {
62        let body = json!({
63            "name": "Department of Defense",
64            "code": "9700",
65            "future_field": {"version": 2}
66        });
67        let a: AgencyRecord = serde_json::from_value(body).expect("decode");
68        assert!(a.extra.contains_key("future_field"));
69        assert_eq!(
70            a.extra.get("future_field").and_then(|v| v.get("version")),
71            Some(&json!(2))
72        );
73    }
74
75    #[test]
76    fn round_trip_emits_extras() {
77        let body = json!({
78            "name": "GSA",
79            "code": "4700",
80            "future": "x"
81        });
82        let a: AgencyRecord = serde_json::from_value(body.clone()).expect("decode");
83        let re = serde_json::to_value(&a).expect("re-encode");
84        assert_eq!(re.get("future"), Some(&json!("x")));
85        assert_eq!(re.get("name"), Some(&json!("GSA")));
86    }
87}