Skip to main content

tango/models/
resolve.rs

1//! Types for `POST /api/resolve/`.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Resolver target — which catalog the resolver searches against.
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "lowercase")]
10pub enum ResolveTargetType {
11    /// SAM.gov vendor catalog.
12    Entity,
13    /// Federal organization catalog (departments / agencies / offices).
14    Organization,
15}
16
17/// Request body for [`Client::resolve`](crate::Client::resolve).
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct ResolveInput {
20    /// Free-text name to match.
21    pub name: String,
22    /// Target catalog.
23    pub target_type: ResolveTargetType,
24    /// Optional state filter (e.g. `"VA"`).
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub state: Option<String>,
27    /// Optional city filter.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub city: Option<String>,
30    /// Optional free-text additional context.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub context: Option<String>,
33}
34
35/// A single candidate from a resolve call.
36#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
37pub struct ResolveCandidate {
38    /// Canonical UEI (entity target) or organization key (organization target).
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub identifier: Option<String>,
41    /// Human-readable name.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub display_name: Option<String>,
44    /// Confidence label (`"low"` / `"medium"` / `"high"`). Pro+ tier only.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub match_tier: Option<String>,
47    /// Forward-compatible bucket for any unrecognized fields the server adds.
48    #[serde(flatten)]
49    pub extra: HashMap<String, Value>,
50}
51
52/// Response from [`Client::resolve`](crate::Client::resolve).
53#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
54pub struct ResolveResult {
55    /// Number of candidates returned (Free tier caps at 3, Pro+ at 5).
56    #[serde(default)]
57    pub count: u32,
58    /// The candidates, ranked by confidence.
59    #[serde(default)]
60    pub candidates: Vec<ResolveCandidate>,
61    /// Forward-compatible bucket for any unrecognized fields the server adds.
62    #[serde(flatten)]
63    pub extra: HashMap<String, Value>,
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use serde_json::json;
70
71    #[test]
72    fn resolve_result_captures_unknown_fields_via_extra() {
73        let body = json!({
74            "count": 1,
75            "candidates": [{
76                "identifier": "ABC",
77                "display_name": "ACME",
78                "match_tier": "high",
79                "future_score": 0.92
80            }],
81            "future_meta": {"version": 2}
82        });
83        let r: ResolveResult = serde_json::from_value(body).expect("decode");
84        assert_eq!(r.count, 1);
85        assert!(r.extra.contains_key("future_meta"));
86        let candidate = &r.candidates[0];
87        assert_eq!(candidate.identifier.as_deref(), Some("ABC"));
88        assert!(candidate.extra.contains_key("future_score"));
89    }
90
91    #[test]
92    fn resolve_target_type_round_trips() {
93        let v = serde_json::to_value(ResolveTargetType::Entity).unwrap();
94        assert_eq!(v, json!("entity"));
95        let back: ResolveTargetType = serde_json::from_value(json!("organization")).unwrap();
96        assert_eq!(back, ResolveTargetType::Organization);
97    }
98}