1use crate::bgp::Binding;
18
19pub trait ServiceClient: Send + Sync {
27 fn query(&self, endpoint: &str, query: &str) -> Result<Vec<Binding>, String>;
28}
29
30pub fn parse_sparql_json_results(body: &str) -> Result<Vec<Binding>, String> {
36 let doc: serde_json::Value =
37 serde_json::from_str(body).map_err(|e| format!("results are not JSON: {e}"))?;
38 let Some(results) = doc.get("results") else {
39 return Ok(Vec::new()); };
41 let bindings = results
42 .get("bindings")
43 .and_then(|b| b.as_array())
44 .ok_or("malformed results: no bindings array")?;
45 let mut out = Vec::with_capacity(bindings.len());
46 for sol in bindings {
47 let obj = sol.as_object().ok_or("malformed solution")?;
48 let mut b = Binding::new();
49 for (var, term) in obj {
50 if let Some(token) = json_term_token(term) {
51 b.insert(var.clone(), token);
52 }
53 }
54 out.push(b);
55 }
56 Ok(out)
57}
58
59fn json_term_token(term: &serde_json::Value) -> Option<String> {
62 let ty = term.get("type")?.as_str()?;
63 let value = term.get("value")?.as_str()?;
64 match ty {
65 "uri" => Some(format!("<{value}>")),
66 "bnode" => Some(format!("_:{value}")),
67 "literal" | "typed-literal" => {
69 let mut tok = String::with_capacity(value.len() + 8);
70 tok.push('"');
71 escape_literal_into(value, &mut tok);
72 tok.push('"');
73 if let Some(lang) = term.get("xml:lang").and_then(|l| l.as_str()) {
74 tok.push('@');
75 tok.push_str(lang);
76 } else if let Some(dt) = term.get("datatype").and_then(|d| d.as_str()) {
77 if dt != "http://www.w3.org/2001/XMLSchema#string" {
80 tok.push_str("^^<");
81 tok.push_str(dt);
82 tok.push('>');
83 }
84 }
85 Some(tok)
86 }
87 _ => None,
88 }
89}
90
91fn escape_literal_into(s: &str, out: &mut String) {
94 for c in s.chars() {
95 match c {
96 '\\' => out.push_str("\\\\"),
97 '"' => out.push_str("\\\""),
98 '\n' => out.push_str("\\n"),
99 '\r' => out.push_str("\\r"),
100 '\t' => out.push_str("\\t"),
101 other => out.push(other),
102 }
103 }
104}
105
106pub fn sparql_json_results(vars: &[String], solutions: &[Binding]) -> String {
116 use serde_json::{json, Map, Value};
117 let bindings: Vec<Value> = solutions
118 .iter()
119 .map(|sol| {
120 let mut obj = Map::new();
121 for (var, token) in sol {
122 if let Some(term) = token_json_term(token) {
123 obj.insert(var.clone(), term);
124 }
125 }
126 Value::Object(obj)
127 })
128 .collect();
129 json!({
130 "head": { "vars": vars },
131 "results": { "bindings": bindings },
132 })
133 .to_string()
134}
135
136pub fn sparql_json_ask(boolean: bool) -> String {
138 serde_json::json!({ "head": {}, "boolean": boolean }).to_string()
139}
140
141fn token_json_term(token: &str) -> Option<serde_json::Value> {
144 use crate::terms;
145 use serde_json::json;
146 if let Some(iri) = terms::iri_content(token) {
147 return Some(json!({ "type": "uri", "value": iri }));
148 }
149 if terms::is_blank(token) {
150 return Some(json!({ "type": "bnode", "value": token.strip_prefix("_:")? }));
151 }
152 if terms::is_literal(token) {
153 let value = terms::literal_lexical(token)?;
154 let lang = terms::lang_tag(token)?;
155 if !lang.is_empty() {
156 return Some(json!({ "type": "literal", "value": value, "xml:lang": lang }));
157 }
158 let dt = terms::literal_datatype(token)?;
159 if dt == "http://www.w3.org/2001/XMLSchema#string" {
160 return Some(json!({ "type": "literal", "value": value }));
161 }
162 return Some(json!({ "type": "literal", "value": value, "datatype": dt }));
163 }
164 None
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn parses_every_term_shape() {
173 let body = r#"{
174 "head": {"vars": ["s", "l", "n", "b", "u"]},
175 "results": {"bindings": [
176 {
177 "s": {"type": "uri", "value": "http://ex/a"},
178 "l": {"type": "literal", "value": "hi", "xml:lang": "en"},
179 "n": {"type": "literal", "value": "42",
180 "datatype": "http://www.w3.org/2001/XMLSchema#integer"},
181 "b": {"type": "bnode", "value": "b0"}
182 },
183 {
184 "s": {"type": "uri", "value": "http://ex/b"},
185 "l": {"type": "typed-literal", "value": "plain",
186 "datatype": "http://www.w3.org/2001/XMLSchema#string"}
187 }
188 ]}
189 }"#;
190 let sols = parse_sparql_json_results(body).unwrap();
191 assert_eq!(sols.len(), 2);
192 assert_eq!(sols[0]["s"], "<http://ex/a>");
193 assert_eq!(sols[0]["l"], "\"hi\"@en");
194 assert_eq!(
195 sols[0]["n"],
196 "\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"
197 );
198 assert_eq!(sols[0]["b"], "_:b0");
199 assert!(!sols[0].contains_key("u"));
201 assert_eq!(sols[1]["l"], "\"plain\"");
202 }
203
204 #[test]
205 fn escapes_literal_lexical_forms() {
206 let body = r#"{"results": {"bindings": [
207 {"l": {"type": "literal", "value": "a \"q\"\nb\\c"}}
208 ]}}"#;
209 let sols = parse_sparql_json_results(body).unwrap();
210 assert_eq!(sols[0]["l"], "\"a \\\"q\\\"\\nb\\\\c\"");
211 }
212
213 #[test]
214 fn ask_and_garbage_documents() {
215 assert!(parse_sparql_json_results(r#"{"boolean": true}"#)
217 .unwrap()
218 .is_empty());
219 assert!(parse_sparql_json_results("not json").is_err());
220 assert!(parse_sparql_json_results(r#"{"results": {}}"#).is_err());
221 }
222
223 #[test]
228 fn serialize_parse_round_trips_every_term_shape() {
229 let vars: Vec<String> = ["s", "l", "n", "b", "e", "u"]
230 .iter()
231 .map(|v| v.to_string())
232 .collect();
233 let sols = vec![
234 binding_of(&[
235 ("s", "<http://ex/a>"),
236 ("l", "\"hi\"@en"),
237 ("n", "\"42\"^^<http://www.w3.org/2001/XMLSchema#integer>"),
238 ("b", "_:b0"),
239 ("e", "\"a \\\"q\\\"\\nb\\\\c\""),
240 ]),
242 binding_of(&[("s", "<http://ex/b>"), ("l", "\"plain\"")]),
243 ];
244 let doc = sparql_json_results(&vars, &sols);
245 let back = parse_sparql_json_results(&doc).unwrap();
246 assert_eq!(back, sols, "serialize→parse must be the identity");
247 assert!(parse_sparql_json_results(&sparql_json_ask(true))
249 .unwrap()
250 .is_empty());
251 }
252
253 fn binding_of(pairs: &[(&str, &str)]) -> Binding {
254 pairs
255 .iter()
256 .map(|(k, v)| (k.to_string(), v.to_string()))
257 .collect()
258 }
259}