qualia_core_db/domains/geospatial/adapters/
sparql_adapter.rs1use crate::domains::geospatial::adapters::{AdapterHttpRequest, DataAdapter};
2use crate::net::disclosure::NetworkDisclosureRegistry;
3
4pub struct SparqlAdapter {
6 pub id: &'static str,
7 pub endpoint: String,
8}
9
10fn quin(s: u64, p: u64, o: u64) -> crate::NQuin {
12 crate::NQuin {
13 subject: s,
14 predicate: p,
15 object: o,
16 context: 0,
17 metadata: 0,
18 parity: s ^ p ^ o,
19 }
20}
21
22impl SparqlAdapter {
23 pub fn new(id: &'static str, endpoint: &str) -> Self {
24 Self {
25 id,
26 endpoint: endpoint.to_string(),
27 }
28 }
29
30 pub fn parse_features(&self, body: &str) -> Result<Vec<crate::NQuin>, String> {
46 use crate::query::lexicon::generate_60bit_token;
47
48 let json: serde_json::Value = serde_json::from_str(body).map_err(|e| e.to_string())?;
49
50 let vars: Vec<String> = json
52 .get("head")
53 .and_then(|h| h.get("vars"))
54 .and_then(|v| v.as_array())
55 .map(|arr| {
56 arr.iter()
57 .filter_map(|v| v.as_str().map(|s| s.to_string()))
58 .collect()
59 })
60 .unwrap_or_default();
61
62 let bindings = match json
64 .get("results")
65 .and_then(|r| r.get("bindings"))
66 .and_then(|b| b.as_array())
67 {
68 Some(b) => b,
69 None => return Ok(Vec::new()),
70 };
71
72 let mut quins = Vec::new();
73
74 for (i, row) in bindings.iter().enumerate() {
75 let mut subject_var: Option<&str> = None;
77 for v in &vars {
78 if let Some(binding) = row.get(v.as_str()) {
79 if binding.get("type").and_then(|t| t.as_str()) == Some("uri") {
80 subject_var = Some(v.as_str());
81 break;
82 }
83 }
84 }
85
86 let subject = match subject_var {
87 Some(v) => {
88 let val = row
89 .get(v)
90 .and_then(|b| b.get("value"))
91 .and_then(|v| v.as_str())
92 .unwrap_or("");
93 generate_60bit_token(val.as_bytes())
94 }
95 None => generate_60bit_token(format!("urn:sparql:row:{}", i).as_bytes()),
96 };
97
98 for v in &vars {
100 if Some(v.as_str()) == subject_var {
101 continue;
102 }
103 let value = match row
104 .get(v.as_str())
105 .and_then(|b| b.get("value"))
106 .and_then(|v| v.as_str())
107 {
108 Some(val) => val,
109 None => continue, };
111 let predicate = generate_60bit_token(format!("urn:sparql:var:{}", v).as_bytes());
112 let object = generate_60bit_token(value.as_bytes());
113 quins.push(quin(subject, predicate, object));
114 }
115 }
116
117 Ok(quins)
118 }
119}
120
121impl DataAdapter for SparqlAdapter {
122 fn adapter_id(&self) -> &'static str {
123 self.id
124 }
125
126 fn build_fetch_request(
127 &self,
128 bbox: (f64, f64, f64, f64),
129 _time_range: (u64, u64),
130 registry: &NetworkDisclosureRegistry,
131 ) -> Result<AdapterHttpRequest, String> {
132 if !registry.check_egress_consent(self.adapter_id(), &self.endpoint) {
133 return Err(format!(
134 "Consent denied or unregistered for SPARQL endpoint {}",
135 self.endpoint
136 ));
137 }
138
139 let sparql_query = format!(
142 "SELECT ?item ?itemLabel ?location WHERE {{
143 ?item wdt:P625 ?location.
144 SERVICE wikibase:box {{
145 ?item wdt:P625 ?location.
146 bd:serviceParam wikibase:cornerSouthWest \"Point({} {})\"^^geo:wktLiteral.
147 bd:serviceParam wikibase:cornerNorthEast \"Point({} {})\"^^geo:wktLiteral.
148 }}
149 SERVICE wikibase:label {{ bd:serviceParam wikibase:language \"[AUTO_LANGUAGE],en\". }}
150}}",
151 bbox.0, bbox.1, bbox.2, bbox.3
152 );
153
154 let url = format!(
155 "{}?query={}&format=json",
156 self.endpoint,
157 urlencoding::encode(&sparql_query)
158 );
159
160 Ok(AdapterHttpRequest::get(url, "SPARQL"))
161 }
162
163 fn parse_response(&self, body: &str) -> Result<Vec<crate::NQuin>, String> {
164 self.parse_features(body)
165 }
166
167 fn primary_endpoint(&self) -> &str {
168 &self.endpoint
169 }
170
171 fn estimate_tile_count(&self, _bbox: (f64, f64, f64, f64)) -> u32 {
172 1 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_sparql_adapter_consent_denied() {
182 let registry = NetworkDisclosureRegistry::new();
183 let adapter = SparqlAdapter::new("sparql_adapter", "https://query.wikidata.org/sparql");
184
185 let res = adapter.fetch_region((0.0, 0.0, 1.0, 1.0), (0, 0), ®istry);
186 assert!(res.is_err());
187 }
188
189 #[test]
190 fn test_sparql_results_parse_features() {
191 use crate::query::lexicon::generate_60bit_token;
192
193 let body = r#"{"head":{"vars":["item","itemLabel","coord"]},
194 "results":{"bindings":[
195 {"item":{"type":"uri","value":"http://www.wikidata.org/entity/Q42"},
196 "itemLabel":{"type":"literal","value":"Douglas Adams"},
197 "coord":{"type":"literal","value":"Point(-0.13 51.5)"}},
198 {"item":{"type":"uri","value":"http://www.wikidata.org/entity/Q64"},
199 "itemLabel":{"type":"literal","value":"Berlin"}}
200 ]}}"#;
201
202 let adapter = SparqlAdapter::new("sparql_adapter", "https://query.wikidata.org/sparql");
203 let quins = adapter.parse_features(body).expect("parse should succeed");
204
205 assert_eq!(quins.len(), 3, "expected 3 quins, got {}", quins.len());
207
208 let q42_subject = generate_60bit_token(b"http://www.wikidata.org/entity/Q42");
210 let label_pred = generate_60bit_token(b"urn:sparql:var:itemLabel");
211 let douglas_obj = generate_60bit_token(b"Douglas Adams");
212
213 let found = quins.iter().any(|q| {
214 q.subject == q42_subject
215 && q.predicate == label_pred
216 && q.object == douglas_obj
217 && q.parity == (q.subject ^ q.predicate ^ q.object)
218 });
219 assert!(found, "expected Q42 itemLabel=Douglas Adams quin not found");
220 }
221}