use aspero_core::{Person, ProviderId, SourceRecord, Usage};
use aspero_engine::{Abstract, Candidate};
use serde_json::{Map, Value, json};
pub fn work(record: &SourceRecord, full: bool) -> Value {
let w = &record.work;
let mut out = Map::new();
out.insert("source".into(), json!(record.source));
out.insert("ids".into(), json!(record.ids));
if let Some(id) = aspero_core::canonical(&record.ids) {
out.insert("canonical".into(), json!(id));
}
let mut put = |key: &str, value: Option<Value>| {
if let Some(v) = value {
out.insert(key.into(), v);
}
};
put("title", w.title.as_ref().map(|t| json!(t)));
if !w.authors.is_empty() {
let authors: Vec<Value> = w.authors.iter().filter_map(|a| author(a, full)).collect();
put("authors", Some(json!(authors)));
}
put("issued", w.issued.map(|d| json!(d.to_string())));
put("type", w.work_type.as_ref().map(|t| json!(t.as_csl())));
put("container", w.container_title.as_ref().map(|c| json!(c)));
put("url", w.url.as_ref().map(|u| json!(u)));
put("updates", (!w.updates.is_empty()).then(|| json!(w.updates)));
put(
"update_to",
(!w.update_to.is_empty()).then(|| json!(w.update_to)),
);
put("open_access", w.open_access.as_ref().map(|oa| json!(oa)));
if full {
if let Ok(Value::Object(all)) = serde_json::to_value(w) {
for (k, v) in all {
out.entry(k).or_insert(v);
}
}
out.insert("fetched_at".into(), json!(record.fetched_at.to_string()));
}
Value::Object(out)
}
fn author(person: &Person, full: bool) -> Option<Value> {
let mut out = Map::new();
out.insert("name".into(), json!(person.display_name()?));
if full && let Ok(Value::Object(parts)) = serde_json::to_value(person) {
out.extend(parts);
} else if let Some(orcid) = &person.orcid {
out.insert("orcid".into(), json!(orcid));
}
Some(Value::Object(out))
}
pub fn usage(usage: &[(ProviderId, Usage)]) -> Value {
let map: Map<String, Value> = usage
.iter()
.map(|(p, u)| (p.to_string(), usage_one(u)))
.collect();
Value::Object(map)
}
pub fn usage_one(u: &Usage) -> Value {
let mut out = Map::new();
if let Some(c) = u.cost {
out.insert("cost".into(), json!(c));
}
if let Some(r) = u.remaining {
out.insert("remaining".into(), json!(r));
}
if let Some(t) = u.resets_at {
out.insert("resets_at".into(), json!(t.to_string()));
}
Value::Object(out)
}
pub fn set_abstract(work: &mut Value, found: Option<&Abstract>) {
match found {
Some(a) => {
work["abstract"] = json!(a.text);
work["abstract_source"] = json!(a.source);
}
None => work["abstract"] = Value::Null,
}
}
pub fn candidate(c: &Candidate, full: bool) -> Value {
let a = &c.assessment;
let checks = if full {
json!({
"title": a.title,
"authors": a.authors,
"year": a.year,
"container": a.container,
"volume": a.volume,
"page": a.page,
"ids": a.ids,
})
} else {
json!({
"title": a.title.agreement,
"authors": a.authors.agreement,
"year": a.year.agreement,
"container": a.container.agreement,
"volume": a.volume.agreement,
"page": a.page.agreement,
"ids": a.ids,
})
};
let found_by: Vec<Value> = c
.sightings
.iter()
.map(|s| json!({"provider": s.provider, "via": s.via.as_str(), "rank": s.rank, "score": s.score}))
.collect();
let mut work = work(&c.record, full);
work["ids"] = json!(c.ids);
work["canonical"] = json!(aspero_core::canonical(&c.ids));
if !c.updates.is_empty() {
work["updates"] = json!(c.updates);
}
json!({
"confidence": (a.confidence * 100.0).round() / 100.0,
"checks": checks,
"found_by": found_by,
"work": work,
})
}
#[cfg(test)]
mod tests {
use aspero_core::{FieldSet, PartialDate, Raw, Work, WorkType};
use jiff::Timestamp;
use super::*;
fn record() -> SourceRecord {
let mut work = Work::default();
work.title = Some("Deep learning".into());
work.authors = [
"Yann LeCun",
"Yoshua Bengio",
"Geoffrey Hinton",
"A. N. Other",
]
.map(Person::literal)
.to_vec();
work.issued = PartialDate::new(2015, Some(5), Some(27));
work.work_type = Some(WorkType::JournalArticle);
work.container_title = Some("Nature".into());
work.abstract_text = Some("Deep learning allows…".into());
let raw = Raw {
media_type: "application/json".into(),
body: "{}".into(),
};
SourceRecord::new(
ProviderId::from_static("crossref"),
vec!["10.1038/nature14539".parse().unwrap()],
work,
FieldSet::ALL,
raw,
Timestamp::UNIX_EPOCH,
)
}
#[test]
fn lean_json_identifies_and_cites_without_extras() {
assert_eq!(
work(&record(), false),
json!({
"source": "crossref",
"ids": ["doi:10.1038/nature14539"],
"canonical": "doi:10.1038/nature14539",
"title": "Deep learning",
"authors": [
{"name": "Yann LeCun"},
{"name": "Yoshua Bengio"},
{"name": "Geoffrey Hinton"},
{"name": "A. N. Other"},
],
"issued": "2015-05-27",
"type": "article-journal",
"container": "Nature",
})
);
}
#[test]
fn full_json_adds_everything() {
let full = work(&record(), true);
assert_eq!(full["abstract"], "Deep learning allows…");
assert_eq!(full["authors"][0]["literal"], "Yann LeCun");
assert!(full.get("fetched_at").is_some());
}
#[test]
fn authors_have_one_shape_lean_or_full() {
let mut r = record();
let mut lecun = Person::new("Yann", "LeCun");
lecun.orcid = Some("0000-0000-0000-0001".into());
r.work.authors = vec![lecun];
assert_eq!(
work(&r, false)["authors"],
json!([{"name": "Yann LeCun", "orcid": "0000-0000-0000-0001"}])
);
assert_eq!(
work(&r, true)["authors"],
json!([{
"name": "Yann LeCun",
"given": "Yann",
"family": "LeCun",
"orcid": "0000-0000-0000-0001",
}])
);
}
}