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(default)]
122 pub extra: BTreeMap<String, String>,
123}
124
125impl ListOTAsOptions {
126 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
127 let mut q = Vec::new();
128 apply_pagination(
129 &mut q,
130 self.page,
131 self.limit,
132 self.cursor.as_deref(),
133 self.shape.as_deref(),
134 self.flat,
135 self.flat_lists,
136 );
137 push_opt(&mut q, "joiner", self.joiner.as_deref());
138 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
139 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
140 push_opt(&mut q, "piid", self.piid.as_deref());
141 push_opt(&mut q, "recipient", self.recipient.as_deref());
142 push_opt(&mut q, "uei", self.uei.as_deref());
143 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
144 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
145 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
146 push_opt(&mut q, "award_date", self.award_date.as_deref());
147 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
148 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
149 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
150 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
151 push_opt(
152 &mut q,
153 "pop_start_date_gte",
154 self.pop_start_date_gte.as_deref(),
155 );
156 push_opt(
157 &mut q,
158 "pop_start_date_lte",
159 self.pop_start_date_lte.as_deref(),
160 );
161 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
162 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
163 push_opt(&mut q, "psc", self.psc.as_deref());
164 push_opt(&mut q, "search", self.search.as_deref());
165 push_opt(&mut q, "ordering", self.ordering.as_deref());
166 for (k, v) in &self.extra {
167 if !v.is_empty() {
168 q.push((k.clone(), v.clone()));
169 }
170 }
171 q
172 }
173}
174
175#[allow(clippy::upper_case_acronyms)]
177#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
178#[non_exhaustive]
179pub struct GetOTAOptions {
180 #[builder(into)]
182 pub shape: Option<String>,
183 #[builder(default)]
185 pub flat: bool,
186 #[builder(default)]
188 pub flat_lists: bool,
189}
190
191impl GetOTAOptions {
192 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
193 let mut q = Vec::new();
194 push_opt(&mut q, "shape", self.shape.as_deref());
195 if self.flat {
196 q.push(("flat".into(), "true".into()));
197 }
198 if self.flat_lists {
199 q.push(("flat_lists".into(), "true".into()));
200 }
201 q
202 }
203}
204
205#[allow(clippy::upper_case_acronyms)]
213#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
214#[non_exhaustive]
215pub struct ListOTIDVsOptions {
216 #[builder(into)]
218 pub page: Option<u32>,
219 #[builder(into)]
221 pub limit: Option<u32>,
222 #[builder(into)]
224 pub cursor: Option<String>,
225 #[builder(into)]
228 pub shape: Option<String>,
229 #[builder(default)]
231 pub flat: bool,
232 #[builder(default)]
234 pub flat_lists: bool,
235
236 #[builder(into)]
238 pub joiner: Option<String>,
239
240 #[builder(into)]
242 pub awarding_agency: Option<String>,
243 #[builder(into)]
245 pub funding_agency: Option<String>,
246 #[builder(into)]
248 pub piid: Option<String>,
249 #[builder(into)]
251 pub recipient: Option<String>,
252 #[builder(into)]
254 pub uei: Option<String>,
255
256 #[builder(into)]
258 pub fiscal_year: Option<String>,
259 #[builder(into)]
261 pub fiscal_year_gte: Option<String>,
262 #[builder(into)]
264 pub fiscal_year_lte: Option<String>,
265
266 #[builder(into)]
268 pub award_date: Option<String>,
269 #[builder(into)]
271 pub award_date_gte: Option<String>,
272 #[builder(into)]
274 pub award_date_lte: Option<String>,
275 #[builder(into)]
277 pub expiring_gte: Option<String>,
278 #[builder(into)]
280 pub expiring_lte: Option<String>,
281 #[builder(into)]
283 pub pop_start_date_gte: Option<String>,
284 #[builder(into)]
286 pub pop_start_date_lte: Option<String>,
287 #[builder(into)]
289 pub pop_end_date_gte: Option<String>,
290 #[builder(into)]
292 pub pop_end_date_lte: Option<String>,
293
294 #[builder(into)]
296 pub psc: Option<String>,
297
298 #[builder(into)]
300 pub search: Option<String>,
301 #[builder(into)]
303 pub ordering: Option<String>,
304
305 #[builder(default)]
307 pub extra: BTreeMap<String, String>,
308}
309
310impl ListOTIDVsOptions {
311 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
312 let mut q = Vec::new();
313 apply_pagination(
314 &mut q,
315 self.page,
316 self.limit,
317 self.cursor.as_deref(),
318 self.shape.as_deref(),
319 self.flat,
320 self.flat_lists,
321 );
322 push_opt(&mut q, "joiner", self.joiner.as_deref());
323 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
324 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
325 push_opt(&mut q, "piid", self.piid.as_deref());
326 push_opt(&mut q, "recipient", self.recipient.as_deref());
327 push_opt(&mut q, "uei", self.uei.as_deref());
328 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
329 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
330 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
331 push_opt(&mut q, "award_date", self.award_date.as_deref());
332 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
333 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
334 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
335 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
336 push_opt(
337 &mut q,
338 "pop_start_date_gte",
339 self.pop_start_date_gte.as_deref(),
340 );
341 push_opt(
342 &mut q,
343 "pop_start_date_lte",
344 self.pop_start_date_lte.as_deref(),
345 );
346 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
347 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
348 push_opt(&mut q, "psc", self.psc.as_deref());
349 push_opt(&mut q, "search", self.search.as_deref());
350 push_opt(&mut q, "ordering", self.ordering.as_deref());
351 for (k, v) in &self.extra {
352 if !v.is_empty() {
353 q.push((k.clone(), v.clone()));
354 }
355 }
356 q
357 }
358}
359
360#[allow(clippy::upper_case_acronyms)]
362#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
363#[non_exhaustive]
364pub struct GetOTIDVOptions {
365 #[builder(into)]
367 pub shape: Option<String>,
368 #[builder(default)]
370 pub flat: bool,
371 #[builder(default)]
373 pub flat_lists: bool,
374}
375
376impl GetOTIDVOptions {
377 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
378 let mut q = Vec::new();
379 push_opt(&mut q, "shape", self.shape.as_deref());
380 if self.flat {
381 q.push(("flat".into(), "true".into()));
382 }
383 if self.flat_lists {
384 q.push(("flat_lists".into(), "true".into()));
385 }
386 q
387 }
388}
389
390#[allow(clippy::upper_case_acronyms)]
398#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
399#[non_exhaustive]
400pub struct ListOTIDVAwardsOptions {
401 #[builder(into)]
403 pub page: Option<u32>,
404 #[builder(into)]
406 pub limit: Option<u32>,
407 #[builder(into)]
409 pub cursor: Option<String>,
410 #[builder(into)]
412 pub shape: Option<String>,
413 #[builder(default)]
415 pub flat: bool,
416 #[builder(default)]
418 pub flat_lists: bool,
419
420 #[builder(into)]
422 pub joiner: Option<String>,
423
424 #[builder(into)]
426 pub awarding_agency: Option<String>,
427 #[builder(into)]
429 pub funding_agency: Option<String>,
430 #[builder(into)]
432 pub piid: Option<String>,
433 #[builder(into)]
435 pub recipient: Option<String>,
436 #[builder(into)]
438 pub uei: Option<String>,
439
440 #[builder(into)]
442 pub fiscal_year: Option<String>,
443 #[builder(into)]
445 pub fiscal_year_gte: Option<String>,
446 #[builder(into)]
448 pub fiscal_year_lte: Option<String>,
449
450 #[builder(into)]
452 pub award_date: Option<String>,
453 #[builder(into)]
455 pub award_date_gte: Option<String>,
456 #[builder(into)]
458 pub award_date_lte: Option<String>,
459 #[builder(into)]
461 pub expiring_gte: Option<String>,
462 #[builder(into)]
464 pub expiring_lte: Option<String>,
465 #[builder(into)]
467 pub pop_start_date_gte: Option<String>,
468 #[builder(into)]
470 pub pop_start_date_lte: Option<String>,
471 #[builder(into)]
473 pub pop_end_date_gte: Option<String>,
474 #[builder(into)]
476 pub pop_end_date_lte: Option<String>,
477
478 #[builder(into)]
480 pub psc: Option<String>,
481
482 #[builder(into)]
484 pub search: Option<String>,
485 #[builder(into)]
487 pub ordering: Option<String>,
488
489 #[builder(default)]
491 pub extra: BTreeMap<String, String>,
492}
493
494impl ListOTIDVAwardsOptions {
495 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
496 let mut q = Vec::new();
497 apply_pagination(
498 &mut q,
499 self.page,
500 self.limit,
501 self.cursor.as_deref(),
502 self.shape.as_deref(),
503 self.flat,
504 self.flat_lists,
505 );
506 push_opt(&mut q, "joiner", self.joiner.as_deref());
507 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
508 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
509 push_opt(&mut q, "piid", self.piid.as_deref());
510 push_opt(&mut q, "recipient", self.recipient.as_deref());
511 push_opt(&mut q, "uei", self.uei.as_deref());
512 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
513 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
514 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
515 push_opt(&mut q, "award_date", self.award_date.as_deref());
516 push_opt(&mut q, "award_date_gte", self.award_date_gte.as_deref());
517 push_opt(&mut q, "award_date_lte", self.award_date_lte.as_deref());
518 push_opt(&mut q, "expiring_gte", self.expiring_gte.as_deref());
519 push_opt(&mut q, "expiring_lte", self.expiring_lte.as_deref());
520 push_opt(
521 &mut q,
522 "pop_start_date_gte",
523 self.pop_start_date_gte.as_deref(),
524 );
525 push_opt(
526 &mut q,
527 "pop_start_date_lte",
528 self.pop_start_date_lte.as_deref(),
529 );
530 push_opt(&mut q, "pop_end_date_gte", self.pop_end_date_gte.as_deref());
531 push_opt(&mut q, "pop_end_date_lte", self.pop_end_date_lte.as_deref());
532 push_opt(&mut q, "psc", self.psc.as_deref());
533 push_opt(&mut q, "search", self.search.as_deref());
534 push_opt(&mut q, "ordering", self.ordering.as_deref());
535 for (k, v) in &self.extra {
536 if !v.is_empty() {
537 q.push((k.clone(), v.clone()));
538 }
539 }
540 q
541 }
542}
543
544impl Client {
549 pub async fn list_otas(&self, opts: ListOTAsOptions) -> Result<Page<Record>> {
551 let q = opts.to_query();
552 let bytes = self.get_bytes("/api/otas/", &q).await?;
553 Page::decode(&bytes)
554 }
555
556 pub async fn get_ota(&self, key: &str, opts: Option<GetOTAOptions>) -> Result<Record> {
558 if key.is_empty() {
559 return Err(Error::Validation {
560 message: "get_ota: key is required".into(),
561 response: None,
562 });
563 }
564 let q = opts.unwrap_or_default().to_query();
565 let path = format!("/api/otas/{}/", urlencoding(key));
566 self.get_json::<Record>(&path, &q).await
567 }
568
569 pub fn iterate_otas(&self, opts: ListOTAsOptions) -> PageStream<Record> {
571 let opts = Arc::new(opts);
572 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
573 let mut next = (*opts).clone();
574 next.page = page;
575 next.cursor = cursor;
576 Box::pin(async move { client.list_otas(next).await })
577 });
578 PageStream::new(self.clone(), fetch)
579 }
580
581 pub async fn list_otidvs(&self, opts: ListOTIDVsOptions) -> Result<Page<Record>> {
583 let q = opts.to_query();
584 let bytes = self.get_bytes("/api/otidvs/", &q).await?;
585 Page::decode(&bytes)
586 }
587
588 pub async fn get_otidv(&self, key: &str, opts: Option<GetOTIDVOptions>) -> Result<Record> {
590 if key.is_empty() {
591 return Err(Error::Validation {
592 message: "get_otidv: key is required".into(),
593 response: None,
594 });
595 }
596 let q = opts.unwrap_or_default().to_query();
597 let path = format!("/api/otidvs/{}/", urlencoding(key));
598 self.get_json::<Record>(&path, &q).await
599 }
600
601 pub fn iterate_otidvs(&self, opts: ListOTIDVsOptions) -> PageStream<Record> {
603 let opts = Arc::new(opts);
604 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
605 let mut next = (*opts).clone();
606 next.page = page;
607 next.cursor = cursor;
608 Box::pin(async move { client.list_otidvs(next).await })
609 });
610 PageStream::new(self.clone(), fetch)
611 }
612
613 pub async fn list_otidv_awards(
615 &self,
616 key: &str,
617 opts: ListOTIDVAwardsOptions,
618 ) -> Result<Page<Record>> {
619 if key.is_empty() {
620 return Err(Error::Validation {
621 message: "list_otidv_awards: key is required".into(),
622 response: None,
623 });
624 }
625 let q = opts.to_query();
626 let path = format!("/api/otidvs/{}/awards/", urlencoding(key));
627 let bytes = self.get_bytes(&path, &q).await?;
628 Page::decode(&bytes)
629 }
630
631 pub fn iterate_otidv_awards(
633 &self,
634 key: &str,
635 opts: ListOTIDVAwardsOptions,
636 ) -> PageStream<Record> {
637 let opts = Arc::new(opts);
638 let key = Arc::new(key.to_string());
639 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
640 let mut next = (*opts).clone();
641 next.page = page;
642 next.cursor = cursor;
643 let key = key.clone();
644 Box::pin(async move { client.list_otidv_awards(&key, next).await })
645 });
646 PageStream::new(self.clone(), fetch)
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
655 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
656 }
657
658 #[test]
659 fn list_otas_all_filters_emit() {
660 let opts = ListOTAsOptions::builder()
661 .awarding_agency("9700")
662 .funding_agency("9800")
663 .piid("FA8650")
664 .recipient("Acme")
665 .uei("UEI123")
666 .fiscal_year("2024")
667 .fiscal_year_gte("2020")
668 .fiscal_year_lte("2024")
669 .award_date("2024-01-01")
670 .award_date_gte("2023-01-01")
671 .award_date_lte("2024-12-31")
672 .expiring_gte("2024-01-01")
673 .expiring_lte("2025-12-31")
674 .pop_start_date_gte("2024-01-01")
675 .pop_start_date_lte("2024-06-30")
676 .pop_end_date_gte("2024-07-01")
677 .pop_end_date_lte("2024-12-31")
678 .psc("D302")
679 .search("hypersonics")
680 .ordering("-award_date")
681 .build();
682 let q = opts.to_query();
683 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
684 assert_eq!(get_q(&q, "funding_agency").as_deref(), Some("9800"));
685 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
686 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
687 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
688 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
689 assert_eq!(get_q(&q, "fiscal_year_gte").as_deref(), Some("2020"));
690 assert_eq!(get_q(&q, "fiscal_year_lte").as_deref(), Some("2024"));
691 assert_eq!(get_q(&q, "award_date").as_deref(), Some("2024-01-01"));
692 assert_eq!(get_q(&q, "psc").as_deref(), Some("D302"));
693 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
694 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
695 }
696
697 #[test]
698 fn list_otas_zero_value_omitted() {
699 let opts = ListOTAsOptions::builder().build();
700 let q = opts.to_query();
701 assert!(q.is_empty(), "expected empty query, got {q:?}");
702 }
703
704 #[test]
705 fn list_otas_extra_emits() {
706 let mut extra = BTreeMap::new();
707 extra.insert("custom_x".to_string(), "xval".to_string());
708 let opts = ListOTAsOptions::builder().extra(extra).build();
709 let q = opts.to_query();
710 assert!(q.contains(&("custom_x".into(), "xval".into())));
711 }
712
713 #[test]
714 fn list_otas_cursor_wins_over_page() {
715 let opts = ListOTAsOptions::builder()
716 .page(3u32)
717 .cursor("abc".to_string())
718 .build();
719 let q = opts.to_query();
720 assert_eq!(get_q(&q, "cursor").as_deref(), Some("abc"));
721 assert_eq!(get_q(&q, "page"), None);
722 }
723
724 #[test]
725 fn list_otas_shape_and_flat_emit() {
726 let opts = ListOTAsOptions::builder()
727 .shape(crate::SHAPE_OTAS_MINIMAL)
728 .flat(true)
729 .flat_lists(true)
730 .joiner("__")
731 .build();
732 let q = opts.to_query();
733 assert_eq!(
734 get_q(&q, "shape").as_deref(),
735 Some(crate::SHAPE_OTAS_MINIMAL)
736 );
737 assert_eq!(get_q(&q, "flat").as_deref(), Some("true"));
738 assert_eq!(get_q(&q, "flat_lists").as_deref(), Some("true"));
739 assert_eq!(get_q(&q, "joiner").as_deref(), Some("__"));
740 }
741
742 #[test]
743 fn list_otidvs_all_filters_emit() {
744 let opts = ListOTIDVsOptions::builder()
745 .awarding_agency("9700")
746 .piid("FA8650")
747 .uei("UEI123")
748 .fiscal_year("2024")
749 .award_date_gte("2023-01-01")
750 .search("hypersonics")
751 .ordering("-award_date")
752 .build();
753 let q = opts.to_query();
754 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
755 assert_eq!(get_q(&q, "piid").as_deref(), Some("FA8650"));
756 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI123"));
757 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
758 assert_eq!(get_q(&q, "award_date_gte").as_deref(), Some("2023-01-01"));
759 assert_eq!(get_q(&q, "search").as_deref(), Some("hypersonics"));
760 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-award_date"));
761 }
762
763 #[test]
764 fn list_otidv_awards_emits_filters() {
765 let opts = ListOTIDVAwardsOptions::builder()
766 .awarding_agency("9700")
767 .recipient("Acme")
768 .search("kw")
769 .build();
770 let q = opts.to_query();
771 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
772 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme"));
773 assert_eq!(get_q(&q, "search").as_deref(), Some("kw"));
774 }
775
776 #[tokio::test]
777 async fn get_ota_validates_empty_key() {
778 let client = Client::builder().api_key("x").build().expect("client");
779 let err = client.get_ota("", None).await.unwrap_err();
780 match err {
781 Error::Validation { message, .. } => {
782 assert!(message.contains("key is required"));
783 }
784 other => panic!("expected Validation, got {other:?}"),
785 }
786 }
787
788 #[tokio::test]
789 async fn get_otidv_validates_empty_key() {
790 let client = Client::builder().api_key("x").build().expect("client");
791 let err = client.get_otidv("", 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 list_otidv_awards_validates_empty_key() {
802 let client = Client::builder().api_key("x").build().expect("client");
803 let err = client
804 .list_otidv_awards("", ListOTIDVAwardsOptions::default())
805 .await
806 .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}