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 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"^has([A-Z]\w*)", "$1"),
259 Replacement::new(r"^(\w+)edBy", "$1"),
260 Replacement::new(r"([^a-zA-Z0-9_])", ""),
261 ],
262 }
263 }
264}
265
266#[derive(Debug, Deserialize, Serialize, PartialEq)]
267#[serde(rename_all = "camelCase")]
268pub struct Settings {
269 #[serde(default)]
271 pub format: FormatSettings,
272 #[serde(default)]
274 pub completion: CompletionSettings,
275 pub backends: Option<BackendsSettings>,
277 pub prefixes: Option<PrefixesSettings>,
279 pub replacements: Option<Replacements>,
281 #[serde(default)]
283 pub auto_line_break: bool,
284}
285
286impl Default for Settings {
287 fn default() -> Self {
288 Self {
289 format: FormatSettings::default(),
290 completion: CompletionSettings::default(),
291 backends: None,
292 prefixes: Some(PrefixesSettings::default()),
293 replacements: Some(Replacements::default()),
294 auto_line_break: false,
295 }
296 }
297}
298
299#[cfg(not(target_arch = "wasm32"))]
300fn load_user_configuration() -> Result<Settings, ConfigError> {
301 Config::builder()
302 .add_source(config::File::with_name("qlue-ls"))
303 .build()?
304 .try_deserialize::<Settings>()
305}
306
307impl Settings {
308 pub fn new() -> Self {
309 #[cfg(not(target_arch = "wasm32"))]
310 match load_user_configuration() {
311 Ok(settings) => {
312 log::info!("Loaded user configuration!!");
313 settings
314 }
315 Err(error) => {
316 log::info!(
317 "Did not load user-configuration:\n{}\n falling back to default values",
318 error
319 );
320 Settings::default()
321 }
322 }
323 #[cfg(target_arch = "wasm32")]
324 Settings::default()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use config::{Config, FileFormat};
332
333 fn parse_yaml<T: serde::de::DeserializeOwned>(yaml: &str) -> T {
334 Config::builder()
335 .add_source(config::File::from_str(yaml, FileFormat::Yaml))
336 .build()
337 .unwrap()
338 .try_deserialize()
339 .unwrap()
340 }
341
342 #[test]
343 fn test_backend_configuration_valid_queries_all_variants() {
344 let yaml = r#"
345 name: TestBackend
346 url: https://example.com/sparql
347 healthCheckUrl: https://example.com/health
348 requestMethod: GET
349 prefixMap:
350 rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns#
351 rdfs: http://www.w3.org/2000/01/rdf-schema#
352 default: false
353 queries:
354 subjectCompletion: SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail WHERE { ?qlue_ls_entity a ?type }
355 predicateCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?qlue_ls_entity ?o }
356 predicateCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] ?qlue_ls_entity [] }
357 objectCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
358 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] [] ?qlue_ls_entity }
359 valuesCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
360 valuesCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
361 "#;
362
363 let config: BackendConfiguration = parse_yaml(yaml);
364
365 assert_eq!(config.name, "TestBackend");
366 assert_eq!(config.url, "https://example.com/sparql");
367 assert!(!config.default);
368 assert_eq!(config.queries.len(), 7);
369 assert!(
370 config
371 .queries
372 .contains_key(&CompletionTemplate::SubjectCompletion)
373 );
374 assert!(
375 config
376 .queries
377 .contains_key(&CompletionTemplate::PredicateCompletionContextSensitive)
378 );
379 assert!(
380 config
381 .queries
382 .contains_key(&CompletionTemplate::PredicateCompletionContextInsensitive)
383 );
384 assert!(
385 config
386 .queries
387 .contains_key(&CompletionTemplate::ObjectCompletionContextSensitive)
388 );
389 assert!(
390 config
391 .queries
392 .contains_key(&CompletionTemplate::ObjectCompletionContextInsensitive)
393 );
394 assert!(
395 config
396 .queries
397 .contains_key(&CompletionTemplate::ValuesCompletionContextSensitive)
398 );
399 assert!(
400 config
401 .queries
402 .contains_key(&CompletionTemplate::ValuesCompletionContextInsensitive)
403 );
404 }
405
406 #[test]
407 fn test_backend_configuration_queries_subset() {
408 let yaml = r#"
409 name: MinimalBackend
410 url: https://example.com/sparql
411 prefixMap: {}
412 queries:
413 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
414 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
415 "#;
416
417 let config: BackendConfiguration = parse_yaml(yaml);
418
419 assert_eq!(config.queries.len(), 2);
420 assert!(
421 config
422 .queries
423 .contains_key(&CompletionTemplate::SubjectCompletion)
424 );
425 assert!(
426 config
427 .queries
428 .contains_key(&CompletionTemplate::ObjectCompletionContextInsensitive)
429 );
430 assert!(
431 !config
432 .queries
433 .contains_key(&CompletionTemplate::PredicateCompletionContextSensitive)
434 );
435 }
436
437 #[test]
438 fn test_backend_configuration_rejects_invalid_query_key() {
439 let yaml = r#"
441 name: TestBackend
442 url: https://example.com/sparql
443 prefixMap: {}
444 queries:
445 invalidQueryType: SELECT ?qlue_ls_entity WHERE { ?s ?p ?o }
446 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
447 "#;
448
449 let result = Config::builder()
450 .add_source(config::File::from_str(yaml, FileFormat::Yaml))
451 .build()
452 .unwrap()
453 .try_deserialize::<BackendConfiguration>();
454 assert!(result.is_err());
455 }
456
457 #[test]
458 fn test_backend_configuration_with_multiline_queries() {
459 let yaml = r#"
460 name: WikidataBackend
461 url: https://query.wikidata.org/sparql
462 healthCheckUrl: https://query.wikidata.org/
463 prefixMap:
464 wd: http://www.wikidata.org/entity/
465 wdt: http://www.wikidata.org/prop/direct/
466 rdfs: http://www.w3.org/2000/01/rdf-schema#
467 default: false
468 queries:
469 subjectCompletion: |
470 SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail
471 WHERE {
472 ?qlue_ls_entity rdfs:label ?qlue_ls_label .
473 OPTIONAL { ?qlue_ls_entity schema:description ?qlue_ls_detail }
474 FILTER(LANG(?qlue_ls_label) = "en")
475 }
476 LIMIT 100
477 predicateCompletionContextSensitive: |
478 SELECT ?qlue_ls_entity WHERE {
479 ?s ?qlue_ls_entity ?o
480 }
481 objectCompletionContextInsensitive: SELECT ?qlue_ls_entity WHERE { [] [] ?qlue_ls_entity }
482 "#;
483
484 let config: BackendConfiguration = parse_yaml(yaml);
485
486 assert_eq!(config.name, "WikidataBackend");
487 assert_eq!(config.url, "https://query.wikidata.org/sparql");
488 assert!(!config.default);
489 assert_eq!(config.prefix_map.len(), 3);
490 assert_eq!(config.queries.len(), 3);
491
492 let subject_query = config
494 .queries
495 .get(&CompletionTemplate::SubjectCompletion)
496 .unwrap();
497 assert!(subject_query.contains("SELECT ?qlue_ls_entity ?qlue_ls_label ?qlue_ls_detail"));
498 assert!(subject_query.contains("FILTER(LANG(?qlue_ls_label) = \"en\")"));
499 }
500
501 #[test]
502 fn test_backends_settings_multiple_backends() {
503 let yaml = r#"
504 backends:
505 wikidata:
506 name: Wikidata
507 url: https://query.wikidata.org/sparql
508 prefixMap:
509 wd: http://www.wikidata.org/entity/
510 queries:
511 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
512 dbpedia:
513 name: DBpedia
514 url: https://dbpedia.org/sparql
515 prefixMap:
516 dbo: http://dbpedia.org/ontology/
517 default: true
518 queries:
519 objectCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?p ?qlue_ls_entity }
520 "#;
521
522 let settings: BackendsSettings = parse_yaml(yaml);
523
524 assert_eq!(settings.backends.len(), 2);
525 assert!(settings.backends.contains_key("wikidata"));
526 assert!(settings.backends.contains_key("dbpedia"));
527
528 let wikidata = settings.backends.get("wikidata").unwrap();
529 assert_eq!(wikidata.name, "Wikidata");
530 assert_eq!(wikidata.queries.len(), 1);
531
532 let dbpedia = settings.backends.get("dbpedia").unwrap();
533 assert_eq!(dbpedia.name, "DBpedia");
534 assert!(dbpedia.default);
535 }
536
537 #[test]
538 fn test_full_settings_deserialization() {
539 let yaml = r#"
540 format:
541 alignPredicates: true
542 alignPrefixes: false
543 separatePrologue: false
544 capitalizeKeywords: true
545 insertSpaces: true
546 tabSize: 2
547 whereNewLine: false
548 filterSameLine: true
549 completion:
550 timeoutMs: 5000
551 resultSizeLimit: 100
552 backends:
553 backends:
554 wikidata:
555 name: Wikidata
556 url: https://query.wikidata.org/sparql
557 healthCheckUrl: https://query.wikidata.org/
558 prefixMap:
559 wd: http://www.wikidata.org/entity/
560 wdt: http://www.wikidata.org/prop/direct/
561 default: true
562 queries:
563 subjectCompletion: SELECT ?qlue_ls_entity WHERE { ?qlue_ls_entity ?p ?o }
564 predicateCompletionContextSensitive: SELECT ?qlue_ls_entity WHERE { ?s ?qlue_ls_entity ?o }
565 prefixes:
566 addMissing: true
567 removeUnused: false
568 "#;
569
570 let settings: Settings = parse_yaml(yaml);
571
572 assert!(settings.format.align_predicates);
573 assert_eq!(settings.completion.timeout_ms, 5000);
574 assert!(settings.backends.is_some());
575
576 let backends = settings.backends.unwrap();
577 assert_eq!(backends.backends.len(), 1);
578
579 let wikidata = backends.backends.get("wikidata").unwrap();
580 assert_eq!(wikidata.name, "Wikidata");
581 assert!(wikidata.default);
582 assert_eq!(wikidata.queries.len(), 2);
583 }
584}