1use crate::client::Client;
12use crate::error::{Error, Result};
13use crate::internal::{apply_pagination, push_opt};
14use crate::pagination::{FetchFn, Page, PageStream};
15use crate::resources::agencies::urlencoding;
16use crate::Record;
17use bon::Builder;
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct IdvSubresourceOptions {
30 #[builder(into)]
32 pub page: Option<u32>,
33 #[builder(into)]
35 pub limit: Option<u32>,
36 #[builder(into)]
38 pub cursor: Option<String>,
39 #[builder(into)]
41 pub shape: Option<String>,
42 #[builder(default)]
44 pub flat: bool,
45 #[builder(default)]
47 pub flat_lists: bool,
48 #[builder(into)]
50 pub joiner: Option<String>,
51 #[builder(into)]
53 pub ordering: Option<String>,
54 #[builder(into)]
56 pub search: Option<String>,
57 #[builder(default)]
59 pub extra: BTreeMap<String, String>,
60}
61
62impl IdvSubresourceOptions {
63 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
64 let mut q = Vec::new();
65 apply_pagination(
66 &mut q,
67 self.page,
68 self.limit,
69 self.cursor.as_deref(),
70 self.shape.as_deref(),
71 self.flat,
72 self.flat_lists,
73 );
74 if self.flat {
75 if let Some(j) = self.joiner.as_deref().filter(|s| !s.is_empty()) {
76 q.push(("joiner".into(), j.into()));
77 }
78 }
79 push_opt(&mut q, "ordering", self.ordering.as_deref());
80 push_opt(&mut q, "search", self.search.as_deref());
81 for (k, v) in &self.extra {
82 if !v.is_empty() {
83 q.push((k.clone(), v.clone()));
84 }
85 }
86 q
87 }
88}
89
90impl Client {
91 pub async fn list_idv_awards(
93 &self,
94 key: &str,
95 opts: IdvSubresourceOptions,
96 ) -> Result<Page<Record>> {
97 list_idv_subresource(self, key, "awards", opts).await
98 }
99
100 pub fn iterate_idv_awards(&self, key: &str, opts: IdvSubresourceOptions) -> PageStream<Record> {
102 iterate_idv_subresource(self, key.to_string(), "awards", opts)
103 }
104
105 pub async fn list_idv_child_idvs(
107 &self,
108 key: &str,
109 opts: IdvSubresourceOptions,
110 ) -> Result<Page<Record>> {
111 list_idv_subresource(self, key, "idvs", opts).await
112 }
113
114 pub fn iterate_idv_child_idvs(
116 &self,
117 key: &str,
118 opts: IdvSubresourceOptions,
119 ) -> PageStream<Record> {
120 iterate_idv_subresource(self, key.to_string(), "idvs", opts)
121 }
122
123 pub async fn list_idv_transactions(
126 &self,
127 key: &str,
128 opts: IdvSubresourceOptions,
129 ) -> Result<Page<Record>> {
130 list_idv_subresource(self, key, "transactions", opts).await
131 }
132
133 pub fn iterate_idv_transactions(
135 &self,
136 key: &str,
137 opts: IdvSubresourceOptions,
138 ) -> PageStream<Record> {
139 iterate_idv_subresource(self, key.to_string(), "transactions", opts)
140 }
141
142 #[deprecated(note = "Deprecated upstream; use get_idv with the comprehensive shape")]
148 pub async fn get_idv_summary(&self, key: &str) -> Result<Record> {
149 if key.is_empty() {
150 return Err(Error::Validation {
151 message: "get_idv_summary: key is required".into(),
152 response: None,
153 });
154 }
155 let path = format!("/api/idvs/{}/summary/", urlencoding(key));
156 self.get_json::<Record>(&path, &[]).await
157 }
158
159 #[deprecated(note = "Deprecated upstream; use list_idv_awards")]
165 pub async fn list_idv_summary_awards(
166 &self,
167 key: &str,
168 opts: IdvSubresourceOptions,
169 ) -> Result<Page<Record>> {
170 list_idv_subresource(self, key, "summary/awards", opts).await
171 }
172
173 pub async fn list_idv_lcats(
175 &self,
176 key: &str,
177 opts: IdvSubresourceOptions,
178 ) -> Result<Page<Record>> {
179 list_idv_subresource(self, key, "lcats", opts).await
180 }
181
182 pub fn iterate_idv_lcats(&self, key: &str, opts: IdvSubresourceOptions) -> PageStream<Record> {
184 iterate_idv_subresource(self, key.to_string(), "lcats", opts)
185 }
186}
187
188async fn list_idv_subresource(
189 client: &Client,
190 key: &str,
191 segment: &str,
192 opts: IdvSubresourceOptions,
193) -> Result<Page<Record>> {
194 if key.is_empty() {
195 return Err(Error::Validation {
196 message: "IDV sub-resource: key is required".into(),
197 response: None,
198 });
199 }
200 let q = opts.to_query();
201 let path = format!("/api/idvs/{}/{segment}/", urlencoding(key));
202 let bytes = client.get_bytes(&path, &q).await?;
203 Page::decode(&bytes)
204}
205
206fn iterate_idv_subresource(
207 client: &Client,
208 key: String,
209 segment: &'static str,
210 opts: IdvSubresourceOptions,
211) -> PageStream<Record> {
212 let opts = Arc::new(opts);
213 let key = Arc::new(key);
214 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
215 let mut next = (*opts).clone();
216 next.page = page;
217 next.cursor = cursor;
218 let key = key.clone();
219 Box::pin(async move { list_idv_subresource(&client, &key, segment, next).await })
220 });
221 PageStream::new(client.clone(), fetch)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
229 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
230 }
231
232 #[test]
233 fn options_emit_pagination_and_search() {
234 let opts = IdvSubresourceOptions::builder()
235 .limit(10u32)
236 .ordering("-award_date")
237 .search("software")
238 .build();
239 let q = opts.to_query();
240 assert_eq!(get_q(&q, "limit").as_deref(), Some("10"));
241 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
242 assert_eq!(get_q(&q, "search").as_deref(), Some("software"));
243 }
244
245 #[test]
246 fn joiner_only_when_flat() {
247 let opts = IdvSubresourceOptions::builder()
248 .joiner("__".to_string())
249 .build();
250 let q = opts.to_query();
251 assert!(!q.iter().any(|(k, _)| k == "joiner"));
252
253 let opts = IdvSubresourceOptions::builder()
254 .flat(true)
255 .joiner("__".to_string())
256 .build();
257 let q = opts.to_query();
258 assert!(q.contains(&("joiner".into(), "__".into())));
259 }
260
261 #[test]
262 fn extra_forwards_arbitrary_params() {
263 let mut extra = BTreeMap::new();
264 extra.insert("region".to_string(), "west".to_string());
265 let opts = IdvSubresourceOptions::builder().extra(extra).build();
266 let q = opts.to_query();
267 assert!(q.contains(&("region".into(), "west".into())));
268 }
269
270 #[tokio::test]
271 async fn list_idv_awards_empty_key_returns_validation() {
272 let client = Client::builder().api_key("x").build().expect("build");
273 let err = client
274 .list_idv_awards("", IdvSubresourceOptions::default())
275 .await
276 .expect_err("must error");
277 match err {
278 Error::Validation { message, .. } => assert!(message.contains("key")),
279 other => panic!("expected Validation, got {other:?}"),
280 }
281 }
282
283 #[tokio::test]
284 async fn list_idv_lcats_empty_key_returns_validation() {
285 let client = Client::builder().api_key("x").build().expect("build");
286 let err = client
287 .list_idv_lcats("", IdvSubresourceOptions::default())
288 .await
289 .expect_err("must error");
290 match err {
291 Error::Validation { message, .. } => assert!(message.contains("key")),
292 other => panic!("expected Validation, got {other:?}"),
293 }
294 }
295
296 #[tokio::test]
297 #[allow(deprecated)]
298 async fn get_idv_summary_empty_key_returns_validation() {
299 let client = Client::builder().api_key("x").build().expect("build");
300 let err = client.get_idv_summary("").await.expect_err("must error");
301 match err {
302 Error::Validation { message, .. } => assert!(message.contains("key")),
303 other => panic!("expected Validation, got {other:?}"),
304 }
305 }
306}