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