1use std::{collections::HashMap, fmt};
33
34#[cfg(not(target_arch = "wasm32"))]
35use config::{Config, ConfigError};
36use serde::{Deserialize, Serialize};
37
38use crate::server::lsp::{SparqlEngine, base_types::LSPAny};
39
40#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
41#[serde(default)]
42pub struct BackendsSettings {
43 pub backends: HashMap<String, BackendConfiguration>,
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
47#[serde(rename_all = "camelCase")]
48pub struct BackendConfiguration {
49 pub name: String,
50 pub url: String,
51 pub health_check_url: Option<String>,
52 pub engine: Option<SparqlEngine>,
53 pub request_method: Option<RequestMethod>,
54 #[serde(default)]
55 pub prefix_map: HashMap<String, String>,
56 #[serde(default)]
57 pub default: bool,
58 #[serde(default)]
59 pub queries: HashMap<CompletionTemplate, String>,
60 pub additional_data: Option<LSPAny>,
61}
62
63#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", try_from = "String")]
65pub(crate) enum CompletionTemplate {
66 Hover,
67 SubjectCompletion,
68 PredicateCompletionContextSensitive,
69 PredicateCompletionContextInsensitive,
70 ObjectCompletionContextSensitive,
71 ObjectCompletionContextInsensitive,
72}
73
74#[derive(Debug)]
75pub struct UnknownTemplateError(String);
76
77impl fmt::Display for UnknownTemplateError {
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79 write!(f, "unknown completion query template \"{}\"", &self.0)
80 }
81}
82
83impl TryFrom<String> for CompletionTemplate {
84 type Error = UnknownTemplateError;
85
86 fn try_from(s: String) -> Result<Self, Self::Error> {
87 match s.as_str() {
88 "hover" => Ok(CompletionTemplate::Hover),
89 "subjectCompletion" => Ok(CompletionTemplate::SubjectCompletion),
90 "predicateCompletion" | "predicateCompletionContextInsensitive" => {
91 Ok(CompletionTemplate::PredicateCompletionContextInsensitive)
92 }
93 "predicateCompletionContextSensitive" => {
94 Ok(CompletionTemplate::PredicateCompletionContextSensitive)
95 }
96 "objectCompletion" | "objectCompletionContextInsensitive" => {
97 Ok(CompletionTemplate::ObjectCompletionContextInsensitive)
98 }
99 "objectCompletionContextSensitive" => {
100 Ok(CompletionTemplate::ObjectCompletionContextSensitive)
101 }
102 _ => Err(UnknownTemplateError(s.to_string())),
103 }
104 }
105}
106
107impl fmt::Display for CompletionTemplate {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 CompletionTemplate::Hover => write!(f, "hover"),
111 CompletionTemplate::SubjectCompletion => write!(f, "subjectCompletion"),
112 CompletionTemplate::PredicateCompletionContextSensitive => {
113 write!(f, "predicateCompletionContextSensitive")
114 }
115 CompletionTemplate::PredicateCompletionContextInsensitive => {
116 write!(f, "predicateCompletionContextInsensitive")
117 }
118 CompletionTemplate::ObjectCompletionContextSensitive => {
119 write!(f, "objectCompletionContextSensitive")
120 }
121 CompletionTemplate::ObjectCompletionContextInsensitive => {
122 write!(f, "objectCompletionContextInsensitive")
123 }
124 }
125 }
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
129#[allow(clippy::upper_case_acronyms)]
130pub enum RequestMethod {
131 GET,
132 POST,
133}
134
135#[derive(Debug, Serialize, Deserialize, PartialEq)]
136#[serde(default)]
137#[serde(rename_all = "camelCase")]
138pub struct CompletionSettings {
139 pub timeout_ms: u32,
140 pub result_size_limit: u32,
141 pub subject_completion_trigger_length: u32,
142 pub object_completion_suffix: bool,
143 pub variable_completion_limit: Option<u32>,
145 pub same_subject_semicolon: bool,
148}
149
150impl Default for CompletionSettings {
151 fn default() -> Self {
152 Self {
153 timeout_ms: 5000,
154 result_size_limit: 100,
155 subject_completion_trigger_length: 3,
156 object_completion_suffix: true,
157 variable_completion_limit: None,
158 same_subject_semicolon: true,
159 }
160 }
161}
162
163#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
164#[serde(default)]
165#[serde(rename_all = "camelCase")]
166pub struct FormatSettings {
167 pub align_predicates: bool,
168 pub align_prefixes: bool,
169 pub separate_prologue: bool,
170 pub capitalize_keywords: bool,
171 pub insert_spaces: Option<bool>,
172 pub tab_size: Option<u8>,
173 pub where_new_line: bool,
174 pub filter_same_line: bool,
175 pub compact: Option<u32>,
176 pub line_length: u32,
177 pub contract_triples: bool,
178}
179
180impl Default for FormatSettings {
181 fn default() -> Self {
182 Self {
183 align_predicates: true,
184 align_prefixes: false,
185 separate_prologue: false,
186 capitalize_keywords: true,
187 insert_spaces: Some(true),
188 tab_size: Some(2),
189 where_new_line: false,
190 filter_same_line: true,
191 compact: None,
192 line_length: 120,
193 contract_triples: false,
194 }
195 }
196}
197
198#[derive(Debug, Serialize, Deserialize, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct PrefixesSettings {
201 pub add_missing: Option<bool>,
202 pub remove_unused: Option<bool>,
203}
204
205impl Default for PrefixesSettings {
206 fn default() -> Self {
207 Self {
208 add_missing: Some(true),
209 remove_unused: Some(false),
210 }
211 }
212}
213#[derive(Debug, Serialize, Deserialize, PartialEq)]
214pub struct Replacement {
215 pub pattern: String,
216 pub replacement: String,
217}
218
219impl Replacement {
220 pub fn new(pattern: &str, replacement: &str) -> Self {
221 Self {
222 pattern: pattern.to_string(),
223 replacement: replacement.to_string(),
224 }
225 }
226}
227
228#[derive(Debug, Serialize, Deserialize, PartialEq)]
229#[serde(rename_all = "camelCase")]
230pub struct Replacements {
231 pub object_variable: Vec<Replacement>,
232}
233
234impl Default for Replacements {
235 fn default() -> Self {
236 Self {
237 object_variable: vec![
238 Replacement::new(r"^has (\w+)", "$1"),
239 Replacement::new(r"\s", "_"),
240 Replacement::new(r"^has([A-Z]\w*)", "$1"),
241 Replacement::new(r"^(\w+)edBy", "$1"),
242 Replacement::new(r"([^a-zA-Z0-9_])", ""),
243 ],
244 }
245 }
246}
247
248#[derive(Debug, Deserialize, Serialize, PartialEq)]
249pub struct Settings {
250 pub format: FormatSettings,
252 pub completion: CompletionSettings,
254 pub backends: Option<BackendsSettings>,
256 pub prefixes: Option<PrefixesSettings>,
258 pub replacements: Option<Replacements>,
260}
261
262impl Default for Settings {
263 fn default() -> Self {
264 Self {
265 format: FormatSettings::default(),
266 completion: CompletionSettings::default(),
267 backends: None,
268 prefixes: Some(PrefixesSettings::default()),
269 replacements: Some(Replacements::default()),
270 }
271 }
272}
273
274#[cfg(not(target_arch = "wasm32"))]
275fn load_user_configuration() -> Result<Settings, ConfigError> {
276 Config::builder()
277 .add_source(config::File::with_name("qlue-ls"))
278 .build()?
279 .try_deserialize::<Settings>()
280}
281
282impl Settings {
283 pub fn new() -> Self {
284 #[cfg(not(target_arch = "wasm32"))]
285 match load_user_configuration() {
286 Ok(settings) => {
287 log::info!("Loaded user configuration!!");
288 settings
289 }
290 Err(error) => {
291 log::info!(
292 "Did not load user-configuration:\n{}\n falling back to default values",
293 error
294 );
295 Settings::default()
296 }
297 }
298 #[cfg(target_arch = "wasm32")]
299 Settings::default()
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use config::{Config, FileFormat};
307
308 fn parse_yaml<T: serde::de::DeserializeOwned>(yaml: &str) -> T {
309 Config::builder()
310 .add_source(config::File::from_str(yaml, FileFormat::Yaml))
311 .build()
312 .unwrap()
313 .try_deserialize()
314 .unwrap()
315 }
316
317 #[test]
318 fn test_backend_configuration_valid_queries_all_variants() {
319 let yaml = r#"
320 name: TestBackend
321 url: https://example.com/sparql
322 healthCheckUrl: https://example.com/health
323 requestMethod: GET
324 prefixMap:
325 rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns#
326 rdfs: http://www.w3.org/2000/01/rdf-schema#
327 default: false
328 queries:
329 subjectCompletion: SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail WHERE { ?qlue_ls_entity a ?type }
330 predicateCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?qlue_ls_entity ?o }
331 predicateCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] ?qlue_ls_entity [] }
332 objectCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
333 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] [] ?qlue_ls_entity }
334 "#;
335
336 let config: BackendConfiguration = parse_yaml(yaml);
337
338 assert_eq!(config.name, "TestBackend");
339 assert_eq!(config.url, "https://example.com/sparql");
340 assert!(!config.default);
341 assert_eq!(config.queries.len(), 5);
342 assert!(
343 config
344 .queries
345 .contains_key(&CompletionTemplate::SubjectCompletion)
346 );
347 assert!(
348 config
349 .queries
350 .contains_key(&CompletionTemplate::PredicateCompletionContextSensitive)
351 );
352 assert!(
353 config
354 .queries
355 .contains_key(&CompletionTemplate::PredicateCompletionContextInsensitive)
356 );
357 assert!(
358 config
359 .queries
360 .contains_key(&CompletionTemplate::ObjectCompletionContextSensitive)
361 );
362 assert!(
363 config
364 .queries
365 .contains_key(&CompletionTemplate::ObjectCompletionContextInsensitive)
366 );
367 }
368
369 #[test]
370 fn test_backend_configuration_queries_subset() {
371 let yaml = r#"
372 name: MinimalBackend
373 url: https://example.com/sparql
374 prefixMap: {}
375 queries:
376 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
377 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
378 "#;
379
380 let config: BackendConfiguration = parse_yaml(yaml);
381
382 assert_eq!(config.queries.len(), 2);
383 assert!(
384 config
385 .queries
386 .contains_key(&CompletionTemplate::SubjectCompletion)
387 );
388 assert!(
389 config
390 .queries
391 .contains_key(&CompletionTemplate::ObjectCompletionContextInsensitive)
392 );
393 assert!(
394 !config
395 .queries
396 .contains_key(&CompletionTemplate::PredicateCompletionContextSensitive)
397 );
398 }
399
400 #[test]
401 fn test_backend_configuration_rejects_invalid_query_key() {
402 let yaml = r#"
404 name: TestBackend
405 url: https://example.com/sparql
406 prefixMap: {}
407 queries:
408 invalidQueryType: SELECT ?qlue_ls_entity WHERE { ?s ?p ?o }
409 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
410 "#;
411
412 let result = Config::builder()
413 .add_source(config::File::from_str(yaml, FileFormat::Yaml))
414 .build()
415 .unwrap()
416 .try_deserialize::<BackendConfiguration>();
417 assert!(result.is_err());
418 }
419
420 #[test]
421 fn test_backend_configuration_with_multiline_queries() {
422 let yaml = r#"
423 name: WikidataBackend
424 url: https://query.wikidata.org/sparql
425 healthCheckUrl: https://query.wikidata.org/
426 prefixMap:
427 wd: http://www.wikidata.org/entity/
428 wdt: http://www.wikidata.org/prop/direct/
429 rdfs: http://www.w3.org/2000/01/rdf-schema#
430 default: false
431 queries:
432 subjectCompletion: |
433 SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail
434 WHERE {
435 ?qlue_ls_entity rdfs:label ?qlue_ls_label .
436 OPTIONAL { ?qlue_ls_entity schema:description ?qlue_ls_detail }
437 FILTER(LANG(?qlue_ls_label) = "en")
438 }
439 LIMIT 100
440 predicateCompletionContextSensitive: |
441 SELECT ?qlue_ls_entity WHERE {
442 ?s ?qlue_ls_entity ?o
443 }
444 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] [] ?qlue_ls_entity }
445 "#;
446
447 let config: BackendConfiguration = parse_yaml(yaml);
448
449 assert_eq!(config.name, "WikidataBackend");
450 assert_eq!(config.url, "https://query.wikidata.org/sparql");
451 assert!(!config.default);
452 assert_eq!(config.prefix_map.len(), 3);
453 assert_eq!(config.queries.len(), 3);
454
455 let subject_query = config
457 .queries
458 .get(&CompletionTemplate::SubjectCompletion)
459 .unwrap();
460 assert!(subject_query.contains("SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail"));
461 assert!(subject_query.contains("FILTER(LANG(?qlue_ls_label) = \"en\")"));
462 }
463
464 #[test]
465 fn test_backends_settings_multiple_backends() {
466 let yaml = r#"
467 backends:
468 wikidata:
469 name: Wikidata
470 url: https://query.wikidata.org/sparql
471 prefixMap:
472 wd: http://www.wikidata.org/entity/
473 queries:
474 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
475 dbpedia:
476 name: DBpedia
477 url: https://dbpedia.org/sparql
478 prefixMap:
479 dbo: http://dbpedia.org/ontology/
480 default: true
481 queries:
482 objectCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
483 "#;
484
485 let settings: BackendsSettings = parse_yaml(yaml);
486
487 assert_eq!(settings.backends.len(), 2);
488 assert!(settings.backends.contains_key("wikidata"));
489 assert!(settings.backends.contains_key("dbpedia"));
490
491 let wikidata = settings.backends.get("wikidata").unwrap();
492 assert_eq!(wikidata.name, "Wikidata");
493 assert_eq!(wikidata.queries.len(), 1);
494
495 let dbpedia = settings.backends.get("dbpedia").unwrap();
496 assert_eq!(dbpedia.name, "DBpedia");
497 assert!(dbpedia.default);
498 }
499
500 #[test]
501 fn test_full_settings_deserialization() {
502 let yaml = r#"
503 format:
504 alignPredicates: true
505 alignPrefixes: false
506 separatePrologue: false
507 capitalizeKeywords: true
508 insertSpaces: true
509 tabSize: 2
510 whereNewLine: false
511 filterSameLine: true
512 completion:
513 timeoutMs: 5000
514 resultSizeLimit: 100
515 backends:
516 backends:
517 wikidata:
518 name: Wikidata
519 url: https://query.wikidata.org/sparql
520 healthCheckUrl: https://query.wikidata.org/
521 prefixMap:
522 wd: http://www.wikidata.org/entity/
523 wdt: http://www.wikidata.org/prop/direct/
524 default: true
525 queries:
526 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
527 predicateCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?qlue_ls_entity ?o }
528 prefixes:
529 addMissing: true
530 removeUnused: false
531 "#;
532
533 let settings: Settings = parse_yaml(yaml);
534
535 assert!(settings.format.align_predicates);
536 assert_eq!(settings.completion.timeout_ms, 5000);
537 assert!(settings.backends.is_some());
538
539 let backends = settings.backends.unwrap();
540 assert_eq!(backends.backends.len(), 1);
541
542 let wikidata = backends.backends.get("wikidata").unwrap();
543 assert_eq!(wikidata.name, "Wikidata");
544 assert!(wikidata.default);
545 assert_eq!(wikidata.queries.len(), 2);
546 }
547}