1use aspero_core::ErrorCategory;
4use aspero_engine::{Disagreement, InputLookup, LookupResult, Outcome, Served};
5use serde_json::{Value, json};
6
7use crate::work;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum Status {
13 Found,
14 Unsupported,
16 NotFound,
17 Failed(ErrorCategory),
19}
20
21impl Status {
22 pub fn of(result: &LookupResult) -> Self {
23 if result.record.is_some() {
24 return Status::Found;
25 }
26 if result.attempts.is_empty() {
27 return Status::Unsupported;
28 }
29 let mut failures = result.attempts.iter().filter_map(|a| match &a.outcome {
30 Outcome::Failed(e) => Some(e.category()),
31 _ => None,
32 });
33 match failures.next() {
34 Some(first) if failures.count() + 1 == result.attempts.len() => Status::Failed(first),
35 _ => Status::NotFound,
36 }
37 }
38
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Status::Found => "found",
42 Status::Unsupported => "unsupported",
43 Status::NotFound => "not_found",
44 Status::Failed(_) => "failed",
45 }
46 }
47}
48
49pub fn invalid(input: &str, error: &str) -> Value {
51 json!({"input": input, "status": "invalid", "error": error})
52}
53
54pub fn item(input: &str, result: &LookupResult, full: bool) -> Value {
57 let status = Status::of(result);
58 let mut item = json!({"input": input, "id": result.id, "status": status.as_str()});
59 if let Some(record) = &result.record {
60 let served = result
62 .attempts
63 .iter()
64 .rfind(|a| matches!(a.outcome, Outcome::Found))
65 .map(|a| a.served);
66 if let Some(s) = served.filter(|s| *s != Served::Live) {
67 item["served"] = json!(s.as_str());
68 }
69 item["work"] = work::work(record, full);
70 }
71 if status == Status::Unsupported {
72 item["error"] = json!(format!(
73 "no enabled provider looks up {} identifiers",
74 result.id.kind()
75 ));
76 }
77 if result.record.is_none() || full {
78 let attempts: Vec<Value> = result
79 .attempts
80 .iter()
81 .map(|a| match &a.outcome {
82 Outcome::Found => json!({"provider": a.provider, "outcome": "found"}),
83 Outcome::NotFound => json!({"provider": a.provider, "outcome": "not_found"}),
84 Outcome::Failed(e) => {
85 json!({"provider": a.provider, "outcome": "failed", "error": e.to_string()})
86 }
87 _ => json!({"provider": a.provider, "outcome": "unknown"}),
88 })
89 .collect();
90 item["attempts"] = json!(attempts);
91 }
92 item
93}
94
95pub fn input(input: &str, lookup: &InputLookup, full: bool) -> Value {
97 let looked = match lookup {
98 InputLookup::Invalid(e) => return invalid(input, &e.to_string()),
99 InputLookup::Looked(looked) => looked,
100 };
101 let mut out = item(input, &looked.result, full);
102 if looked.with_punctuation {
103 out["note"] = json!("found with the trailing punctuation kept");
104 }
105 if let Some(found) = &looked.abstract_text {
106 work::set_abstract(&mut out["work"], found.as_ref());
107 }
108 if let Some(latest) = &looked.latest {
109 out["latest"] = work::work(latest, false);
110 }
111 if !looked.disagreements.is_empty() {
112 out["disagreements"] = disagreements(&looked.disagreements);
113 }
114 out
115}
116
117pub fn disagreements(found: &[Disagreement]) -> Value {
120 found
121 .iter()
122 .map(|d| {
123 json!({
124 "field": d.field,
125 "value": d.value,
126 d.registry.as_str(): d.registry_value,
127 })
128 })
129 .collect()
130}
131
132#[cfg(test)]
133mod tests {
134 use aspero_core::{Error, ProviderId};
135 use aspero_engine::Attempt;
136
137 use super::*;
138
139 fn result(outcomes: Vec<Outcome>) -> LookupResult {
140 LookupResult {
141 id: "doi:10.1234/x".parse().unwrap(),
142 record: None,
143 attempts: outcomes
144 .into_iter()
145 .map(|outcome| Attempt {
146 provider: ProviderId::from_static("crossref"),
147 outcome,
148 served: Served::Live,
149 })
150 .collect(),
151 }
152 }
153
154 #[test]
155 fn statuses_say_why_nothing_was_found() {
156 assert_eq!(Status::of(&result(vec![])), Status::Unsupported);
157 assert_eq!(
158 Status::of(&result(vec![Outcome::NotFound])),
159 Status::NotFound
160 );
161 let failed = || Outcome::Failed(Error::Timeout);
162 assert_eq!(
163 Status::of(&result(vec![failed(), Outcome::NotFound])),
164 Status::NotFound
165 );
166 assert!(matches!(
167 Status::of(&result(vec![failed(), failed()])),
168 Status::Failed(_)
169 ));
170 }
171
172 #[test]
173 fn items_without_a_record_carry_their_attempts() {
174 assert_eq!(
175 item("10.1234/x", &result(vec![Outcome::NotFound]), false),
176 json!({
177 "input": "10.1234/x",
178 "id": "doi:10.1234/x",
179 "status": "not_found",
180 "attempts": [{"provider": "crossref", "outcome": "not_found"}],
181 })
182 );
183 let unsupported = item("10.1234/x", &result(vec![]), false);
184 assert_eq!(unsupported["status"], "unsupported");
185 assert!(unsupported["error"].as_str().unwrap().contains("doi"));
186 }
187}