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
224 #[builder(default)]
226 pub extra: BTreeMap<String, String>,
227}
228
229impl ListPscOptions {
230 fn to_query(&self) -> Vec<(String, String)> {
231 let mut q = Vec::new();
232 apply_pagination(
233 &mut q,
234 self.page,
235 self.limit,
236 self.cursor.as_deref(),
237 self.shape.as_deref(),
238 self.flat,
239 self.flat_lists,
240 );
241 push_opt(&mut q, "search", self.search.as_deref());
242 for (k, v) in &self.extra {
243 if !v.is_empty() {
244 q.push((k.clone(), v.clone()));
245 }
246 }
247 q
248 }
249}
250
251#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
257#[non_exhaustive]
258pub struct ListMasSinsOptions {
259 #[builder(into)]
261 pub page: Option<u32>,
262 #[builder(into)]
264 pub limit: Option<u32>,
265 #[builder(into)]
267 pub cursor: Option<String>,
268 #[builder(into)]
270 pub shape: Option<String>,
271 #[builder(default)]
273 pub flat: bool,
274 #[builder(default)]
276 pub flat_lists: bool,
277
278 #[builder(into)]
280 pub search: Option<String>,
281
282 #[builder(default)]
284 pub extra: BTreeMap<String, String>,
285}
286
287impl ListMasSinsOptions {
288 fn to_query(&self) -> Vec<(String, String)> {
289 let mut q = Vec::new();
290 apply_pagination(
291 &mut q,
292 self.page,
293 self.limit,
294 self.cursor.as_deref(),
295 self.shape.as_deref(),
296 self.flat,
297 self.flat_lists,
298 );
299 push_opt(&mut q, "search", self.search.as_deref());
300 for (k, v) in &self.extra {
301 if !v.is_empty() {
302 q.push((k.clone(), v.clone()));
303 }
304 }
305 q
306 }
307}
308
309#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
315#[non_exhaustive]
316pub struct ListAssistanceListingsOptions {
317 #[builder(into)]
319 pub page: Option<u32>,
320 #[builder(into)]
322 pub limit: Option<u32>,
323 #[builder(into)]
325 pub cursor: Option<String>,
326 #[builder(into)]
328 pub shape: Option<String>,
329 #[builder(default)]
331 pub flat: bool,
332 #[builder(default)]
334 pub flat_lists: bool,
335
336 #[builder(into)]
338 pub search: Option<String>,
339
340 #[builder(default)]
342 pub extra: BTreeMap<String, String>,
343}
344
345impl ListAssistanceListingsOptions {
346 fn to_query(&self) -> Vec<(String, String)> {
347 let mut q = Vec::new();
348 apply_pagination(
349 &mut q,
350 self.page,
351 self.limit,
352 self.cursor.as_deref(),
353 self.shape.as_deref(),
354 self.flat,
355 self.flat_lists,
356 );
357 push_opt(&mut q, "search", self.search.as_deref());
358 for (k, v) in &self.extra {
359 if !v.is_empty() {
360 q.push((k.clone(), v.clone()));
361 }
362 }
363 q
364 }
365}
366
367#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
373#[non_exhaustive]
374pub struct ListBusinessTypesOptions {
375 #[builder(into)]
377 pub page: Option<u32>,
378 #[builder(into)]
380 pub limit: Option<u32>,
381 #[builder(into)]
383 pub cursor: Option<String>,
384 #[builder(into)]
386 pub shape: Option<String>,
387 #[builder(default)]
389 pub flat: bool,
390 #[builder(default)]
392 pub flat_lists: bool,
393
394 #[builder(into)]
396 pub search: Option<String>,
397
398 #[builder(default)]
400 pub extra: BTreeMap<String, String>,
401}
402
403impl ListBusinessTypesOptions {
404 fn to_query(&self) -> Vec<(String, String)> {
405 let mut q = Vec::new();
406 apply_pagination(
407 &mut q,
408 self.page,
409 self.limit,
410 self.cursor.as_deref(),
411 self.shape.as_deref(),
412 self.flat,
413 self.flat_lists,
414 );
415 push_opt(&mut q, "search", self.search.as_deref());
416 for (k, v) in &self.extra {
417 if !v.is_empty() {
418 q.push((k.clone(), v.clone()));
419 }
420 }
421 q
422 }
423}
424
425#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
431#[non_exhaustive]
432pub struct ListOfficesOptions {
433 #[builder(into)]
435 pub page: Option<u32>,
436 #[builder(into)]
438 pub limit: Option<u32>,
439 #[builder(into)]
441 pub cursor: Option<String>,
442 #[builder(into)]
444 pub shape: Option<String>,
445 #[builder(default)]
447 pub flat: bool,
448 #[builder(default)]
450 pub flat_lists: bool,
451
452 #[builder(into)]
454 pub search: Option<String>,
455
456 #[builder(default)]
458 pub extra: BTreeMap<String, String>,
459}
460
461impl ListOfficesOptions {
462 fn to_query(&self) -> Vec<(String, String)> {
463 let mut q = Vec::new();
464 apply_pagination(
465 &mut q,
466 self.page,
467 self.limit,
468 self.cursor.as_deref(),
469 self.shape.as_deref(),
470 self.flat,
471 self.flat_lists,
472 );
473 push_opt(&mut q, "search", self.search.as_deref());
474 for (k, v) in &self.extra {
475 if !v.is_empty() {
476 q.push((k.clone(), v.clone()));
477 }
478 }
479 q
480 }
481}
482
483#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
493#[non_exhaustive]
494pub struct ListDepartmentsOptions {
495 #[builder(into)]
497 pub page: Option<u32>,
498 #[builder(into)]
500 pub limit: Option<u32>,
501 #[builder(into)]
503 pub cursor: Option<String>,
504 #[builder(into)]
506 pub shape: Option<String>,
507 #[builder(default)]
509 pub flat: bool,
510 #[builder(default)]
512 pub flat_lists: bool,
513
514 #[builder(into)]
516 pub search: Option<String>,
517
518 #[builder(default)]
520 pub extra: BTreeMap<String, String>,
521}
522
523impl ListDepartmentsOptions {
524 fn to_query(&self) -> Vec<(String, String)> {
525 let mut q = Vec::new();
526 apply_pagination(
527 &mut q,
528 self.page,
529 self.limit,
530 self.cursor.as_deref(),
531 self.shape.as_deref(),
532 self.flat,
533 self.flat_lists,
534 );
535 push_opt(&mut q, "search", self.search.as_deref());
536 for (k, v) in &self.extra {
537 if !v.is_empty() {
538 q.push((k.clone(), v.clone()));
539 }
540 }
541 q
542 }
543}
544
545impl Client {
550 pub async fn list_organizations(&self, opts: ListOrganizationsOptions) -> Result<Page<Record>> {
554 let q = opts.to_query();
555 let bytes = self.get_bytes("/api/organizations/", &q).await?;
556 Page::decode(&bytes)
557 }
558
559 pub fn iterate_organizations(&self, opts: ListOrganizationsOptions) -> PageStream<Record> {
561 let opts = Arc::new(opts);
562 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
563 let mut next = (*opts).clone();
564 next.page = page;
565 next.cursor = cursor;
566 Box::pin(async move { client.list_organizations(next).await })
567 });
568 PageStream::new(self.clone(), fetch)
569 }
570
571 pub async fn get_organization(&self, key: &str) -> Result<Record> {
573 if key.is_empty() {
574 return Err(Error::Validation {
575 message: "get_organization: organization key is required".into(),
576 response: None,
577 });
578 }
579 let path = format!("/api/organizations/{}/", urlencoding(key));
580 self.get_json::<Record>(&path, &[]).await
581 }
582
583 pub async fn list_naics(&self, opts: ListNaicsOptions) -> Result<Page<Record>> {
587 let q = opts.to_query();
588 let bytes = self.get_bytes("/api/naics/", &q).await?;
589 Page::decode(&bytes)
590 }
591
592 pub fn iterate_naics(&self, opts: ListNaicsOptions) -> PageStream<Record> {
594 let opts = Arc::new(opts);
595 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
596 let mut next = (*opts).clone();
597 next.page = page;
598 next.cursor = cursor;
599 Box::pin(async move { client.list_naics(next).await })
600 });
601 PageStream::new(self.clone(), fetch)
602 }
603
604 pub async fn get_naics(&self, code: &str) -> Result<Record> {
606 if code.is_empty() {
607 return Err(Error::Validation {
608 message: "get_naics: NAICS code is required".into(),
609 response: None,
610 });
611 }
612 let path = format!("/api/naics/{}/", urlencoding(code));
613 self.get_json::<Record>(&path, &[]).await
614 }
615
616 pub async fn list_psc(&self, opts: ListPscOptions) -> Result<Page<Record>> {
620 let q = opts.to_query();
621 let bytes = self.get_bytes("/api/psc/", &q).await?;
622 Page::decode(&bytes)
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 let bytes = self.get_bytes("/api/mas_sins/", &q).await?;
655 Page::decode(&bytes)
656 }
657
658 pub fn iterate_mas_sins(&self, opts: ListMasSinsOptions) -> PageStream<Record> {
660 let opts = Arc::new(opts);
661 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
662 let mut next = (*opts).clone();
663 next.page = page;
664 next.cursor = cursor;
665 Box::pin(async move { client.list_mas_sins(next).await })
666 });
667 PageStream::new(self.clone(), fetch)
668 }
669
670 pub async fn get_mas_sin(&self, sin: &str) -> Result<Record> {
672 if sin.is_empty() {
673 return Err(Error::Validation {
674 message: "get_mas_sin: MAS SIN is required".into(),
675 response: None,
676 });
677 }
678 let path = format!("/api/mas_sins/{}/", urlencoding(sin));
679 self.get_json::<Record>(&path, &[]).await
680 }
681
682 pub async fn list_assistance_listings(
686 &self,
687 opts: ListAssistanceListingsOptions,
688 ) -> Result<Page<Record>> {
689 let q = opts.to_query();
690 let bytes = self.get_bytes("/api/assistance_listings/", &q).await?;
691 Page::decode(&bytes)
692 }
693
694 pub fn iterate_assistance_listings(
696 &self,
697 opts: ListAssistanceListingsOptions,
698 ) -> PageStream<Record> {
699 let opts = Arc::new(opts);
700 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
701 let mut next = (*opts).clone();
702 next.page = page;
703 next.cursor = cursor;
704 Box::pin(async move { client.list_assistance_listings(next).await })
705 });
706 PageStream::new(self.clone(), fetch)
707 }
708
709 pub async fn get_assistance_listing(&self, number: &str) -> Result<Record> {
712 if number.is_empty() {
713 return Err(Error::Validation {
714 message: "get_assistance_listing: assistance listing number is required".into(),
715 response: None,
716 });
717 }
718 let path = format!("/api/assistance_listings/{}/", urlencoding(number));
719 self.get_json::<Record>(&path, &[]).await
720 }
721
722 pub async fn list_business_types(
727 &self,
728 opts: ListBusinessTypesOptions,
729 ) -> Result<Page<Record>> {
730 let q = opts.to_query();
731 let bytes = self.get_bytes("/api/business_types/", &q).await?;
732 Page::decode(&bytes)
733 }
734
735 pub fn iterate_business_types(&self, opts: ListBusinessTypesOptions) -> PageStream<Record> {
737 let opts = Arc::new(opts);
738 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
739 let mut next = (*opts).clone();
740 next.page = page;
741 next.cursor = cursor;
742 Box::pin(async move { client.list_business_types(next).await })
743 });
744 PageStream::new(self.clone(), fetch)
745 }
746
747 pub async fn get_business_type(&self, code: &str) -> Result<Record> {
750 if code.is_empty() {
751 return Err(Error::Validation {
752 message: "get_business_type: business type code is required".into(),
753 response: None,
754 });
755 }
756 let path = format!("/api/business_types/{}/", urlencoding(code));
757 self.get_json::<Record>(&path, &[]).await
758 }
759
760 pub async fn list_offices(&self, opts: ListOfficesOptions) -> Result<Page<Record>> {
764 let q = opts.to_query();
765 let bytes = self.get_bytes("/api/offices/", &q).await?;
766 Page::decode(&bytes)
767 }
768
769 pub fn iterate_offices(&self, opts: ListOfficesOptions) -> PageStream<Record> {
771 let opts = Arc::new(opts);
772 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
773 let mut next = (*opts).clone();
774 next.page = page;
775 next.cursor = cursor;
776 Box::pin(async move { client.list_offices(next).await })
777 });
778 PageStream::new(self.clone(), fetch)
779 }
780
781 pub async fn get_office(&self, code: &str) -> Result<Record> {
784 if code.is_empty() {
785 return Err(Error::Validation {
786 message: "get_office: office code is required".into(),
787 response: None,
788 });
789 }
790 let path = format!("/api/offices/{}/", urlencoding(code));
791 self.get_json::<Record>(&path, &[]).await
792 }
793
794 pub async fn list_departments(&self, opts: ListDepartmentsOptions) -> Result<Page<Record>> {
802 let q = opts.to_query();
803 let bytes = self.get_bytes("/api/departments/", &q).await?;
804 Page::decode(&bytes)
805 }
806
807 pub fn iterate_departments(&self, opts: ListDepartmentsOptions) -> PageStream<Record> {
809 let opts = Arc::new(opts);
810 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
811 let mut next = (*opts).clone();
812 next.page = page;
813 next.cursor = cursor;
814 Box::pin(async move { client.list_departments(next).await })
815 });
816 PageStream::new(self.clone(), fetch)
817 }
818
819 pub async fn get_department(&self, code: &str) -> Result<Record> {
822 if code.is_empty() {
823 return Err(Error::Validation {
824 message: "get_department: department code is required".into(),
825 response: None,
826 });
827 }
828 let path = format!("/api/departments/{}/", urlencoding(code));
829 self.get_json::<Record>(&path, &[]).await
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
838 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
839 }
840
841 #[test]
844 fn organizations_emits_filters() {
845 let opts = ListOrganizationsOptions::builder()
846 .search("Defense")
847 .r#type("agency")
848 .level("1")
849 .cgac("097")
850 .parent("DOD")
851 .include_inactive(false)
852 .build();
853 let q = opts.to_query();
854 assert_eq!(get_q(&q, "search").as_deref(), Some("Defense"));
855 assert_eq!(get_q(&q, "type").as_deref(), Some("agency"));
856 assert_eq!(get_q(&q, "level").as_deref(), Some("1"));
857 assert_eq!(get_q(&q, "cgac").as_deref(), Some("097"));
858 assert_eq!(get_q(&q, "parent").as_deref(), Some("DOD"));
859 assert_eq!(get_q(&q, "include_inactive").as_deref(), Some("false"));
860 }
861
862 #[tokio::test]
863 async fn get_organization_empty_key_is_validation() {
864 let c = Client::builder().api_key("k").build().expect("client");
865 let err = c.get_organization("").await.expect_err("must error");
866 assert!(matches!(err, Error::Validation { .. }));
867 }
868
869 #[test]
872 fn naics_emits_size_filters() {
873 let opts = ListNaicsOptions::builder()
874 .search("software")
875 .revenue_limit_gte("1000000")
876 .employee_limit_lte("500")
877 .build();
878 let q = opts.to_query();
879 assert_eq!(get_q(&q, "search").as_deref(), Some("software"));
880 assert_eq!(get_q(&q, "revenue_limit_gte").as_deref(), Some("1000000"));
881 assert_eq!(get_q(&q, "employee_limit_lte").as_deref(), Some("500"));
882 }
883
884 #[tokio::test]
885 async fn get_naics_empty_code_is_validation() {
886 let c = Client::builder().api_key("k").build().expect("client");
887 let err = c.get_naics("").await.expect_err("must error");
888 assert!(matches!(err, Error::Validation { .. }));
889 }
890
891 #[test]
894 fn psc_emits_search() {
895 let opts = ListPscOptions::builder().search("services").build();
896 let q = opts.to_query();
897 assert_eq!(get_q(&q, "search").as_deref(), Some("services"));
898 }
899
900 #[tokio::test]
901 async fn get_psc_empty_code_is_validation() {
902 let c = Client::builder().api_key("k").build().expect("client");
903 let err = c.get_psc("").await.expect_err("must error");
904 assert!(matches!(err, Error::Validation { .. }));
905 }
906
907 #[test]
910 fn mas_sins_emits_search() {
911 let opts = ListMasSinsOptions::builder().search("54151S").build();
912 let q = opts.to_query();
913 assert_eq!(get_q(&q, "search").as_deref(), Some("54151S"));
914 }
915
916 #[tokio::test]
917 async fn get_mas_sin_empty_is_validation() {
918 let c = Client::builder().api_key("k").build().expect("client");
919 let err = c.get_mas_sin("").await.expect_err("must error");
920 assert!(matches!(err, Error::Validation { .. }));
921 }
922
923 #[test]
926 fn assistance_listings_paginates() {
927 let opts = ListAssistanceListingsOptions::builder()
928 .page(2u32)
929 .limit(50u32)
930 .build();
931 let q = opts.to_query();
932 assert_eq!(get_q(&q, "page").as_deref(), Some("2"));
933 assert_eq!(get_q(&q, "limit").as_deref(), Some("50"));
934 }
935
936 #[tokio::test]
937 async fn get_assistance_listing_empty_is_validation() {
938 let c = Client::builder().api_key("k").build().expect("client");
939 let err = c.get_assistance_listing("").await.expect_err("must error");
940 assert!(matches!(err, Error::Validation { .. }));
941 }
942
943 #[test]
946 fn business_types_passes_shape() {
947 let opts = ListBusinessTypesOptions::builder()
948 .shape("code,name")
949 .build();
950 let q = opts.to_query();
951 assert_eq!(get_q(&q, "shape").as_deref(), Some("code,name"));
952 }
953
954 #[tokio::test]
955 async fn get_business_type_empty_is_validation() {
956 let c = Client::builder().api_key("k").build().expect("client");
957 let err = c.get_business_type("").await.expect_err("must error");
958 assert!(matches!(err, Error::Validation { .. }));
959 }
960
961 #[test]
964 fn offices_emits_search() {
965 let opts = ListOfficesOptions::builder().search("FA8650").build();
966 let q = opts.to_query();
967 assert_eq!(get_q(&q, "search").as_deref(), Some("FA8650"));
968 }
969
970 #[tokio::test]
971 async fn get_office_empty_is_validation() {
972 let c = Client::builder().api_key("k").build().expect("client");
973 let err = c.get_office("").await.expect_err("must error");
974 assert!(matches!(err, Error::Validation { .. }));
975 }
976
977 #[test]
980 fn departments_passes_cursor() {
981 let opts = ListDepartmentsOptions::builder()
982 .cursor("abc".to_string())
983 .build();
984 let q = opts.to_query();
985 assert_eq!(get_q(&q, "cursor").as_deref(), Some("abc"));
986 }
987
988 #[tokio::test]
989 async fn get_department_empty_is_validation() {
990 let c = Client::builder().api_key("k").build().expect("client");
991 let err = c.get_department("").await.expect_err("must error");
992 assert!(matches!(err, Error::Validation { .. }));
993 }
994
995 #[test]
998 fn extra_keys_pass_through() {
999 let mut extra = BTreeMap::new();
1000 extra.insert("custom".into(), "value".into());
1001 let opts = ListNaicsOptions::builder().extra(extra).build();
1002 let q = opts.to_query();
1003 assert_eq!(get_q(&q, "custom").as_deref(), Some("value"));
1004 }
1005
1006 #[test]
1007 fn empty_extra_value_is_skipped() {
1008 let mut extra = BTreeMap::new();
1009 extra.insert("skip".into(), String::new());
1010 let opts = ListPscOptions::builder().extra(extra).build();
1011 let q = opts.to_query();
1012 assert_eq!(get_q(&q, "skip"), None);
1013 }
1014}