axioval_icdd/
lib.rs

1//! ICDD project-assembly contracts.
2//!
3//! Assembly records project documents and declared inter-document links. It deliberately
4//! does not define federation, semantic identity, or rule execution; those remain engine
5//! concerns once their stable contracts exist.
6
7use std::collections::BTreeSet;
8
9use thiserror::Error;
10
11/// A project document with a stable assembly-local identifier.
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct Document {
14    id: String,
15    media_type: String,
16    digest: String,
17}
18
19impl Document {
20    /// Creates a document descriptor without reading its bytes.
21    #[must_use]
22    pub fn new(
23        id: impl Into<String>,
24        media_type: impl Into<String>,
25        digest: impl Into<String>,
26    ) -> Self {
27        Self {
28            id: id.into(),
29            media_type: media_type.into(),
30            digest: digest.into(),
31        }
32    }
33    /// Returns the assembly-local document identifier.
34    #[must_use]
35    pub fn id(&self) -> &str {
36        &self.id
37    }
38    /// Returns the declared media type.
39    #[must_use]
40    pub fn media_type(&self) -> &str {
41        &self.media_type
42    }
43    /// Returns the producer-supplied content digest string.
44    #[must_use]
45    pub fn digest(&self) -> &str {
46        &self.digest
47    }
48}
49
50/// A declared directed relationship between two documents.
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct ProjectLink {
53    from: String,
54    to: String,
55    relation: String,
56}
57
58impl ProjectLink {
59    /// Creates a link; endpoint existence is verified by assembly.
60    #[must_use]
61    pub fn new(
62        from: impl Into<String>,
63        to: impl Into<String>,
64        relation: impl Into<String>,
65    ) -> Self {
66        Self {
67            from: from.into(),
68            to: to.into(),
69            relation: relation.into(),
70        }
71    }
72    /// Returns the origin document ID.
73    #[must_use]
74    pub fn from(&self) -> &str {
75        &self.from
76    }
77    /// Returns the target document ID.
78    #[must_use]
79    pub fn to(&self) -> &str {
80        &self.to
81    }
82    /// Returns the un-interpreted declared relationship.
83    #[must_use]
84    pub fn relation(&self) -> &str {
85        &self.relation
86    }
87}
88
89/// Validated project assembly with no federation behavior.
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct ProjectAssembly {
92    documents: Vec<Document>,
93    links: Vec<ProjectLink>,
94}
95
96impl ProjectAssembly {
97    /// Returns documents in the supplied manifest order.
98    #[must_use]
99    pub fn documents(&self) -> &[Document] {
100        &self.documents
101    }
102    /// Returns links in the supplied manifest order.
103    #[must_use]
104    pub fn links(&self) -> &[ProjectLink] {
105        &self.links
106    }
107}
108
109/// Assembles an ICDD project manifest from external container data.
110pub trait ProjectAssembler {
111    /// Validates references without parsing or federating project contents.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`ICDDAssemblyError::EmptyDocumentId`],
116    /// [`ICDDAssemblyError::DuplicateDocument`], or
117    /// [`ICDDAssemblyError::UnknownDocument`] for an invalid manifest.
118    fn assemble(
119        &self,
120        documents: impl IntoIterator<Item = Document>,
121        links: impl IntoIterator<Item = ProjectLink>,
122    ) -> Result<ProjectAssembly, ICDDAssemblyError>;
123}
124
125/// In-memory conformance double for ICDD manifest assembly.
126#[derive(Clone, Debug, Default)]
127pub struct InMemoryProjectAssembler;
128
129impl InMemoryProjectAssembler {
130    /// Creates an in-memory manifest assembler.
131    #[must_use]
132    pub const fn new() -> Self {
133        Self
134    }
135}
136
137impl ProjectAssembler for InMemoryProjectAssembler {
138    fn assemble(
139        &self,
140        documents: impl IntoIterator<Item = Document>,
141        links: impl IntoIterator<Item = ProjectLink>,
142    ) -> Result<ProjectAssembly, ICDDAssemblyError> {
143        let documents: Vec<_> = documents.into_iter().collect();
144        let mut ids = BTreeSet::new();
145        for document in &documents {
146            if document.id.is_empty() {
147                return Err(ICDDAssemblyError::EmptyDocumentId);
148            }
149            if !ids.insert(document.id.clone()) {
150                return Err(ICDDAssemblyError::DuplicateDocument {
151                    id: document.id.clone(),
152                });
153            }
154        }
155        let links: Vec<_> = links.into_iter().collect();
156        for link in &links {
157            if !ids.contains(&link.from) {
158                return Err(ICDDAssemblyError::UnknownDocument {
159                    id: link.from.clone(),
160                });
161            }
162            if !ids.contains(&link.to) {
163                return Err(ICDDAssemblyError::UnknownDocument {
164                    id: link.to.clone(),
165                });
166            }
167        }
168        Ok(ProjectAssembly { documents, links })
169    }
170}
171
172/// Isolated seam for an ISO 21597-1 container reader.
173pub trait IcddContainerReader {
174    /// Reads external container data into an already-validated assembly.
175    ///
176    /// # Errors
177    ///
178    /// Returns a reader failure, including [`ICDDAssemblyError::IntegrationUnavailable`].
179    fn read(&self, locator: &str) -> Result<ProjectAssembly, ICDDAssemblyError>;
180}
181
182/// Explicit placeholder used until a container parser is selected and linked.
183#[derive(Clone, Debug, Default)]
184pub struct UnavailableIcddContainerReader;
185
186impl IcddContainerReader for UnavailableIcddContainerReader {
187    fn read(&self, _locator: &str) -> Result<ProjectAssembly, ICDDAssemblyError> {
188        Err(ICDDAssemblyError::IntegrationUnavailable {
189            integration: "ISO 21597-1 ICDD container reader",
190        })
191    }
192}
193
194/// Errors produced by project assembly.
195#[derive(Debug, Error, Eq, PartialEq)]
196pub enum ICDDAssemblyError {
197    /// Document IDs are required for deterministic link validation.
198    #[error("project document ID must not be empty")]
199    EmptyDocumentId,
200    /// Multiple descriptors claimed an assembly-local ID.
201    #[error("duplicate project document: {id}")]
202    DuplicateDocument {
203        /// Duplicate assembly-local document identifier.
204        id: String,
205    },
206    /// A declared relationship points at no declared document.
207    #[error("declared link references unknown project document: {id}")]
208    UnknownDocument {
209        /// Referenced but undeclared assembly-local document identifier.
210        id: String,
211    },
212    /// A real ISO container reader has not been integrated.
213    #[error("external integration unavailable: {integration}")]
214    IntegrationUnavailable {
215        /// Named external component that has not been linked.
216        integration: &'static str,
217    },
218}