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 ListDibbsRfqsOptions {
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 open: Option<bool>,
41 #[builder(into)]
43 pub nsn: Option<String>,
44 #[builder(into)]
46 pub part_number: Option<String>,
47 #[builder(into)]
49 pub solicitation: Option<String>,
50 #[builder(into)]
52 pub purchase_request: Option<String>,
53 #[builder(into)]
55 pub set_aside: Option<String>,
56 #[builder(into)]
58 pub status_code: Option<String>,
59 #[builder(into)]
61 pub organization: Option<String>,
62 #[builder(into)]
64 pub quantity_min: Option<String>,
65 #[builder(into)]
67 pub quantity_max: Option<String>,
68 #[builder(into)]
70 pub return_by_date_after: Option<String>,
71 #[builder(into)]
73 pub return_by_date_before: Option<String>,
74 #[builder(into)]
76 pub issue_date_after: Option<String>,
77 #[builder(into)]
79 pub issue_date_before: Option<String>,
80 #[builder(into)]
82 pub search: Option<String>,
83 #[builder(into)]
85 pub ordering: Option<String>,
86
87 #[builder(default)]
89 pub extra: BTreeMap<String, String>,
90}
91
92impl ListDibbsRfqsOptions {
93 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
94 let mut q = Vec::new();
95 apply_pagination(
96 &mut q,
97 self.page,
98 self.limit,
99 self.cursor.as_deref(),
100 self.shape.as_deref(),
101 self.flat,
102 self.flat_lists,
103 );
104 push_opt_bool(&mut q, "open", self.open);
105 push_opt(&mut q, "nsn", self.nsn.as_deref());
106 push_opt(&mut q, "part_number", self.part_number.as_deref());
107 push_opt(&mut q, "solicitation", self.solicitation.as_deref());
108 push_opt(&mut q, "purchase_request", self.purchase_request.as_deref());
109 push_opt(&mut q, "set_aside", self.set_aside.as_deref());
110 push_opt(&mut q, "status_code", self.status_code.as_deref());
111 push_opt(&mut q, "organization", self.organization.as_deref());
112 push_opt(&mut q, "quantity_min", self.quantity_min.as_deref());
113 push_opt(&mut q, "quantity_max", self.quantity_max.as_deref());
114 push_opt(
115 &mut q,
116 "return_by_date_after",
117 self.return_by_date_after.as_deref(),
118 );
119 push_opt(
120 &mut q,
121 "return_by_date_before",
122 self.return_by_date_before.as_deref(),
123 );
124 push_opt(&mut q, "issue_date_after", self.issue_date_after.as_deref());
125 push_opt(
126 &mut q,
127 "issue_date_before",
128 self.issue_date_before.as_deref(),
129 );
130 push_opt(&mut q, "search", self.search.as_deref());
131 push_opt(&mut q, "ordering", self.ordering.as_deref());
132 for (k, v) in &self.extra {
133 if !v.is_empty() {
134 q.push((k.clone(), v.clone()));
135 }
136 }
137 q
138 }
139}
140
141#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct ListDibbsRfpsOptions {
145 #[builder(into)]
147 pub page: Option<u32>,
148 #[builder(into)]
150 pub limit: Option<u32>,
151 #[builder(into)]
153 pub cursor: Option<String>,
154 #[builder(into)]
156 pub shape: Option<String>,
157 #[builder(default)]
159 pub flat: bool,
160 #[builder(default)]
162 pub flat_lists: bool,
163
164 pub open: Option<bool>,
166 #[builder(into)]
168 pub nsn: Option<String>,
169 #[builder(into)]
171 pub part_number: Option<String>,
172 #[builder(into)]
174 pub solicitation: Option<String>,
175 #[builder(into)]
177 pub buyer_code: Option<String>,
178 #[builder(into)]
180 pub organization: Option<String>,
181 #[builder(into)]
183 pub issued_date_after: Option<String>,
184 #[builder(into)]
186 pub issued_date_before: Option<String>,
187 #[builder(into)]
189 pub closes_date_after: Option<String>,
190 #[builder(into)]
192 pub closes_date_before: Option<String>,
193 #[builder(into)]
195 pub search: Option<String>,
196 #[builder(into)]
198 pub ordering: Option<String>,
199
200 #[builder(default)]
202 pub extra: BTreeMap<String, String>,
203}
204
205impl ListDibbsRfpsOptions {
206 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
207 let mut q = Vec::new();
208 apply_pagination(
209 &mut q,
210 self.page,
211 self.limit,
212 self.cursor.as_deref(),
213 self.shape.as_deref(),
214 self.flat,
215 self.flat_lists,
216 );
217 push_opt_bool(&mut q, "open", self.open);
218 push_opt(&mut q, "nsn", self.nsn.as_deref());
219 push_opt(&mut q, "part_number", self.part_number.as_deref());
220 push_opt(&mut q, "solicitation", self.solicitation.as_deref());
221 push_opt(&mut q, "buyer_code", self.buyer_code.as_deref());
222 push_opt(&mut q, "organization", self.organization.as_deref());
223 push_opt(
224 &mut q,
225 "issued_date_after",
226 self.issued_date_after.as_deref(),
227 );
228 push_opt(
229 &mut q,
230 "issued_date_before",
231 self.issued_date_before.as_deref(),
232 );
233 push_opt(
234 &mut q,
235 "closes_date_after",
236 self.closes_date_after.as_deref(),
237 );
238 push_opt(
239 &mut q,
240 "closes_date_before",
241 self.closes_date_before.as_deref(),
242 );
243 push_opt(&mut q, "search", self.search.as_deref());
244 push_opt(&mut q, "ordering", self.ordering.as_deref());
245 for (k, v) in &self.extra {
246 if !v.is_empty() {
247 q.push((k.clone(), v.clone()));
248 }
249 }
250 q
251 }
252}
253
254#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
258#[non_exhaustive]
259pub struct ListDibbsAwardsOptions {
260 #[builder(into)]
262 pub page: Option<u32>,
263 #[builder(into)]
265 pub limit: Option<u32>,
266 #[builder(into)]
268 pub cursor: Option<String>,
269 #[builder(into)]
271 pub shape: Option<String>,
272 #[builder(default)]
274 pub flat: bool,
275 #[builder(default)]
277 pub flat_lists: bool,
278
279 #[builder(into)]
281 pub nsn: Option<String>,
282 #[builder(into)]
284 pub part_number: Option<String>,
285 #[builder(into)]
287 pub solicitation: Option<String>,
288 #[builder(into)]
290 pub award_number: Option<String>,
291 #[builder(into)]
293 pub delivery_order_number: Option<String>,
294 #[builder(into)]
296 pub purchase_request: Option<String>,
297 #[builder(into)]
299 pub awardee_cage: Option<String>,
300 #[builder(into)]
302 pub entity: Option<String>,
303 #[builder(into)]
305 pub organization: Option<String>,
306 #[builder(into)]
308 pub award_date_after: Option<String>,
309 #[builder(into)]
311 pub award_date_before: Option<String>,
312 #[builder(into)]
314 pub posted_date_after: Option<String>,
315 #[builder(into)]
317 pub posted_date_before: Option<String>,
318 #[builder(into)]
320 pub total_contract_price_min: Option<String>,
321 #[builder(into)]
323 pub total_contract_price_max: Option<String>,
324 #[builder(into)]
326 pub search: Option<String>,
327 #[builder(into)]
329 pub ordering: Option<String>,
330
331 #[builder(default)]
333 pub extra: BTreeMap<String, String>,
334}
335
336impl ListDibbsAwardsOptions {
337 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
338 let mut q = Vec::new();
339 apply_pagination(
340 &mut q,
341 self.page,
342 self.limit,
343 self.cursor.as_deref(),
344 self.shape.as_deref(),
345 self.flat,
346 self.flat_lists,
347 );
348 push_opt(&mut q, "nsn", self.nsn.as_deref());
349 push_opt(&mut q, "part_number", self.part_number.as_deref());
350 push_opt(&mut q, "solicitation", self.solicitation.as_deref());
351 push_opt(&mut q, "award_number", self.award_number.as_deref());
352 push_opt(
353 &mut q,
354 "delivery_order_number",
355 self.delivery_order_number.as_deref(),
356 );
357 push_opt(&mut q, "purchase_request", self.purchase_request.as_deref());
358 push_opt(&mut q, "awardee_cage", self.awardee_cage.as_deref());
359 push_opt(&mut q, "entity", self.entity.as_deref());
360 push_opt(&mut q, "organization", self.organization.as_deref());
361 push_opt(&mut q, "award_date_after", self.award_date_after.as_deref());
362 push_opt(
363 &mut q,
364 "award_date_before",
365 self.award_date_before.as_deref(),
366 );
367 push_opt(
368 &mut q,
369 "posted_date_after",
370 self.posted_date_after.as_deref(),
371 );
372 push_opt(
373 &mut q,
374 "posted_date_before",
375 self.posted_date_before.as_deref(),
376 );
377 push_opt(
378 &mut q,
379 "total_contract_price_min",
380 self.total_contract_price_min.as_deref(),
381 );
382 push_opt(
383 &mut q,
384 "total_contract_price_max",
385 self.total_contract_price_max.as_deref(),
386 );
387 push_opt(&mut q, "search", self.search.as_deref());
388 push_opt(&mut q, "ordering", self.ordering.as_deref());
389 for (k, v) in &self.extra {
390 if !v.is_empty() {
391 q.push((k.clone(), v.clone()));
392 }
393 }
394 q
395 }
396}
397
398#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
400#[non_exhaustive]
401pub struct GetDibbsOptions {
402 #[builder(into)]
404 pub shape: Option<String>,
405 #[builder(default)]
407 pub flat: bool,
408 #[builder(default)]
410 pub flat_lists: bool,
411}
412
413impl GetDibbsOptions {
414 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
415 let mut q = Vec::new();
416 push_opt(&mut q, "shape", self.shape.as_deref());
417 if self.flat {
418 q.push(("flat".into(), "true".into()));
419 }
420 if self.flat_lists {
421 q.push(("flat_lists".into(), "true".into()));
422 }
423 q
424 }
425}
426
427impl Client {
428 pub async fn list_dibbs_rfqs(&self, opts: ListDibbsRfqsOptions) -> Result<Page<Record>> {
430 let q = opts.to_query();
431 self.get_page("/api/dibbs/rfqs/", &q).await
432 }
433
434 pub async fn get_dibbs_rfq(&self, uuid: &str, opts: Option<GetDibbsOptions>) -> Result<Record> {
436 if uuid.is_empty() {
437 return Err(Error::Validation {
438 message: "get_dibbs_rfq: uuid is required".into(),
439 response: None,
440 });
441 }
442 let q = opts.unwrap_or_default().to_query();
443 let path = format!("/api/dibbs/rfqs/{}/", urlencoding(uuid));
444 self.get_json::<Record>(&path, &q).await
445 }
446
447 pub fn iterate_dibbs_rfqs(&self, opts: ListDibbsRfqsOptions) -> PageStream<Record> {
449 let opts = Arc::new(opts);
450 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
451 let mut next = (*opts).clone();
452 next.page = page;
453 next.cursor = cursor;
454 Box::pin(async move { client.list_dibbs_rfqs(next).await })
455 });
456 PageStream::new(self.clone(), fetch)
457 }
458
459 pub async fn list_dibbs_rfps(&self, opts: ListDibbsRfpsOptions) -> Result<Page<Record>> {
461 let q = opts.to_query();
462 self.get_page("/api/dibbs/rfps/", &q).await
463 }
464
465 pub async fn get_dibbs_rfp(&self, uuid: &str, opts: Option<GetDibbsOptions>) -> Result<Record> {
467 if uuid.is_empty() {
468 return Err(Error::Validation {
469 message: "get_dibbs_rfp: uuid is required".into(),
470 response: None,
471 });
472 }
473 let q = opts.unwrap_or_default().to_query();
474 let path = format!("/api/dibbs/rfps/{}/", urlencoding(uuid));
475 self.get_json::<Record>(&path, &q).await
476 }
477
478 pub fn iterate_dibbs_rfps(&self, opts: ListDibbsRfpsOptions) -> PageStream<Record> {
480 let opts = Arc::new(opts);
481 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
482 let mut next = (*opts).clone();
483 next.page = page;
484 next.cursor = cursor;
485 Box::pin(async move { client.list_dibbs_rfps(next).await })
486 });
487 PageStream::new(self.clone(), fetch)
488 }
489
490 pub async fn list_dibbs_awards(&self, opts: ListDibbsAwardsOptions) -> Result<Page<Record>> {
492 let q = opts.to_query();
493 self.get_page("/api/dibbs/awards/", &q).await
494 }
495
496 pub async fn get_dibbs_award(
498 &self,
499 uuid: &str,
500 opts: Option<GetDibbsOptions>,
501 ) -> Result<Record> {
502 if uuid.is_empty() {
503 return Err(Error::Validation {
504 message: "get_dibbs_award: uuid is required".into(),
505 response: None,
506 });
507 }
508 let q = opts.unwrap_or_default().to_query();
509 let path = format!("/api/dibbs/awards/{}/", urlencoding(uuid));
510 self.get_json::<Record>(&path, &q).await
511 }
512
513 pub fn iterate_dibbs_awards(&self, opts: ListDibbsAwardsOptions) -> PageStream<Record> {
515 let opts = Arc::new(opts);
516 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
517 let mut next = (*opts).clone();
518 next.page = page;
519 next.cursor = cursor;
520 Box::pin(async move { client.list_dibbs_awards(next).await })
521 });
522 PageStream::new(self.clone(), fetch)
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
531 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
532 }
533
534 #[test]
535 fn list_dibbs_rfqs_all_filters_emit() {
536 let opts = ListDibbsRfqsOptions::builder()
537 .open(true)
538 .nsn("v-nsn")
539 .part_number("v-part_number")
540 .solicitation("v-solicitation")
541 .purchase_request("v-purchase_request")
542 .set_aside("v-set_aside")
543 .status_code("v-status_code")
544 .organization("v-organization")
545 .quantity_min("v-quantity_min")
546 .quantity_max("v-quantity_max")
547 .return_by_date_after("v-return_by_date_after")
548 .return_by_date_before("v-return_by_date_before")
549 .issue_date_after("v-issue_date_after")
550 .issue_date_before("v-issue_date_before")
551 .search("v-search")
552 .ordering("v-ordering")
553 .build();
554 let q = opts.to_query();
555 assert_eq!(get_q(&q, "open").as_deref(), Some("true"));
556 assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn"));
557 assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number"));
558 assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation"));
559 assert_eq!(
560 get_q(&q, "purchase_request").as_deref(),
561 Some("v-purchase_request")
562 );
563 assert_eq!(get_q(&q, "set_aside").as_deref(), Some("v-set_aside"));
564 assert_eq!(get_q(&q, "status_code").as_deref(), Some("v-status_code"));
565 assert_eq!(get_q(&q, "organization").as_deref(), Some("v-organization"));
566 assert_eq!(get_q(&q, "quantity_min").as_deref(), Some("v-quantity_min"));
567 assert_eq!(get_q(&q, "quantity_max").as_deref(), Some("v-quantity_max"));
568 assert_eq!(
569 get_q(&q, "return_by_date_after").as_deref(),
570 Some("v-return_by_date_after")
571 );
572 assert_eq!(
573 get_q(&q, "return_by_date_before").as_deref(),
574 Some("v-return_by_date_before")
575 );
576 assert_eq!(
577 get_q(&q, "issue_date_after").as_deref(),
578 Some("v-issue_date_after")
579 );
580 assert_eq!(
581 get_q(&q, "issue_date_before").as_deref(),
582 Some("v-issue_date_before")
583 );
584 assert_eq!(get_q(&q, "search").as_deref(), Some("v-search"));
585 assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering"));
586 assert_eq!(q.len(), 16);
587 }
588
589 #[test]
590 fn list_dibbs_rfqs_open_false_is_a_filter_not_an_absence() {
591 let opts = ListDibbsRfqsOptions::builder().open(false).build();
592 assert_eq!(get_q(&opts.to_query(), "open").as_deref(), Some("false"));
593 }
594
595 #[test]
596 fn list_dibbs_rfqs_zero_value_omitted() {
597 assert!(ListDibbsRfqsOptions::builder()
598 .build()
599 .to_query()
600 .is_empty());
601 }
602
603 #[test]
604 fn list_dibbs_rfqs_cursor_wins_over_page_and_shape_emits() {
605 let opts = ListDibbsRfqsOptions::builder()
606 .page(3u32)
607 .cursor("c0".to_string())
608 .shape(crate::SHAPE_DIBBS_RFQS_MINIMAL)
609 .build();
610 let q = opts.to_query();
611 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
612 assert_eq!(get_q(&q, "page"), None);
613 assert_eq!(
614 get_q(&q, "shape").as_deref(),
615 Some(crate::SHAPE_DIBBS_RFQS_MINIMAL)
616 );
617 }
618
619 #[test]
620 fn list_dibbs_rfqs_extra_emits() {
621 let mut extra = BTreeMap::new();
622 extra.insert("custom_x".to_string(), "xv".to_string());
623 let q = ListDibbsRfqsOptions::builder()
624 .extra(extra)
625 .build()
626 .to_query();
627 assert!(q.contains(&("custom_x".into(), "xv".into())));
628 }
629
630 #[tokio::test]
631 async fn get_dibbs_rfq_validates_empty_uuid() {
632 let client = Client::builder().api_key("x").build().expect("client");
633 let err = client.get_dibbs_rfq("", None).await.unwrap_err();
634 match err {
635 Error::Validation { message, .. } => {
636 assert!(message.contains("uuid is required"));
637 }
638 other => panic!("expected Validation, got {other:?}"),
639 }
640 }
641
642 #[test]
643 fn list_dibbs_rfps_all_filters_emit() {
644 let opts = ListDibbsRfpsOptions::builder()
645 .open(true)
646 .nsn("v-nsn")
647 .part_number("v-part_number")
648 .solicitation("v-solicitation")
649 .buyer_code("v-buyer_code")
650 .organization("v-organization")
651 .issued_date_after("v-issued_date_after")
652 .issued_date_before("v-issued_date_before")
653 .closes_date_after("v-closes_date_after")
654 .closes_date_before("v-closes_date_before")
655 .search("v-search")
656 .ordering("v-ordering")
657 .build();
658 let q = opts.to_query();
659 assert_eq!(get_q(&q, "open").as_deref(), Some("true"));
660 assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn"));
661 assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number"));
662 assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation"));
663 assert_eq!(get_q(&q, "buyer_code").as_deref(), Some("v-buyer_code"));
664 assert_eq!(get_q(&q, "organization").as_deref(), Some("v-organization"));
665 assert_eq!(
666 get_q(&q, "issued_date_after").as_deref(),
667 Some("v-issued_date_after")
668 );
669 assert_eq!(
670 get_q(&q, "issued_date_before").as_deref(),
671 Some("v-issued_date_before")
672 );
673 assert_eq!(
674 get_q(&q, "closes_date_after").as_deref(),
675 Some("v-closes_date_after")
676 );
677 assert_eq!(
678 get_q(&q, "closes_date_before").as_deref(),
679 Some("v-closes_date_before")
680 );
681 assert_eq!(get_q(&q, "search").as_deref(), Some("v-search"));
682 assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering"));
683 assert_eq!(q.len(), 12);
684 }
685
686 #[test]
687 fn list_dibbs_rfps_open_false_is_a_filter_not_an_absence() {
688 let opts = ListDibbsRfpsOptions::builder().open(false).build();
689 assert_eq!(get_q(&opts.to_query(), "open").as_deref(), Some("false"));
690 }
691
692 #[test]
693 fn list_dibbs_rfps_zero_value_omitted() {
694 assert!(ListDibbsRfpsOptions::builder()
695 .build()
696 .to_query()
697 .is_empty());
698 }
699
700 #[test]
701 fn list_dibbs_rfps_cursor_wins_over_page_and_shape_emits() {
702 let opts = ListDibbsRfpsOptions::builder()
703 .page(3u32)
704 .cursor("c0".to_string())
705 .shape(crate::SHAPE_DIBBS_RFPS_MINIMAL)
706 .build();
707 let q = opts.to_query();
708 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
709 assert_eq!(get_q(&q, "page"), None);
710 assert_eq!(
711 get_q(&q, "shape").as_deref(),
712 Some(crate::SHAPE_DIBBS_RFPS_MINIMAL)
713 );
714 }
715
716 #[test]
717 fn list_dibbs_rfps_extra_emits() {
718 let mut extra = BTreeMap::new();
719 extra.insert("custom_x".to_string(), "xv".to_string());
720 let q = ListDibbsRfpsOptions::builder()
721 .extra(extra)
722 .build()
723 .to_query();
724 assert!(q.contains(&("custom_x".into(), "xv".into())));
725 }
726
727 #[tokio::test]
728 async fn get_dibbs_rfp_validates_empty_uuid() {
729 let client = Client::builder().api_key("x").build().expect("client");
730 let err = client.get_dibbs_rfp("", None).await.unwrap_err();
731 match err {
732 Error::Validation { message, .. } => {
733 assert!(message.contains("uuid is required"));
734 }
735 other => panic!("expected Validation, got {other:?}"),
736 }
737 }
738
739 #[test]
740 fn list_dibbs_awards_all_filters_emit() {
741 let opts = ListDibbsAwardsOptions::builder()
742 .nsn("v-nsn")
743 .part_number("v-part_number")
744 .solicitation("v-solicitation")
745 .award_number("v-award_number")
746 .delivery_order_number("v-delivery_order_number")
747 .purchase_request("v-purchase_request")
748 .awardee_cage("v-awardee_cage")
749 .entity("v-entity")
750 .organization("v-organization")
751 .award_date_after("v-award_date_after")
752 .award_date_before("v-award_date_before")
753 .posted_date_after("v-posted_date_after")
754 .posted_date_before("v-posted_date_before")
755 .total_contract_price_min("v-total_contract_price_min")
756 .total_contract_price_max("v-total_contract_price_max")
757 .search("v-search")
758 .ordering("v-ordering")
759 .build();
760 let q = opts.to_query();
761 assert_eq!(get_q(&q, "nsn").as_deref(), Some("v-nsn"));
762 assert_eq!(get_q(&q, "part_number").as_deref(), Some("v-part_number"));
763 assert_eq!(get_q(&q, "solicitation").as_deref(), Some("v-solicitation"));
764 assert_eq!(get_q(&q, "award_number").as_deref(), Some("v-award_number"));
765 assert_eq!(
766 get_q(&q, "delivery_order_number").as_deref(),
767 Some("v-delivery_order_number")
768 );
769 assert_eq!(
770 get_q(&q, "purchase_request").as_deref(),
771 Some("v-purchase_request")
772 );
773 assert_eq!(get_q(&q, "awardee_cage").as_deref(), Some("v-awardee_cage"));
774 assert_eq!(get_q(&q, "entity").as_deref(), Some("v-entity"));
775 assert_eq!(get_q(&q, "organization").as_deref(), Some("v-organization"));
776 assert_eq!(
777 get_q(&q, "award_date_after").as_deref(),
778 Some("v-award_date_after")
779 );
780 assert_eq!(
781 get_q(&q, "award_date_before").as_deref(),
782 Some("v-award_date_before")
783 );
784 assert_eq!(
785 get_q(&q, "posted_date_after").as_deref(),
786 Some("v-posted_date_after")
787 );
788 assert_eq!(
789 get_q(&q, "posted_date_before").as_deref(),
790 Some("v-posted_date_before")
791 );
792 assert_eq!(
793 get_q(&q, "total_contract_price_min").as_deref(),
794 Some("v-total_contract_price_min")
795 );
796 assert_eq!(
797 get_q(&q, "total_contract_price_max").as_deref(),
798 Some("v-total_contract_price_max")
799 );
800 assert_eq!(get_q(&q, "search").as_deref(), Some("v-search"));
801 assert_eq!(get_q(&q, "ordering").as_deref(), Some("v-ordering"));
802 assert_eq!(q.len(), 17);
803 }
804
805 #[test]
806 fn list_dibbs_awards_zero_value_omitted() {
807 assert!(ListDibbsAwardsOptions::builder()
808 .build()
809 .to_query()
810 .is_empty());
811 }
812
813 #[test]
814 fn list_dibbs_awards_cursor_wins_over_page_and_shape_emits() {
815 let opts = ListDibbsAwardsOptions::builder()
816 .page(3u32)
817 .cursor("c0".to_string())
818 .shape(crate::SHAPE_DIBBS_AWARDS_MINIMAL)
819 .build();
820 let q = opts.to_query();
821 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
822 assert_eq!(get_q(&q, "page"), None);
823 assert_eq!(
824 get_q(&q, "shape").as_deref(),
825 Some(crate::SHAPE_DIBBS_AWARDS_MINIMAL)
826 );
827 }
828
829 #[test]
830 fn list_dibbs_awards_extra_emits() {
831 let mut extra = BTreeMap::new();
832 extra.insert("custom_x".to_string(), "xv".to_string());
833 let q = ListDibbsAwardsOptions::builder()
834 .extra(extra)
835 .build()
836 .to_query();
837 assert!(q.contains(&("custom_x".into(), "xv".into())));
838 }
839
840 #[tokio::test]
841 async fn get_dibbs_award_validates_empty_uuid() {
842 let client = Client::builder().api_key("x").build().expect("client");
843 let err = client.get_dibbs_award("", None).await.unwrap_err();
844 match err {
845 Error::Validation { message, .. } => {
846 assert!(message.contains("uuid is required"));
847 }
848 other => panic!("expected Validation, got {other:?}"),
849 }
850 }
851
852 #[test]
853 fn get_options_emits() {
854 let q = GetDibbsOptions::builder()
855 .shape("a,b")
856 .flat(true)
857 .flat_lists(true)
858 .build()
859 .to_query();
860 assert_eq!(get_q(&q, "shape").as_deref(), Some("a,b"));
861 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
862 assert_eq!(get_q(&q, "flat_lists").as_deref(), Some("true"));
863 }
864}