1use crate::client::Client;
4use crate::error::Result;
5use crate::internal::{apply_pagination, push_opt};
6use crate::pagination::{FetchFn, Page, PageStream};
7use crate::Record;
8use bon::Builder;
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
18#[non_exhaustive]
19pub struct ListSubawardsOptions {
20 #[builder(into)]
22 pub page: Option<u32>,
23 #[builder(into)]
25 pub limit: Option<u32>,
26 #[builder(into)]
28 pub cursor: Option<String>,
29 #[builder(into)]
31 pub shape: Option<String>,
32 #[builder(default)]
34 pub flat: bool,
35 #[builder(default)]
37 pub flat_lists: bool,
38
39 #[builder(into)]
41 pub award_key: Option<String>,
42 #[builder(into)]
44 pub prime_uei: Option<String>,
45 #[builder(into)]
47 pub sub_uei: Option<String>,
48 #[builder(into)]
50 pub awarding_agency: Option<String>,
51 #[builder(into)]
53 pub funding_agency: Option<String>,
54 #[builder(into)]
56 pub fiscal_year: Option<String>,
57 #[builder(into)]
59 pub fiscal_year_gte: Option<String>,
60 #[builder(into)]
62 pub fiscal_year_lte: Option<String>,
63 #[builder(into)]
65 pub recipient: Option<String>,
66 #[builder(into)]
69 pub ordering: Option<String>,
70
71 #[builder(default)]
73 pub extra: BTreeMap<String, String>,
74}
75
76impl ListSubawardsOptions {
77 fn to_query(&self) -> Vec<(String, String)> {
78 let mut q = Vec::new();
79 apply_pagination(
80 &mut q,
81 self.page,
82 self.limit,
83 self.cursor.as_deref(),
84 self.shape.as_deref(),
85 self.flat,
86 self.flat_lists,
87 );
88 push_opt(&mut q, "award_key", self.award_key.as_deref());
89 push_opt(&mut q, "prime_uei", self.prime_uei.as_deref());
90 push_opt(&mut q, "sub_uei", self.sub_uei.as_deref());
91 push_opt(&mut q, "awarding_agency", self.awarding_agency.as_deref());
92 push_opt(&mut q, "funding_agency", self.funding_agency.as_deref());
93 push_opt(&mut q, "fiscal_year", self.fiscal_year.as_deref());
94 push_opt(&mut q, "fiscal_year_gte", self.fiscal_year_gte.as_deref());
95 push_opt(&mut q, "fiscal_year_lte", self.fiscal_year_lte.as_deref());
96 push_opt(&mut q, "recipient", self.recipient.as_deref());
97 push_opt(&mut q, "ordering", self.ordering.as_deref());
98 for (k, v) in &self.extra {
99 if !v.is_empty() {
100 q.push((k.clone(), v.clone()));
101 }
102 }
103 q
104 }
105}
106
107impl Client {
108 pub async fn list_subawards(&self, opts: ListSubawardsOptions) -> Result<Page<Record>> {
110 let q = opts.to_query();
111 let bytes = self.get_bytes("/api/subawards/", &q).await?;
112 Page::decode(&bytes)
113 }
114
115 pub fn iterate_subawards(&self, opts: ListSubawardsOptions) -> PageStream<Record> {
117 let opts = Arc::new(opts);
118 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
119 let mut next = (*opts).clone();
120 next.page = page;
121 next.cursor = cursor;
122 Box::pin(async move { client.list_subawards(next).await })
123 });
124 PageStream::new(self.clone(), fetch)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
133 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
134 }
135
136 #[test]
137 fn subawards_emits_all_filters() {
138 let opts = ListSubawardsOptions::builder()
139 .award_key("PIID-123")
140 .prime_uei("UEI-PRIME")
141 .sub_uei("UEI-SUB")
142 .awarding_agency("9700")
143 .funding_agency("9700")
144 .fiscal_year("2024")
145 .fiscal_year_gte("2023")
146 .fiscal_year_lte("2025")
147 .recipient("Acme Inc")
148 .ordering("-last_modified_date")
149 .build();
150 let q = opts.to_query();
151 assert_eq!(get_q(&q, "award_key").as_deref(), Some("PIID-123"));
152 assert_eq!(get_q(&q, "prime_uei").as_deref(), Some("UEI-PRIME"));
153 assert_eq!(get_q(&q, "sub_uei").as_deref(), Some("UEI-SUB"));
154 assert_eq!(get_q(&q, "awarding_agency").as_deref(), Some("9700"));
155 assert_eq!(get_q(&q, "funding_agency").as_deref(), Some("9700"));
156 assert_eq!(get_q(&q, "fiscal_year").as_deref(), Some("2024"));
157 assert_eq!(get_q(&q, "fiscal_year_gte").as_deref(), Some("2023"));
158 assert_eq!(get_q(&q, "fiscal_year_lte").as_deref(), Some("2025"));
159 assert_eq!(get_q(&q, "recipient").as_deref(), Some("Acme Inc"));
160 assert_eq!(
161 get_q(&q, "ordering").as_deref(),
162 Some("-last_modified_date")
163 );
164 }
165
166 #[test]
167 fn subawards_pagination_emits() {
168 let opts = ListSubawardsOptions::builder()
169 .page(2u32)
170 .limit(50u32)
171 .shape(crate::SHAPE_SUBAWARDS_MINIMAL)
172 .build();
173 let q = opts.to_query();
174 assert_eq!(get_q(&q, "page").as_deref(), Some("2"));
175 assert_eq!(get_q(&q, "limit").as_deref(), Some("50"));
176 assert_eq!(
177 get_q(&q, "shape").as_deref(),
178 Some(crate::SHAPE_SUBAWARDS_MINIMAL)
179 );
180 }
181
182 #[test]
183 fn subawards_extra_map() {
184 let mut extra = BTreeMap::new();
185 extra.insert("x".to_string(), "y".to_string());
186 let opts = ListSubawardsOptions::builder().extra(extra).build();
187 let q = opts.to_query();
188 assert_eq!(get_q(&q, "x").as_deref(), Some("y"));
189 }
190}