Skip to main content

boxology_contract/
implementation.rs

1//! Implementation-local import registration descriptors.
2
3use std::error::Error;
4use std::fmt;
5
6use crate::{BoxId, CapabilityId, ContractDescriptor, ContractRevision};
7
8/// One implementation import slot and its checked-in foreign contract view.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ImportDescriptor {
11    slot_id: BoxId,
12    expected_revision: ContractRevision,
13    capabilities: Vec<CapabilityId>,
14}
15
16impl ImportDescriptor {
17    /// Constructs an import slot, preserving the supplied capability order.
18    pub fn new(
19        slot_id: BoxId,
20        expected_revision: ContractRevision,
21        capabilities: impl IntoIterator<Item = CapabilityId>,
22    ) -> Result<Self, ImportDescriptorError> {
23        let mut accepted: Vec<CapabilityId> = Vec::new();
24        for capability in capabilities {
25            if capability.box_id() != &slot_id {
26                return Err(ImportDescriptorError::CapabilityPackageMismatch {
27                    slot_id,
28                    capability,
29                });
30            }
31            if accepted.iter().any(|known| known == &capability) {
32                return Err(ImportDescriptorError::DuplicateCapability { capability });
33            }
34            accepted.push(capability);
35        }
36        Ok(Self {
37            slot_id,
38            expected_revision,
39            capabilities: accepted,
40        })
41    }
42
43    /// Returns the import-slot identity, which is the foreign package id in v0.
44    pub fn slot_id(&self) -> &BoxId {
45        &self.slot_id
46    }
47
48    /// Returns the expected revision from the foreign checked-in schema.
49    pub fn expected_revision(&self) -> &ContractRevision {
50        &self.expected_revision
51    }
52
53    /// Returns imported capability identities in their supplied order.
54    pub fn capabilities(&self) -> &[CapabilityId] {
55        &self.capabilities
56    }
57}
58
59/// A failure to construct an import descriptor.
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ImportDescriptorError {
63    /// An imported capability belonged to a package other than its slot.
64    CapabilityPackageMismatch {
65        /// The package serving as the import-slot identity.
66        slot_id: BoxId,
67        /// The mismatched qualified capability identity.
68        capability: CapabilityId,
69    },
70    /// A capability identity appeared more than once in the import set.
71    DuplicateCapability {
72        /// The repeated capability identity.
73        capability: CapabilityId,
74    },
75}
76
77impl fmt::Display for ImportDescriptorError {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            Self::CapabilityPackageMismatch {
81                slot_id,
82                capability,
83            } => write!(
84                formatter,
85                "capability {capability} does not belong to import slot {slot_id}"
86            ),
87            Self::DuplicateCapability { capability } => {
88                write!(formatter, "duplicate imported capability: {capability}")
89            }
90        }
91    }
92}
93
94impl Error for ImportDescriptorError {}
95
96/// Implementation-local registration data for one outward contract.
97///
98/// Construction happens at runtime from a contract owned by generated static
99/// storage, so cloning this value shares rather than duplicates that contract.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ImplementationDescriptor {
102    contract: &'static ContractDescriptor,
103    imports: Vec<ImportDescriptor>,
104}
105
106impl ImplementationDescriptor {
107    /// Constructs implementation registration while preserving import order.
108    pub fn new(
109        contract: &'static ContractDescriptor,
110        imports: impl IntoIterator<Item = ImportDescriptor>,
111    ) -> Result<Self, ImplementationDescriptorError> {
112        let mut accepted: Vec<ImportDescriptor> = Vec::new();
113        for import in imports {
114            if accepted
115                .iter()
116                .any(|known| known.slot_id() == import.slot_id())
117            {
118                return Err(ImplementationDescriptorError::DuplicateImportSlot {
119                    slot_id: import.slot_id,
120                });
121            }
122            accepted.push(import);
123        }
124        Ok(Self {
125            contract,
126            imports: accepted,
127        })
128    }
129
130    /// Returns the shared outward contract.
131    pub fn contract(&self) -> &'static ContractDescriptor {
132        self.contract
133    }
134
135    /// Returns implementation-private imports in their supplied order.
136    pub fn imports(&self) -> &[ImportDescriptor] {
137        &self.imports
138    }
139}
140
141/// A failure to construct an implementation descriptor.
142#[derive(Debug, Clone, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum ImplementationDescriptorError {
145    /// An import-slot package appeared more than once.
146    DuplicateImportSlot {
147        /// The repeated package and slot identity.
148        slot_id: BoxId,
149    },
150}
151
152impl fmt::Display for ImplementationDescriptorError {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Self::DuplicateImportSlot { slot_id } => {
156                write!(formatter, "duplicate import slot: {slot_id}")
157            }
158        }
159    }
160}
161
162impl Error for ImplementationDescriptorError {}
163
164#[cfg(test)]
165mod tests {
166    use std::sync::LazyLock;
167
168    use super::*;
169    use crate::{
170        CapabilityDescriptor, CapabilityName, CapabilityShape, ExposureLevel, Idempotency,
171        TypeDescriptor,
172    };
173
174    fn box_id(value: &str) -> BoxId {
175        BoxId::new(value).unwrap()
176    }
177
178    fn capability(package: &str, name: &str) -> CapabilityId {
179        CapabilityId::new(box_id(package), CapabilityName::new(name).unwrap())
180    }
181
182    fn import(package: &str, revision: &str, names: &[&str]) -> ImportDescriptor {
183        ImportDescriptor::new(
184            box_id(package),
185            ContractRevision::new(revision).unwrap(),
186            names.iter().map(|name| capability(package, name)),
187        )
188        .unwrap()
189    }
190
191    static CONTRACT: LazyLock<ContractDescriptor> = LazyLock::new(|| {
192        let id = capability("greeter", "greet");
193        let descriptor = CapabilityDescriptor::new(
194            id,
195            TypeDescriptor::string(),
196            TypeDescriptor::string(),
197            TypeDescriptor::string(),
198            CapabilityShape::Unary,
199            ExposureLevel::External,
200            Idempotency::None,
201            None,
202        );
203        ContractDescriptor::new(
204            box_id("greeter"),
205            [descriptor],
206            ContractRevision::new("greeter-r1").unwrap(),
207        )
208        .unwrap()
209    });
210
211    #[test]
212    fn import_view_preserves_revision_and_capability_order() {
213        let descriptor = import("hello", "hello-r7", &["health", "greet"]);
214
215        assert_eq!(descriptor.slot_id(), &box_id("hello"));
216        assert_eq!(descriptor.expected_revision().as_str(), "hello-r7");
217        assert_eq!(
218            descriptor.capabilities(),
219            &[capability("hello", "health"), capability("hello", "greet")]
220        );
221    }
222
223    #[test]
224    fn import_rejects_cross_package_capability_exactly() {
225        let error = ImportDescriptor::new(
226            box_id("hello"),
227            ContractRevision::new("r1").unwrap(),
228            [capability("other", "greet")],
229        )
230        .unwrap_err();
231        assert_eq!(
232            error,
233            ImportDescriptorError::CapabilityPackageMismatch {
234                slot_id: box_id("hello"),
235                capability: capability("other", "greet"),
236            }
237        );
238        assert_eq!(
239            error.to_string(),
240            "capability other.greet does not belong to import slot hello"
241        );
242    }
243
244    #[test]
245    fn import_rejects_duplicate_capability_exactly() {
246        let repeated = capability("hello", "greet");
247        let error = ImportDescriptor::new(
248            box_id("hello"),
249            ContractRevision::new("r1").unwrap(),
250            [repeated.clone(), repeated],
251        )
252        .unwrap_err();
253        assert_eq!(
254            error,
255            ImportDescriptorError::DuplicateCapability {
256                capability: capability("hello", "greet"),
257            }
258        );
259        assert_eq!(
260            error.to_string(),
261            "duplicate imported capability: hello.greet"
262        );
263    }
264
265    #[test]
266    fn implementation_shares_contract_and_preserves_import_order() {
267        let hello = import("hello", "r1", &["greet"]);
268        let audit = import("audit", "r2", &["record"]);
269        let descriptor =
270            ImplementationDescriptor::new(&CONTRACT, [hello.clone(), audit.clone()]).unwrap();
271
272        assert!(std::ptr::eq(descriptor.contract(), &*CONTRACT));
273        assert_eq!(descriptor.imports(), &[hello, audit]);
274    }
275
276    #[test]
277    fn implementation_rejects_duplicate_slots_exactly() {
278        let first = import("hello", "r1", &["greet"]);
279        let second = import("hello", "r2", &["health"]);
280        let error = ImplementationDescriptor::new(&CONTRACT, [first, second]).unwrap_err();
281
282        assert_eq!(
283            error,
284            ImplementationDescriptorError::DuplicateImportSlot {
285                slot_id: box_id("hello"),
286            }
287        );
288        assert_eq!(error.to_string(), "duplicate import slot: hello");
289    }
290
291    #[test]
292    fn private_import_changes_leave_outward_contract_unchanged() {
293        let without_imports = ImplementationDescriptor::new(&CONTRACT, []).unwrap();
294        let with_imports =
295            ImplementationDescriptor::new(&CONTRACT, [import("hello", "r1", &["greet"])]).unwrap();
296
297        assert!(std::ptr::eq(
298            without_imports.contract(),
299            with_imports.contract()
300        ));
301        assert_eq!(
302            without_imports.contract().revision(),
303            with_imports.contract().revision()
304        );
305        assert_ne!(without_imports.imports(), with_imports.imports());
306    }
307
308    #[test]
309    fn implementation_descriptors_are_structural_static_plain_data() {
310        fn assert_bounds<T: Send + Sync + 'static>() {}
311
312        let build = || {
313            ImplementationDescriptor::new(&CONTRACT, [import("hello", "r1", &["greet"])]).unwrap()
314        };
315        assert_eq!(build(), build());
316        assert_eq!(build(), build().clone());
317        assert_bounds::<ImportDescriptor>();
318        assert_bounds::<ImportDescriptorError>();
319        assert_bounds::<ImplementationDescriptor>();
320        assert_bounds::<ImplementationDescriptorError>();
321    }
322}