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