1use crate::client::Client;
9use crate::error::{Error, Result};
10use crate::internal::{apply_pagination, push_opt, push_opt_bool};
11use crate::pagination::{FetchFn, Page, PageStream};
12use crate::resources::agencies::urlencoding;
13use crate::Record;
14use bon::Builder;
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct ListFederalRegisterDocumentsOptions {
24 #[builder(into)]
26 pub page: Option<u32>,
27 #[builder(into)]
29 pub limit: Option<u32>,
30 #[builder(into)]
32 pub cursor: Option<String>,
33 #[builder(into)]
35 pub shape: Option<String>,
36 #[builder(default)]
38 pub flat: bool,
39 #[builder(default)]
41 pub flat_lists: bool,
42
43 #[builder(into)]
45 pub search: Option<String>,
46 #[builder(into)]
48 pub document_number: Option<String>,
49 #[builder(into)]
51 pub r#type: Option<String>,
52 #[builder(into)]
54 pub agency: Option<String>,
55 #[builder(into)]
57 pub fr_agency: Option<String>,
58 #[builder(into)]
60 pub publication_date_after: Option<String>,
61 #[builder(into)]
63 pub publication_date_before: Option<String>,
64 #[builder(into)]
66 pub effective_on_after: Option<String>,
67 #[builder(into)]
69 pub effective_on_before: Option<String>,
70 #[builder(into)]
72 pub comments_close_on_after: Option<String>,
73 #[builder(into)]
75 pub comments_close_on_before: Option<String>,
76 pub comments_open: Option<bool>,
78 #[builder(into)]
80 pub cfr_title: Option<String>,
81 #[builder(into)]
83 pub cfr_part: Option<String>,
84 pub significant: Option<bool>,
86 #[builder(into)]
88 pub rin: Option<String>,
89 #[builder(into)]
91 pub executive_order_number: Option<String>,
92
93 #[builder(into)]
95 pub ordering: Option<String>,
96
97 #[builder(default)]
99 pub extra: BTreeMap<String, String>,
100}
101
102impl ListFederalRegisterDocumentsOptions {
103 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
104 let mut q = Vec::new();
105 apply_pagination(
106 &mut q,
107 self.page,
108 self.limit,
109 self.cursor.as_deref(),
110 self.shape.as_deref(),
111 self.flat,
112 self.flat_lists,
113 );
114 push_opt(&mut q, "search", self.search.as_deref());
115 push_opt(&mut q, "document_number", self.document_number.as_deref());
116 push_opt(&mut q, "type", self.r#type.as_deref());
117 push_opt(&mut q, "agency", self.agency.as_deref());
118 push_opt(&mut q, "fr_agency", self.fr_agency.as_deref());
119 for (key, value) in [
120 ("publication_date_after", &self.publication_date_after),
121 ("publication_date_before", &self.publication_date_before),
122 ("effective_on_after", &self.effective_on_after),
123 ("effective_on_before", &self.effective_on_before),
124 ("comments_close_on_after", &self.comments_close_on_after),
125 ("comments_close_on_before", &self.comments_close_on_before),
126 ] {
127 push_opt(&mut q, key, value.as_deref());
128 }
129 push_opt_bool(&mut q, "comments_open", self.comments_open);
130 push_opt(&mut q, "cfr_title", self.cfr_title.as_deref());
131 push_opt(&mut q, "cfr_part", self.cfr_part.as_deref());
132 push_opt_bool(&mut q, "significant", self.significant);
133 push_opt(&mut q, "rin", self.rin.as_deref());
134 push_opt(
135 &mut q,
136 "executive_order_number",
137 self.executive_order_number.as_deref(),
138 );
139 push_opt(&mut q, "ordering", self.ordering.as_deref());
140 for (k, v) in &self.extra {
141 if !v.is_empty() {
142 q.push((k.clone(), v.clone()));
143 }
144 }
145 q
146 }
147}
148
149#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
151#[non_exhaustive]
152pub struct GetFederalRegisterDocumentOptions {
153 #[builder(into)]
155 pub shape: Option<String>,
156 #[builder(default)]
158 pub flat: bool,
159 #[builder(default)]
161 pub flat_lists: bool,
162}
163
164impl GetFederalRegisterDocumentOptions {
165 pub(crate) fn to_query(&self) -> Vec<(String, String)> {
166 let mut q = Vec::new();
167 push_opt(&mut q, "shape", self.shape.as_deref());
168 if self.flat {
169 q.push(("flat".into(), "true".into()));
170 }
171 if self.flat_lists {
172 q.push(("flat_lists".into(), "true".into()));
173 }
174 q
175 }
176}
177
178impl Client {
179 pub async fn list_federal_register_documents(
181 &self,
182 opts: ListFederalRegisterDocumentsOptions,
183 ) -> Result<Page<Record>> {
184 let q = opts.to_query();
185 self.get_page("/api/federal_register/", &q).await
186 }
187
188 pub async fn get_federal_register_document(
193 &self,
194 document_uuid: &str,
195 opts: Option<GetFederalRegisterDocumentOptions>,
196 ) -> Result<Record> {
197 if document_uuid.is_empty() {
198 return Err(Error::Validation {
199 message: "get_federal_register_document: document_uuid is required".into(),
200 response: None,
201 });
202 }
203 let q = opts.unwrap_or_default().to_query();
204 let path = format!("/api/federal_register/{}/", urlencoding(document_uuid));
205 self.get_json::<Record>(&path, &q).await
206 }
207
208 pub fn iterate_federal_register_documents(
210 &self,
211 opts: ListFederalRegisterDocumentsOptions,
212 ) -> PageStream<Record> {
213 let opts = Arc::new(opts);
214 let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
215 let mut next = (*opts).clone();
216 next.page = page;
217 next.cursor = cursor;
218 Box::pin(async move { client.list_federal_register_documents(next).await })
219 });
220 PageStream::new(self.clone(), fetch)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
229 q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
230 }
231
232 #[test]
233 fn list_federal_register_documents_all_filters_emit() {
234 let opts = ListFederalRegisterDocumentsOptions::builder()
235 .search("\"air quality\"")
236 .document_number("2016-31922")
237 .r#type("Rule|Proposed Rule")
238 .agency("EPA")
239 .fr_agency("environmental-protection-agency")
240 .publication_date_after("2024-01-01")
241 .publication_date_before("2024-12-31")
242 .effective_on_after("2024-02-01")
243 .effective_on_before("2024-11-30")
244 .comments_close_on_after("2024-03-01")
245 .comments_close_on_before("2024-10-31")
246 .comments_open(false)
247 .cfr_title("40")
248 .cfr_part("52")
249 .significant(true)
250 .rin("2060-AV16")
251 .executive_order_number("14028")
252 .ordering("rank")
253 .build();
254 let q = opts.to_query();
255 for (k, v) in [
256 ("search", "\"air quality\""),
257 ("document_number", "2016-31922"),
258 ("type", "Rule|Proposed Rule"),
259 ("agency", "EPA"),
260 ("fr_agency", "environmental-protection-agency"),
261 ("publication_date_after", "2024-01-01"),
262 ("publication_date_before", "2024-12-31"),
263 ("effective_on_after", "2024-02-01"),
264 ("effective_on_before", "2024-11-30"),
265 ("comments_close_on_after", "2024-03-01"),
266 ("comments_close_on_before", "2024-10-31"),
267 ("comments_open", "false"),
268 ("cfr_title", "40"),
269 ("cfr_part", "52"),
270 ("significant", "true"),
271 ("rin", "2060-AV16"),
272 ("executive_order_number", "14028"),
273 ("ordering", "rank"),
274 ] {
275 assert_eq!(get_q(&q, k).as_deref(), Some(v), "{k}");
276 }
277 assert_eq!(q.len(), 18);
278 }
279
280 #[test]
281 fn list_federal_register_documents_zero_value_omitted() {
282 assert!(ListFederalRegisterDocumentsOptions::builder()
283 .build()
284 .to_query()
285 .is_empty());
286 }
287
288 #[test]
289 fn default_shapes_never_name_full_text() {
290 for shape in [
291 crate::SHAPE_FEDERAL_REGISTER_MINIMAL,
292 crate::SHAPE_FEDERAL_REGISTER_COMPREHENSIVE,
293 ] {
294 assert!(!shape.split(',').any(|f| f == "full_text"), "{shape}");
295 }
296 }
297
298 #[tokio::test]
299 async fn empty_uuid_is_rejected_before_any_request() {
300 let client = Client::builder().api_key("x").build().expect("client");
301 let err = client
302 .get_federal_register_document("", None)
303 .await
304 .unwrap_err();
305 assert!(matches!(err, Error::Validation { .. }));
306 }
307}