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