1use crate::client::Client;
4use crate::error::{Error, Result};
5use crate::internal::{apply_pagination, push_opt};
6use crate::pagination::{FetchFn, Page, PageStream};
7use crate::resources::agencies::urlencoding;
8use crate::Record;
9use bon::Builder;
10use std::collections::BTreeMap;
11use std::sync::Arc;
12
13#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct ListGsaElibraryContractsOptions {
18 #[builder(into)]
20 pub page: Option<u32>,
21 #[builder(into)]
23 pub limit: Option<u32>,
24 #[builder(into)]
26 pub cursor: Option<String>,
27 #[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 schedule: Option<String>,
42 #[builder(into)]
44 pub contract_number: Option<String>,
45 #[builder(into)]
47 pub key: Option<String>,
48 #[builder(into)]
50 pub piid: Option<String>,
51 #[builder(into)]
53 pub uei: Option<String>,
54 #[builder(into)]
56 pub sin: Option<String>,
57 #[builder(into)]
59 pub search: Option<String>,
60 #[builder(into)]
62 pub ordering: Option<String>,
63
64 #[builder(default)]
66 pub extra: BTreeMap<String, String>,
67}
68
69impl ListGsaElibraryContractsOptions {
70 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
71 let mut q = Vec::new();
72 apply_pagination(
73 &mut q,
74 self.page,
75 self.limit,
76 self.cursor.as_deref(),
77 self.shape.as_deref(),
78 self.flat,
79 self.flat_lists,
80 );
81 push_opt(&mut q, "schedule", self.schedule.as_deref());
82 push_opt(&mut q, "contract_number", self.contract_number.as_deref());
83 push_opt(&mut q, "key", self.key.as_deref());
84 push_opt(&mut q, "piid", self.piid.as_deref());
85 push_opt(&mut q, "uei", self.uei.as_deref());
86 push_opt(&mut q, "sin", self.sin.as_deref());
87 push_opt(&mut q, "search", self.search.as_deref());
88 push_opt(&mut q, "ordering", self.ordering.as_deref());
89 for (k, v) in &self.extra {
90 if !v.is_empty() {
91 q.push((k.clone(), v.clone()));
92 }
93 }
94 q
95 }
96}
97
98#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
100#[non_exhaustive]
101pub struct GetGsaElibraryContractOptions {
102 #[builder(into)]
104 pub shape: Option<String>,
105 #[builder(default)]
107 pub flat: bool,
108 #[builder(default)]
110 pub flat_lists: bool,
111}
112
113impl GetGsaElibraryContractOptions {
114 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
115 let mut q = Vec::new();
116 push_opt(&mut q, "shape", self.shape.as_deref());
117 if self.flat {
118 q.push(("flat".into(), "true".into()));
119 }
120 if self.flat_lists {
121 q.push(("flat_lists".into(), "true".into()));
122 }
123 q
124 }
125}
126
127impl Client {
128 pub async fn list_gsa_elibrary_contracts(
130 &self,
131 opts: ListGsaElibraryContractsOptions,
132 ) -> Result<Page<Record>> {
133 let q = opts.to_query();
134 let bytes = self.get_bytes("/api/gsa_elibrary_contracts/", &q).await?;
135 Page::decode(&bytes)
136 }
137
138 pub async fn get_gsa_elibrary_contract(
141 &self,
142 uuid: &str,
143 opts: Option<GetGsaElibraryContractOptions>,
144 ) -> Result<Record> {
145 if uuid.is_empty() {
146 return Err(Error::Validation {
147 message: "get_gsa_elibrary_contract: uuid is required".into(),
148 response: None,
149 });
150 }
151 let q = opts.unwrap_or_default().to_query();
152 let path = format!("/api/gsa_elibrary_contracts/{}/", urlencoding(uuid));
153 self.get_json::<Record>(&path, &q).await
154 }
155
156 pub fn iterate_gsa_elibrary_contracts(
158 &self,
159 opts: ListGsaElibraryContractsOptions,
160 ) -> PageStream<Record> {
161 let opts = Arc::new(opts);
162 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
163 let mut next = (*opts).clone();
164 next.page = page;
165 next.cursor = cursor;
166 Box::pin(async move { client.list_gsa_elibrary_contracts(next).await })
167 });
168 PageStream::new(self.clone(), fetch)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
177 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
178 }
179
180 #[test]
181 fn list_gsa_all_filters_emit() {
182 let opts = ListGsaElibraryContractsOptions::builder()
183 .schedule("MAS")
184 .contract_number("GS-35F-0119Y")
185 .key("internal-key")
186 .piid("GS35F0119Y")
187 .uei("UEI12345")
188 .sin("54151S")
189 .search("cyber")
190 .ordering("-contract_number")
191 .build();
192 let q = opts.to_query();
193 assert_eq!(get_q(&q, "schedule").as_deref(), Some("MAS"));
194 assert_eq!(
195 get_q(&q, "contract_number").as_deref(),
196 Some("GS-35F-0119Y")
197 );
198 assert_eq!(get_q(&q, "key").as_deref(), Some("internal-key"));
199 assert_eq!(get_q(&q, "piid").as_deref(), Some("GS35F0119Y"));
200 assert_eq!(get_q(&q, "uei").as_deref(), Some("UEI12345"));
201 assert_eq!(get_q(&q, "sin").as_deref(), Some("54151S"));
202 assert_eq!(get_q(&q, "search").as_deref(), Some("cyber"));
203 assert_eq!(get_q(&q, "ordering").as_deref(), Some("-contract_number"));
204 }
205
206 #[test]
207 fn list_gsa_zero_value_omitted() {
208 let opts = ListGsaElibraryContractsOptions::builder().build();
209 let q = opts.to_query();
210 assert!(q.is_empty(), "expected empty query, got {q:?}");
211 }
212
213 #[test]
214 fn list_gsa_shape_emits() {
215 let opts = ListGsaElibraryContractsOptions::builder()
216 .shape(crate::SHAPE_GSA_ELIBRARY_CONTRACTS_MINIMAL)
217 .build();
218 let q = opts.to_query();
219 assert_eq!(
220 get_q(&q, "shape").as_deref(),
221 Some(crate::SHAPE_GSA_ELIBRARY_CONTRACTS_MINIMAL)
222 );
223 }
224
225 #[test]
226 fn list_gsa_cursor_wins_over_page() {
227 let opts = ListGsaElibraryContractsOptions::builder()
228 .page(4u32)
229 .cursor("c0".to_string())
230 .build();
231 let q = opts.to_query();
232 assert_eq!(get_q(&q, "cursor").as_deref(), Some("c0"));
233 assert_eq!(get_q(&q, "page"), None);
234 }
235
236 #[test]
237 fn list_gsa_extra_emits() {
238 let mut extra = BTreeMap::new();
239 extra.insert("custom_x".to_string(), "xv".to_string());
240 let opts = ListGsaElibraryContractsOptions::builder()
241 .extra(extra)
242 .build();
243 let q = opts.to_query();
244 assert!(q.contains(&("custom_x".into(), "xv".into())));
245 }
246
247 #[tokio::test]
248 async fn get_gsa_validates_empty_uuid() {
249 let client = Client::builder().api_key("x").build().expect("client");
250 let err = client
251 .get_gsa_elibrary_contract("", None)
252 .await
253 .unwrap_err();
254 match err {
255 Error::Validation { message, .. } => {
256 assert!(message.contains("uuid is required"));
257 }
258 other => panic!("expected Validation, got {other:?}"),
259 }
260 }
261}