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#[allow(clippy::upper_case_acronyms)]
23#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct ListOTAsOptions {
26 #[builder(into)]
29 pub page: Option<u32>,
30 #[builder(into)]
32 pub limit: Option<u32>,
33 #[builder(into)]
36 pub cursor: Option<String>,
37 #[builder(into)]
40 pub shape: Option<String>,
41 #[builder(default)]
43 pub flat: bool,
44 #[builder(default)]
46 pub flat_lists: bool,
47
48 #[builder(into)]
50 pub joiner: Option<String>,
51
52 #[builder(into)]
55 pub awarding_agency: Option<String>,
56 #[builder(into)]
58 pub funding_agency: Option<String>,
59 #[builder(into)]
61 pub piid: Option<String>,
62 #[builder(into)]
64 pub recipient: Option<String>,
65 #[builder(into)]
67 pub uei: Option<String>,
68
69 #[builder(into)]
72 pub fiscal_year: Option<String>,
73 #[builder(into)]
75 pub fiscal_year_gte: Option<String>,
76 #[builder(into)]
78 pub fiscal_year_lte: Option<String>,
79
80 #[builder(into)]
83 pub award_date: Option<String>,
84 #[builder(into)]
86 pub award_date_gte: Option<String>,
87 #[builder(into)]
89 pub award_date_lte: Option<String>,
90 #[builder(into)]
92 pub expiring_gte: Option<String>,
93 #[builder(into)]
95 pub expiring_lte: Option<String>,
96 #[builder(into)]
98 pub pop_start_date_gte: Option<String>,
99 #[builder(into)]
101 pub pop_start_date_lte: Option<String>,
102 #[builder(into)]
104 pub pop_end_date_gte: Option<String>,
105 #[builder(into)]
107 pub pop_end_date_lte: Option<String>,
108
109 #[builder(into)]
111 pub psc: Option<String>,
112
113 #[builder(into)]
115 pub search: Option<String>,
116 #[builder(into)]
118 pub ordering: Option<String>,
119
120 #[builder(into)]
122 pub key: Option<String>,
123
124 #[builder(default)]
126 pub extra: BTreeMap<String, String>,
127}
128
129impl ListOTAsOptions {
130 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
131 let mut q = Vec::new();
132 apply_pagination(
133 &mut q,
134 self.page,
135 self.limit,
136 self.cursor.as_deref(),
137 self.shape.as_deref(),
138 self.flat,
139 self.flat_lists,
140 );
141 push_opt(&mut q, "joiner", self.joiner.as_deref());
142 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
143 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
144 push_opt(&mut q, "piid", self.piid.as_deref());
145 push_opt(&mut q, "key", self.key.as_deref());
146 push_opt(&mut q, "recipient", self.recipient.as_deref());
147 push_opt(&mut q, "uei", self.uei.as_deref());
148 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
149 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
150 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
151 push_opt(&mut q, "award_date", self.award_date.as_deref());
152 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
153 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
154 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
155 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
156 push_opt(
157 &mut q,
158 "pop_start_date_gte",
159 self.pop_start_date_gte.as_deref(),
160 );
161 push_opt(
162 &mut q,
163 "pop_start_date_lte",
164 self.pop_start_date_lte.as_deref(),
165 );
166 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
167 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
168 push_opt(&mut q, "psc", self.psc.as_deref());
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#[allow(clippy::upper_case_acronyms)]
182#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
183#[non_exhaustive]
184pub struct GetOTAOptions {
185 #[builder(into)]
187 pub shape: Option<String>,
188 #[builder(default)]
190 pub flat: bool,
191 #[builder(default)]
193 pub flat_lists: bool,
194}
195
196impl GetOTAOptions {
197 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
198 let mut q = Vec::new();
199 push_opt(&mut q, "shape", self.shape.as_deref());
200 if self.flat {
201 q.push(("flat".into(), "true".into()));
202 }
203 if self.flat_lists {
204 q.push(("flat_lists".into(), "true".into()));
205 }
206 q
207 }
208}
209
210#[allow(clippy::upper_case_acronyms)]
218#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
219#[non_exhaustive]
220pub struct ListOTIDVsOptions {
221 #[builder(into)]
223 pub page: Option<u32>,
224 #[builder(into)]
226 pub limit: Option<u32>,
227 #[builder(into)]
229 pub cursor: Option<String>,
230 #[builder(into)]
233 pub shape: Option<String>,
234 #[builder(default)]
236 pub flat: bool,
237 #[builder(default)]
239 pub flat_lists: bool,
240
241 #[builder(into)]
243 pub joiner: Option<String>,
244
245 #[builder(into)]
247 pub awarding_agency: Option<String>,
248 #[builder(into)]
250 pub funding_agency: Option<String>,
251 #[builder(into)]
253 pub piid: Option<String>,
254 #[builder(into)]
256 pub recipient: Option<String>,
257 #[builder(into)]
259 pub uei: Option<String>,
260
261 #[builder(into)]
263 pub fiscal_year: Option<String>,
264 #[builder(into)]
266 pub fiscal_year_gte: Option<String>,
267 #[builder(into)]
269 pub fiscal_year_lte: Option<String>,
270
271 #[builder(into)]
273 pub award_date: Option<String>,
274 #[builder(into)]
276 pub award_date_gte: Option<String>,
277 #[builder(into)]
279 pub award_date_lte: Option<String>,
280 #[builder(into)]
282 pub expiring_gte: Option<String>,
283 #[builder(into)]
285 pub expiring_lte: Option<String>,
286 #[builder(into)]
288 pub pop_start_date_gte: Option<String>,
289 #[builder(into)]
291 pub pop_start_date_lte: Option<String>,
292 #[builder(into)]
294 pub pop_end_date_gte: Option<String>,
295 #[builder(into)]
297 pub pop_end_date_lte: Option<String>,
298
299 #[builder(into)]
301 pub psc: Option<String>,
302
303 #[builder(into)]
305 pub search: Option<String>,
306 #[builder(into)]
308 pub ordering: Option<String>,
309
310 #[builder(into)]
312 pub key: Option<String>,
313
314 #[builder(default)]
316 pub extra: BTreeMap<String, String>,
317}
318
319impl ListOTIDVsOptions {
320 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
321 let mut q = Vec::new();
322 apply_pagination(
323 &mut q,
324 self.page,
325 self.limit,
326 self.cursor.as_deref(),
327 self.shape.as_deref(),
328 self.flat,
329 self.flat_lists,
330 );
331 push_opt(&mut q, "joiner", self.joiner.as_deref());
332 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
333 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
334 push_opt(&mut q, "piid", self.piid.as_deref());
335 push_opt(&mut q, "key", self.key.as_deref());
336 push_opt(&mut q, "recipient", self.recipient.as_deref());
337 push_opt(&mut q, "uei", self.uei.as_deref());
338 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
339 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
340 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
341 push_opt(&mut q, "award_date", self.award_date.as_deref());
342 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
343 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
344 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
345 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
346 push_opt(
347 &mut q,
348 "pop_start_date_gte",
349 self.pop_start_date_gte.as_deref(),
350 );
351 push_opt(
352 &mut q,
353 "pop_start_date_lte",
354 self.pop_start_date_lte.as_deref(),
355 );
356 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
357 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
358 push_opt(&mut q, "psc", self.psc.as_deref());
359 push_opt(&mut q, "search", self.search.as_deref());
360 push_opt(&mut q, "ordering", self.ordering.as_deref());
361 for (k, v) in &self.extra {
362 if !v.is_empty() {
363 q.push((k.clone(), v.clone()));
364 }
365 }
366 q
367 }
368}
369
370#[allow(clippy::upper_case_acronyms)]
372#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
373#[non_exhaustive]
374pub struct GetOTIDVOptions {
375 #[builder(into)]
377 pub shape: Option<String>,
378 #[builder(default)]
380 pub flat: bool,
381 #[builder(default)]
383 pub flat_lists: bool,
384}
385
386impl GetOTIDVOptions {
387 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
388 let mut q = Vec::new();
389 push_opt(&mut q, "shape", self.shape.as_deref());
390 if self.flat {
391 q.push(("flat".into(), "true".into()));
392 }
393 if self.flat_lists {
394 q.push(("flat_lists".into(), "true".into()));
395 }
396 q
397 }
398}
399
400#[allow(clippy::upper_case_acronyms)]
408#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
409#[non_exhaustive]
410pub struct ListOTIDVAwardsOptions {
411 #[builder(into)]
413 pub page: Option<u32>,
414 #[builder(into)]
416 pub limit: Option<u32>,
417 #[builder(into)]
419 pub cursor: Option<String>,
420 #[builder(into)]
422 pub shape: Option<String>,
423 #[builder(default)]
425 pub flat: bool,
426 #[builder(default)]
428 pub flat_lists: bool,
429
430 #[builder(into)]
432 pub joiner: Option<String>,
433
434 #[builder(into)]
436 pub awarding_agency: Option<String>,
437 #[builder(into)]
439 pub funding_agency: Option<String>,
440 #[builder(into)]
442 pub piid: Option<String>,
443 #[builder(into)]
445 pub recipient: Option<String>,
446 #[builder(into)]
448 pub uei: Option<String>,
449
450 #[builder(into)]
452 pub fiscal_year: Option<String>,
453 #[builder(into)]
455 pub fiscal_year_gte: Option<String>,
456 #[builder(into)]
458 pub fiscal_year_lte: Option<String>,
459
460 #[builder(into)]
462 pub award_date: Option<String>,
463 #[builder(into)]
465 pub award_date_gte: Option<String>,
466 #[builder(into)]
468 pub award_date_lte: Option<String>,
469 #[builder(into)]
471 pub expiring_gte: Option<String>,
472 #[builder(into)]
474 pub expiring_lte: Option<String>,
475 #[builder(into)]
477 pub pop_start_date_gte: Option<String>,
478 #[builder(into)]
480 pub pop_start_date_lte: Option<String>,
481 #[builder(into)]
483 pub pop_end_date_gte: Option<String>,
484 #[builder(into)]
486 pub pop_end_date_lte: Option<String>,
487
488 #[builder(into)]
490 pub psc: Option<String>,
491
492 #[builder(into)]
494 pub search: Option<String>,
495 #[builder(into)]
497 pub ordering: Option<String>,
498
499 #[builder(into)]
501 pub key: Option<String>,
502
503 #[builder(default)]
505 pub extra: BTreeMap<String, String>,
506}
507
508impl ListOTIDVAwardsOptions {
509 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
510 let mut q = Vec::new();
511 apply_pagination(
512 &mut q,
513 self.page,
514 self.limit,
515 self.cursor.as_deref(),
516 self.shape.as_deref(),
517 self.flat,
518 self.flat_lists,
519 );
520 push_opt(&mut q, "joiner", self.joiner.as_deref());
521 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
522 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
523 push_opt(&mut q, "piid", self.piid.as_deref());
524 push_opt(&mut q, "key", self.key.as_deref());
525 push_opt(&mut q, "recipient", self.recipient.as_deref());
526 push_opt(&mut q, "uei", self.uei.as_deref());
527 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
528 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
529 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
530 push_opt(&mut q, "award_date", self.award_date.as_deref());
531 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
532 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
533 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
534 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
535 push_opt(
536 &mut q,
537 "pop_start_date_gte",
538 self.pop_start_date_gte.as_deref(),
539 );
540 push_opt(
541 &mut q,
542 "pop_start_date_lte",
543 self.pop_start_date_lte.as_deref(),
544 );
545 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
546 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
547 push_opt(&mut q, "psc", self.psc.as_deref());
548 push_opt(&mut q, "search", self.search.as_deref());
549 push_opt(&mut q, "ordering", self.ordering.as_deref());
550 for (k, v) in &self.extra {
551 if !v.is_empty() {
552 q.push((k.clone(), v.clone()));
553 }
554 }
555 q
556 }
557}
558
559impl Client {
564 pub async fn list_otas(&self, opts: ListOTAsOptions) -> Result<Page<Record>> {
566 let q = opts.to_query();
567 self.get_page("/api/otas/", &q).await
568 }
569
570 pub async fn get_ota(&self, key: &str, opts: Option<GetOTAOptions>) -> Result<Record> {
572 if key.is_empty() {
573 return Err(Error::Validation {
574 message: "get_ota: key is required".into(),
575 response: None,
576 });
577 }
578 let q = opts.unwrap_or_default().to_query();
579 let path = format!("/api/otas/{}/", urlencoding(key));
580 self.get_json::<Record>(&path, &q).await
581 }
582
583 pub fn iterate_otas(&self, opts: ListOTAsOptions) -> PageStream<Record> {
585 let opts = Arc::new(opts);
586 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
587 let mut next = (*opts).clone();
588 next.page = page;
589 next.cursor = cursor;
590 Box::pin(async move { client.list_otas(next).await })
591 });
592 PageStream::new(self.clone(), fetch)
593 }
594
595 pub async fn list_otidvs(&self, opts: ListOTIDVsOptions) -> Result<Page<Record>> {
597 let q = opts.to_query();
598 self.get_page("/api/otidvs/", &q).await
599 }
600
601 pub async fn get_otidv(&self, key: &str, opts: Option<GetOTIDVOptions>) -> Result<Record> {
603 if key.is_empty() {
604 return Err(Error::Validation {
605 message: "get_otidv: key is required".into(),
606 response: None,
607 });
608 }
609 let q = opts.unwrap_or_default().to_query();
610 let path = format!("/api/otidvs/{}/", urlencoding(key));
611 self.get_json::<Record>(&path, &q).await
612 }
613
614 pub fn iterate_otidvs(&self, opts: ListOTIDVsOptions) -> PageStream<Record> {
616 let opts = Arc::new(opts);
617 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
618 let mut next = (*opts).clone();
619 next.page = page;
620 next.cursor = cursor;
621 Box::pin(async move { client.list_otidvs(next).await })
622 });
623 PageStream::new(self.clone(), fetch)
624 }
625
626 pub async fn list_otidv_awards(
628 &self,
629 key: &str,
630 opts: ListOTIDVAwardsOptions,
631 ) -> Result<Page<Record>> {
632 if key.is_empty() {
633 return Err(Error::Validation {
634 message: "list_otidv_awards: key is required".into(),
635 response: None,
636 });
637 }
638 let q = opts.to_query();
639 let path = format!("/api/otidvs/{}/awards/", urlencoding(key));
640 self.get_page(&path, &q).await
641 }
642
643 pub fn iterate_otidv_awards(
645 &self,
646 key: &str,
647 opts: ListOTIDVAwardsOptions,
648 ) -> PageStream<Record> {
649 let opts = Arc::new(opts);
650 let key = Arc::new(key.to_string());
651 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
652 let mut next = (*opts).clone();
653 next.page = page;
654 next.cursor = cursor;
655 let key = key.clone();
656 Box::pin(async move { client.list_otidv_awards(&key, next).await })
657 });
658 PageStream::new(self.clone(), fetch)
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
667 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
668 }
669
670 #[test]
671 fn list_otas_all_filters_emit() {
672 let opts = ListOTAsOptions::builder()
673 .awarding_agency("9700")
674 .funding_agency("9800")
675 .piid("FA8650")
676 .recipient("Acme")
677 .uei("UEI123")
678 .fiscal_year("2024")
679 .fiscal_year_gte("2020")
680 .fiscal_year_lte("2024")
681 .award_date("2024-01-01")
682 .award_date_gte("2023-01-01")
683 .award_date_lte("2024-12-31")
684 .expiring_gte("2024-01-01")
685 .expiring_lte("2025-12-31")
686 .pop_start_date_gte("2024-01-01")
687 .pop_start_date_lte("2024-06-30")
688 .pop_end_date_gte("2024-07-01")
689 .pop_end_date_lte("2024-12-31")
690 .psc("D302")
691 .search("hypersonics")
692 .ordering("-award_date")
693 .build();
694 let q = opts.to_query();
695 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
696 assert_eq!(get_q(&q, "funding_agency").as_deref(), Some("9800"));
697 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
698 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
699 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
700 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
701 assert_eq!(get_q(&q, "fiscal_year_gte").as_deref(), Some("2020"));
702 assert_eq!(get_q(&q, "fiscal_year_lte").as_deref(), Some("2024"));
703 assert_eq!(get_q(&q, "award_date").as_deref(), Some("2024-01-01"));
704 assert_eq!(get_q(&q, "psc").as_deref(), Some("D302"));
705 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
706 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
707 }
708
709 #[test]
710 fn list_otas_zero_value_omitted() {
711 let opts = ListOTAsOptions::builder().build();
712 let q = opts.to_query();
713 assert!(q.is_empty(), "expected empty query, got {q:?}");
714 }
715
716 #[test]
717 fn list_otas_extra_emits() {
718 let mut extra = BTreeMap::new();
719 extra.insert("custom_x".to_string(), "xval".to_string());
720 let opts = ListOTAsOptions::builder().extra(extra).build();
721 let q = opts.to_query();
722 assert!(q.contains(&("custom_x".into(), "xval".into())));
723 }
724
725 #[test]
726 fn list_otas_cursor_wins_over_page() {
727 let opts = ListOTAsOptions::builder()
728 .page(3u32)
729 .cursor("abc".to_string())
730 .build();
731 let q = opts.to_query();
732 assert_eq!(get_q(&q, "cursor").as_deref(), Some("abc"));
733 assert_eq!(get_q(&q, "page"), None);
734 }
735
736 #[test]
737 fn list_otas_shape_and_flat_emit() {
738 let opts = ListOTAsOptions::builder()
739 .shape(crate::SHAPE_OTAS_MINIMAL)
740 .flat(true)
741 .flat_lists(true)
742 .joiner("__")
743 .build();
744 let q = opts.to_query();
745 assert_eq!(
746 get_q(&q, "shape").as_deref(),
747 Some(crate::SHAPE_OTAS_MINIMAL)
748 );
749 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
750 assert_eq!(get_q(&q, "flat_lists").as_deref(), Some("true"));
751 assert_eq!(get_q(&q, "joiner").as_deref(), Some("__"));
752 }
753
754 #[test]
755 fn list_otidvs_all_filters_emit() {
756 let opts = ListOTIDVsOptions::builder()
757 .awarding_agency("9700")
758 .piid("FA8650")
759 .uei("UEI123")
760 .fiscal_year("2024")
761 .award_date_gte("2023-01-01")
762 .search("hypersonics")
763 .ordering("-award_date")
764 .build();
765 let q = opts.to_query();
766 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
767 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
768 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
769 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
770 assert_eq!(get_q(&q, "award_date_gte").as_deref(), Some("2023-01-01"));
771 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
772 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
773 }
774
775 #[test]
776 fn list_otidv_awards_emits_filters() {
777 let opts = ListOTIDVAwardsOptions::builder()
778 .awarding_agency("9700")
779 .recipient("Acme")
780 .search("kw")
781 .build();
782 let q = opts.to_query();
783 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
784 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
785 assert_eq!(get_q(&q, "search").as_deref(), Some("kw"));
786 }
787
788 #[tokio::test]
789 async fn get_ota_validates_empty_key() {
790 let client = Client::builder().api_key("x").build().expect("client");
791 let err = client.get_ota("", None).await.unwrap_err();
792 match err {
793 Error::Validation { message, .. } => {
794 assert!(message.contains("key is required"));
795 }
796 other => panic!("expected Validation, got {other:?}"),
797 }
798 }
799
800 #[tokio::test]
801 async fn get_otidv_validates_empty_key() {
802 let client = Client::builder().api_key("x").build().expect("client");
803 let err = client.get_otidv("", None).await.unwrap_err();
804 match err {
805 Error::Validation { message, .. } => {
806 assert!(message.contains("key is required"));
807 }
808 other => panic!("expected Validation, got {other:?}"),
809 }
810 }
811
812 #[tokio::test]
813 async fn list_otidv_awards_validates_empty_key() {
814 let client = Client::builder().api_key("x").build().expect("client");
815 let err = client
816 .list_otidv_awards("", ListOTIDVAwardsOptions::default())
817 .await
818 .unwrap_err();
819 match err {
820 Error::Validation { message, .. } => {
821 assert!(message.contains("key is required"));
822 }
823 other => panic!("expected Validation, got {other:?}"),
824 }
825 }
826
827 #[test]
828 fn ota_family_key_filter_emits() {
829 let q = ListOTAsOptions::builder().key("K1").build().to_query();
830 assert_eq!(get_q(&q, "key").as_deref(), Some("K1"));
831 let q = ListOTIDVsOptions::builder().key("K2").build().to_query();
832 assert_eq!(get_q(&q, "key").as_deref(), Some("K2"));
833 let q = ListOTIDVAwardsOptions::builder()
834 .key("K3")
835 .build()
836 .to_query();
837 assert_eq!(get_q(&q, "key").as_deref(), Some("K3"));
838 }
839}