1use aspero_core::{Person, ProviderId, SourceRecord, Usage};
4use aspero_engine::{Abstract, Candidate};
5use serde_json::{Map, Value, json};
6
7pub fn work(record: &SourceRecord, full: bool) -> Value {
9 let w = &record.work;
10 let mut out = Map::new();
11 out.insert("source".into(), json!(record.source));
12 out.insert("ids".into(), json!(record.ids));
13 if let Some(id) = aspero_core::canonical(&record.ids) {
15 out.insert("canonical".into(), json!(id));
16 }
17 let mut put = |key: &str, value: Option<Value>| {
18 if let Some(v) = value {
19 out.insert(key.into(), v);
20 }
21 };
22 put("title", w.title.as_ref().map(|t| json!(t)));
23 if !w.authors.is_empty() {
24 let authors: Vec<Value> = w.authors.iter().filter_map(|a| author(a, full)).collect();
25 put("authors", Some(json!(authors)));
26 }
27 put("issued", w.issued.map(|d| json!(d.to_string())));
28 put("type", w.work_type.as_ref().map(|t| json!(t.as_csl())));
29 put("container", w.container_title.as_ref().map(|c| json!(c)));
30 put("url", w.url.as_ref().map(|u| json!(u)));
31 put("updates", (!w.updates.is_empty()).then(|| json!(w.updates)));
33 put(
34 "update_to",
35 (!w.update_to.is_empty()).then(|| json!(w.update_to)),
36 );
37 put("open_access", w.open_access.as_ref().map(|oa| json!(oa)));
39 if full {
40 if let Ok(Value::Object(all)) = serde_json::to_value(w) {
42 for (k, v) in all {
43 out.entry(k).or_insert(v);
44 }
45 }
46 out.insert("fetched_at".into(), json!(record.fetched_at.to_string()));
47 }
48 Value::Object(out)
49}
50
51fn author(person: &Person, full: bool) -> Option<Value> {
54 let mut out = Map::new();
55 out.insert("name".into(), json!(person.display_name()?));
56 if full && let Ok(Value::Object(parts)) = serde_json::to_value(person) {
57 out.extend(parts);
58 } else if let Some(orcid) = &person.orcid {
59 out.insert("orcid".into(), json!(orcid));
60 }
61 Some(Value::Object(out))
62}
63
64pub fn usage(usage: &[(ProviderId, Usage)]) -> Value {
65 let map: Map<String, Value> = usage
66 .iter()
67 .map(|(p, u)| (p.to_string(), usage_one(u)))
68 .collect();
69 Value::Object(map)
70}
71
72pub fn usage_one(u: &Usage) -> Value {
73 let mut out = Map::new();
74 if let Some(c) = u.cost {
75 out.insert("cost".into(), json!(c));
76 }
77 if let Some(r) = u.remaining {
78 out.insert("remaining".into(), json!(r));
79 }
80 if let Some(t) = u.resets_at {
81 out.insert("resets_at".into(), json!(t.to_string()));
82 }
83 Value::Object(out)
84}
85
86pub fn set_abstract(work: &mut Value, found: Option<&Abstract>) {
89 match found {
90 Some(a) => {
91 work["abstract"] = json!(a.text);
92 work["abstract_source"] = json!(a.source);
93 }
94 None => work["abstract"] = Value::Null,
95 }
96}
97
98pub fn candidate(c: &Candidate, full: bool) -> Value {
101 let a = &c.assessment;
102 let checks = if full {
103 json!({
104 "title": a.title,
105 "authors": a.authors,
106 "year": a.year,
107 "container": a.container,
108 "volume": a.volume,
109 "page": a.page,
110 "ids": a.ids,
111 })
112 } else {
113 json!({
114 "title": a.title.agreement,
115 "authors": a.authors.agreement,
116 "year": a.year.agreement,
117 "container": a.container.agreement,
118 "volume": a.volume.agreement,
119 "page": a.page.agreement,
120 "ids": a.ids,
121 })
122 };
123 let found_by: Vec<Value> = c
124 .sightings
125 .iter()
126 .map(|s| json!({"provider": s.provider, "via": s.via.as_str(), "rank": s.rank, "score": s.score}))
127 .collect();
128 let mut work = work(&c.record, full);
129 work["ids"] = json!(c.ids);
130 work["canonical"] = json!(aspero_core::canonical(&c.ids));
131 if !c.updates.is_empty() {
132 work["updates"] = json!(c.updates);
133 }
134 json!({
135 "confidence": (a.confidence * 100.0).round() / 100.0,
136 "checks": checks,
137 "found_by": found_by,
138 "work": work,
139 })
140}
141
142#[cfg(test)]
143mod tests {
144 use aspero_core::{FieldSet, PartialDate, Raw, Work, WorkType};
145 use jiff::Timestamp;
146
147 use super::*;
148
149 fn record() -> SourceRecord {
150 let mut work = Work::default();
151 work.title = Some("Deep learning".into());
152 work.authors = [
153 "Yann LeCun",
154 "Yoshua Bengio",
155 "Geoffrey Hinton",
156 "A. N. Other",
157 ]
158 .map(Person::literal)
159 .to_vec();
160 work.issued = PartialDate::new(2015, Some(5), Some(27));
161 work.work_type = Some(WorkType::JournalArticle);
162 work.container_title = Some("Nature".into());
163 work.abstract_text = Some("Deep learning allows…".into());
164 let raw = Raw {
165 media_type: "application/json".into(),
166 body: "{}".into(),
167 };
168 SourceRecord::new(
169 ProviderId::from_static("crossref"),
170 vec!["10.1038/nature14539".parse().unwrap()],
171 work,
172 FieldSet::ALL,
173 raw,
174 Timestamp::UNIX_EPOCH,
175 )
176 }
177
178 #[test]
179 fn lean_json_identifies_and_cites_without_extras() {
180 assert_eq!(
181 work(&record(), false),
182 json!({
183 "source": "crossref",
184 "ids": ["doi:10.1038/nature14539"],
185 "canonical": "doi:10.1038/nature14539",
186 "title": "Deep learning",
187 "authors": [
188 {"name": "Yann LeCun"},
189 {"name": "Yoshua Bengio"},
190 {"name": "Geoffrey Hinton"},
191 {"name": "A. N. Other"},
192 ],
193 "issued": "2015-05-27",
194 "type": "article-journal",
195 "container": "Nature",
196 })
197 );
198 }
199
200 #[test]
201 fn full_json_adds_everything() {
202 let full = work(&record(), true);
203 assert_eq!(full["abstract"], "Deep learning allows…");
204 assert_eq!(full["authors"][0]["literal"], "Yann LeCun");
205 assert!(full.get("fetched_at").is_some());
206 }
207
208 #[test]
209 fn authors_have_one_shape_lean_or_full() {
210 let mut r = record();
211 let mut lecun = Person::new("Yann", "LeCun");
212 lecun.orcid = Some("0000-0000-0000-0001".into());
213 r.work.authors = vec![lecun];
214 assert_eq!(
215 work(&r, false)["authors"],
216 json!([{"name": "Yann LeCun", "orcid": "0000-0000-0000-0001"}])
217 );
218 assert_eq!(
219 work(&r, true)["authors"],
220 json!([{
221 "name": "Yann LeCun",
222 "given": "Yann",
223 "family": "LeCun",
224 "orcid": "0000-0000-0000-0001",
225 }])
226 );
227 }
228}