Skip to main content

semantic_scholar/
lib.rs

1mod client;
2pub mod error;
3pub mod models;
4
5pub use error::{Error, Result};
6pub use models::*;
7
8use client::HttpClient;
9
10/// Commonly requested field sets for the Semantic Scholar API.
11pub mod fields {
12    /// Fields returned by default for paper search results.
13    pub const PAPER_SEARCH: &str = "paperId,title,year,citationCount,authors";
14
15    /// Detailed fields for a single paper lookup.
16    pub const PAPER_DETAIL: &str = "paperId,title,abstract,year,citationCount,\
17        referenceCount,influentialCitationCount,isOpenAccess,openAccessPdf,\
18        fieldsOfStudy,s2FieldsOfStudy,publicationTypes,publicationDate,\
19        journal,authors,tldr,venue";
20
21    /// Fields returned for citation/reference entries.
22    pub const CITATION: &str = "paperId,title,year,citationCount,authors";
23
24    /// Fields returned by default for author search results.
25    pub const AUTHOR_SEARCH: &str = "authorId,name,paperCount,citationCount,hIndex";
26
27    /// Detailed fields for a single author lookup.
28    pub const AUTHOR_DETAIL: &str =
29        "authorId,name,affiliations,homepage,paperCount,citationCount,hIndex";
30}
31
32// ---------------------------------------------------------------------------
33// Client
34// ---------------------------------------------------------------------------
35
36/// Semantic Scholar API client.
37///
38/// # Examples
39///
40/// ```no_run
41/// # async fn example() -> semantic_scholar::Result<()> {
42/// let client = semantic_scholar::SemanticScholar::new()?;
43/// let results = client.search_papers("attention is all you need")
44///     .limit(5)
45///     .send()
46///     .await?;
47/// for paper in &results.data {
48///     println!("{paper}");
49/// }
50/// # Ok(())
51/// # }
52/// ```
53#[derive(Clone, Debug)]
54pub struct SemanticScholar {
55    client: HttpClient,
56}
57
58impl SemanticScholar {
59    /// Create a client without an API key.
60    ///
61    /// Unauthenticated requests share a global rate limit pool
62    /// (5 000 req / 5 min across *all* unauthenticated users).
63    pub fn new() -> Result<Self> {
64        Self::build(None)
65    }
66
67    /// Create a client with an API key for dedicated rate limits.
68    pub fn with_api_key(api_key: &str) -> Result<Self> {
69        Self::build(Some(api_key))
70    }
71
72    /// Override the base URL (for testing or self-hosted instances).
73    pub fn set_base_url(&mut self, url: impl Into<String>) {
74        self.client.set_base_url(url);
75    }
76
77    fn build(api_key: Option<&str>) -> Result<Self> {
78        Ok(Self {
79            client: HttpClient::new(api_key)?,
80        })
81    }
82
83    // -- Paper endpoints ---------------------------------------------------
84
85    /// Search for papers by keyword.
86    pub fn search_papers(&self, query: &str) -> SearchPapersRequest<'_> {
87        SearchPapersRequest::new(&self.client, query)
88    }
89
90    /// Get a single paper by its identifier.
91    ///
92    /// Accepts S2 paper ID, `DOI:…`, `ARXIV:…`, `CorpusId:…`, etc.
93    pub fn get_paper(&self, paper_id: &str) -> GetPaperRequest<'_> {
94        GetPaperRequest::new(&self.client, paper_id)
95    }
96
97    /// Convenience: get a paper by DOI.
98    pub fn get_paper_by_doi(&self, doi: &str) -> GetPaperRequest<'_> {
99        self.get_paper(&format!("DOI:{doi}"))
100    }
101
102    /// Convenience: get a paper by ArXiv ID.
103    pub fn get_paper_by_arxiv(&self, arxiv_id: &str) -> GetPaperRequest<'_> {
104        self.get_paper(&format!("ARXIV:{arxiv_id}"))
105    }
106
107    /// Get papers that cite the given paper.
108    pub fn get_citations(&self, paper_id: &str) -> GetCitationsRequest<'_> {
109        GetCitationsRequest::new(&self.client, paper_id)
110    }
111
112    /// Get papers referenced by the given paper.
113    pub fn get_references(&self, paper_id: &str) -> GetReferencesRequest<'_> {
114        GetReferencesRequest::new(&self.client, paper_id)
115    }
116
117    // -- Author endpoints --------------------------------------------------
118
119    /// Search for authors by name.
120    pub fn search_authors(&self, query: &str) -> SearchAuthorsRequest<'_> {
121        SearchAuthorsRequest::new(&self.client, query)
122    }
123
124    /// Get a single author by their Semantic Scholar ID.
125    pub fn get_author(&self, author_id: &str) -> GetAuthorRequest<'_> {
126        GetAuthorRequest::new(&self.client, author_id)
127    }
128
129    // -- Recommendations ---------------------------------------------------
130
131    /// Get recommended papers based on a given paper.
132    pub fn get_recommendations(&self, paper_id: &str) -> GetRecommendationsRequest<'_> {
133        GetRecommendationsRequest::new(&self.client, paper_id)
134    }
135}
136
137// ===========================================================================
138// Request builders
139// ===========================================================================
140
141/// Builder for `GET /graph/v1/paper/search`.
142pub struct SearchPapersRequest<'a> {
143    client: &'a HttpClient,
144    query: String,
145    limit: Option<u32>,
146    offset: Option<u32>,
147    year: Option<String>,
148    fields_of_study: Option<String>,
149    publication_types: Option<String>,
150    open_access_pdf: Option<bool>,
151    min_citation_count: Option<u32>,
152    fields: String,
153}
154
155impl<'a> SearchPapersRequest<'a> {
156    fn new(client: &'a HttpClient, query: &str) -> Self {
157        Self {
158            client,
159            query: query.to_string(),
160            limit: None,
161            offset: None,
162            year: None,
163            fields_of_study: None,
164            publication_types: None,
165            open_access_pdf: None,
166            min_citation_count: None,
167            fields: fields::PAPER_SEARCH.to_string(),
168        }
169    }
170
171    pub fn limit(mut self, limit: u32) -> Self {
172        self.limit = Some(limit);
173        self
174    }
175    pub fn offset(mut self, offset: u32) -> Self {
176        self.offset = Some(offset);
177        self
178    }
179    /// Filter by year range, e.g. `"2020-2024"`, `"2020-"`, `"-2015"`.
180    pub fn year(mut self, year: &str) -> Self {
181        self.year = Some(year.to_string());
182        self
183    }
184    /// Filter by fields of study (comma-separated).
185    pub fn fields_of_study(mut self, fos: &str) -> Self {
186        self.fields_of_study = Some(fos.to_string());
187        self
188    }
189    pub fn publication_types(mut self, types: &str) -> Self {
190        self.publication_types = Some(types.to_string());
191        self
192    }
193    pub fn open_access_pdf(mut self) -> Self {
194        self.open_access_pdf = Some(true);
195        self
196    }
197    pub fn min_citation_count(mut self, count: u32) -> Self {
198        self.min_citation_count = Some(count);
199        self
200    }
201    /// Override which fields are returned (comma-separated).
202    pub fn fields(mut self, fields: &str) -> Self {
203        self.fields = fields.to_string();
204        self
205    }
206
207    pub async fn send(self) -> Result<PaperSearchResponse> {
208        let limit_str = self.limit.map(|l| l.to_string());
209        let offset_str = self.offset.map(|o| o.to_string());
210        let min_cc_str = self.min_citation_count.map(|c| c.to_string());
211
212        let mut params: Vec<(&str, &str)> = vec![("query", &self.query), ("fields", &self.fields)];
213        if let Some(ref l) = limit_str {
214            params.push(("limit", l));
215        }
216        if let Some(ref o) = offset_str {
217            params.push(("offset", o));
218        }
219        if let Some(ref y) = self.year {
220            params.push(("year", y));
221        }
222        if let Some(ref fos) = self.fields_of_study {
223            params.push(("fieldsOfStudy", fos));
224        }
225        if let Some(ref pt) = self.publication_types {
226            params.push(("publicationTypes", pt));
227        }
228        if self.open_access_pdf == Some(true) {
229            params.push(("openAccessPdf", ""));
230        }
231        if let Some(ref cc) = min_cc_str {
232            params.push(("minCitationCount", cc));
233        }
234
235        self.client.get("/graph/v1/paper/search", &params).await
236    }
237}
238
239/// Builder for `GET /graph/v1/paper/{paper_id}`.
240pub struct GetPaperRequest<'a> {
241    client: &'a HttpClient,
242    paper_id: String,
243    fields: String,
244}
245
246impl<'a> GetPaperRequest<'a> {
247    fn new(client: &'a HttpClient, paper_id: &str) -> Self {
248        Self {
249            client,
250            paper_id: paper_id.to_string(),
251            fields: fields::PAPER_DETAIL.to_string(),
252        }
253    }
254
255    pub fn fields(mut self, fields: &str) -> Self {
256        self.fields = fields.to_string();
257        self
258    }
259
260    pub async fn send(self) -> Result<Paper> {
261        let path = format!("/graph/v1/paper/{}", self.paper_id);
262        let params = [("fields", self.fields.as_str())];
263        self.client.get(&path, &params).await
264    }
265}
266
267/// Builder for `GET /graph/v1/paper/{paper_id}/citations`.
268pub struct GetCitationsRequest<'a> {
269    client: &'a HttpClient,
270    paper_id: String,
271    limit: Option<u32>,
272    offset: Option<u32>,
273    fields: String,
274}
275
276impl<'a> GetCitationsRequest<'a> {
277    fn new(client: &'a HttpClient, paper_id: &str) -> Self {
278        Self {
279            client,
280            paper_id: paper_id.to_string(),
281            limit: None,
282            offset: None,
283            fields: fields::CITATION.to_string(),
284        }
285    }
286
287    pub fn limit(mut self, limit: u32) -> Self {
288        self.limit = Some(limit);
289        self
290    }
291    pub fn offset(mut self, offset: u32) -> Self {
292        self.offset = Some(offset);
293        self
294    }
295    pub fn fields(mut self, fields: &str) -> Self {
296        self.fields = fields.to_string();
297        self
298    }
299
300    pub async fn send(self) -> Result<CitationResponse> {
301        let path = format!("/graph/v1/paper/{}/citations", self.paper_id);
302        let limit_str = self.limit.map(|l| l.to_string());
303        let offset_str = self.offset.map(|o| o.to_string());
304
305        let mut params: Vec<(&str, &str)> = vec![("fields", &self.fields)];
306        if let Some(ref l) = limit_str {
307            params.push(("limit", l));
308        }
309        if let Some(ref o) = offset_str {
310            params.push(("offset", o));
311        }
312
313        self.client.get(&path, &params).await
314    }
315}
316
317/// Builder for `GET /graph/v1/paper/{paper_id}/references`.
318pub struct GetReferencesRequest<'a> {
319    client: &'a HttpClient,
320    paper_id: String,
321    limit: Option<u32>,
322    offset: Option<u32>,
323    fields: String,
324}
325
326impl<'a> GetReferencesRequest<'a> {
327    fn new(client: &'a HttpClient, paper_id: &str) -> Self {
328        Self {
329            client,
330            paper_id: paper_id.to_string(),
331            limit: None,
332            offset: None,
333            fields: fields::CITATION.to_string(),
334        }
335    }
336
337    pub fn limit(mut self, limit: u32) -> Self {
338        self.limit = Some(limit);
339        self
340    }
341    pub fn offset(mut self, offset: u32) -> Self {
342        self.offset = Some(offset);
343        self
344    }
345    pub fn fields(mut self, fields: &str) -> Self {
346        self.fields = fields.to_string();
347        self
348    }
349
350    pub async fn send(self) -> Result<ReferenceResponse> {
351        let path = format!("/graph/v1/paper/{}/references", self.paper_id);
352        let limit_str = self.limit.map(|l| l.to_string());
353        let offset_str = self.offset.map(|o| o.to_string());
354
355        let mut params: Vec<(&str, &str)> = vec![("fields", &self.fields)];
356        if let Some(ref l) = limit_str {
357            params.push(("limit", l));
358        }
359        if let Some(ref o) = offset_str {
360            params.push(("offset", o));
361        }
362
363        self.client.get(&path, &params).await
364    }
365}
366
367/// Builder for `GET /graph/v1/author/search`.
368pub struct SearchAuthorsRequest<'a> {
369    client: &'a HttpClient,
370    query: String,
371    limit: Option<u32>,
372    offset: Option<u32>,
373    fields: String,
374}
375
376impl<'a> SearchAuthorsRequest<'a> {
377    fn new(client: &'a HttpClient, query: &str) -> Self {
378        Self {
379            client,
380            query: query.to_string(),
381            limit: None,
382            offset: None,
383            fields: fields::AUTHOR_SEARCH.to_string(),
384        }
385    }
386
387    pub fn limit(mut self, limit: u32) -> Self {
388        self.limit = Some(limit);
389        self
390    }
391    pub fn offset(mut self, offset: u32) -> Self {
392        self.offset = Some(offset);
393        self
394    }
395    pub fn fields(mut self, fields: &str) -> Self {
396        self.fields = fields.to_string();
397        self
398    }
399
400    pub async fn send(self) -> Result<AuthorSearchResponse> {
401        let limit_str = self.limit.map(|l| l.to_string());
402        let offset_str = self.offset.map(|o| o.to_string());
403
404        let mut params: Vec<(&str, &str)> = vec![("query", &self.query), ("fields", &self.fields)];
405        if let Some(ref l) = limit_str {
406            params.push(("limit", l));
407        }
408        if let Some(ref o) = offset_str {
409            params.push(("offset", o));
410        }
411
412        self.client.get("/graph/v1/author/search", &params).await
413    }
414}
415
416/// Builder for `GET /graph/v1/author/{author_id}`.
417pub struct GetAuthorRequest<'a> {
418    client: &'a HttpClient,
419    author_id: String,
420    fields: String,
421}
422
423impl<'a> GetAuthorRequest<'a> {
424    fn new(client: &'a HttpClient, author_id: &str) -> Self {
425        Self {
426            client,
427            author_id: author_id.to_string(),
428            fields: fields::AUTHOR_DETAIL.to_string(),
429        }
430    }
431
432    pub fn fields(mut self, fields: &str) -> Self {
433        self.fields = fields.to_string();
434        self
435    }
436
437    pub async fn send(self) -> Result<Author> {
438        let path = format!("/graph/v1/author/{}", self.author_id);
439        let params = [("fields", self.fields.as_str())];
440        self.client.get(&path, &params).await
441    }
442}
443
444/// Builder for `GET /recommendations/v1/papers/forpaper/{paper_id}`.
445pub struct GetRecommendationsRequest<'a> {
446    client: &'a HttpClient,
447    paper_id: String,
448    limit: Option<u32>,
449    fields: String,
450}
451
452impl<'a> GetRecommendationsRequest<'a> {
453    fn new(client: &'a HttpClient, paper_id: &str) -> Self {
454        Self {
455            client,
456            paper_id: paper_id.to_string(),
457            limit: None,
458            fields: fields::PAPER_SEARCH.to_string(),
459        }
460    }
461
462    pub fn limit(mut self, limit: u32) -> Self {
463        self.limit = Some(limit);
464        self
465    }
466    pub fn fields(mut self, fields: &str) -> Self {
467        self.fields = fields.to_string();
468        self
469    }
470
471    pub async fn send(self) -> Result<RecommendationResponse> {
472        let path = format!("/recommendations/v1/papers/forpaper/{}", self.paper_id);
473        let limit_str = self.limit.map(|l| l.to_string());
474
475        let mut params: Vec<(&str, &str)> = vec![("fields", &self.fields)];
476        if let Some(ref l) = limit_str {
477            params.push(("limit", l));
478        }
479
480        self.client.get(&path, &params).await
481    }
482}