1use std::collections::BTreeSet;
8
9use thiserror::Error;
10
11#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct Document {
14 id: String,
15 media_type: String,
16 digest: String,
17}
18
19impl Document {
20 #[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 #[must_use]
35 pub fn id(&self) -> &str {
36 &self.id
37 }
38 #[must_use]
40 pub fn media_type(&self) -> &str {
41 &self.media_type
42 }
43 #[must_use]
45 pub fn digest(&self) -> &str {
46 &self.digest
47 }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct ProjectLink {
53 from: String,
54 to: String,
55 relation: String,
56}
57
58impl ProjectLink {
59 #[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 #[must_use]
74 pub fn from(&self) -> &str {
75 &self.from
76 }
77 #[must_use]
79 pub fn to(&self) -> &str {
80 &self.to
81 }
82 #[must_use]
84 pub fn relation(&self) -> &str {
85 &self.relation
86 }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct ProjectAssembly {
92 documents: Vec<Document>,
93 links: Vec<ProjectLink>,
94}
95
96impl ProjectAssembly {
97 #[must_use]
99 pub fn documents(&self) -> &[Document] {
100 &self.documents
101 }
102 #[must_use]
104 pub fn links(&self) -> &[ProjectLink] {
105 &self.links
106 }
107}
108
109pub trait ProjectAssembler {
111 fn assemble(
119 &self,
120 documents: impl IntoIterator<Item = Document>,
121 links: impl IntoIterator<Item = ProjectLink>,
122 ) -> Result<ProjectAssembly, ICDDAssemblyError>;
123}
124
125#[derive(Clone, Debug, Default)]
127pub struct InMemoryProjectAssembler;
128
129impl InMemoryProjectAssembler {
130 #[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
172pub trait IcddContainerReader {
174 fn read(&self, locator: &str) -> Result<ProjectAssembly, ICDDAssemblyError>;
180}
181
182#[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#[derive(Debug, Error, Eq, PartialEq)]
196pub enum ICDDAssemblyError {
197 #[error("project document ID must not be empty")]
199 EmptyDocumentId,
200 #[error("duplicate project document: {id}")]
202 DuplicateDocument {
203 id: String,
205 },
206 #[error("declared link references unknown project document: {id}")]
208 UnknownDocument {
209 id: String,
211 },
212 #[error("external integration unavailable: {integration}")]
214 IntegrationUnavailable {
215 integration: &'static str,
217 },
218}