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