1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//! `GET /api/federal_register/` — Federal Register documents: rules, proposed rules, notices and presidential documents published since 1994 (Tango API 5.3.0).
//!
//! The detail route takes a document's `uuid`, not its `document_number`: the Federal Register reused some document numbers before 2016, so a number is not unique on its own.
//! Look a document up by number with [`ListFederalRegisterDocumentsOptions::document_number`].
//!
//! `full_text` can run to several MB. It is served on [`Client::get_federal_register_document`] only when named in `shape`, and neither default shape includes it.
use crate::client::Client;
use crate::error::{Error, Result};
use crate::internal::{apply_pagination, push_opt, push_opt_bool};
use crate::pagination::{FetchFn, Page, PageStream};
use crate::resources::agencies::urlencoding;
use crate::Record;
use bon::Builder;
use std::collections::BTreeMap;
use std::sync::Arc;
/// Options for [`Client::list_federal_register_documents`] and [`Client::iterate_federal_register_documents`].
///
/// All seventeen of the API's filters are named fields, plus `ordering`.
#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListFederalRegisterDocumentsOptions {
/// 1-based page number.
#[builder(into)]
pub page: Option<u32>,
/// Page size (server caps at 100).
#[builder(into)]
pub limit: Option<u32>,
/// Keyset cursor.
#[builder(into)]
pub cursor: Option<String>,
/// Comma-separated field selector. Use [`SHAPE_FEDERAL_REGISTER_MINIMAL`](crate::SHAPE_FEDERAL_REGISTER_MINIMAL) or roll your own. `full_text` is not available on the list.
#[builder(into)]
pub shape: Option<String>,
/// Collapse nested objects into dot-separated keys.
#[builder(default)]
pub flat: bool,
/// When [`flat`](Self::flat) is also true, flatten list-valued fields.
#[builder(default)]
pub flat_lists: bool,
/// Ranked full-text search over the title, abstract and action. Wrap in double quotes for a phrase.
#[builder(into)]
pub search: Option<String>,
/// Exact FR document number, e.g. `2016-31922`. OR several with `|`. A number the Federal Register reused before 2016 matches every document that carries it.
#[builder(into)]
pub document_number: Option<String>,
/// Document type: `Notice`, `Rule`, `Proposed Rule`, `Presidential Document`, `Correction`, `Sunshine Act Document` or `Uncategorized Document` (case-insensitive). OR several with `|`. Documents published before 2008 are mostly `Uncategorized Document`.
#[builder(into)]
pub r#type: Option<String>,
/// A Tango agency name, abbreviation, code or organization key, e.g. `EPA`. Matches a document when any agency it lists falls within that organization, so a department includes its sub-agencies. OR several with `|`.
#[builder(into)]
pub agency: Option<String>,
/// The Federal Register's own agency slug, e.g. `environmental-protection-agency`. OR several with `|`.
#[builder(into)]
pub fr_agency: Option<String>,
/// Published on or after (`YYYY-MM-DD`).
#[builder(into)]
pub publication_date_after: Option<String>,
/// Published on or before (`YYYY-MM-DD`).
#[builder(into)]
pub publication_date_before: Option<String>,
/// Effective on or after (`YYYY-MM-DD`).
#[builder(into)]
pub effective_on_after: Option<String>,
/// Effective on or before (`YYYY-MM-DD`).
#[builder(into)]
pub effective_on_before: Option<String>,
/// Comment period closes on or after (`YYYY-MM-DD`).
#[builder(into)]
pub comments_close_on_after: Option<String>,
/// Comment period closes on or before (`YYYY-MM-DD`).
#[builder(into)]
pub comments_close_on_before: Option<String>,
/// `Some(true)` for documents whose comment period closes today or later, `Some(false)` for those already closed. Documents with no comment deadline match neither.
pub comments_open: Option<bool>,
/// A CFR title number, e.g. `40`.
#[builder(into)]
pub cfr_title: Option<String>,
/// A CFR part number, e.g. `52`. Requires [`cfr_title`](Self::cfr_title), and both must match the same CFR reference.
#[builder(into)]
pub cfr_part: Option<String>,
/// Significant under Executive Order 12866.
pub significant: Option<bool>,
/// A Regulation Identifier Number, e.g. `2060-AV16`. OR several with `|`.
#[builder(into)]
pub rin: Option<String>,
/// Exact executive order number.
#[builder(into)]
pub executive_order_number: Option<String>,
/// Sort key: `publication_date` (the server default, as `-publication_date`), `effective_on`, `comments_close_on`, `document_number` or `rank`, prefixed with `-` for descending. `rank` requires a non-empty [`search`](Self::search).
#[builder(into)]
pub ordering: Option<String>,
/// Escape hatch for filter keys not yet first-classed on this struct.
#[builder(default)]
pub extra: BTreeMap<String, String>,
}
impl ListFederalRegisterDocumentsOptions {
pub(crate) fn to_query(&self) -> Vec<(String, String)> {
let mut q = Vec::new();
apply_pagination(
&mut q,
self.page,
self.limit,
self.cursor.as_deref(),
self.shape.as_deref(),
self.flat,
self.flat_lists,
);
push_opt(&mut q, "search", self.search.as_deref());
push_opt(&mut q, "document_number", self.document_number.as_deref());
push_opt(&mut q, "type", self.r#type.as_deref());
push_opt(&mut q, "agency", self.agency.as_deref());
push_opt(&mut q, "fr_agency", self.fr_agency.as_deref());
for (key, value) in [
("publication_date_after", &self.publication_date_after),
("publication_date_before", &self.publication_date_before),
("effective_on_after", &self.effective_on_after),
("effective_on_before", &self.effective_on_before),
("comments_close_on_after", &self.comments_close_on_after),
("comments_close_on_before", &self.comments_close_on_before),
] {
push_opt(&mut q, key, value.as_deref());
}
push_opt_bool(&mut q, "comments_open", self.comments_open);
push_opt(&mut q, "cfr_title", self.cfr_title.as_deref());
push_opt(&mut q, "cfr_part", self.cfr_part.as_deref());
push_opt_bool(&mut q, "significant", self.significant);
push_opt(&mut q, "rin", self.rin.as_deref());
push_opt(
&mut q,
"executive_order_number",
self.executive_order_number.as_deref(),
);
push_opt(&mut q, "ordering", self.ordering.as_deref());
for (k, v) in &self.extra {
if !v.is_empty() {
q.push((k.clone(), v.clone()));
}
}
q
}
}
/// Options for [`Client::get_federal_register_document`].
#[derive(Debug, Clone, Default, Builder, PartialEq, Eq)]
#[non_exhaustive]
pub struct GetFederalRegisterDocumentOptions {
/// Shape selector. When empty, the server returns its detail default, which [`SHAPE_FEDERAL_REGISTER_COMPREHENSIVE`](crate::SHAPE_FEDERAL_REGISTER_COMPREHENSIVE) mirrors. Name `full_text` to get the document's plain text, which can run to several MB.
#[builder(into)]
pub shape: Option<String>,
/// Flatten nested objects into dot-separated keys.
#[builder(default)]
pub flat: bool,
/// When `flat=true`, also flatten list-valued nested fields.
#[builder(default)]
pub flat_lists: bool,
}
impl GetFederalRegisterDocumentOptions {
pub(crate) fn to_query(&self) -> Vec<(String, String)> {
let mut q = Vec::new();
push_opt(&mut q, "shape", self.shape.as_deref());
if self.flat {
q.push(("flat".into(), "true".into()));
}
if self.flat_lists {
q.push(("flat_lists".into(), "true".into()));
}
q
}
}
impl Client {
/// `GET /api/federal_register/` — one page of Federal Register documents, newest publication first by default.
pub async fn list_federal_register_documents(
&self,
opts: ListFederalRegisterDocumentsOptions,
) -> Result<Page<Record>> {
let q = opts.to_query();
self.get_page("/api/federal_register/", &q).await
}
/// `GET /api/federal_register/{uuid}/` — one Federal Register document by its `uuid`.
///
/// The route takes the `uuid`, not the `document_number`, which is not unique on its own; find a document by number with [`list_federal_register_documents`](Self::list_federal_register_documents) and [`ListFederalRegisterDocumentsOptions::document_number`].
/// `full_text` is returned only when named in `shape`.
pub async fn get_federal_register_document(
&self,
document_uuid: &str,
opts: Option<GetFederalRegisterDocumentOptions>,
) -> Result<Record> {
if document_uuid.is_empty() {
return Err(Error::Validation {
message: "get_federal_register_document: document_uuid is required".into(),
response: None,
});
}
let q = opts.unwrap_or_default().to_query();
let path = format!("/api/federal_register/{}/", urlencoding(document_uuid));
self.get_json::<Record>(&path, &q).await
}
/// Stream every Federal Register document matching `opts`.
pub fn iterate_federal_register_documents(
&self,
opts: ListFederalRegisterDocumentsOptions,
) -> PageStream<Record> {
let opts = Arc::new(opts);
let fetch: FetchFn<Record> = Box::new(move |client, page, cursor| {
let mut next = (*opts).clone();
next.page = page;
next.cursor = cursor;
Box::pin(async move { client.list_federal_register_documents(next).await })
});
PageStream::new(self.clone(), fetch)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_q(q: &[(String, String)], k: &str) -> Option<String> {
q.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone())
}
#[test]
fn list_federal_register_documents_all_filters_emit() {
let opts = ListFederalRegisterDocumentsOptions::builder()
.search("\"air quality\"")
.document_number("2016-31922")
.r#type("Rule|Proposed Rule")
.agency("EPA")
.fr_agency("environmental-protection-agency")
.publication_date_after("2024-01-01")
.publication_date_before("2024-12-31")
.effective_on_after("2024-02-01")
.effective_on_before("2024-11-30")
.comments_close_on_after("2024-03-01")
.comments_close_on_before("2024-10-31")
.comments_open(false)
.cfr_title("40")
.cfr_part("52")
.significant(true)
.rin("2060-AV16")
.executive_order_number("14028")
.ordering("rank")
.build();
let q = opts.to_query();
for (k, v) in [
("search", "\"air quality\""),
("document_number", "2016-31922"),
("type", "Rule|Proposed Rule"),
("agency", "EPA"),
("fr_agency", "environmental-protection-agency"),
("publication_date_after", "2024-01-01"),
("publication_date_before", "2024-12-31"),
("effective_on_after", "2024-02-01"),
("effective_on_before", "2024-11-30"),
("comments_close_on_after", "2024-03-01"),
("comments_close_on_before", "2024-10-31"),
("comments_open", "false"),
("cfr_title", "40"),
("cfr_part", "52"),
("significant", "true"),
("rin", "2060-AV16"),
("executive_order_number", "14028"),
("ordering", "rank"),
] {
assert_eq!(get_q(&q, k).as_deref(), Some(v), "{k}");
}
assert_eq!(q.len(), 18);
}
#[test]
fn list_federal_register_documents_zero_value_omitted() {
assert!(ListFederalRegisterDocumentsOptions::builder()
.build()
.to_query()
.is_empty());
}
#[test]
fn default_shapes_never_name_full_text() {
for shape in [
crate::SHAPE_FEDERAL_REGISTER_MINIMAL,
crate::SHAPE_FEDERAL_REGISTER_COMPREHENSIVE,
] {
assert!(!shape.split(',').any(|f| f == "full_text"), "{shape}");
}
}
#[tokio::test]
async fn empty_uuid_is_rejected_before_any_request() {
let client = Client::builder().api_key("x").build().expect("client");
let err = client
.get_federal_register_document("", None)
.await
.unwrap_err();
assert!(matches!(err, Error::Validation { .. }));
}
}