1use crate::client::Client;
8use crate::error::{Error, Result};
9use crate::internal::{apply_pagination, push_opt, push_opt_bool};
10use crate::pagination::{FetchFn, Page, PageStream};
11use crate::resources::agencies::urlencoding;
12use crate::Record;
13use bon::Builder;
14use std::collections::BTreeMap;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct ListOrganizationsOptions {
25 #[builder(into)]
27 pub page: Option<u32>,
28 #[builder(into)]
30 pub limit: Option<u32>,
31 #[builder(into)]
33 pub cursor: Option<String>,
34 #[builder(into)]
36 pub shape: Option<String>,
37 #[builder(default)]
39 pub flat: bool,
40 #[builder(default)]
42 pub flat_lists: bool,
43
44 #[builder(into)]
46 pub search: Option<String>,
47 #[builder(into)]
49 pub r#type: Option<String>,
50 #[builder(into)]
52 pub level: Option<String>,
53 #[builder(into)]
55 pub cgac: Option<String>,
56 #[builder(into)]
58 pub parent: Option<String>,
59 pub include_inactive: Option<bool>,
62
63 #[builder(default)]
65 pub extra: BTreeMap<String, String>,
66}
67
68impl ListOrganizationsOptions {
69 fn to_query(&self) -> Vec<(String, String)> {
70 let mut q = Vec::new();
71 apply_pagination(
72 &mut q,
73 self.page,
74 self.limit,
75 self.cursor.as_deref(),
76 self.shape.as_deref(),
77 self.flat,
78 self.flat_lists,
79 );
80 push_opt(&mut q, "search", self.search.as_deref());
81 push_opt(&mut q, "type", self.r#type.as_deref());
82 push_opt(&mut q, "level", self.level.as_deref());
83 push_opt(&mut q, "cgac", self.cgac.as_deref());
84 push_opt(&mut q, "parent", self.parent.as_deref());
85 push_opt_bool(&mut q, "include_inactive", self.include_inactive);
86 for (k, v) in &self.extra {
87 if !v.is_empty() {
88 q.push((k.clone(), v.clone()));
89 }
90 }
91 q
92 }
93}
94
95#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct ListNaicsOptions {
103 #[builder(into)]
105 pub page: Option<u32>,
106 #[builder(into)]
108 pub limit: Option<u32>,
109 #[builder(into)]
111 pub cursor: Option<String>,
112 #[builder(into)]
114 pub shape: Option<String>,
115 #[builder(default)]
117 pub flat: bool,
118 #[builder(default)]
120 pub flat_lists: bool,
121
122 #[builder(into)]
124 pub search: Option<String>,
125 #[builder(into)]
127 pub revenue_limit: Option<String>,
128 #[builder(into)]
130 pub employee_limit: Option<String>,
131 #[builder(into)]
133 pub revenue_limit_gte: Option<String>,
134 #[builder(into)]
136 pub revenue_limit_lte: Option<String>,
137 #[builder(into)]
139 pub employee_limit_gte: Option<String>,
140 #[builder(into)]
142 pub employee_limit_lte: Option<String>,
143
144 #[builder(default)]
146 pub extra: BTreeMap<String, String>,
147}
148
149impl ListNaicsOptions {
150 fn to_query(&self) -> Vec<(String, String)> {
151 let mut q = Vec::new();
152 apply_pagination(
153 &mut q,
154 self.page,
155 self.limit,
156 self.cursor.as_deref(),
157 self.shape.as_deref(),
158 self.flat,
159 self.flat_lists,
160 );
161 push_opt(&mut q, "search", self.search.as_deref());
162 push_opt(&mut q, "revenue_limit", self.revenue_limit.as_deref());
163 push_opt(&mut q, "employee_limit", self.employee_limit.as_deref());
164 push_opt(
165 &mut q,
166 "revenue_limit_gte",
167 self.revenue_limit_gte.as_deref(),
168 );
169 push_opt(
170 &mut q,
171 "revenue_limit_lte",
172 self.revenue_limit_lte.as_deref(),
173 );
174 push_opt(
175 &mut q,
176 "employee_limit_gte",
177 self.employee_limit_gte.as_deref(),
178 );
179 push_opt(
180 &mut q,
181 "employee_limit_lte",
182 self.employee_limit_lte.as_deref(),
183 );
184 for (k, v) in &self.extra {
185 if !v.is_empty() {
186 q.push((k.clone(), v.clone()));
187 }
188 }
189 q
190 }
191}
192
193#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
199#[non_exhaustive]
200pub struct ListPscOptions {
201 #[builder(into)]
203 pub page: Option<u32>,
204 #[builder(into)]
206 pub limit: Option<u32>,
207 #[builder(into)]
209 pub cursor: Option<String>,
210 #[builder(into)]
212 pub shape: Option<String>,
213 #[builder(default)]
215 pub flat: bool,
216 #[builder(default)]
218 pub flat_lists: bool,
219
220 #[builder(into)]
222 pub search: Option<String>,
223 pub has_awards: Option<bool>,
225
226 #[builder(default)]
228 pub extra: BTreeMap<String, String>,
229}
230
231impl ListPscOptions {
232 fn to_query(&self) -> Vec<(String, String)> {
233 let mut q = Vec::new();
234 apply_pagination(
235 &mut q,
236 self.page,
237 self.limit,
238 self.cursor.as_deref(),
239 self.shape.as_deref(),
240 self.flat,
241 self.flat_lists,
242 );
243 push_opt(&mut q, "search", self.search.as_deref());
244 push_opt_bool(&mut q, "has_awards", self.has_awards);
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)]
260#[non_exhaustive]
261pub struct ListMasSinsOptions {
262 #[builder(into)]
264 pub page: Option<u32>,
265 #[builder(into)]
267 pub limit: Option<u32>,
268 #[builder(into)]
270 pub cursor: Option<String>,
271 #[builder(into)]
273 pub shape: Option<String>,
274 #[builder(default)]
276 pub flat: bool,
277 #[builder(default)]
279 pub flat_lists: bool,
280
281 #[builder(into)]
283 pub search: Option<String>,
284
285 #[builder(default)]
287 pub extra: BTreeMap<String, String>,
288}
289
290impl ListMasSinsOptions {
291 fn to_query(&self) -> Vec<(String, String)> {
292 let mut q = Vec::new();
293 apply_pagination(
294 &mut q,
295 self.page,
296 self.limit,
297 self.cursor.as_deref(),
298 self.shape.as_deref(),
299 self.flat,
300 self.flat_lists,
301 );
302 push_opt(&mut q, "search", self.search.as_deref());
303 for (k, v) in &self.extra {
304 if !v.is_empty() {
305 q.push((k.clone(), v.clone()));
306 }
307 }
308 q
309 }
310}
311
312#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
318#[non_exhaustive]
319pub struct ListAssistanceListingsOptions {
320 #[builder(into)]
322 pub page: Option<u32>,
323 #[builder(into)]
325 pub limit: Option<u32>,
326 #[builder(into)]
328 pub cursor: Option<String>,
329 #[builder(into)]
331 pub shape: Option<String>,
332 #[builder(default)]
334 pub flat: bool,
335 #[builder(default)]
337 pub flat_lists: bool,
338
339 #[builder(into)]
341 pub search: Option<String>,
342
343 #[builder(default)]
345 pub extra: BTreeMap<String, String>,
346}
347
348impl ListAssistanceListingsOptions {
349 fn to_query(&self) -> Vec<(String, String)> {
350 let mut q = Vec::new();
351 apply_pagination(
352 &mut q,
353 self.page,
354 self.limit,
355 self.cursor.as_deref(),
356 self.shape.as_deref(),
357 self.flat,
358 self.flat_lists,
359 );
360 push_opt(&mut q, "search", self.search.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#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
376#[non_exhaustive]
377pub struct ListBusinessTypesOptions {
378 #[builder(into)]
380 pub page: Option<u32>,
381 #[builder(into)]
383 pub limit: Option<u32>,
384 #[builder(into)]
386 pub cursor: Option<String>,
387 #[builder(into)]
389 pub shape: Option<String>,
390 #[builder(default)]
392 pub flat: bool,
393 #[builder(default)]
395 pub flat_lists: bool,
396
397 #[builder(into)]
399 pub search: Option<String>,
400
401 #[builder(default)]
403 pub extra: BTreeMap<String, String>,
404}
405
406impl ListBusinessTypesOptions {
407 fn to_query(&self) -> Vec<(String, String)> {
408 let mut q = Vec::new();
409 apply_pagination(
410 &mut q,
411 self.page,
412 self.limit,
413 self.cursor.as_deref(),
414 self.shape.as_deref(),
415 self.flat,
416 self.flat_lists,
417 );
418 push_opt(&mut q, "search", self.search.as_deref());
419 for (k, v) in &self.extra {
420 if !v.is_empty() {
421 q.push((k.clone(), v.clone()));
422 }
423 }
424 q
425 }
426}
427
428#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
434#[non_exhaustive]
435pub struct ListOfficesOptions {
436 #[builder(into)]
438 pub page: Option<u32>,
439 #[builder(into)]
441 pub limit: Option<u32>,
442 #[builder(into)]
444 pub cursor: Option<String>,
445 #[builder(into)]
447 pub shape: Option<String>,
448 #[builder(default)]
450 pub flat: bool,
451 #[builder(default)]
453 pub flat_lists: bool,
454
455 #[builder(into)]
457 pub search: Option<String>,
458
459 #[builder(default)]
461 pub extra: BTreeMap<String, String>,
462}
463
464impl ListOfficesOptions {
465 fn to_query(&self) -> Vec<(String, String)> {
466 let mut q = Vec::new();
467 apply_pagination(
468 &mut q,
469 self.page,
470 self.limit,
471 self.cursor.as_deref(),
472 self.shape.as_deref(),
473 self.flat,
474 self.flat_lists,
475 );
476 push_opt(&mut q, "search", self.search.as_deref());
477 for (k, v) in &self.extra {
478 if !v.is_empty() {
479 q.push((k.clone(), v.clone()));
480 }
481 }
482 q
483 }
484}
485
486#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
496#[non_exhaustive]
497pub struct ListDepartmentsOptions {
498 #[builder(into)]
500 pub page: Option<u32>,
501 #[builder(into)]
503 pub limit: Option<u32>,
504 #[builder(into)]
506 pub cursor: Option<String>,
507 #[builder(into)]
509 pub shape: Option<String>,
510 #[builder(default)]
512 pub flat: bool,
513 #[builder(default)]
515 pub flat_lists: bool,
516
517 #[builder(into)]
519 pub search: Option<String>,
520
521 #[builder(default)]
523 pub extra: BTreeMap<String, String>,
524}
525
526impl ListDepartmentsOptions {
527 fn to_query(&self) -> Vec<(String, String)> {
528 let mut q = Vec::new();
529 apply_pagination(
530 &mut q,
531 self.page,
532 self.limit,
533 self.cursor.as_deref(),
534 self.shape.as_deref(),
535 self.flat,
536 self.flat_lists,
537 );
538 push_opt(&mut q, "search", self.search.as_deref());
539 for (k, v) in &self.extra {
540 if !v.is_empty() {
541 q.push((k.clone(), v.clone()));
542 }
543 }
544 q
545 }
546}
547
548impl Client {
553 pub async fn list_organizations(&self, opts: ListOrganizationsOptions) -> Result<Page<Record>> {
557 let q = opts.to_query();
558 self.get_page("/api/organizations/", &q).await
559 }
560
561 pub fn iterate_organizations(&self, opts: ListOrganizationsOptions) -> PageStream<Record> {
563 let opts = Arc::new(opts);
564 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
565 let mut next = (*opts).clone();
566 next.page = page;
567 next.cursor = cursor;
568 Box::pin(async move { client.list_organizations(next).await })
569 });
570 PageStream::new(self.clone(), fetch)
571 }
572
573 pub async fn get_organization(&self, key: &str) -> Result<Record> {
575 if key.is_empty() {
576 return Err(Error::Validation {
577 message: "get_organization: organization key is required".into(),
578 response: None,
579 });
580 }
581 let path = format!("/api/organizations/{}/", urlencoding(key));
582 self.get_json::<Record>(&path, &[]).await
583 }
584
585 pub async fn list_naics(&self, opts: ListNaicsOptions) -> Result<Page<Record>> {
589 let q = opts.to_query();
590 self.get_page("/api/naics/", &q).await
591 }
592
593 pub fn iterate_naics(&self, opts: ListNaicsOptions) -> PageStream<Record> {
595 let opts = Arc::new(opts);
596 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
597 let mut next = (*opts).clone();
598 next.page = page;
599 next.cursor = cursor;
600 Box::pin(async move { client.list_naics(next).await })
601 });
602 PageStream::new(self.clone(), fetch)
603 }
604
605 pub async fn get_naics(&self, code: &str) -> Result<Record> {
607 if code.is_empty() {
608 return Err(Error::Validation {
609 message: "get_naics: NAICS code is required".into(),
610 response: None,
611 });
612 }
613 let path = format!("/api/naics/{}/", urlencoding(code));
614 self.get_json::<Record>(&path, &[]).await
615 }
616
617 pub async fn list_psc(&self, opts: ListPscOptions) -> Result<Page<Record>> {
621 let q = opts.to_query();
622 self.get_page("/api/psc/", &q).await
623 }
624
625 pub fn iterate_psc(&self, opts: ListPscOptions) -> PageStream<Record> {
627 let opts = Arc::new(opts);
628 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
629 let mut next = (*opts).clone();
630 next.page = page;
631 next.cursor = cursor;
632 Box::pin(async move { client.list_psc(next).await })
633 });
634 PageStream::new(self.clone(), fetch)
635 }
636
637 pub async fn get_psc(&self, code: &str) -> Result<Record> {
639 if code.is_empty() {
640 return Err(Error::Validation {
641 message: "get_psc: PSC code is required".into(),
642 response: None,
643 });
644 }
645 let path = format!("/api/psc/{}/", urlencoding(code));
646 self.get_json::<Record>(&path, &[]).await
647 }
648
649 pub async fn list_mas_sins(&self, opts: ListMasSinsOptions) -> Result<Page<Record>> {
653 let q = opts.to_query();
654 self.get_page("/api/mas_sins/", &q).await
655 }
656
657 pub fn iterate_mas_sins(&self, opts: ListMasSinsOptions) -> PageStream<Record> {
659 let opts = Arc::new(opts);
660 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
661 let mut next = (*opts).clone();
662 next.page = page;
663 next.cursor = cursor;
664 Box::pin(async move { client.list_mas_sins(next).await })
665 });
666 PageStream::new(self.clone(), fetch)
667 }
668
669 pub async fn get_mas_sin(&self, sin: &str) -> Result<Record> {
671 if sin.is_empty() {
672 return Err(Error::Validation {
673 message: "get_mas_sin: MAS SIN is required".into(),
674 response: None,
675 });
676 }
677 let path = format!("/api/mas_sins/{}/", urlencoding(sin));
678 self.get_json::<Record>(&path, &[]).await
679 }
680
681 pub async fn list_assistance_listings(
685 &self,
686 opts: ListAssistanceListingsOptions,
687 ) -> Result<Page<Record>> {
688 let q = opts.to_query();
689 self.get_page("/api/assistance_listings/", &q).await
690 }
691
692 pub fn iterate_assistance_listings(
694 &self,
695 opts: ListAssistanceListingsOptions,
696 ) -> PageStream<Record> {
697 let opts = Arc::new(opts);
698 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
699 let mut next = (*opts).clone();
700 next.page = page;
701 next.cursor = cursor;
702 Box::pin(async move { client.list_assistance_listings(next).await })
703 });
704 PageStream::new(self.clone(), fetch)
705 }
706
707 pub async fn get_assistance_listing(&self, number: &str) -> Result<Record> {
710 if number.is_empty() {
711 return Err(Error::Validation {
712 message: "get_assistance_listing: assistance listing number is required".into(),
713 response: None,
714 });
715 }
716 let path = format!("/api/assistance_listings/{}/", urlencoding(number));
717 self.get_json::<Record>(&path, &[]).await
718 }
719
720 pub async fn list_business_types(
725 &self,
726 opts: ListBusinessTypesOptions,
727 ) -> Result<Page<Record>> {
728 let q = opts.to_query();
729 self.get_page("/api/business_types/", &q).await
730 }
731
732 pub fn iterate_business_types(&self, opts: ListBusinessTypesOptions) -> PageStream<Record> {
734 let opts = Arc::new(opts);
735 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
736 let mut next = (*opts).clone();
737 next.page = page;
738 next.cursor = cursor;
739 Box::pin(async move { client.list_business_types(next).await })
740 });
741 PageStream::new(self.clone(), fetch)
742 }
743
744 pub async fn get_business_type(&self, code: &str) -> Result<Record> {
747 if code.is_empty() {
748 return Err(Error::Validation {
749 message: "get_business_type: business type code is required".into(),
750 response: None,
751 });
752 }
753 let path = format!("/api/business_types/{}/", urlencoding(code));
754 self.get_json::<Record>(&path, &[]).await
755 }
756
757 pub async fn list_offices(&self, opts: ListOfficesOptions) -> Result<Page<Record>> {
761 let q = opts.to_query();
762 self.get_page("/api/offices/", &q).await
763 }
764
765 pub fn iterate_offices(&self, opts: ListOfficesOptions) -> PageStream<Record> {
767 let opts = Arc::new(opts);
768 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
769 let mut next = (*opts).clone();
770 next.page = page;
771 next.cursor = cursor;
772 Box::pin(async move { client.list_offices(next).await })
773 });
774 PageStream::new(self.clone(), fetch)
775 }
776
777 pub async fn get_office(&self, code: &str) -> Result<Record> {
780 if code.is_empty() {
781 return Err(Error::Validation {
782 message: "get_office: office code is required".into(),
783 response: None,
784 });
785 }
786 let path = format!("/api/offices/{}/", urlencoding(code));
787 self.get_json::<Record>(&path, &[]).await
788 }
789
790 pub async fn list_departments(&self, opts: ListDepartmentsOptions) -> Result<Page<Record>> {
798 let q = opts.to_query();
799 self.get_page("/api/departments/", &q).await
800 }
801
802 pub fn iterate_departments(&self, opts: ListDepartmentsOptions) -> PageStream<Record> {
804 let opts = Arc::new(opts);
805 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
806 let mut next = (*opts).clone();
807 next.page = page;
808 next.cursor = cursor;
809 Box::pin(async move { client.list_departments(next).await })
810 });
811 PageStream::new(self.clone(), fetch)
812 }
813
814 pub async fn get_department(&self, code: &str) -> Result<Record> {
819 if code.is_empty() {
820 return Err(Error::Validation {
821 message: "get_department: department code is required".into(),
822 response: None,
823 });
824 }
825 let path = format!("/api/departments/{}/", urlencoding(code));
826 self.get_json::<Record>(&path, &[]).await
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
835 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
836 }
837
838 #[test]
839 fn psc_has_awards_emits() {
840 let q = ListPscOptions::builder()
841 .has_awards(true)
842 .build()
843 .to_query();
844 assert_eq!(get_q(&q, "has_awards").as_deref(), Some("true"));
845 assert!(ListPscOptions::builder().build().to_query().is_empty());
846 }
847
848 #[test]
851 fn organizations_emits_filters() {
852 let opts = ListOrganizationsOptions::builder()
853 .search("Defense")
854 .r#type("agency")
855 .level("1")
856 .cgac("097")
857 .parent("DOD")
858 .include_inactive(false)
859 .build();
860 let q = opts.to_query();
861 assert_eq!(get_q(&q, "search").as_deref(), Some("Defense"));
862 assert_eq!(get_q(&q, "type").as_deref(), Some("agency"));
863 assert_eq!(get_q(&q, "level").as_deref(), Some("1"));
864 assert_eq!(get_q(&q, "cgac").as_deref(), Some("097"));
865 assert_eq!(get_q(&q, "parent").as_deref(), Some("DOD"));
866 assert_eq!(get_q(&q, "include_inactive").as_deref(), Some("false"));
867 }
868
869 #[tokio::test]
870 async fn get_organization_empty_key_is_validation() {
871 let c = Client::builder().api_key("k").build().expect("client");
872 let err = c.get_organization("").await.expect_err("must error");
873 assert!(matches!(err, Error::Validation { .. }));
874 }
875
876 #[test]
879 fn naics_emits_size_filters() {
880 let opts = ListNaicsOptions::builder()
881 .search("software")
882 .revenue_limit_gte("1000000")
883 .employee_limit_lte("500")
884 .build();
885 let q = opts.to_query();
886 assert_eq!(get_q(&q, "search").as_deref(), Some("software"));
887 assert_eq!(get_q(&q, "revenue_limit_gte").as_deref(), Some("1000000"));
888 assert_eq!(get_q(&q, "employee_limit_lte").as_deref(), Some("500"));
889 }
890
891 #[tokio::test]
892 async fn get_naics_empty_code_is_validation() {
893 let c = Client::builder().api_key("k").build().expect("client");
894 let err = c.get_naics("").await.expect_err("must error");
895 assert!(matches!(err, Error::Validation { .. }));
896 }
897
898 #[test]
901 fn psc_emits_search() {
902 let opts = ListPscOptions::builder().search("services").build();
903 let q = opts.to_query();
904 assert_eq!(get_q(&q, "search").as_deref(), Some("services"));
905 }
906
907 #[tokio::test]
908 async fn get_psc_empty_code_is_validation() {
909 let c = Client::builder().api_key("k").build().expect("client");
910 let err = c.get_psc("").await.expect_err("must error");
911 assert!(matches!(err, Error::Validation { .. }));
912 }
913
914 #[test]
917 fn mas_sins_emits_search() {
918 let opts = ListMasSinsOptions::builder().search("54151S").build();
919 let q = opts.to_query();
920 assert_eq!(get_q(&q, "search").as_deref(), Some("54151S"));
921 }
922
923 #[tokio::test]
924 async fn get_mas_sin_empty_is_validation() {
925 let c = Client::builder().api_key("k").build().expect("client");
926 let err = c.get_mas_sin("").await.expect_err("must error");
927 assert!(matches!(err, Error::Validation { .. }));
928 }
929
930 #[test]
933 fn assistance_listings_paginates() {
934 let opts = ListAssistanceListingsOptions::builder()
935 .page(2u32)
936 .limit(50u32)
937 .build();
938 let q = opts.to_query();
939 assert_eq!(get_q(&q, "page").as_deref(), Some("2"));
940 assert_eq!(get_q(&q, "limit").as_deref(), Some("50"));
941 }
942
943 #[tokio::test]
944 async fn get_assistance_listing_empty_is_validation() {
945 let c = Client::builder().api_key("k").build().expect("client");
946 let err = c.get_assistance_listing("").await.expect_err("must error");
947 assert!(matches!(err, Error::Validation { .. }));
948 }
949
950 #[test]
953 fn business_types_passes_shape() {
954 let opts = ListBusinessTypesOptions::builder()
955 .shape("code,name")
956 .build();
957 let q = opts.to_query();
958 assert_eq!(get_q(&q, "shape").as_deref(), Some("code,name"));
959 }
960
961 #[tokio::test]
962 async fn get_business_type_empty_is_validation() {
963 let c = Client::builder().api_key("k").build().expect("client");
964 let err = c.get_business_type("").await.expect_err("must error");
965 assert!(matches!(err, Error::Validation { .. }));
966 }
967
968 #[test]
971 fn offices_emits_search() {
972 let opts = ListOfficesOptions::builder().search("FA8650").build();
973 let q = opts.to_query();
974 assert_eq!(get_q(&q, "search").as_deref(), Some("FA8650"));
975 }
976
977 #[tokio::test]
978 async fn get_office_empty_is_validation() {
979 let c = Client::builder().api_key("k").build().expect("client");
980 let err = c.get_office("").await.expect_err("must error");
981 assert!(matches!(err, Error::Validation { .. }));
982 }
983
984 #[test]
987 fn departments_passes_cursor() {
988 let opts = ListDepartmentsOptions::builder()
989 .cursor("abc".to_string())
990 .build();
991 let q = opts.to_query();
992 assert_eq!(get_q(&q, "cursor").as_deref(), Some("abc"));
993 }
994
995 #[tokio::test]
996 async fn get_department_empty_is_validation() {
997 let c = Client::builder().api_key("k").build().expect("client");
998 let err = c.get_department("").await.expect_err("must error");
999 assert!(matches!(err, Error::Validation { .. }));
1000 }
1001
1002 #[test]
1005 fn extra_keys_pass_through() {
1006 let mut extra = BTreeMap::new();
1007 extra.insert("custom".into(), "value".into());
1008 let opts = ListNaicsOptions::builder().extra(extra).build();
1009 let q = opts.to_query();
1010 assert_eq!(get_q(&q, "custom").as_deref(), Some("value"));
1011 }
1012
1013 #[test]
1014 fn empty_extra_value_is_skipped() {
1015 let mut extra = BTreeMap::new();
1016 extra.insert("skip".into(), String::new());
1017 let opts = ListPscOptions::builder().extra(extra).build();
1018 let q = opts.to_query();
1019 assert_eq!(get_q(&q, "skip"), None);
1020 }
1021}