1use crate::client::Client;
7use crate::error::{Error, Result};
8use crate::internal::{apply_pagination, push_opt, push_opt_bool};
9use crate::pagination::{FetchFn, Page, PageStream};
10use crate::resources::agencies::urlencoding;
11use crate::Record;
12use bon::Builder;
13use std::collections::BTreeMap;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct ListExclusionsOptions {
20 #[builder(into)]
22 pub page: Option<u32>,
23 #[builder(into)]
25 pub limit: Option<u32>,
26 #[builder(into)]
28 pub cursor: Option<String>,
29 #[builder(into)]
31 pub shape: Option<String>,
32 #[builder(default)]
34 pub flat: bool,
35 #[builder(default)]
37 pub flat_lists: bool,
38
39 pub active: Option<bool>,
41 pub delisted: Option<bool>,
43 #[builder(into)]
45 pub classification_type: Option<String>,
46 #[builder(into)]
48 pub exclusion_type: Option<String>,
49 #[builder(into)]
51 pub exclusion_program: Option<String>,
52 #[builder(into)]
54 pub agency: Option<String>,
55 #[builder(into)]
57 pub excluding_agency_code: Option<String>,
58 #[builder(into)]
60 pub excluding_agency_name: Option<String>,
61 #[builder(into)]
63 pub uei: Option<String>,
64 #[builder(into)]
66 pub cage_code: Option<String>,
67 #[builder(into)]
69 pub npi: Option<String>,
70 #[builder(into)]
72 pub entity_uei: Option<String>,
73 #[builder(into)]
75 pub activate_date_after: Option<String>,
76 #[builder(into)]
78 pub activate_date_before: Option<String>,
79 #[builder(into)]
81 pub termination_date_after: Option<String>,
82 #[builder(into)]
84 pub termination_date_before: Option<String>,
85 #[builder(into)]
87 pub update_date_after: Option<String>,
88 #[builder(into)]
90 pub update_date_before: Option<String>,
91 #[builder(into)]
93 pub search: Option<String>,
94 #[builder(into)]
96 pub ordering: Option<String>,
97
98 #[builder(default)]
100 pub extra: BTreeMap<String, String>,
101}
102
103impl ListExclusionsOptions {
104 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
105 let mut q = Vec::new();
106 apply_pagination(
107 &mut q,
108 self.page,
109 self.limit,
110 self.cursor.as_deref(),
111 self.shape.as_deref(),
112 self.flat,
113 self.flat_lists,
114 );
115 push_opt_bool(&mut q, "active", self.active);
116 push_opt_bool(&mut q, "delisted", self.delisted);
117 push_opt(
118 &mut q,
119 "classification_type",
120 self.classification_type.as_deref(),
121 );
122 push_opt(&mut q, "exclusion_type", self.exclusion_type.as_deref());
123 push_opt(
124 &mut q,
125 "exclusion_program",
126 self.exclusion_program.as_deref(),
127 );
128 push_opt(&mut q, "agency", self.agency.as_deref());
129 push_opt(
130 &mut q,
131 "excluding_agency_code",
132 self.excluding_agency_code.as_deref(),
133 );
134 push_opt(
135 &mut q,
136 "excluding_agency_name",
137 self.excluding_agency_name.as_deref(),
138 );
139 push_opt(&mut q, "uei", self.uei.as_deref());
140 push_opt(&mut q, "cage_code", self.cage_code.as_deref());
141 push_opt(&mut q, "npi", self.npi.as_deref());
142 push_opt(&mut q, "entity_uei", self.entity_uei.as_deref());
143 push_opt(
144 &mut q,
145 "activate_date_after",
146 self.activate_date_after.as_deref(),
147 );
148 push_opt(
149 &mut q,
150 "activate_date_before",
151 self.activate_date_before.as_deref(),
152 );
153 push_opt(
154 &mut q,
155 "termination_date_after",
156 self.termination_date_after.as_deref(),
157 );
158 push_opt(
159 &mut q,
160 "termination_date_before",
161 self.termination_date_before.as_deref(),
162 );
163 push_opt(
164 &mut q,
165 "update_date_after",
166 self.update_date_after.as_deref(),
167 );
168 push_opt(
169 &mut q,
170 "update_date_before",
171 self.update_date_before.as_deref(),
172 );
173 push_opt(&mut q, "search", self.search.as_deref());
174 push_opt(&mut q, "ordering", self.ordering.as_deref());
175 for (k, v) in &self.extra {
176 if !v.is_empty() {
177 q.push((k.clone(), v.clone()));
178 }
179 }
180 q
181 }
182}
183
184#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
186#[non_exhaustive]
187pub struct GetExclusionOptions {
188 #[builder(into)]
190 pub shape: Option<String>,
191 #[builder(default)]
193 pub flat: bool,
194 #[builder(default)]
196 pub flat_lists: bool,
197}
198
199impl GetExclusionOptions {
200 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
201 let mut q = Vec::new();
202 push_opt(&mut q, "shape", self.shape.as_deref());
203 if self.flat {
204 q.push(("flat".into(), "true".into()));
205 }
206 if self.flat_lists {
207 q.push(("flat_lists".into(), "true".into()));
208 }
209 q
210 }
211}
212
213impl Client {
214 pub async fn list_exclusions(&self, opts: ListExclusionsOptions) -> Result<Page<Record>> {
216 let q = opts.to_query();
217 self.get_page("/api/exclusions/", &q).await
218 }
219
220 pub async fn get_exclusion(
222 &self,
223 exclusion_key: &str,
224 opts: Option<GetExclusionOptions>,
225 ) -> Result<Record> {
226 if exclusion_key.is_empty() {
227 return Err(Error::Validation {
228 message: "get_exclusion: exclusion_key is required".into(),
229 response: None,
230 });
231 }
232 let q = opts.unwrap_or_default().to_query();
233 let path = format!("/api/exclusions/{}/", urlencoding(exclusion_key));
234 self.get_json::<Record>(&path, &q).await
235 }
236
237 pub fn iterate_exclusions(&self, opts: ListExclusionsOptions) -> PageStream<Record> {
239 let opts = Arc::new(opts);
240 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
241 let mut next = (*opts).clone();
242 next.page = page;
243 next.cursor = cursor;
244 Box::pin(async move { client.list_exclusions(next).await })
245 });
246 PageStream::new(self.clone(), fetch)
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
255 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
256 }
257
258 #[test]
259 fn list_exclusions_all_filters_emit() {
260 let opts = ListExclusionsOptions::builder()
261 .active(true)
262 .delisted(true)
263 .classification_type("v-classification_type")
264 .exclusion_type("v-exclusion_type")
265 .exclusion_program("v-exclusion_program")
266 .excluding_agency_code("v-excluding_agency_code")
267 .excluding_agency_name("v-excluding_agency_name")
268 .uei("v-uei")
269 .cage_code("v-cage_code")
270 .npi("v-npi")
271 .entity_uei("v-entity_uei")
272 .activate_date_after("v-activate_date_after")
273 .activate_date_before("v-activate_date_before")
274 .termination_date_after("v-termination_date_after")
275 .termination_date_before("v-termination_date_before")
276 .update_date_after("v-update_date_after")
277 .update_date_before("v-update_date_before")
278 .search("v-search")
279 .ordering("v-ordering")
280 .build();
281 let q = opts.to_query();
282 assert_eq!(get_q(&q, "active").as_deref(), Some("true"));
283 assert_eq!(get_q(&q, "delisted").as_deref(), Some("true"));
284 assert_eq!(
285 get_q(&q, "classification_type").as_deref(),
286 Some("v-classification_type")
287 );
288 assert_eq!(
289 get_q(&q, "exclusion_type").as_deref(),
290 Some("v-exclusion_type")
291 );
292 assert_eq!(
293 get_q(&q, "exclusion_program").as_deref(),
294 Some("v-exclusion_program")
295 );
296 assert_eq!(
297 get_q(&q, "excluding_agency_code").as_deref(),
298 Some("v-excluding_agency_code")
299 );
300 assert_eq!(
301 get_q(&q, "excluding_agency_name").as_deref(),
302 Some("v-excluding_agency_name")
303 );
304 assert_eq!(get_q(&q, "uei").as_deref(), Some("v-uei"));
305 assert_eq!(get_q(&q, "cage_code").as_deref(), Some("v-cage_code"));
306 assert_eq!(get_q(&q, "npi").as_deref(), Some("v-npi"));
307 assert_eq!(get_q(&q, "entity_uei").as_deref(), Some("v-entity_uei"));
308 assert_eq!(
309 get_q(&q, "activate_date_after").as_deref(),
310 Some("v-activate_date_after")
311 );
312 assert_eq!(
313 get_q(&q, "activate_date_before").as_deref(),
314 Some("v-activate_date_before")
315 );
316 assert_eq!(
317 get_q(&q, "termination_date_after").as_deref(),
318 Some("v-termination_date_after")
319 );
320 assert_eq!(
321 get_q(&q, "termination_date_before").as_deref(),
322 Some("v-termination_date_before")
323 );
324 assert_eq!(
325 get_q(&q, "update_date_after").as_deref(),
326 Some("v-update_date_after")
327 );
328 assert_eq!(
329 get_q(&q, "update_date_before").as_deref(),
330 Some("v-update_date_before")
331 );
332 assert_eq!(get_q(&q, "search").as_deref(), Some("v-search"));
333 assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering"));
334 assert_eq!(q.len(), 19);
335 }
336
337 #[test]
338 fn list_exclusions_active_false_is_a_filter_not_an_absence() {
339 let opts = ListExclusionsOptions::builder().active(false).build();
340 assert_eq!(get_q(&opts.to_query(), "active").as_deref(), Some("false"));
341 }
342
343 #[test]
344 fn list_exclusions_zero_value_omitted() {
345 assert!(ListExclusionsOptions::builder()
346 .build()
347 .to_query()
348 .is_empty());
349 }
350
351 #[test]
352 fn list_exclusions_cursor_wins_over_page_and_shape_emits() {
353 let opts = ListExclusionsOptions::builder()
354 .page(3u32)
355 .cursor("c0".to_string())
356 .shape(crate::SHAPE_EXCLUSIONS_MINIMAL)
357 .build();
358 let q = opts.to_query();
359 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
360 assert_eq!(get_q(&q, "page"), None);
361 assert_eq!(
362 get_q(&q, "shape").as_deref(),
363 Some(crate::SHAPE_EXCLUSIONS_MINIMAL)
364 );
365 }
366
367 #[test]
368 fn list_exclusions_extra_emits() {
369 let mut extra = BTreeMap::new();
370 extra.insert("custom_x".to_string(), "xv".to_string());
371 let q = ListExclusionsOptions::builder()
372 .extra(extra)
373 .build()
374 .to_query();
375 assert!(q.contains(&("custom_x".into(), "xv".into())));
376 }
377
378 #[tokio::test]
379 async fn get_exclusion_validates_empty_exclusion_key() {
380 let client = Client::builder().api_key("x").build().expect("client");
381 let err = client.get_exclusion("", None).await.unwrap_err();
382 match err {
383 Error::Validation { message, .. } => {
384 assert!(message.contains("exclusion_key is required"));
385 }
386 other => panic!("expected Validation, got {other:?}"),
387 }
388 }
389
390 #[test]
391 fn get_options_emits() {
392 let q = GetExclusionOptions::builder()
393 .shape("a,b")
394 .flat(true)
395 .flat_lists(true)
396 .build()
397 .to_query();
398 assert_eq!(get_q(&q, "shape").as_deref(), Some("a,b"));
399 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
400 assert_eq!(get_q(&q, "flat_lists").as_deref(), Some("true"));
401 }
402
403 #[test]
404 fn list_exclusions_agency_filter_emits() {
405 let q = ListExclusionsOptions::builder()
406 .agency("DOD|HHS")
407 .build()
408 .to_query();
409 assert_eq!(get_q(&q, "agency").as_deref(), Some("DOD|HHS"));
410 }
411}