axioval_axiolid/
lib.rs

1#![allow(clippy::doc_markdown)]
2
3//! Geometry-evidence adapter contracts independent of `OpenBIM`.
4//!
5//! Axiolid consumes source-qualified subjects, so proprietary CAD, mesh, and B-rep
6//! producers can participate without an IFC dependency. No geometry kernel is silently
7//! substituted: missing backends return [`AxiolidError::IntegrationUnavailable`].
8
9use std::collections::BTreeMap;
10
11use thiserror::Error;
12
13/// Stable identity scope for a geometry-producing source.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct SourceIdentity(String);
16
17impl SourceIdentity {
18    /// Creates a source identity.
19    #[must_use]
20    pub fn new(value: impl Into<String>) -> Self {
21        Self(value.into())
22    }
23
24    /// Returns the source identity text.
25    #[must_use]
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31/// Declares whether an item is exact B-rep evidence or an approximation.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum Exactness {
34    /// The producer asserts that this is exact geometry evidence.
35    Exact,
36    /// The producer asserts that this evidence is an approximation.
37    Approximate,
38}
39
40/// Geometry evidence for one source-qualified subject.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct GeometryEvidence {
43    subject_id: String,
44    exactness: Exactness,
45    provenance: String,
46}
47
48impl GeometryEvidence {
49    /// Creates evidence. `subject_id` may be local or already source-qualified.
50    #[must_use]
51    pub fn new(
52        subject_id: impl Into<String>,
53        exactness: Exactness,
54        provenance: impl Into<String>,
55    ) -> Self {
56        Self {
57            subject_id: subject_id.into(),
58            exactness,
59            provenance: provenance.into(),
60        }
61    }
62
63    /// Returns the source-qualified subject identifier after insertion.
64    #[must_use]
65    pub fn subject_id(&self) -> &str {
66        &self.subject_id
67    }
68
69    /// Returns the fidelity declaration without upgrading approximate geometry.
70    #[must_use]
71    pub fn exactness(&self) -> Exactness {
72        self.exactness
73    }
74
75    /// Returns the opaque origin locator for the supplied evidence.
76    #[must_use]
77    pub fn provenance(&self) -> &str {
78        &self.provenance
79    }
80}
81
82/// Read-only geometry evidence lookup.
83pub trait GeometrySource {
84    /// Returns the configured identity scope.
85    fn source_identity(&self) -> &SourceIdentity;
86    /// Looks up evidence by a source-local subject ID.
87    fn geometry_for(&self, local_id: &str) -> Option<&GeometryEvidence>;
88}
89
90/// In-memory geometry adapter used to conformance-test any CAD source.
91#[derive(Clone, Debug)]
92pub struct InMemoryGeometrySource {
93    identity: SourceIdentity,
94    evidence: BTreeMap<String, GeometryEvidence>,
95}
96
97impl InMemoryGeometrySource {
98    /// Creates an initially empty evidence source.
99    #[must_use]
100    pub fn new(identity: SourceIdentity) -> Self {
101        Self {
102            identity,
103            evidence: BTreeMap::new(),
104        }
105    }
106
107    /// Inserts evidence only when its subject belongs to this source.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`AxiolidError::ForeignSubject`] for an ID qualified by another
112    /// source, or [`AxiolidError::EmptySubjectId`] for an empty local ID.
113    pub fn insert(&mut self, mut evidence: GeometryEvidence) -> Result<(), AxiolidError> {
114        let prefix = format!("{}:", self.identity.as_str());
115        if evidence.subject_id.contains(':') && !evidence.subject_id.starts_with(&prefix) {
116            return Err(AxiolidError::ForeignSubject {
117                subject_id: evidence.subject_id,
118            });
119        }
120        let local_id = evidence
121            .subject_id
122            .strip_prefix(&prefix)
123            .unwrap_or(&evidence.subject_id)
124            .to_owned();
125        if local_id.is_empty() {
126            return Err(AxiolidError::EmptySubjectId);
127        }
128        evidence.subject_id = format!("{prefix}{local_id}");
129        self.evidence.insert(local_id, evidence);
130        Ok(())
131    }
132}
133
134impl GeometrySource for InMemoryGeometrySource {
135    fn source_identity(&self) -> &SourceIdentity {
136        &self.identity
137    }
138    fn geometry_for(&self, local_id: &str) -> Option<&GeometryEvidence> {
139        self.evidence.get(local_id)
140    }
141}
142
143/// Isolated seam for a real geometry kernel or CAD SDK.
144pub trait GeometryBackend {
145    /// Resolves one subject's evidence through the external backend.
146    ///
147    /// # Errors
148    ///
149    /// Returns a backend failure, including [`AxiolidError::IntegrationUnavailable`].
150    fn resolve(&self, subject_id: &str) -> Result<GeometryEvidence, AxiolidError>;
151}
152
153/// Explicit placeholder used while no external geometry backend is linked.
154#[derive(Clone, Debug, Default)]
155pub struct UnavailableGeometryBackend;
156
157impl GeometryBackend for UnavailableGeometryBackend {
158    fn resolve(&self, _subject_id: &str) -> Result<GeometryEvidence, AxiolidError> {
159        Err(AxiolidError::IntegrationUnavailable {
160            integration: "Axiolid geometry kernel",
161        })
162    }
163}
164
165/// Errors from geometry evidence adaptation.
166#[derive(Debug, Error, Eq, PartialEq)]
167pub enum AxiolidError {
168    /// An empty source-local subject cannot be safely qualified.
169    #[error("geometry subject ID must not be empty")]
170    EmptySubjectId,
171    /// Evidence belonged to a different source scope.
172    #[error("geometry evidence belongs to a foreign subject: {subject_id}")]
173    ForeignSubject {
174        /// The supplied source-qualified subject identity.
175        subject_id: String,
176    },
177    /// A required kernel or CAD SDK integration is deliberately not implemented.
178    #[error("external integration unavailable: {integration}")]
179    IntegrationUnavailable {
180        /// Named external component that has not been linked.
181        integration: &'static str,
182    },
183}