qualia_core_db/domains/geospatial/adapters/
gbif_adapter.rs1use crate::domains::geospatial::adapters::AdapterHttpRequest;
2use crate::net::disclosure::NetworkDisclosureRegistry;
3
4pub struct GbifAdapter {
6 pub id: &'static str,
7 pub occurrence_endpoint: String,
8}
9
10impl GbifAdapter {
11 pub fn new(id: &'static str, occurrence_endpoint: &str) -> Self {
12 Self {
13 id,
14 occurrence_endpoint: occurrence_endpoint.to_string(),
15 }
16 }
17}
18
19impl GbifAdapter {
20 pub fn adapter_id(&self) -> &'static str {
21 self.id
22 }
23
24 pub fn build_fetch_request(
25 &self,
26 bbox: (f64, f64, f64, f64),
27 time_range: (u64, u64),
28 registry: &NetworkDisclosureRegistry,
29 ) -> Result<AdapterHttpRequest, String> {
30 if !registry.check_egress_consent(self.adapter_id(), &self.occurrence_endpoint) {
31 return Err(format!(
32 "Consent denied or unregistered for endpoint {} by adapter {}",
33 self.occurrence_endpoint,
34 self.adapter_id()
35 ));
36 }
37
38 let gbif_query = format!(
39 "{}?decimalLatitude={},{}&decimalLongitude={},{}&year={},{}",
40 self.occurrence_endpoint, bbox.1, bbox.3, bbox.0, bbox.2, time_range.0, time_range.1
41 );
42
43 Ok(AdapterHttpRequest::get(gbif_query, "GBIF"))
44 }
45
46 pub fn parse_features(&self, body: &str) -> Result<Vec<crate::NQuin>, String> {
54 use crate::lexicon::generate_60bit_token;
55
56 let title = generate_60bit_token(b"http://purl.org/dc/terms/title");
58 let ty = generate_60bit_token(b"http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
59 let license = generate_60bit_token(b"http://purl.org/dc/terms/license");
60 let created = generate_60bit_token(b"http://purl.org/dc/terms/created");
61 let source = generate_60bit_token(b"http://purl.org/dc/terms/source");
62 let lat_p = generate_60bit_token(b"http://www.w3.org/2003/01/geo/wgs84_pos#lat");
63 let long_p = generate_60bit_token(b"http://www.w3.org/2003/01/geo/wgs84_pos#long");
64 let kind = generate_60bit_token(b"https://www.gbif.org/occurrence");
65
66 fn quin(s: u64, p: u64, o: u64) -> crate::NQuin {
67 crate::NQuin {
68 subject: s,
69 predicate: p,
70 object: o,
71 context: 0,
72 metadata: 0,
73 parity: s ^ p ^ o,
74 }
75 }
76
77 let json: serde_json::Value = serde_json::from_str(body).map_err(|e| e.to_string())?;
78
79 let mut quins = Vec::new();
80 let results = match json.get("results").and_then(|r| r.as_array()) {
81 Some(arr) => arr,
82 None => return Ok(quins),
83 };
84
85 for rec in results {
86 let key = match rec.get("key").and_then(|k| k.as_u64()) {
88 Some(k) => k,
89 None => continue,
90 };
91 let occ_iri = format!("https://www.gbif.org/occurrence/{key}");
92 let subject = generate_60bit_token(occ_iri.as_bytes());
93
94 quins.push(quin(subject, ty, kind));
96
97 if let Some(name) = rec.get("scientificName").and_then(|v| v.as_str()) {
98 quins.push(quin(subject, title, generate_60bit_token(name.as_bytes())));
99 }
100 if let Some(lic) = rec.get("license").and_then(|v| v.as_str()) {
101 quins.push(quin(subject, license, generate_60bit_token(lic.as_bytes())));
102 }
103 if let Some(date) = rec.get("eventDate").and_then(|v| v.as_str()) {
104 quins.push(quin(
105 subject,
106 created,
107 generate_60bit_token(date.as_bytes()),
108 ));
109 }
110
111 quins.push(quin(
113 subject,
114 source,
115 generate_60bit_token(occ_iri.as_bytes()),
116 ));
117
118 if let Some(lat) = rec.get("decimalLatitude").and_then(|v| v.as_f64()) {
119 quins.push(quin(subject, lat_p, lat.to_bits()));
120 }
121 if let Some(lon) = rec.get("decimalLongitude").and_then(|v| v.as_f64()) {
122 quins.push(quin(subject, long_p, lon.to_bits()));
123 }
124 }
125
126 Ok(quins)
127 }
128
129 pub fn fetch_region(
130 &self,
131 bbox: (f64, f64, f64, f64),
132 time_range: (u64, u64),
133 registry: &NetworkDisclosureRegistry,
134 ) -> Result<(), String> {
135 let request = self.build_fetch_request(bbox, time_range, registry)?;
136 super::execute_http_request_status(&request)?;
137 Ok(())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_gbif_adapter_egress() {
148 let adapter = GbifAdapter::new("gbif_adapter", "https://api.gbif.org/v1/occurrence/search");
149 let registry = NetworkDisclosureRegistry::new();
150
151 let res1 = adapter.fetch_region((10.0, 45.0, 11.0, 46.0), (2020, 2024), ®istry);
152 assert!(res1.is_err());
153
154 let mut registry = registry;
155 registry.register_egress(
156 adapter.adapter_id(),
157 "https://api.gbif.org/v1/occurrence/search",
158 "Fetch species occurrence records in bbox",
159 "User enables biodiversity layer",
160 );
161
162 let res2 = adapter.fetch_region((10.0, 45.0, 11.0, 46.0), (2020, 2024), ®istry);
163 assert!(res2.is_ok());
164 }
165
166 #[test]
167 fn test_gbif_parse_features() {
168 use crate::lexicon::generate_60bit_token;
169
170 let adapter = GbifAdapter::new("gbif_adapter", "https://api.gbif.org/v1/occurrence/search");
171
172 let body = r#"{
176 "offset": 0,
177 "limit": 20,
178 "endOfRecords": false,
179 "count": 1234,
180 "results": [
181 {
182 "key": 123456,
183 "scientificName": "Panthera leo",
184 "decimalLatitude": -1.2,
185 "decimalLongitude": 36.8,
186 "eventDate": "2019-05-01T00:00:00",
187 "datasetKey": "abc-def",
188 "license": "http://creativecommons.org/licenses/by/4.0/legalcode"
189 },
190 {
191 "key": 654321,
192 "scientificName": "Loxodonta africana"
193 }
194 ]
195 }"#;
196
197 let quins = adapter.parse_features(body).expect("valid JSON parses");
198 assert_eq!(quins.len(), 10, "expected 10 quins, got {}", quins.len());
200
201 let title = generate_60bit_token(b"http://purl.org/dc/terms/title");
203 let subj1 = generate_60bit_token(b"https://www.gbif.org/occurrence/123456");
204 let title_obj1 = generate_60bit_token(b"Panthera leo");
205
206 assert!(
207 quins
208 .iter()
209 .any(|q| q.subject == subj1 && q.predicate == title && q.object == title_obj1),
210 "expected TITLE quin for Panthera leo not found"
211 );
212
213 let empty = adapter
215 .parse_features(r#"{"offset":0,"count":0}"#)
216 .expect("no results field is not an error");
217 assert!(empty.is_empty());
218
219 assert!(adapter.parse_features("{not json").is_err());
221 }
222}