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    Datatype,
80    Ontology,
81    Other,
82}
83
84impl EntityKind {
85    pub fn as_str(&self) -> &'static str {
86        match self {
87            Self::Class => "class",
88            Self::ObjectProperty => "object_property",
89            Self::DataProperty => "data_property",
90            Self::AnnotationProperty => "annotation_property",
91            Self::Individual => "individual",
92            Self::Datatype => "datatype",
93            Self::Ontology => "ontology",
94            Self::Other => "other",
95        }
96    }
97}
98
99#[derive(Debug, Clone, Default, Serialize, Deserialize)]
100pub struct SourceLocation {
101    pub line: Option<u64>,
102    pub column: Option<u64>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub start_byte: Option<u64>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub end_byte: Option<u64>,
107}
108
109impl SourceLocation {
110    pub fn is_empty(&self) -> bool {
111        self.line.is_none()
112            && self.column.is_none()
113            && self.start_byte.is_none()
114            && self.end_byte.is_none()
115    }
116
117    pub fn at_line_col(line: u64, column: u64) -> Self {
118        Self { line: Some(line), column: Some(column), start_byte: None, end_byte: None }
119    }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum DiagnosticSeverity {
125    Error,
126    Warning,
127    Info,
128}
129
130impl DiagnosticSeverity {
131    pub fn as_str(&self) -> &'static str {
132        match self {
133            Self::Error => "error",
134            Self::Warning => "warning",
135            Self::Info => "info",
136        }
137    }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case")]
142pub enum DiagnosticCode {
143    ParseError,
144    BrokenImport,
145    UndefinedPrefix,
146    DuplicateLabel,
147    MissingLabel,
148    OrphanClass,
149    OwlBridgeFailed,
150    IoReadError,
151    PluginViolation,
152}
153
154impl DiagnosticCode {
155    pub fn as_str(&self) -> &'static str {
156        match self {
157            Self::ParseError => "parse_error",
158            Self::BrokenImport => "broken_import",
159            Self::UndefinedPrefix => "undefined_prefix",
160            Self::DuplicateLabel => "duplicate_label",
161            Self::MissingLabel => "missing_label",
162            Self::OrphanClass => "orphan_class",
163            Self::OwlBridgeFailed => "owl_bridge_failed",
164            Self::IoReadError => "io_read_error",
165            Self::PluginViolation => "plugin_violation",
166        }
167    }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Diagnostic {
172    pub code: DiagnosticCode,
173    pub severity: DiagnosticSeverity,
174    pub message: String,
175    pub file: PathBuf,
176    pub range: SourceLocation,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub entity_iri: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub quick_fix: Option<String>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub plugin_id: Option<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub plugin_code: Option<String>,
185}
186
187impl Diagnostic {
188    pub fn display_code(&self) -> String {
189        if let (Some(plugin_id), Some(code)) = (&self.plugin_id, &self.plugin_code) {
190            format!("plugin:{plugin_id}:{code}")
191        } else {
192            self.code.as_str().to_string()
193        }
194    }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct OntologyDocument {
199    pub id: String,
200    pub path: PathBuf,
201    pub format: OntologyFormat,
202    pub base_iri: Option<String>,
203    pub imports: Vec<String>,
204    pub namespaces: BTreeMap<String, String>,
205    pub parse_status: ParseStatus,
206    pub content_hash: String,
207    pub modified_time: u64,
208    pub parse_message: Option<String>,
209    pub parse_error_location: Option<SourceLocation>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct Entity {
214    pub iri: String,
215    pub short_name: String,
216    pub kind: EntityKind,
217    pub ontology_id: String,
218    pub source_location: SourceLocation,
219    pub labels: Vec<String>,
220    pub comments: Vec<String>,
221    pub deprecated: bool,
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    pub obo_id: Option<String>,
224    #[serde(default, skip_serializing_if = "PropertyCharacteristics::is_empty")]
225    pub characteristics: PropertyCharacteristics,
226}
227
228#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
229pub struct PropertyCharacteristics {
230    pub functional: bool,
231    pub inverse_functional: bool,
232    pub transitive: bool,
233    pub symmetric: bool,
234    pub asymmetric: bool,
235    pub reflexive: bool,
236    pub irreflexive: bool,
237}
238
239impl PropertyCharacteristics {
240    pub fn is_empty(&self) -> bool {
241        !self.functional
242            && !self.inverse_functional
243            && !self.transitive
244            && !self.symmetric
245            && !self.asymmetric
246            && !self.reflexive
247            && !self.irreflexive
248    }
249}
250
251impl Default for Entity {
252    fn default() -> Self {
253        Self {
254            iri: String::new(),
255            short_name: String::new(),
256            kind: EntityKind::Class,
257            ontology_id: String::new(),
258            source_location: SourceLocation::default(),
259            labels: Vec::new(),
260            comments: Vec::new(),
261            deprecated: false,
262            obo_id: None,
263            characteristics: PropertyCharacteristics::default(),
264        }
265    }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct Annotation {
270    pub subject: String,
271    pub predicate: String,
272    pub object: String,
273    pub ontology_id: String,
274    #[serde(default, skip_serializing_if = "SourceLocation::is_empty")]
275    pub source_location: SourceLocation,
276}
277
278/// Annotation attached to an axiom (not an entity annotation assertion).
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280pub struct AxiomAnnotation {
281    pub predicate: String,
282    pub value: String,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct Axiom {
287    pub id: String,
288    pub ontology_id: String,
289    pub subject: String,
290    pub predicate: String,
291    pub object: String,
292    pub axiom_kind: String,
293    #[serde(default, skip_serializing_if = "SourceLocation::is_empty")]
294    pub source_location: SourceLocation,
295    /// Annotations on this axiom (Horned `AnnotatedComponent.ann`).
296    #[serde(default, skip_serializing_if = "Vec::is_empty")]
297    pub annotations: Vec<AxiomAnnotation>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct Namespace {
302    pub prefix: String,
303    pub iri: String,
304    pub ontology_id: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Import {
309    pub ontology_id: String,
310    pub import_iri: String,
311}
312
313/// Snake_case axiom kind stored in [`Axiom::axiom_kind`] and SQL `axioms.axiom_kind`.
314pub const AXIOM_KIND_SUB_CLASS_OF: &str = "sub_class_of";
315pub const AXIOM_KIND_EQUIVALENT_CLASS: &str = "equivalent_class";
316pub const AXIOM_KIND_DISJOINT_CLASS: &str = "disjoint_class";
317pub const AXIOM_KIND_DOMAIN: &str = "domain";
318pub const AXIOM_KIND_RANGE: &str = "range";
319pub const AXIOM_KIND_PROPERTY_CHAIN: &str = "property_chain";
320pub const AXIOM_KIND_CLASS_ASSERTION: &str = "class_assertion";
321pub const AXIOM_KIND_OBJECT_PROPERTY_ASSERTION: &str = "object_property_assertion";
322pub const AXIOM_KIND_DATA_PROPERTY_ASSERTION: &str = "data_property_assertion";
323pub const AXIOM_KIND_HAS_KEY: &str = "has_key";
324pub const AXIOM_KIND_DISJOINT_UNION: &str = "disjoint_union";
325pub const AXIOM_KIND_INVERSE_OBJECT_PROPERTIES: &str = "inverse_object_properties";
326pub const AXIOM_KIND_EQUIVALENT_OBJECT_PROPERTIES: &str = "equivalent_object_properties";
327pub const AXIOM_KIND_DISJOINT_OBJECT_PROPERTIES: &str = "disjoint_object_properties";
328pub const AXIOM_KIND_EQUIVALENT_DATA_PROPERTIES: &str = "equivalent_data_properties";
329pub const AXIOM_KIND_DISJOINT_DATA_PROPERTIES: &str = "disjoint_data_properties";
330pub const AXIOM_KIND_SUB_OBJECT_PROPERTY_OF: &str = "sub_object_property_of";
331pub const AXIOM_KIND_SUB_DATA_PROPERTY_OF: &str = "sub_data_property_of";
332pub const AXIOM_KIND_NEGATIVE_OBJECT_PROPERTY_ASSERTION: &str =
333    "negative_object_property_assertion";
334pub const AXIOM_KIND_NEGATIVE_DATA_PROPERTY_ASSERTION: &str = "negative_data_property_assertion";
335pub const AXIOM_KIND_SAME_INDIVIDUAL: &str = "same_individual";
336pub const AXIOM_KIND_DIFFERENT_INDIVIDUALS: &str = "different_individuals";
337pub const AXIOM_KIND_DATATYPE_DEFINITION: &str = "datatype_definition";
338
339#[cfg(test)]
340mod tests {
341    use super::ParseStatus;
342
343    #[test]
344    fn parse_status_as_str_matches_serde() {
345        assert_eq!(ParseStatus::Ok.as_str(), "ok");
346        assert_eq!(ParseStatus::Warning.as_str(), "warning");
347        assert_eq!(ParseStatus::Error.as_str(), "error");
348    }
349}