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