Skip to main content

lsp_max/runtime/control_plane/admission/
validation.rs

1use super::types::GraphAdmissionError;
2
3pub fn resolve_db_path() -> std::path::PathBuf {
4    if let Ok(path_str) = std::env::var("LSP_MAX_DB_PATH") {
5        if !path_str.trim().is_empty() {
6            return std::path::PathBuf::from(path_str);
7        }
8    }
9
10    let config_path = if let Ok(path_str) = std::env::var("LSP_MAX_CONFIG") {
11        Some(std::path::PathBuf::from(path_str))
12    } else if let Ok(home) = std::env::var("HOME") {
13        Some(std::path::PathBuf::from(home).join(".lsp-max-config.json"))
14    } else {
15        Some(std::path::PathBuf::from(".lsp-max-config.json"))
16    };
17
18    if let Some(path) = config_path {
19        if path.exists() {
20            if let Ok(content) = std::fs::read_to_string(path) {
21                if let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) {
22                    if let Some(db_path_val) = value.get("database_path").and_then(|v| v.as_str()) {
23                        if !db_path_val.trim().is_empty() {
24                            return std::path::PathBuf::from(db_path_val);
25                        }
26                    }
27                }
28            }
29        }
30    }
31
32    let is_test =
33        std::env::var("CARGO_MANIFEST_DIR").is_ok() || std::env::var("LSP_MAX_TEST").is_ok();
34
35    if is_test {
36        let temp_dir = std::env::temp_dir();
37        temp_dir.join(format!("lsp-max-db-{}", uuid::Uuid::new_v4()))
38    } else if let Ok(home) = std::env::var("HOME") {
39        std::path::PathBuf::from(home).join(".local/share/lsp-max/db")
40    } else {
41        std::env::temp_dir().join("lsp-max-db")
42    }
43}
44
45pub fn validate_shacl_shapes(quads: &[oxigraph::model::Quad]) -> Result<(), GraphAdmissionError> {
46    for quad in quads {
47        let predicate_str = quad.predicate.as_str();
48        let object_str = match &quad.object {
49            oxigraph::model::Term::Literal(l) => l.value(),
50            oxigraph::model::Term::NamedNode(n) => n.as_str(),
51            oxigraph::model::Term::BlankNode(b) => b.as_str(),
52            _ => continue, // Non-literal terms (blank nodes, RDF-star) carry no string value
53        };
54
55        // Severity validation
56        if predicate_str == "urn:lsp-max:core:severity"
57            && object_str != "error"
58            && object_str != "warning"
59            && object_str != "info"
60            && object_str != "hint"
61        {
62            return Err(GraphAdmissionError::NamespaceViolation(format!(
63                "SHACL PropertyShape violation: invalid severity value '{}'",
64                object_str
65            )));
66        }
67
68        // Datatype validation for line/char numbers
69        if (predicate_str == "urn:lsp-max:core:startLine"
70            || predicate_str == "urn:lsp-max:core:startCharacter"
71            || predicate_str == "urn:lsp-max:core:endLine"
72            || predicate_str == "urn:lsp-max:core:endCharacter")
73            && object_str.parse::<u32>().is_err()
74        {
75            return Err(GraphAdmissionError::ParsingFailed(format!(
76                "SHACL PropertyShape violation: expected integer for {}, got '{}'",
77                predicate_str, object_str
78            )));
79        }
80
81        // positionEncoding validation
82        if predicate_str == "urn:lsp-max:core:positionEncoding"
83            && object_str != "utf-8"
84            && object_str != "utf-16"
85            && object_str != "utf-32"
86        {
87            return Err(GraphAdmissionError::NamespaceViolation(format!(
88                "SHACL PropertyShape violation: invalid positionEncoding '{}'",
89                object_str
90            )));
91        }
92
93        // version validation
94        if predicate_str == "urn:lsp-max:core:version" && object_str.is_empty() {
95            return Err(GraphAdmissionError::NamespaceViolation(
96                "SHACL PropertyShape violation: version is empty".to_string(),
97            ));
98        }
99
100        // projectRoot validation
101        if predicate_str == "urn:lsp-max:core:projectRoot" && object_str.is_empty() {
102            return Err(GraphAdmissionError::NamespaceViolation(
103                "SHACL PropertyShape violation: projectRoot is empty".to_string(),
104            ));
105        }
106
107        // uri validation
108        if predicate_str == "urn:lsp-max:core:uri" && object_str.is_empty() {
109            return Err(GraphAdmissionError::NamespaceViolation(
110                "SHACL PropertyShape violation: uri is empty".to_string(),
111            ));
112        }
113
114        // languageId validation
115        if predicate_str == "urn:lsp-max:core:languageId" && object_str.is_empty() {
116            return Err(GraphAdmissionError::NamespaceViolation(
117                "SHACL PropertyShape violation: languageId is empty".to_string(),
118            ));
119        }
120    }
121    Ok(())
122}
123
124pub struct StoreFactory;
125
126impl StoreFactory {
127    pub fn open() -> Result<oxigraph::store::Store, String> {
128        let path = resolve_db_path();
129        if let Some(parent) = path.parent() {
130            let _ = std::fs::create_dir_all(parent);
131        }
132        let _ = std::fs::create_dir_all(&path);
133        oxigraph::store::Store::new().map_err(|e| e.to_string())
134    }
135}