pubmed_parser/europe_pmc/
search.rs

1//! Europe PMC `search` endpoint response parsing.
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6
7use super::models::EuropePmcResult;
8
9/// Parsed response from the Europe PMC `search` endpoint.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
11#[serde(from = "RawSearchResponse")]
12pub struct EuropePmcSearchResponse {
13    /// Total number of records matching the query (across all pages).
14    pub hit_count: u64,
15    /// Cursor mark to pass as `cursorMark` to fetch the next page, if any.
16    /// Europe PMC keeps returning the same value once the last page is reached.
17    pub next_cursor_mark: Option<String>,
18    /// The records on this page.
19    pub results: Vec<EuropePmcResult>,
20}
21
22#[derive(Deserialize)]
23#[serde(rename_all = "camelCase")]
24struct RawSearchResponse {
25    #[serde(default)]
26    hit_count: u64,
27    #[serde(default)]
28    next_cursor_mark: Option<String>,
29    #[serde(default)]
30    result_list: RawResultList,
31}
32
33#[derive(Deserialize, Default)]
34struct RawResultList {
35    #[serde(default)]
36    result: Vec<EuropePmcResult>,
37}
38
39impl From<RawSearchResponse> for EuropePmcSearchResponse {
40    fn from(raw: RawSearchResponse) -> Self {
41        Self {
42            hit_count: raw.hit_count,
43            next_cursor_mark: raw.next_cursor_mark,
44            results: raw.result_list.result,
45        }
46    }
47}
48
49/// Parse a Europe PMC `search` JSON response.
50pub fn parse_search_response(json: &str) -> Result<EuropePmcSearchResponse> {
51    Ok(serde_json::from_str(json)?)
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn test_parse_search_lite() {
60        let json = r#"{
61            "hitCount": 2,
62            "nextCursorMark": "AoJ123",
63            "resultList": {
64                "result": [
65                    {
66                        "id": "33515491",
67                        "source": "MED",
68                        "pmid": "33515491",
69                        "pmcid": "PMC7894017",
70                        "doi": "10.1000/x",
71                        "title": "First",
72                        "authorString": "Smith J.",
73                        "journalTitle": "Nature",
74                        "pubYear": "2021",
75                        "isOpenAccess": "Y"
76                    },
77                    {
78                        "id": "PPR12345",
79                        "source": "PPR",
80                        "title": "A preprint",
81                        "pubYear": "2022"
82                    }
83                ]
84            }
85        }"#;
86
87        let resp = parse_search_response(json).unwrap();
88        assert_eq!(resp.hit_count, 2);
89        assert_eq!(resp.next_cursor_mark.as_deref(), Some("AoJ123"));
90        assert_eq!(resp.results.len(), 2);
91        assert_eq!(resp.results[0].pmcid.as_deref(), Some("PMC7894017"));
92        assert_eq!(resp.results[0].pub_year.as_deref(), Some("2021"));
93        assert_eq!(resp.results[1].source, "PPR");
94    }
95
96    #[test]
97    fn test_parse_search_core_keeps_extra_fields() {
98        let json = r#"{
99            "hitCount": 1,
100            "resultList": {
101                "result": [
102                    {
103                        "id": "1",
104                        "source": "MED",
105                        "title": "Has extra",
106                        "citedByCount": 42,
107                        "inEPMC": "Y"
108                    }
109                ]
110            }
111        }"#;
112
113        let resp = parse_search_response(json).unwrap();
114        let result = &resp.results[0];
115        assert_eq!(
116            result.extra.get("citedByCount").and_then(|v| v.as_i64()),
117            Some(42)
118        );
119        assert_eq!(
120            result.extra.get("inEPMC").and_then(|v| v.as_str()),
121            Some("Y")
122        );
123    }
124
125    #[test]
126    fn test_parse_empty_results() {
127        let json = r#"{"hitCount": 0, "resultList": {"result": []}}"#;
128        let resp = parse_search_response(json).unwrap();
129        assert_eq!(resp.hit_count, 0);
130        assert!(resp.results.is_empty());
131        assert!(resp.next_cursor_mark.is_none());
132    }
133}