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