Skip to main content

tango/resources/
vehicle_subresources.rs

1//! `GET /api/vehicles/{uuid}/awardees/` and
2//! `GET /api/vehicles/{uuid}/orders/` — list and stream the awardees
3//! (child-IDV-holding entities) and task orders for a contracting vehicle.
4
5use crate::client::Client;
6use crate::error::{Error, Result};
7use crate::internal::{apply_pagination, push_opt};
8use crate::pagination::{FetchFn, Page, PageStream};
9use crate::resources::agencies::urlencoding;
10use crate::Record;
11use bon::Builder;
12use std::sync::Arc;
13
14/// Options for [`Client::list_vehicle_awardees`] and
15/// [`Client::iterate_vehicle_awardees`].
16#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct ListVehicleAwardeesOptions {
19    /// 1-based page number. Mutually exclusive with [`cursor`](Self::cursor).
20    #[builder(into)]
21    pub page: Option<u32>,
22    /// Page size (server caps at 100).
23    #[builder(into)]
24    pub limit: Option<u32>,
25    /// Keyset cursor for cursor-paginated endpoints.
26    #[builder(into)]
27    pub cursor: Option<String>,
28    /// Comma-separated field selector. Use a [`SHAPE_*`](crate::shapes)
29    /// constant or roll your own.
30    #[builder(into)]
31    pub shape: Option<String>,
32    /// Collapse nested objects into dot-separated keys.
33    #[builder(default)]
34    pub flat: bool,
35    /// When [`flat`](Self::flat) is also true, flatten list-valued fields.
36    #[builder(default)]
37    pub flat_lists: bool,
38    /// Entity-aware full-text search across the awardees' IDV fields.
39    #[builder(into)]
40    pub search: Option<String>,
41}
42
43impl ListVehicleAwardeesOptions {
44    fn to_query(&self) -> Vec<(String, String)> {
45        let mut q = Vec::new();
46        apply_pagination(
47            &mut q,
48            self.page,
49            self.limit,
50            self.cursor.as_deref(),
51            self.shape.as_deref(),
52            self.flat,
53            self.flat_lists,
54        );
55        push_opt(&mut q, "search", self.search.as_deref());
56        q
57    }
58}
59
60/// Options for [`Client::list_vehicle_orders`] and
61/// [`Client::iterate_vehicle_orders`].
62#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct ListVehicleOrdersOptions {
65    /// 1-based page number.
66    #[builder(into)]
67    pub page: Option<u32>,
68    /// Page size (server caps at 100).
69    #[builder(into)]
70    pub limit: Option<u32>,
71    /// Keyset cursor.
72    #[builder(into)]
73    pub cursor: Option<String>,
74    /// Comma-separated field selector.
75    #[builder(into)]
76    pub shape: Option<String>,
77    /// Collapse nested objects into dot-separated keys.
78    #[builder(default)]
79    pub flat: bool,
80    /// When [`flat`](Self::flat) is also true, flatten list-valued fields.
81    #[builder(default)]
82    pub flat_lists: bool,
83    /// Server-side sort: `award_date` (the default, as `-award_date`), `obligated` or `total_contract_value`, prefixed with `-` for descending.
84    #[builder(into)]
85    pub ordering: Option<String>,
86}
87
88impl ListVehicleOrdersOptions {
89    fn to_query(&self) -> Vec<(String, String)> {
90        let mut q = Vec::new();
91        apply_pagination(
92            &mut q,
93            self.page,
94            self.limit,
95            self.cursor.as_deref(),
96            self.shape.as_deref(),
97            self.flat,
98            self.flat_lists,
99        );
100        push_opt(&mut q, "ordering", self.ordering.as_deref());
101        q
102    }
103}
104
105impl Client {
106    /// `GET /api/vehicles/{uuid}/awardees/` — entities holding child IDVs
107    /// under this contracting vehicle.
108    pub async fn list_vehicle_awardees(
109        &self,
110        uuid: &str,
111        opts: ListVehicleAwardeesOptions,
112    ) -> Result<Page<Record>> {
113        list_vehicle_awardees_inner(self, uuid, opts).await
114    }
115
116    /// `GET /api/vehicles/{uuid}/orders/` — task orders placed under this
117    /// vehicle's child IDVs.
118    pub async fn list_vehicle_orders(
119        &self,
120        uuid: &str,
121        opts: ListVehicleOrdersOptions,
122    ) -> Result<Page<Record>> {
123        list_vehicle_orders_inner(self, uuid, opts).await
124    }
125
126    /// Stream every awardee under this contracting vehicle.
127    pub fn iterate_vehicle_awardees(
128        &self,
129        uuid: &str,
130        opts: ListVehicleAwardeesOptions,
131    ) -> PageStream<Record> {
132        let opts = Arc::new(opts);
133        let uuid = Arc::new(uuid.to_string());
134        let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
135            let mut next = (*opts).clone();
136            next.page = page;
137            next.cursor = cursor;
138            let uuid = uuid.clone();
139            Box::pin(async move { list_vehicle_awardees_inner(&client, &uuid, next).await })
140        });
141        PageStream::new(self.clone(), fetch)
142    }
143
144    /// Stream every task order placed under this contracting vehicle.
145    pub fn iterate_vehicle_orders(
146        &self,
147        uuid: &str,
148        opts: ListVehicleOrdersOptions,
149    ) -> PageStream<Record> {
150        let opts = Arc::new(opts);
151        let uuid = Arc::new(uuid.to_string());
152        let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
153            let mut next = (*opts).clone();
154            next.page = page;
155            next.cursor = cursor;
156            let uuid = uuid.clone();
157            Box::pin(async move { list_vehicle_orders_inner(&client, &uuid, next).await })
158        });
159        PageStream::new(self.clone(), fetch)
160    }
161}
162
163async fn list_vehicle_awardees_inner(
164    client: &Client,
165    uuid: &str,
166    opts: ListVehicleAwardeesOptions,
167) -> Result<Page<Record>> {
168    if uuid.is_empty() {
169        return Err(Error::Validation {
170            message: "list_vehicle_awardees: uuid is required".into(),
171            response: None,
172        });
173    }
174    let q = opts.to_query();
175    let path = format!("/api/vehicles/{}/awardees/", urlencoding(uuid));
176    client.get_page(&path, &q).await
177}
178
179async fn list_vehicle_orders_inner(
180    client: &Client,
181    uuid: &str,
182    opts: ListVehicleOrdersOptions,
183) -> Result<Page<Record>> {
184    if uuid.is_empty() {
185        return Err(Error::Validation {
186            message: "list_vehicle_orders: uuid is required".into(),
187            response: None,
188        });
189    }
190    let q = opts.to_query();
191    let path = format!("/api/vehicles/{}/orders/", urlencoding(uuid));
192    client.get_page(&path, &q).await
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn awardees_opts_emit_pagination() {
201        let opts = ListVehicleAwardeesOptions::builder()
202            .page(2u32)
203            .limit(25u32)
204            .shape("vehicle_awardees(minimal)")
205            .build();
206        let q = opts.to_query();
207        assert!(q.contains(&("page".into(), "2".into())));
208        assert!(q.contains(&("limit".into(), "25".into())));
209        assert!(q.contains(&("shape".into(), "vehicle_awardees(minimal)".into())));
210    }
211
212    #[test]
213    fn awardees_search_and_orders_ordering_emit() {
214        let q = ListVehicleAwardeesOptions::builder()
215            .search("cyber")
216            .build()
217            .to_query();
218        assert!(q.contains(&("search".into(), "cyber".into())));
219        let q = ListVehicleOrdersOptions::builder()
220            .ordering("-obligated")
221            .build()
222            .to_query();
223        assert!(q.contains(&("ordering".into(), "-obligated".into())));
224    }
225
226    #[test]
227    fn orders_opts_emit_flat() {
228        let opts = ListVehicleOrdersOptions::builder()
229            .flat(true)
230            .flat_lists(true)
231            .build();
232        let q = opts.to_query();
233        assert!(q.contains(&("flat".into(), "true".into())));
234        assert!(q.contains(&("flat_lists".into(), "true".into())));
235    }
236
237    #[tokio::test]
238    async fn list_vehicle_awardees_empty_uuid_returns_validation() {
239        let client = Client::builder().api_key("x").build().expect("build");
240        let err = client
241            .list_vehicle_awardees("", ListVehicleAwardeesOptions::default())
242            .await
243            .expect_err("must error");
244        match err {
245            Error::Validation { message, .. } => {
246                assert!(message.contains("uuid"));
247            }
248            other => panic!("expected Validation, got {other:?}"),
249        }
250    }
251
252    #[tokio::test]
253    async fn list_vehicle_orders_empty_uuid_returns_validation() {
254        let client = Client::builder().api_key("x").build().expect("build");
255        let err = client
256            .list_vehicle_orders("", ListVehicleOrdersOptions::default())
257            .await
258            .expect_err("must error");
259        match err {
260            Error::Validation { message, .. } => {
261                assert!(message.contains("uuid"));
262            }
263            other => panic!("expected Validation, got {other:?}"),
264        }
265    }
266}