Skip to main content

tango/resources/
resolve_validate.rs

1//! `POST /api/resolve/` and `POST /api/validate/`.
2//!
3//! `resolve` fuzzy-matches a free-text name against a catalog (entities or
4//! organizations) and returns ranked candidates. `validate` checks whether a
5//! given identifier (PIID, solicitation, UEI) is well-formed and known.
6
7use crate::client::Client;
8use crate::error::{Error, Result};
9use crate::models::{ResolveInput, ResolveResult, ValidateInput, ValidateResult};
10
11impl Client {
12    /// `POST /api/resolve/` — fuzzy-match a name to entity or organization
13    /// candidates.
14    ///
15    /// `input.target_type` is an enum and is therefore always valid;
16    /// `input.name` must be non-empty (validated client-side).
17    pub async fn resolve(&self, input: ResolveInput) -> Result<ResolveResult> {
18        if input.name.is_empty() {
19            return Err(Error::Validation {
20                message: "Resolve: name is required".into(),
21                response: None,
22            });
23        }
24        self.post_json::<_, ResolveResult>("/api/resolve/", &input)
25            .await
26    }
27
28    /// `POST /api/validate/` — check whether an identifier (PIID, solicitation,
29    /// or UEI) is well-formed and known.
30    pub async fn validate(&self, input: ValidateInput) -> Result<ValidateResult> {
31        if input.value.is_empty() {
32            return Err(Error::Validation {
33                message: "Validate: value is required".into(),
34                response: None,
35            });
36        }
37        self.post_json::<_, ValidateResult>("/api/validate/", &input)
38            .await
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::models::{ResolveTargetType, ValidateInputType};
46
47    fn client() -> Client {
48        // Validation must trip BEFORE any HTTP call, so the unreachable base
49        // URL is safe — the request must never be issued.
50        Client::builder()
51            .api_key("k")
52            .base_url("http://localhost:1".to_string())
53            .build()
54            .expect("build client")
55    }
56
57    #[tokio::test]
58    async fn resolve_rejects_empty_name() {
59        let err = client()
60            .resolve(ResolveInput {
61                name: String::new(),
62                target_type: ResolveTargetType::Entity,
63                state: None,
64                city: None,
65                context: None,
66            })
67            .await
68            .unwrap_err();
69        assert!(matches!(err, Error::Validation { .. }));
70    }
71
72    #[tokio::test]
73    async fn validate_rejects_empty_value() {
74        let err = client()
75            .validate(ValidateInput {
76                kind: ValidateInputType::Uei,
77                value: String::new(),
78            })
79            .await
80            .unwrap_err();
81        assert!(matches!(err, Error::Validation { .. }));
82    }
83}