1use crate::client::Client;
5use crate::error::{Error, Result};
6use crate::internal::{apply_pagination, push_opt};
7use crate::pagination::{FetchFn, Page, PageStream};
8use crate::resources::agencies::urlencoding;
9use crate::Record;
10use bon::Builder;
11use std::collections::BTreeMap;
12use std::sync::Arc;
13
14#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct ListItdashboardOptions {
26 #[builder(into)]
28 pub page: Option<u32>,
29 #[builder(into)]
31 pub limit: Option<u32>,
32 #[builder(into)]
34 pub cursor: Option<String>,
35 #[builder(into)]
39 pub shape: Option<String>,
40 #[builder(default)]
42 pub flat: bool,
43 #[builder(default)]
45 pub flat_lists: bool,
46
47 #[builder(into)]
49 pub search: Option<String>,
50 #[builder(into)]
52 pub agency_code: Option<String>,
53 #[builder(into)]
55 pub agency_name: Option<String>,
56 #[builder(into)]
58 pub agency: Option<String>,
59 #[builder(into)]
61 pub type_of_investment: Option<String>,
62
63 #[builder(into)]
65 pub updated_time_after: Option<String>,
66 #[builder(into)]
68 pub updated_time_before: Option<String>,
69
70 #[builder(into)]
74 pub cio_rating: Option<String>,
75 #[builder(into)]
77 pub cio_rating_max: Option<String>,
78 #[builder(into)]
80 pub performance_risk: Option<String>,
81
82 #[builder(into)]
84 pub previous_uii: Option<String>,
85
86 #[builder(default)]
88 pub extra: BTreeMap<String, String>,
89}
90
91impl ListItdashboardOptions {
92 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
93 let mut q = Vec::new();
94 apply_pagination(
95 &mut q,
96 self.page,
97 self.limit,
98 self.cursor.as_deref(),
99 self.shape.as_deref(),
100 self.flat,
101 self.flat_lists,
102 );
103 push_opt(&mut q, "search", self.search.as_deref());
104 push_opt(&mut q, "previous_uii", self.previous_uii.as_deref());
105 push_opt(&mut q, "agency_code", self.agency_code.as_deref());
106 push_opt(&mut q, "agency_name", self.agency_name.as_deref());
107 push_opt(&mut q, "agency", self.agency.as_deref());
108 push_opt(
109 &mut q,
110 "type_of_investment",
111 self.type_of_investment.as_deref(),
112 );
113 push_opt(
114 &mut q,
115 "updated_time_after",
116 self.updated_time_after.as_deref(),
117 );
118 push_opt(
119 &mut q,
120 "updated_time_before",
121 self.updated_time_before.as_deref(),
122 );
123 push_opt(&mut q, "cio_rating", self.cio_rating.as_deref());
124 push_opt(&mut q, "cio_rating_max", self.cio_rating_max.as_deref());
125 push_opt(&mut q, "performance_risk", self.performance_risk.as_deref());
126 for (k, v) in &self.extra {
127 if !v.is_empty() {
128 q.push((k.clone(), v.clone()));
129 }
130 }
131 q
132 }
133}
134
135#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
137#[non_exhaustive]
138pub struct GetItdashboardOptions {
139 #[builder(into)]
141 pub shape: Option<String>,
142 #[builder(default)]
144 pub flat: bool,
145 #[builder(default)]
147 pub flat_lists: bool,
148}
149
150impl GetItdashboardOptions {
151 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
152 let mut q = Vec::new();
153 push_opt(&mut q, "shape", self.shape.as_deref());
154 if self.flat {
155 q.push(("flat".into(), "true".into()));
156 }
157 if self.flat_lists {
158 q.push(("flat_lists".into(), "true".into()));
159 }
160 q
161 }
162}
163
164impl Client {
165 pub async fn list_itdashboard(&self, opts: ListItdashboardOptions) -> Result<Page<Record>> {
167 let q = opts.to_query();
168 self.get_page("/api/itdashboard/", &q).await
169 }
170
171 pub async fn get_itdashboard(
174 &self,
175 uii: &str,
176 opts: Option<GetItdashboardOptions>,
177 ) -> Result<Record> {
178 if uii.is_empty() {
179 return Err(Error::Validation {
180 message: "get_itdashboard: uii is required".into(),
181 response: None,
182 });
183 }
184 let q = opts.unwrap_or_default().to_query();
185 let path = format!("/api/itdashboard/{}/", urlencoding(uii));
186 self.get_json::<Record>(&path, &q).await
187 }
188
189 pub fn iterate_itdashboard(&self, opts: ListItdashboardOptions) -> PageStream<Record> {
191 let opts = Arc::new(opts);
192 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
193 let mut next = (*opts).clone();
194 next.page = page;
195 next.cursor = cursor;
196 Box::pin(async move { client.list_itdashboard(next).await })
197 });
198 PageStream::new(self.clone(), fetch)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
207 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
208 }
209
210 #[test]
211 fn list_itdashboard_all_filters_emit() {
212 let opts = ListItdashboardOptions::builder()
213 .search("cloud")
214 .agency_code("009")
215 .agency_name("Department of State")
216 .type_of_investment("Major")
217 .updated_time_after("2024-01-01T00:00:00Z")
218 .updated_time_before("2024-12-31T23:59:59Z")
219 .cio_rating("3")
220 .cio_rating_max("5")
221 .performance_risk("2")
222 .build();
223 let q = opts.to_query();
224 assert_eq!(get_q(&q, "search").as_deref(), Some("cloud"));
225 assert_eq!(get_q(&q, "agency_code").as_deref(), Some("009"));
226 assert_eq!(
227 get_q(&q, "agency_name").as_deref(),
228 Some("Department of State")
229 );
230 assert_eq!(get_q(&q, "type_of_investment").as_deref(), Some("Major"));
231 assert_eq!(
232 get_q(&q, "updated_time_after").as_deref(),
233 Some("2024-01-01T00:00:00Z")
234 );
235 assert_eq!(
236 get_q(&q, "updated_time_before").as_deref(),
237 Some("2024-12-31T23:59:59Z")
238 );
239 assert_eq!(get_q(&q, "cio_rating").as_deref(), Some("3"));
240 assert_eq!(get_q(&q, "cio_rating_max").as_deref(), Some("5"));
241 assert_eq!(get_q(&q, "performance_risk").as_deref(), Some("2"));
242 }
243
244 #[test]
245 fn list_itdashboard_zero_value_omitted() {
246 let opts = ListItdashboardOptions::builder().build();
247 let q = opts.to_query();
248 assert!(q.is_empty(), "expected empty query, got {q:?}");
249 }
250
251 #[test]
252 fn list_itdashboard_shape_emits() {
253 let opts = ListItdashboardOptions::builder()
254 .shape(crate::SHAPE_ITDASHBOARD_INVESTMENTS_MINIMAL)
255 .build();
256 let q = opts.to_query();
257 assert_eq!(
258 get_q(&q, "shape").as_deref(),
259 Some(crate::SHAPE_ITDASHBOARD_INVESTMENTS_MINIMAL)
260 );
261 }
262
263 #[test]
264 fn list_itdashboard_cursor_wins_over_page() {
265 let opts = ListItdashboardOptions::builder()
266 .page(3u32)
267 .cursor("c0".to_string())
268 .build();
269 let q = opts.to_query();
270 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
271 assert_eq!(get_q(&q, "page"), None);
272 }
273
274 #[test]
275 fn list_itdashboard_extra_emits() {
276 let mut extra = BTreeMap::new();
277 extra.insert("custom_x".to_string(), "xv".to_string());
278 let opts = ListItdashboardOptions::builder().extra(extra).build();
279 let q = opts.to_query();
280 assert!(q.contains(&("custom_x".into(), "xv".into())));
281 }
282
283 #[tokio::test]
284 async fn get_itdashboard_validates_empty_uii() {
285 let client = Client::builder().api_key("x").build().expect("client");
286 let err = client.get_itdashboard("", None).await.unwrap_err();
287 match err {
288 Error::Validation { message, .. } => {
289 assert!(message.contains("uii is required"));
290 }
291 other => panic!("expected Validation, got {other:?}"),
292 }
293 }
294
295 #[test]
296 fn list_itdashboard_previous_uii_emits() {
297 let q = ListItdashboardOptions::builder()
298 .previous_uii("015-000000100")
299 .build()
300 .to_query();
301 assert_eq!(get_q(&q, "previous_uii").as_deref(), Some("015-000000100"));
302 }
303
304 #[test]
305 fn list_itdashboard_agency_filter_emits() {
306 let q = ListItdashboardOptions::builder()
307 .agency("DOT")
308 .build()
309 .to_query();
310 assert_eq!(get_q(&q, "agency").as_deref(), Some("DOT"));
311 }
312}