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