Skip to main content

papers_openalex/
params.rs

1/// Query parameters shared by all 7 list endpoints (works, authors, sources,
2/// institutions, topics, publishers, funders). All fields are optional.
3///
4/// Supports both struct-update syntax and the bon builder pattern:
5///
6/// ```
7/// use papers_openalex::ListParams;
8///
9/// // Struct-update syntax
10/// let params = ListParams {
11///     search: Some("machine learning".into()),
12///     per_page: Some(10),
13///     ..Default::default()
14/// };
15///
16/// // Builder syntax
17/// let params = ListParams::builder()
18///     .search("machine learning")
19///     .filter("publication_year:2024,is_oa:true")
20///     .sort("cited_by_count:desc")
21///     .per_page(10)
22///     .page(1)
23///     .build();
24/// ```
25///
26/// # Pagination
27///
28/// Two pagination modes are available (mutually exclusive):
29///
30/// - **Offset pagination:** set `page` (max `page * per_page <= 10,000`)
31/// - **Cursor pagination:** set `cursor` to `"*"` for the first page, then pass
32///   [`ListMeta::next_cursor`](crate::ListMeta::next_cursor) from the previous
33///   response. When `next_cursor` is `None`, there are no more results.
34///
35/// # Sampling
36///
37/// Set `sample` to get a random sample instead of paginated results. Use `seed`
38/// for reproducibility.
39///
40/// # Grouping
41///
42/// Set `group_by` to aggregate results by a field. The response will include a
43/// [`group_by`](crate::ListResponse::group_by) array with key, display name,
44/// and count for each group.
45#[derive(Debug, Default, Clone, bon::Builder)]
46#[builder(on(String, into))]
47pub struct ListParams {
48    /// Filter expression. Comma-separated AND conditions, pipe (`|`) for OR
49    /// (max 50 alternatives). Supports negation (`!`), comparison (`>`, `<`),
50    /// and ranges (`2020-2023`).
51    ///
52    /// Example: `"publication_year:2024,is_oa:true"` or `"type:article|preprint"`
53    pub filter: Option<String>,
54
55    /// Full-text search query. For works, searches across title, abstract, and
56    /// fulltext. For other entities, searches `display_name`. Supports stemming
57    /// and stop-word removal.
58    pub search: Option<String>,
59
60    /// Sort field with optional direction suffix. Append `:desc` for descending
61    /// order. Multiple fields can be comma-separated.
62    ///
63    /// Available fields: `display_name`, `cited_by_count`, `works_count`,
64    /// `publication_date`, `relevance_score` (only with active search).
65    ///
66    /// Example: `"cited_by_count:desc"`
67    pub sort: Option<String>,
68
69    /// Results per page (1-200, default 25).
70    ///
71    /// Note: the API query key is `per-page` (hyphenated); this field handles
72    /// the mapping automatically.
73    pub per_page: Option<u32>,
74
75    /// Page number for offset pagination. Maximum accessible:
76    /// `page * per_page <= 10,000`. For deeper results, use cursor pagination.
77    pub page: Option<u32>,
78
79    /// Cursor for cursor-based pagination. Start with `"*"`, then pass
80    /// `meta.next_cursor` from the previous response. When `next_cursor` is
81    /// `None`, there are no more results. Mutually exclusive with `page`.
82    pub cursor: Option<String>,
83
84    /// Return a random sample of this many results instead of paginated results.
85    /// Use with `seed` for reproducibility.
86    pub sample: Option<u32>,
87
88    /// Seed value for reproducible random sampling. Only meaningful when
89    /// `sample` is set.
90    pub seed: Option<u32>,
91
92    /// Comma-separated list of fields to include in the response. Reduces
93    /// payload size. Unselected fields will be omitted.
94    ///
95    /// Example: `"id,display_name,cited_by_count"`
96    pub select: Option<String>,
97
98    /// Aggregate results by a field and return counts. The response will include
99    /// a `group_by` array with `key`, `key_display_name`, and `count` for each
100    /// group.
101    ///
102    /// Example: `"type"` groups works by article/preprint/etc.
103    pub group_by: Option<String>,
104}
105
106impl ListParams {
107    pub(crate) fn to_query_pairs(&self) -> Vec<(&str, String)> {
108        let mut pairs = Vec::new();
109        if let Some(v) = &self.filter {
110            pairs.push(("filter", v.clone()));
111        }
112        if let Some(v) = &self.search {
113            pairs.push(("search", v.clone()));
114        }
115        if let Some(v) = &self.sort {
116            pairs.push(("sort", v.clone()));
117        }
118        if let Some(v) = self.per_page {
119            pairs.push(("per-page", v.to_string()));
120        }
121        if let Some(v) = self.page {
122            pairs.push(("page", v.to_string()));
123        }
124        if let Some(v) = &self.cursor {
125            pairs.push(("cursor", v.clone()));
126        }
127        if let Some(v) = self.sample {
128            pairs.push(("sample", v.to_string()));
129        }
130        if let Some(v) = self.seed {
131            pairs.push(("seed", v.to_string()));
132        }
133        if let Some(v) = &self.select {
134            pairs.push(("select", v.clone()));
135        }
136        if let Some(v) = &self.group_by {
137            pairs.push(("group_by", v.clone()));
138        }
139        pairs
140    }
141}
142
143/// Query parameters for single-entity GET endpoints. Only field selection is
144/// supported.
145///
146/// ```
147/// use papers_openalex::GetParams;
148///
149/// // No field selection (return full entity)
150/// let params = GetParams::default();
151///
152/// // Select specific fields to reduce payload
153/// let params = GetParams::builder()
154///     .select("id,display_name,cited_by_count")
155///     .build();
156/// ```
157#[derive(Debug, Default, Clone, bon::Builder)]
158#[builder(on(String, into))]
159pub struct GetParams {
160    /// Comma-separated list of fields to include in the response.
161    ///
162    /// Example: `"id,display_name,cited_by_count"`
163    pub select: Option<String>,
164}
165
166impl GetParams {
167    pub(crate) fn to_query_pairs(&self) -> Vec<(&str, String)> {
168        let mut pairs = Vec::new();
169        if let Some(v) = &self.select {
170            pairs.push(("select", v.clone()));
171        }
172        pairs
173    }
174}
175
176/// Parameters for semantic search (`/find/works`). Uses AI embeddings to find
177/// conceptually similar works. Requires an API key. Costs 1,000 credits per
178/// request.
179///
180/// ```
181/// use papers_openalex::FindWorksParams;
182///
183/// let params = FindWorksParams::builder()
184///     .query("machine learning for drug discovery")
185///     .count(10)
186///     .filter("publication_year:>2020")
187///     .build();
188/// ```
189#[derive(Debug, Clone, bon::Builder)]
190#[builder(on(String, into))]
191pub struct FindWorksParams {
192    /// Text to find similar works for. Can be a title, abstract, or research
193    /// question. Maximum 10,000 characters. For POST requests, this is sent in
194    /// the JSON body as `{"query": "..."}`.
195    pub query: String,
196
197    /// Number of results to return (1-100, default 25). Results are ranked by
198    /// similarity score.
199    pub count: Option<u32>,
200
201    /// Filter expression to constrain results (same syntax as list endpoints).
202    /// Applied after semantic ranking.
203    pub filter: Option<String>,
204}
205
206impl FindWorksParams {
207    pub(crate) fn to_query_pairs(&self) -> Vec<(&str, String)> {
208        let mut pairs = Vec::new();
209        pairs.push(("query", self.query.clone()));
210        if let Some(v) = self.count {
211            pairs.push(("count", v.to_string()));
212        }
213        if let Some(v) = &self.filter {
214            pairs.push(("filter", v.clone()));
215        }
216        pairs
217    }
218
219    pub(crate) fn to_post_query_pairs(&self) -> Vec<(&str, String)> {
220        let mut pairs = Vec::new();
221        if let Some(v) = self.count {
222            pairs.push(("count", v.to_string()));
223        }
224        if let Some(v) = &self.filter {
225            pairs.push(("filter", v.clone()));
226        }
227        pairs
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_list_params_default() {
237        let params = ListParams::default();
238        assert!(params.filter.is_none());
239        assert!(params.search.is_none());
240        assert!(params.sort.is_none());
241        assert!(params.per_page.is_none());
242        assert!(params.page.is_none());
243        assert!(params.cursor.is_none());
244        assert!(params.sample.is_none());
245        assert!(params.seed.is_none());
246        assert!(params.select.is_none());
247        assert!(params.group_by.is_none());
248    }
249
250    #[test]
251    fn test_list_params_builder() {
252        let params = ListParams::builder()
253            .search("machine learning")
254            .per_page(10)
255            .sort("cited_by_count:desc")
256            .build();
257        assert_eq!(params.search.as_deref(), Some("machine learning"));
258        assert_eq!(params.per_page, Some(10));
259        assert_eq!(params.sort.as_deref(), Some("cited_by_count:desc"));
260    }
261
262    #[test]
263    fn test_list_params_struct_update() {
264        let params = ListParams {
265            search: Some("test".into()),
266            per_page: Some(5),
267            ..Default::default()
268        };
269        assert_eq!(params.search.as_deref(), Some("test"));
270        assert_eq!(params.per_page, Some(5));
271        assert!(params.filter.is_none());
272    }
273
274    #[test]
275    fn test_get_params_default() {
276        let params = GetParams::default();
277        assert!(params.select.is_none());
278    }
279
280    #[test]
281    fn test_find_works_params_builder() {
282        let params = FindWorksParams::builder()
283            .query("drug discovery")
284            .count(10)
285            .build();
286        assert_eq!(params.query, "drug discovery");
287        assert_eq!(params.count, Some(10));
288        assert!(params.filter.is_none());
289    }
290}