1use crate::sparql::QueryOutput;
12
13pub fn results_envelope_json(out: &QueryOutput, extra: &str) -> String {
22 let mut s = String::from("{");
23 match out {
24 QueryOutput::Ask(b) => {
25 s.push_str(r#""kind":"ask","boolean":"#);
26 s.push_str(if *b { "true" } else { "false" });
27 }
28 QueryOutput::Select(project, solutions) => {
29 let mut vars: Vec<&str> = project.iter().map(String::as_str).collect();
31 if vars.is_empty() {
32 let mut seen = std::collections::BTreeSet::new();
33 for sol in solutions {
34 for k in sol.keys() {
35 if seen.insert(k.as_str()) {
36 vars.push(k.as_str());
37 }
38 }
39 }
40 }
41 s.push_str(r#""kind":"select","vars":["#);
42 for (i, v) in vars.iter().enumerate() {
43 if i > 0 {
44 s.push(',');
45 }
46 push_json_string(&mut s, v);
47 }
48 s.push_str(r#"],"rows":["#);
49 for (i, sol) in solutions.iter().enumerate() {
50 if i > 0 {
51 s.push(',');
52 }
53 s.push('{');
54 let mut first = true;
55 for var in &vars {
56 if let Some(term) = sol.get(*var) {
57 if !first {
58 s.push(',');
59 }
60 first = false;
61 push_json_string(&mut s, var);
62 s.push(':');
63 push_json_string(&mut s, term);
64 }
65 }
66 s.push('}');
67 }
68 s.push(']');
69 }
70 QueryOutput::Construct(triples) => {
71 s.push_str(r#""kind":"construct","triples":["#);
72 for (i, (a, b, c)) in triples.iter().enumerate() {
73 if i > 0 {
74 s.push(',');
75 }
76 s.push('[');
77 push_json_string(&mut s, a);
78 s.push(',');
79 push_json_string(&mut s, b);
80 s.push(',');
81 push_json_string(&mut s, c);
82 s.push(']');
83 }
84 s.push(']');
85 }
86 }
87 s.push_str(extra);
88 s.push('}');
89 s
90}
91
92pub fn push_json_string(out: &mut String, v: &str) {
97 out.push('"');
98 for c in v.chars() {
99 match c {
100 '"' => out.push_str("\\\""),
101 '\\' => out.push_str("\\\\"),
102 '\n' => out.push_str("\\n"),
103 '\r' => out.push_str("\\r"),
104 '\t' => out.push_str("\\t"),
105 '\u{08}' => out.push_str("\\b"),
106 '\u{0C}' => out.push_str("\\f"),
107 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
108 c => out.push(c),
109 }
110 }
111 out.push('"');
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::Binding;
118 use serde_json::{json, Value};
119
120 fn parse(s: &str) -> Value {
121 serde_json::from_str(s).unwrap_or_else(|e| panic!("invalid JSON ({e}): {s}"))
122 }
123
124 fn row(pairs: &[(&str, &str)]) -> Binding {
125 pairs
126 .iter()
127 .map(|(k, v)| (k.to_string(), v.to_string()))
128 .collect()
129 }
130
131 #[test]
132 fn ask_envelope() {
133 assert_eq!(
134 parse(&results_envelope_json(&QueryOutput::Ask(true), "")),
135 json!({"kind":"ask","boolean":true})
136 );
137 assert_eq!(
138 parse(&results_envelope_json(&QueryOutput::Ask(false), "")),
139 json!({"kind":"ask","boolean":false})
140 );
141 }
142
143 #[test]
144 fn select_envelope_matches_reference() {
145 let out = QueryOutput::Select(
146 vec!["s".into(), "o".into()],
147 vec![
148 row(&[("s", "<a>"), ("o", "<b>")]),
149 row(&[("s", "<c>")]),
151 ],
152 );
153 let got = parse(&results_envelope_json(&out, ""));
154 let want = json!({
155 "kind": "select",
156 "vars": ["s", "o"],
157 "rows": [ {"s": "<a>", "o": "<b>"}, {"s": "<c>"} ],
158 });
159 assert_eq!(got, want);
160 }
161
162 #[test]
163 fn select_unprojected_uses_union_of_keys() {
164 let out = QueryOutput::Select(vec![], vec![row(&[("y", "1"), ("x", "2")])]);
165 let got = parse(&results_envelope_json(&out, ""));
166 let vars: std::collections::BTreeSet<String> = got["vars"]
168 .as_array()
169 .unwrap()
170 .iter()
171 .map(|v| v.as_str().unwrap().to_string())
172 .collect();
173 assert_eq!(
174 vars,
175 ["x".to_string(), "y".to_string()].into_iter().collect()
176 );
177 assert_eq!(got["rows"][0]["x"], json!("2"));
178 }
179
180 #[test]
181 fn escaping_matches_serde_json() {
182 let tricky = "a\"b\\c\nd\te\r\u{08}\u{0c}\u{1}f—🜨";
185 let out = QueryOutput::Select(vec!["v".into()], vec![row(&[("v", tricky)])]);
186 let got = parse(&results_envelope_json(&out, ""));
187 assert_eq!(got["rows"][0]["v"], json!(tricky));
188 let mut buf = String::new();
190 push_json_string(&mut buf, tricky);
191 assert_eq!(buf, serde_json::to_string(tricky).unwrap());
192 }
193
194 #[test]
195 fn construct_triples_and_extra_member() {
196 let out = QueryOutput::Construct(vec![("<s>".into(), "<p>".into(), "\"lit\"".into())]);
197 let got = parse(&results_envelope_json(&out, r#","remote":{"bytes":42}"#));
198 assert_eq!(got["kind"], json!("construct"));
199 assert_eq!(got["triples"], json!([["<s>", "<p>", "\"lit\""]]));
200 assert_eq!(got["remote"]["bytes"], json!(42));
201 }
202
203 #[test]
204 fn empty_select_is_valid() {
205 let out = QueryOutput::Select(vec!["x".into()], vec![]);
206 let got = parse(&results_envelope_json(&out, ""));
207 assert_eq!(got, json!({"kind":"select","vars":["x"],"rows":[]}));
208 }
209}