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 let bytes = self.get_bytes("/api/otas/", &q).await?;
568 Page::decode(&bytes)
569 }
570
571 pub async fn get_ota(&self, key: &str, opts: Option<GetOTAOptions>) -> Result<Record> {
573 if key.is_empty() {
574 return Err(Error::Validation {
575 message: "get_ota: key is required".into(),
576 response: None,
577 });
578 }
579 let q = opts.unwrap_or_default().to_query();
580 let path = format!("/api/otas/{}/", urlencoding(key));
581 self.get_json::<Record>(&path, &q).await
582 }
583
584 pub fn iterate_otas(&self, opts: ListOTAsOptions) -> PageStream<Record> {
586 let opts = Arc::new(opts);
587 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
588 let mut next = (*opts).clone();
589 next.page = page;
590 next.cursor = cursor;
591 Box::pin(async move { client.list_otas(next).await })
592 });
593 PageStream::new(self.clone(), fetch)
594 }
595
596 pub async fn list_otidvs(&self, opts: ListOTIDVsOptions) -> Result<Page<Record>> {
598 let q = opts.to_query();
599 let bytes = self.get_bytes("/api/otidvs/", &q).await?;
600 Page::decode(&bytes)
601 }
602
603 pub async fn get_otidv(&self, key: &str, opts: Option<GetOTIDVOptions>) -> Result<Record> {
605 if key.is_empty() {
606 return Err(Error::Validation {
607 message: "get_otidv: key is required".into(),
608 response: None,
609 });
610 }
611 let q = opts.unwrap_or_default().to_query();
612 let path = format!("/api/otidvs/{}/", urlencoding(key));
613 self.get_json::<Record>(&path, &q).await
614 }
615
616 pub fn iterate_otidvs(&self, opts: ListOTIDVsOptions) -> PageStream<Record> {
618 let opts = Arc::new(opts);
619 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
620 let mut next = (*opts).clone();
621 next.page = page;
622 next.cursor = cursor;
623 Box::pin(async move { client.list_otidvs(next).await })
624 });
625 PageStream::new(self.clone(), fetch)
626 }
627
628 pub async fn list_otidv_awards(
630 &self,
631 key: &str,
632 opts: ListOTIDVAwardsOptions,
633 ) -> Result<Page<Record>> {
634 if key.is_empty() {
635 return Err(Error::Validation {
636 message: "list_otidv_awards: key is required".into(),
637 response: None,
638 });
639 }
640 let q = opts.to_query();
641 let path = format!("/api/otidvs/{}/awards/", urlencoding(key));
642 let bytes = self.get_bytes(&path, &q).await?;
643 Page::decode(&bytes)
644 }
645
646 pub fn iterate_otidv_awards(
648 &self,
649 key: &str,
650 opts: ListOTIDVAwardsOptions,
651 ) -> PageStream<Record> {
652 let opts = Arc::new(opts);
653 let key = Arc::new(key.to_string());
654 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
655 let mut next = (*opts).clone();
656 next.page = page;
657 next.cursor = cursor;
658 let key = key.clone();
659 Box::pin(async move { client.list_otidv_awards(&key, next).await })
660 });
661 PageStream::new(self.clone(), fetch)
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
670 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
671 }
672
673 #[test]
674 fn list_otas_all_filters_emit() {
675 let opts = ListOTAsOptions::builder()
676 .awarding_agency("9700")
677 .funding_agency("9800")
678 .piid("FA8650")
679 .recipient("Acme")
680 .uei("UEI123")
681 .fiscal_year("2024")
682 .fiscal_year_gte("2020")
683 .fiscal_year_lte("2024")
684 .award_date("2024-01-01")
685 .award_date_gte("2023-01-01")
686 .award_date_lte("2024-12-31")
687 .expiring_gte("2024-01-01")
688 .expiring_lte("2025-12-31")
689 .pop_start_date_gte("2024-01-01")
690 .pop_start_date_lte("2024-06-30")
691 .pop_end_date_gte("2024-07-01")
692 .pop_end_date_lte("2024-12-31")
693 .psc("D302")
694 .search("hypersonics")
695 .ordering("-award_date")
696 .build();
697 let q = opts.to_query();
698 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
699 assert_eq!(get_q(&q, "funding_agency").as_deref(), Some("9800"));
700 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
701 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
702 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
703 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
704 assert_eq!(get_q(&q, "fiscal_year_gte").as_deref(), Some("2020"));
705 assert_eq!(get_q(&q, "fiscal_year_lte").as_deref(), Some("2024"));
706 assert_eq!(get_q(&q, "award_date").as_deref(), Some("2024-01-01"));
707 assert_eq!(get_q(&q, "psc").as_deref(), Some("D302"));
708 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
709 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
710 }
711
712 #[test]
713 fn list_otas_zero_value_omitted() {
714 let opts = ListOTAsOptions::builder().build();
715 let q = opts.to_query();
716 assert!(q.is_empty(), "expected empty query, got {q:?}");
717 }
718
719 #[test]
720 fn list_otas_extra_emits() {
721 let mut extra = BTreeMap::new();
722 extra.insert("custom_x".to_string(), "xval".to_string());
723 let opts = ListOTAsOptions::builder().extra(extra).build();
724 let q = opts.to_query();
725 assert!(q.contains(&("custom_x".into(), "xval".into())));
726 }
727
728 #[test]
729 fn list_otas_cursor_wins_over_page() {
730 let opts = ListOTAsOptions::builder()
731 .page(3u32)
732 .cursor("abc".to_string())
733 .build();
734 let q = opts.to_query();
735 assert_eq!(get_q(&q, "cursor").as_deref(), Some("abc"));
736 assert_eq!(get_q(&q, "page"), None);
737 }
738
739 #[test]
740 fn list_otas_shape_and_flat_emit() {
741 let opts = ListOTAsOptions::builder()
742 .shape(crate::SHAPE_OTAS_MINIMAL)
743 .flat(true)
744 .flat_lists(true)
745 .joiner("__")
746 .build();
747 let q = opts.to_query();
748 assert_eq!(
749 get_q(&q, "shape").as_deref(),
750 Some(crate::SHAPE_OTAS_MINIMAL)
751 );
752 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
753 assert_eq!(get_q(&q, "flat_lists").as_deref(), Some("true"));
754 assert_eq!(get_q(&q, "joiner").as_deref(), Some("__"));
755 }
756
757 #[test]
758 fn list_otidvs_all_filters_emit() {
759 let opts = ListOTIDVsOptions::builder()
760 .awarding_agency("9700")
761 .piid("FA8650")
762 .uei("UEI123")
763 .fiscal_year("2024")
764 .award_date_gte("2023-01-01")
765 .search("hypersonics")
766 .ordering("-award_date")
767 .build();
768 let q = opts.to_query();
769 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
770 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
771 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
772 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
773 assert_eq!(get_q(&q, "award_date_gte").as_deref(), Some("2023-01-01"));
774 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
775 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
776 }
777
778 #[test]
779 fn list_otidv_awards_emits_filters() {
780 let opts = ListOTIDVAwardsOptions::builder()
781 .awarding_agency("9700")
782 .recipient("Acme")
783 .search("kw")
784 .build();
785 let q = opts.to_query();
786 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
787 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
788 assert_eq!(get_q(&q, "search").as_deref(), Some("kw"));
789 }
790
791 #[tokio::test]
792 async fn get_ota_validates_empty_key() {
793 let client = Client::builder().api_key("x").build().expect("client");
794 let err = client.get_ota("", None).await.unwrap_err();
795 match err {
796 Error::Validation { message, .. } => {
797 assert!(message.contains("key is required"));
798 }
799 other => panic!("expected Validation, got {other:?}"),
800 }
801 }
802
803 #[tokio::test]
804 async fn get_otidv_validates_empty_key() {
805 let client = Client::builder().api_key("x").build().expect("client");
806 let err = client.get_otidv("", None).await.unwrap_err();
807 match err {
808 Error::Validation { message, .. } => {
809 assert!(message.contains("key is required"));
810 }
811 other => panic!("expected Validation, got {other:?}"),
812 }
813 }
814
815 #[tokio::test]
816 async fn list_otidv_awards_validates_empty_key() {
817 let client = Client::builder().api_key("x").build().expect("client");
818 let err = client
819 .list_otidv_awards("", ListOTIDVAwardsOptions::default())
820 .await
821 .unwrap_err();
822 match err {
823 Error::Validation { message, .. } => {
824 assert!(message.contains("key is required"));
825 }
826 other => panic!("expected Validation, got {other:?}"),
827 }
828 }
829
830 #[test]
831 fn ota_family_key_filter_emits() {
832 let q = ListOTAsOptions::builder().key("K1").build().to_query();
833 assert_eq!(get_q(&q, "key").as_deref(), Some("K1"));
834 let q = ListOTIDVsOptions::builder().key("K2").build().to_query();
835 assert_eq!(get_q(&q, "key").as_deref(), Some("K2"));
836 let q = ListOTIDVAwardsOptions::builder()
837 .key("K3")
838 .build()
839 .to_query();
840 assert_eq!(get_q(&q, "key").as_deref(), Some("K3"));
841 }
842}