Skip to main content

papers_core/
summary.rs

1use papers_openalex::{Author, Domain, Field, Funder, Institution, ListMeta, ListResponse, Publisher, Source, Subfield, Topic, Work};
2use papers_openalex::OpenAlexError;
3use serde::Serialize;
4
5/// Slim wrapper returned by all list functions — keeps meta but drops group_by
6/// and maps full entities to their summary equivalents.
7#[derive(Serialize)]
8pub struct SlimListResponse<S: Serialize> {
9    pub meta: ListMeta,
10    pub results: Vec<S>,
11}
12
13pub fn summary_list_result<T, S: Serialize>(
14    result: Result<ListResponse<T>, OpenAlexError>,
15    f: impl Fn(T) -> S,
16) -> Result<SlimListResponse<S>, OpenAlexError> {
17    result.map(|r| SlimListResponse {
18        meta: r.meta,
19        results: r.results.into_iter().map(f).collect(),
20    })
21}
22
23// ── WorkSummary ───────────────────────────────────────────────────────────
24
25#[derive(Serialize)]
26pub struct WorkSummary {
27    pub id: String,
28    pub title: Option<String>,
29    pub doi: Option<String>,
30    pub publication_year: Option<i32>,
31    pub r#type: Option<String>,
32    pub authors: Vec<String>,
33    pub journal: Option<String>,
34    pub is_oa: Option<bool>,
35    pub oa_url: Option<String>,
36    pub cited_by_count: Option<i64>,
37    pub primary_topic: Option<String>,
38    pub abstract_text: Option<String>,
39}
40
41impl From<Work> for WorkSummary {
42    fn from(w: Work) -> Self {
43        let authors = w
44            .authorships
45            .unwrap_or_default()
46            .into_iter()
47            .filter_map(|a| a.author.and_then(|au| au.display_name))
48            .collect();
49
50        let journal = w
51            .primary_location
52            .as_ref()
53            .and_then(|l| l.source.as_ref())
54            .and_then(|s| s.display_name.clone());
55
56        let is_oa = w.open_access.as_ref().and_then(|oa| oa.is_oa);
57        let oa_url = w.open_access.and_then(|oa| oa.oa_url);
58
59        let primary_topic = w
60            .primary_topic
61            .and_then(|t| t.display_name);
62
63        WorkSummary {
64            id: w.id,
65            title: w.display_name,
66            doi: w.doi,
67            publication_year: w.publication_year,
68            r#type: w.r#type,
69            authors,
70            journal,
71            is_oa,
72            oa_url,
73            cited_by_count: w.cited_by_count,
74            primary_topic,
75            abstract_text: w.abstract_text,
76        }
77    }
78}
79
80// ── AuthorSummary ─────────────────────────────────────────────────────────
81
82#[derive(Serialize)]
83pub struct AuthorSummary {
84    pub id: String,
85    pub display_name: Option<String>,
86    pub orcid: Option<String>,
87    pub works_count: Option<i64>,
88    pub cited_by_count: Option<i64>,
89    pub h_index: Option<i64>,
90    pub last_known_institutions: Vec<String>,
91    pub top_topics: Vec<String>,
92}
93
94impl From<Author> for AuthorSummary {
95    fn from(a: Author) -> Self {
96        let h_index = a.summary_stats.as_ref().and_then(|s| s.h_index);
97
98        let last_known_institutions = a
99            .last_known_institutions
100            .unwrap_or_default()
101            .into_iter()
102            .filter_map(|i| i.display_name)
103            .collect();
104
105        let top_topics = a
106            .topics
107            .unwrap_or_default()
108            .into_iter()
109            .take(3)
110            .filter_map(|t| t.display_name)
111            .collect();
112
113        AuthorSummary {
114            id: a.id,
115            display_name: a.display_name,
116            orcid: a.orcid,
117            works_count: a.works_count,
118            cited_by_count: a.cited_by_count,
119            h_index,
120            last_known_institutions,
121            top_topics,
122        }
123    }
124}
125
126// ── SourceSummary ─────────────────────────────────────────────────────────
127
128#[derive(Serialize)]
129pub struct SourceSummary {
130    pub id: String,
131    pub display_name: Option<String>,
132    pub issn_l: Option<String>,
133    pub r#type: Option<String>,
134    pub is_oa: Option<bool>,
135    pub is_in_doaj: Option<bool>,
136    pub works_count: Option<i64>,
137    pub cited_by_count: Option<i64>,
138    pub h_index: Option<i64>,
139    pub host_organization_name: Option<String>,
140}
141
142impl From<Source> for SourceSummary {
143    fn from(s: Source) -> Self {
144        let h_index = s.summary_stats.as_ref().and_then(|st| st.h_index);
145
146        SourceSummary {
147            id: s.id,
148            display_name: s.display_name,
149            issn_l: s.issn_l,
150            r#type: s.r#type,
151            is_oa: s.is_oa,
152            is_in_doaj: s.is_in_doaj,
153            works_count: s.works_count,
154            cited_by_count: s.cited_by_count,
155            h_index,
156            host_organization_name: s.host_organization_name,
157        }
158    }
159}
160
161// ── InstitutionSummary ────────────────────────────────────────────────────
162
163#[derive(Serialize)]
164pub struct InstitutionSummary {
165    pub id: String,
166    pub display_name: Option<String>,
167    pub ror: Option<String>,
168    pub country_code: Option<String>,
169    pub r#type: Option<String>,
170    pub city: Option<String>,
171    pub works_count: Option<i64>,
172    pub cited_by_count: Option<i64>,
173    pub h_index: Option<i64>,
174}
175
176impl From<Institution> for InstitutionSummary {
177    fn from(i: Institution) -> Self {
178        let h_index = i.summary_stats.as_ref().and_then(|s| s.h_index);
179        let city = i.geo.and_then(|g| g.city);
180
181        InstitutionSummary {
182            id: i.id,
183            display_name: i.display_name,
184            ror: i.ror,
185            country_code: i.country_code,
186            r#type: i.r#type,
187            city,
188            works_count: i.works_count,
189            cited_by_count: i.cited_by_count,
190            h_index,
191        }
192    }
193}
194
195// ── TopicSummary ──────────────────────────────────────────────────────────
196
197#[derive(Serialize)]
198pub struct TopicSummary {
199    pub id: String,
200    pub display_name: Option<String>,
201    pub description: Option<String>,
202    pub subfield: Option<String>,
203    pub field: Option<String>,
204    pub domain: Option<String>,
205    pub works_count: Option<i64>,
206    pub cited_by_count: Option<i64>,
207}
208
209impl From<Topic> for TopicSummary {
210    fn from(t: Topic) -> Self {
211        let subfield = t.subfield.and_then(|s| s.display_name);
212        let field = t.field.and_then(|f| f.display_name);
213        let domain = t.domain.and_then(|d| d.display_name);
214
215        TopicSummary {
216            id: t.id,
217            display_name: t.display_name,
218            description: t.description,
219            subfield,
220            field,
221            domain,
222            works_count: t.works_count,
223            cited_by_count: t.cited_by_count,
224        }
225    }
226}
227
228// ── PublisherSummary ──────────────────────────────────────────────────────
229
230#[derive(Serialize)]
231pub struct PublisherSummary {
232    pub id: String,
233    pub display_name: Option<String>,
234    pub hierarchy_level: Option<i32>,
235    pub country_codes: Option<Vec<String>>,
236    pub works_count: Option<i64>,
237    pub cited_by_count: Option<i64>,
238}
239
240impl From<Publisher> for PublisherSummary {
241    fn from(p: Publisher) -> Self {
242        PublisherSummary {
243            id: p.id,
244            display_name: p.display_name,
245            hierarchy_level: p.hierarchy_level,
246            country_codes: p.country_codes,
247            works_count: p.works_count,
248            cited_by_count: p.cited_by_count,
249        }
250    }
251}
252
253// ── FunderSummary ─────────────────────────────────────────────────────────
254
255#[derive(Serialize)]
256pub struct FunderSummary {
257    pub id: String,
258    pub display_name: Option<String>,
259    pub country_code: Option<String>,
260    pub description: Option<String>,
261    pub awards_count: Option<i64>,
262    pub works_count: Option<i64>,
263    pub cited_by_count: Option<i64>,
264}
265
266impl From<Funder> for FunderSummary {
267    fn from(f: Funder) -> Self {
268        FunderSummary {
269            id: f.id,
270            display_name: f.display_name,
271            country_code: f.country_code,
272            description: f.description,
273            awards_count: f.awards_count,
274            works_count: f.works_count,
275            cited_by_count: f.cited_by_count,
276        }
277    }
278}
279
280// ── DomainSummary ────────────────────────────────────────────────────────
281
282#[derive(Serialize)]
283pub struct DomainSummary {
284    pub id: String,
285    pub display_name: Option<String>,
286    pub description: Option<String>,
287    pub fields: Vec<String>,
288    pub works_count: Option<i64>,
289    pub cited_by_count: Option<i64>,
290}
291
292impl From<Domain> for DomainSummary {
293    fn from(d: Domain) -> Self {
294        let fields = d
295            .fields
296            .unwrap_or_default()
297            .into_iter()
298            .filter_map(|f| f.display_name)
299            .collect();
300
301        DomainSummary {
302            id: d.id,
303            display_name: d.display_name,
304            description: d.description,
305            fields,
306            works_count: d.works_count,
307            cited_by_count: d.cited_by_count,
308        }
309    }
310}
311
312// ── FieldSummary ─────────────────────────────────────────────────────────
313
314#[derive(Serialize)]
315pub struct FieldSummary {
316    pub id: String,
317    pub display_name: Option<String>,
318    pub description: Option<String>,
319    pub domain: Option<String>,
320    pub subfield_count: usize,
321    pub works_count: Option<i64>,
322    pub cited_by_count: Option<i64>,
323}
324
325impl From<Field> for FieldSummary {
326    fn from(f: Field) -> Self {
327        let domain = f.domain.and_then(|d| d.display_name);
328        let subfield_count = f.subfields.as_ref().map_or(0, |s| s.len());
329
330        FieldSummary {
331            id: f.id,
332            display_name: f.display_name,
333            description: f.description,
334            domain,
335            subfield_count,
336            works_count: f.works_count,
337            cited_by_count: f.cited_by_count,
338        }
339    }
340}
341
342// ── SubfieldSummary ──────────────────────────────────────────────────────
343
344#[derive(Serialize)]
345pub struct SubfieldSummary {
346    pub id: String,
347    pub display_name: Option<String>,
348    pub description: Option<String>,
349    pub field: Option<String>,
350    pub domain: Option<String>,
351    pub works_count: Option<i64>,
352    pub cited_by_count: Option<i64>,
353}
354
355impl From<Subfield> for SubfieldSummary {
356    fn from(s: Subfield) -> Self {
357        let field = s.field.and_then(|f| f.display_name);
358        let domain = s.domain.and_then(|d| d.display_name);
359
360        SubfieldSummary {
361            id: s.id,
362            display_name: s.display_name,
363            description: s.description,
364            field,
365            domain,
366            works_count: s.works_count,
367            cited_by_count: s.cited_by_count,
368        }
369    }
370}