Skip to main content

ontocore_core/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4
5/// Wire format (LSP JSON) uses snake_case via serde; SQL CLI uses [`OntologyFormat::as_str`].
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum OntologyFormat {
9    Turtle,
10    RdfXml,
11    Owl,
12    OwlXml,
13    JsonLd,
14    NTriples,
15    NQuads,
16    TriG,
17    Obo,
18    Unknown,
19}
20
21impl OntologyFormat {
22    pub fn from_extension(ext: &str) -> Self {
23        match ext.to_ascii_lowercase().as_str() {
24            "ttl" => Self::Turtle,
25            "rdf" => Self::RdfXml,
26            "owl" => Self::Owl,
27            "owx" => Self::OwlXml,
28            "jsonld" | "json-ld" => Self::JsonLd,
29            "nt" => Self::NTriples,
30            "nq" => Self::NQuads,
31            "trig" => Self::TriG,
32            "obo" => Self::Obo,
33            _ => Self::Unknown,
34        }
35    }
36
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Turtle => "turtle",
40            Self::RdfXml => "rdf_xml",
41            Self::Owl => "owl",
42            Self::OwlXml => "owl_xml",
43            Self::JsonLd => "json_ld",
44            Self::NTriples => "n_triples",
45            Self::NQuads => "n_quads",
46            Self::TriG => "trig",
47            Self::Obo => "obo",
48            Self::Unknown => "unknown",
49        }
50    }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum ParseStatus {
56    Ok,
57    Warning,
58    Error,
59}
60
61impl ParseStatus {
62    pub fn as_str(&self) -> &'static str {
63        match self {
64            Self::Ok => "ok",
65            Self::Warning => "warning",
66            Self::Error => "error",
67        }
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum EntityKind {
74    Class,
75    ObjectProperty,
76    DataProperty,
77    AnnotationProperty,
78    Individual,
79    Ontology,
80    Other,
81}
82
83impl EntityKind {
84    pub fn as_str(&self) -> &'static str {
85        match self {
86            Self::Class => "class",
87            Self::ObjectProperty => "object_property",
88            Self::DataProperty => "data_property",
89            Self::AnnotationProperty => "annotation_property",
90            Self::Individual => "individual",
91            Self::Ontology => "ontology",
92            Self::Other => "other",
93        }
94    }
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct SourceLocation {
99    pub line: Option<u64>,
100    pub column: Option<u64>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub start_byte: Option<u64>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub end_byte: Option<u64>,
105}
106
107impl SourceLocation {
108    pub fn is_empty(&self) -> bool {
109        self.line.is_none()
110            && self.column.is_none()
111            && self.start_byte.is_none()
112            && self.end_byte.is_none()
113    }
114
115    pub fn at_line_col(line: u64, column: u64) -> Self {
116        Self { line: Some(line), column: Some(column), start_byte: None, end_byte: None }
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum DiagnosticSeverity {
123    Error,
124    Warning,
125    Info,
126}
127
128impl DiagnosticSeverity {
129    pub fn as_str(&self) -> &'static str {
130        match self {
131            Self::Error => "error",
132            Self::Warning => "warning",
133            Self::Info => "info",
134        }
135    }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
139#[serde(rename_all = "snake_case")]
140pub enum DiagnosticCode {
141    ParseError,
142    BrokenImport,
143    UndefinedPrefix,
144    DuplicateLabel,
145    MissingLabel,
146    OrphanClass,
147    OwlBridgeFailed,
148    IoReadError,
149    PluginViolation,
150}
151
152impl DiagnosticCode {
153    pub fn as_str(&self) -> &'static str {
154        match self {
155            Self::ParseError => "parse_error",
156            Self::BrokenImport => "broken_import",
157            Self::UndefinedPrefix => "undefined_prefix",
158            Self::DuplicateLabel => "duplicate_label",
159            Self::MissingLabel => "missing_label",
160            Self::OrphanClass => "orphan_class",
161            Self::OwlBridgeFailed => "owl_bridge_failed",
162            Self::IoReadError => "io_read_error",
163            Self::PluginViolation => "plugin_violation",
164        }
165    }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct Diagnostic {
170    pub code: DiagnosticCode,
171    pub severity: DiagnosticSeverity,
172    pub message: String,
173    pub file: PathBuf,
174    pub range: SourceLocation,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub entity_iri: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub quick_fix: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub plugin_id: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub plugin_code: Option<String>,
183}
184
185impl Diagnostic {
186    pub fn display_code(&self) -> String {
187        if let (Some(plugin_id), Some(code)) = (&self.plugin_id, &self.plugin_code) {
188            format!("plugin:{plugin_id}:{code}")
189        } else {
190            self.code.as_str().to_string()
191        }
192    }
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct OntologyDocument {
197    pub id: String,
198    pub path: PathBuf,
199    pub format: OntologyFormat,
200    pub base_iri: Option<String>,
201    pub imports: Vec<String>,
202    pub namespaces: BTreeMap<String, String>,
203    pub parse_status: ParseStatus,
204    pub content_hash: String,
205    pub modified_time: u64,
206    pub parse_message: Option<String>,
207    pub parse_error_location: Option<SourceLocation>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Entity {
212    pub iri: String,
213    pub short_name: String,
214    pub kind: EntityKind,
215    pub ontology_id: String,
216    pub source_location: SourceLocation,
217    pub labels: Vec<String>,
218    pub comments: Vec<String>,
219    pub deprecated: bool,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub obo_id: Option<String>,
222    #[serde(default, skip_serializing_if = "PropertyCharacteristics::is_empty")]
223    pub characteristics: PropertyCharacteristics,
224}
225
226#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
227pub struct PropertyCharacteristics {
228    pub functional: bool,
229    pub inverse_functional: bool,
230    pub transitive: bool,
231    pub symmetric: bool,
232    pub asymmetric: bool,
233    pub reflexive: bool,
234    pub irreflexive: bool,
235}
236
237impl PropertyCharacteristics {
238    pub fn is_empty(&self) -> bool {
239        !self.functional
240            && !self.inverse_functional
241            && !self.transitive
242            && !self.symmetric
243            && !self.asymmetric
244            && !self.reflexive
245            && !self.irreflexive
246    }
247}
248
249impl Default for Entity {
250    fn default() -> Self {
251        Self {
252            iri: String::new(),
253            short_name: String::new(),
254            kind: EntityKind::Class,
255            ontology_id: String::new(),
256            source_location: SourceLocation::default(),
257            labels: Vec::new(),
258            comments: Vec::new(),
259            deprecated: false,
260            obo_id: None,
261            characteristics: PropertyCharacteristics::default(),
262        }
263    }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct Annotation {
268    pub subject: String,
269    pub predicate: String,
270    pub object: String,
271    pub ontology_id: String,
272    #[serde(default, skip_serializing_if = "SourceLocation::is_empty")]
273    pub source_location: SourceLocation,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct Axiom {
278    pub id: String,
279    pub ontology_id: String,
280    pub subject: String,
281    pub predicate: String,
282    pub object: String,
283    pub axiom_kind: String,
284    #[serde(default, skip_serializing_if = "SourceLocation::is_empty")]
285    pub source_location: SourceLocation,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct Namespace {
290    pub prefix: String,
291    pub iri: String,
292    pub ontology_id: String,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct Import {
297    pub ontology_id: String,
298    pub import_iri: String,
299}
300
301/// Snake_case axiom kind stored in [`Axiom::axiom_kind`] and SQL `axioms.axiom_kind`.
302pub const AXIOM_KIND_SUB_CLASS_OF: &str = "sub_class_of";
303pub const AXIOM_KIND_EQUIVALENT_CLASS: &str = "equivalent_class";
304pub const AXIOM_KIND_DISJOINT_CLASS: &str = "disjoint_class";
305pub const AXIOM_KIND_DOMAIN: &str = "domain";
306pub const AXIOM_KIND_RANGE: &str = "range";
307pub const AXIOM_KIND_PROPERTY_CHAIN: &str = "property_chain";
308pub const AXIOM_KIND_CLASS_ASSERTION: &str = "class_assertion";
309pub const AXIOM_KIND_OBJECT_PROPERTY_ASSERTION: &str = "object_property_assertion";
310pub const AXIOM_KIND_DATA_PROPERTY_ASSERTION: &str = "data_property_assertion";
311
312#[cfg(test)]
313mod tests {
314    use super::ParseStatus;
315
316    #[test]
317    fn parse_status_as_str_matches_serde() {
318        assert_eq!(ParseStatus::Ok.as_str(), "ok");
319        assert_eq!(ParseStatus::Warning.as_str(), "warning");
320        assert_eq!(ParseStatus::Error.as_str(), "error");
321    }
322}