bc_envelope/extension/attachment/
attachment_impl.rs

1use anyhow::{ bail, Result };
2
3use crate::{
4    base::envelope::EnvelopeCase,
5    known_values,
6    Assertion,
7    Envelope,
8    EnvelopeEncodable,
9    Error,
10};
11
12/// Support for adding vendor-specific attachments to Gordian Envelopes.
13///
14/// This module extends Gordian Envelope with the ability to add vendor-specific attachments
15/// to an envelope. Attachments provide a standardized way for different applications to
16/// include their own data in an envelope without interfering with the main data structure
17/// or with other attachments.
18///
19/// Each attachment has:
20/// * A payload (arbitrary data)
21/// * A required vendor identifier (typically a reverse domain name)
22/// * An optional conformsTo URI that indicates the format of the attachment
23///
24/// This allows for a common envelope format that can be extended by different vendors
25/// while maintaining interoperability.
26///
27/// # Example
28///
29/// ```
30/// use bc_envelope::prelude::*;
31///
32/// // Create a base envelope
33/// let envelope = Envelope::new("Alice")
34///     .add_assertion("knows", "Bob");
35///
36/// // Add a vendor-specific attachment
37/// let with_attachment = envelope.add_attachment(
38///     "Custom data for this envelope",
39///     "com.example",
40///     Some("https://example.com/attachment-format/v1")
41/// );
42///
43/// // The attachment is added as an assertion with the 'attachment' predicate
44/// assert!(!with_attachment.assertions_with_predicate(known_values::ATTACHMENT).is_empty());
45///
46/// // The attachment can be extracted later
47/// let attachment = with_attachment.attachments().unwrap()[0].clone();
48/// let payload = attachment.attachment_payload().unwrap();
49/// let vendor = attachment.attachment_vendor().unwrap();
50/// let format = attachment.attachment_conforms_to().unwrap();
51///
52/// assert_eq!(payload.format_flat(), r#""Custom data for this envelope""#);
53/// assert_eq!(vendor, "com.example");
54/// assert_eq!(format, Some("https://example.com/attachment-format/v1".to_string()));
55/// ```
56/// Methods for creating and accessing attachments at the assertion level
57impl Assertion {
58    /// Creates a new attachment assertion.
59    ///
60    /// An attachment assertion consists of:
61    /// * The predicate `known_values::ATTACHMENT`
62    /// * An object that is a wrapped envelope containing:
63    ///   * The payload (as the subject)
64    ///   * A required `'vendor': String` assertion
65    ///   * An optional `'conformsTo': String` assertion
66    ///
67    /// See [BCR-2023-006](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2023-006-envelope-attachment.md)
68    /// for the detailed specification.
69    ///
70    /// # Parameters
71    ///
72    /// * `payload` - The content of the attachment
73    /// * `vendor` - A string that uniquely identifies the vendor (typically a reverse domain name)
74    /// * `conforms_to` - An optional URI that identifies the format of the attachment
75    ///
76    /// # Returns
77    ///
78    /// A new attachment assertion
79    ///
80    /// # Examples
81    ///
82    /// Example:
83    ///
84    /// Create an attachment assertion that contains vendor-specific data,
85    /// then use it to access the payload, vendor ID, and conformsTo value.
86    ///
87    /// The assertion will have a predicate of "attachment" and an object that's
88    /// a wrapped envelope containing the payload with vendor and conformsTo
89    /// assertions added to it.
90    ///
91    pub fn new_attachment(
92        payload: impl EnvelopeEncodable,
93        vendor: &str,
94        conforms_to: Option<&str>
95    ) -> Self {
96        let conforms_to: Option<String> = conforms_to.map(|c| c.to_string());
97        Self::new(
98            known_values::ATTACHMENT,
99            payload
100                .into_envelope()
101                .wrap_envelope()
102                .add_assertion(known_values::VENDOR, vendor.to_string())
103                .add_optional_assertion(known_values::CONFORMS_TO, conforms_to)
104        )
105    }
106
107    /// Returns the payload of an attachment assertion.
108    ///
109    /// This extracts the subject of the wrapped envelope that is the object of this attachment assertion.
110    ///
111    /// # Returns
112    ///
113    /// The payload envelope
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if the assertion is not a valid attachment assertion
118    pub fn attachment_payload(&self) -> Result<Envelope> {
119        self.object().unwrap_envelope()
120    }
121
122    /// Returns the vendor identifier of an attachment assertion.
123    ///
124    /// # Returns
125    ///
126    /// The vendor string
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the assertion is not a valid attachment assertion
131    pub fn attachment_vendor(&self) -> Result<String> {
132        self.object().extract_object_for_predicate(known_values::VENDOR)
133    }
134
135    /// Returns the optional conformsTo URI of an attachment assertion.
136    ///
137    /// # Returns
138    ///
139    /// The conformsTo string if present, or None
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the assertion is not a valid attachment assertion
144    pub fn attachment_conforms_to(&self) -> Result<Option<String>> {
145        self.object().extract_optional_object_for_predicate(known_values::CONFORMS_TO)
146    }
147
148    /// Validates that an assertion is a proper attachment assertion.
149    ///
150    /// This ensures:
151    /// - The attachment assertion's predicate is `known_values::ATTACHMENT`
152    /// - The attachment assertion's object is an envelope
153    /// - The attachment assertion's object has a `'vendor': String` assertion
154    /// - The attachment assertion's object has an optional `'conformsTo': String` assertion
155    ///
156    /// # Returns
157    ///
158    /// Ok(()) if the assertion is a valid attachment assertion
159    ///
160    /// # Errors
161    ///
162    /// Returns `EnvelopeError::InvalidAttachment` if the assertion is not a valid attachment assertion
163    pub fn validate_attachment(&self) -> Result<()> {
164        let payload = self.attachment_payload()?;
165        let vendor = self.attachment_vendor()?;
166        let conforms_to: Option<String> = self.attachment_conforms_to()?;
167        let assertion = Assertion::new_attachment(payload, vendor.as_str(), conforms_to.as_deref());
168        let e: Envelope = assertion.to_envelope();
169        if !e.is_equivalent_to(&self.clone().to_envelope()) {
170            bail!(Error::InvalidAttachment);
171        }
172        Ok(())
173    }
174}
175
176/// Methods for creating attachment envelopes
177impl Envelope {
178    /// Creates a new envelope with an attachment as its subject.
179    ///
180    /// This creates an envelope whose subject is an attachment assertion, using the
181    /// provided payload, vendor, and optional conformsTo URI.
182    ///
183    /// # Parameters
184    ///
185    /// * `payload` - The content of the attachment
186    /// * `vendor` - A string that uniquely identifies the vendor (typically a reverse domain name)
187    /// * `conforms_to` - An optional URI that identifies the format of the attachment
188    ///
189    /// # Returns
190    ///
191    /// A new envelope with the attachment as its subject
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use bc_envelope::prelude::*;
197    /// use bc_envelope::base::envelope::EnvelopeCase;
198    ///
199    /// // Create an attachment envelope
200    /// let envelope = Envelope::new_attachment(
201    ///     "Attachment data",
202    ///     "com.example",
203    ///     Some("https://example.com/format/v1")
204    /// );
205    ///
206    /// // The envelope is an assertion
207    /// assert!(matches!(envelope.case(), EnvelopeCase::Assertion(_)));
208    /// ```
209    pub fn new_attachment(
210        payload: impl EnvelopeEncodable,
211        vendor: &str,
212        conforms_to: Option<&str>
213    ) -> Self {
214        Assertion::new_attachment(payload, vendor, conforms_to).to_envelope()
215    }
216
217    /// Returns a new envelope with an added `'attachment': Envelope` assertion.
218    ///
219    /// This adds an attachment assertion to an existing envelope.
220    ///
221    /// # Parameters
222    ///
223    /// * `payload` - The content of the attachment
224    /// * `vendor` - A string that uniquely identifies the vendor (typically a reverse domain name)
225    /// * `conforms_to` - An optional URI that identifies the format of the attachment
226    ///
227    /// # Returns
228    ///
229    /// A new envelope with the attachment assertion added
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use bc_envelope::prelude::*;
235    ///
236    /// // Create a base envelope
237    /// let envelope = Envelope::new("User data")
238    ///     .add_assertion("name", "Alice");
239    ///
240    /// // Add an attachment
241    /// let with_attachment = envelope.add_attachment(
242    ///     "Vendor-specific metadata",
243    ///     "com.example",
244    ///     Some("https://example.com/metadata/v1")
245    /// );
246    ///
247    /// // The original envelope is unchanged
248    /// assert_eq!(envelope.assertions().len(), 1);
249    ///
250    /// // The new envelope has an additional attachment assertion
251    /// assert_eq!(with_attachment.assertions().len(), 2);
252    /// assert!(with_attachment.assertions_with_predicate(known_values::ATTACHMENT).len() > 0);
253    /// ```
254    pub fn add_attachment(
255        &self,
256        payload: impl EnvelopeEncodable,
257        vendor: &str,
258        conforms_to: Option<&str>
259    ) -> Self {
260        self.add_assertion_envelope(
261            Assertion::new_attachment(payload, vendor, conforms_to)
262        ).unwrap()
263    }
264}
265
266/// Methods for accessing attachments in envelopes
267impl Envelope {
268    /// Returns the payload of an attachment envelope.
269    ///
270    /// # Returns
271    ///
272    /// The payload envelope
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if the envelope is not a valid attachment envelope
277    pub fn attachment_payload(&self) -> Result<Self> {
278        if let EnvelopeCase::Assertion(assertion) = self.case() {
279            Ok(assertion.attachment_payload()?)
280        } else {
281            bail!(Error::InvalidAttachment)
282        }
283    }
284
285    /// Returns the vendor identifier of an attachment envelope.
286    ///
287    /// # Returns
288    ///
289    /// The vendor string
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if the envelope is not a valid attachment envelope
294    pub fn attachment_vendor(&self) -> Result<String> {
295        if let EnvelopeCase::Assertion(assertion) = self.case() {
296            Ok(assertion.attachment_vendor()?)
297        } else {
298            bail!(Error::InvalidAttachment);
299        }
300    }
301
302    /// Returns the optional conformsTo URI of an attachment envelope.
303    ///
304    /// # Returns
305    ///
306    /// The conformsTo string if present, or None
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the envelope is not a valid attachment envelope
311    pub fn attachment_conforms_to(&self) -> Result<Option<String>> {
312        if let EnvelopeCase::Assertion(assertion) = self.case() {
313            Ok(assertion.attachment_conforms_to()?)
314        } else {
315            bail!(Error::InvalidAttachment);
316        }
317    }
318
319    /// Searches the envelope's assertions for attachments that match the given vendor and conformsTo.
320    ///
321    /// This method finds all attachment assertions in the envelope that match the specified
322    /// criteria:
323    ///
324    /// * If `vendor` is `None`, matches any vendor
325    /// * If `conformsTo` is `None`, matches any conformsTo value
326    /// * If both are `None`, matches all attachments
327    ///
328    /// # Parameters
329    ///
330    /// * `vendor` - Optional vendor identifier to match
331    /// * `conforms_to` - Optional conformsTo URI to match
332    ///
333    /// # Returns
334    ///
335    /// A vector of matching attachment envelopes
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if any of the envelope's attachments are invalid
340    ///
341    /// # Examples
342    ///
343    /// ```
344    /// use bc_envelope::prelude::*;
345    ///
346    /// // Create an envelope with two attachments from the same vendor
347    /// let envelope = Envelope::new("Data")
348    ///     .add_attachment("Attachment 1", "com.example", Some("https://example.com/format/v1"))
349    ///     .add_attachment("Attachment 2", "com.example", Some("https://example.com/format/v2"));
350    ///
351    /// // Find all attachments
352    /// let all_attachments = envelope.attachments().unwrap();
353    /// assert_eq!(all_attachments.len(), 2);
354    ///
355    /// // Find attachments by vendor
356    /// let vendor_attachments = envelope
357    ///     .attachments_with_vendor_and_conforms_to(Some("com.example"), None)
358    ///     .unwrap();
359    /// assert_eq!(vendor_attachments.len(), 2);
360    ///
361    /// // Find attachments by specific format
362    /// let v1_attachments = envelope
363    ///     .attachments_with_vendor_and_conforms_to(None, Some("https://example.com/format/v1"))
364    ///     .unwrap();
365    /// assert_eq!(v1_attachments.len(), 1);
366    /// ```
367    pub fn attachments_with_vendor_and_conforms_to(
368        &self,
369        vendor: Option<&str>,
370        conforms_to: Option<&str>
371    ) -> Result<Vec<Self>> {
372        let assertions = self.assertions_with_predicate(known_values::ATTACHMENT);
373        for assertion in &assertions {
374            Self::validate_attachment(assertion)?;
375        }
376        let matching_assertions: Vec<_> = assertions
377            .into_iter()
378            .filter(|assertion| {
379                if let Some(vendor) = vendor {
380                    if let Ok(v) = assertion.attachment_vendor() {
381                        if v != vendor {
382                            return false;
383                        }
384                    }
385                }
386                if let Some(conforms_to) = conforms_to {
387                    if let Ok(c) = assertion.attachment_conforms_to() {
388                        if let Some(c) = c {
389                            if c != conforms_to {
390                                return false;
391                            }
392                        } else {
393                            return false;
394                        }
395                    }
396                }
397                true
398            })
399            .collect();
400        Result::Ok(matching_assertions)
401    }
402
403    /// Returns all attachments in the envelope.
404    ///
405    /// This is equivalent to calling `attachments_with_vendor_and_conforms_to(None, None)`.
406    ///
407    /// # Returns
408    ///
409    /// A vector of all attachment envelopes
410    ///
411    /// # Errors
412    ///
413    /// Returns an error if any of the envelope's attachments are invalid
414    pub fn attachments(&self) -> Result<Vec<Self>> {
415        self.attachments_with_vendor_and_conforms_to(None::<&str>, None::<&str>)
416    }
417
418    /// Validates that an envelope is a proper attachment envelope.
419    ///
420    /// This ensures the envelope is an assertion envelope with the predicate `attachment`
421    /// and the required structure for an attachment.
422    ///
423    /// # Returns
424    ///
425    /// Ok(()) if the envelope is a valid attachment envelope
426    ///
427    /// # Errors
428    ///
429    /// Returns `EnvelopeError::InvalidAttachment` if the envelope is not a valid attachment envelope
430    pub fn validate_attachment(&self) -> Result<()> {
431        if let EnvelopeCase::Assertion(assertion) = self.case() {
432            assertion.validate_attachment()?;
433            Ok(())
434        } else {
435            bail!(Error::InvalidAttachment);
436        }
437    }
438
439    /// Finds a single attachment matching the given vendor and conformsTo.
440    ///
441    /// This works like `attachments_with_vendor_and_conforms_to` but returns a single
442    /// attachment envelope rather than a vector. It requires that exactly one attachment
443    /// matches the criteria.
444    ///
445    /// # Parameters
446    ///
447    /// * `vendor` - Optional vendor identifier to match
448    /// * `conforms_to` - Optional conformsTo URI to match
449    ///
450    /// # Returns
451    ///
452    /// The matching attachment envelope
453    ///
454    /// # Errors
455    ///
456    /// * Returns `EnvelopeError::NonexistentAttachment` if no attachments match
457    /// * Returns `EnvelopeError::AmbiguousAttachment` if more than one attachment matches
458    /// * Returns an error if any of the envelope's attachments are invalid
459    ///
460    /// # Examples
461    ///
462    /// ```
463    /// use bc_envelope::prelude::*;
464    ///
465    /// // Create an envelope with an attachment
466    /// let envelope = Envelope::new("Data")
467    ///     .add_attachment(
468    ///         "Metadata",
469    ///         "com.example",
470    ///         Some("https://example.com/format/v1")
471    ///     );
472    ///
473    /// // Find a specific attachment by vendor and format
474    /// let attachment = envelope
475    ///     .attachment_with_vendor_and_conforms_to(
476    ///         Some("com.example"),
477    ///         Some("https://example.com/format/v1")
478    ///     )
479    ///     .unwrap();
480    ///
481    /// // Access the attachment payload
482    /// let payload = attachment.attachment_payload().unwrap();
483    /// assert_eq!(payload.extract_subject::<String>().unwrap(), "Metadata");
484    /// ```
485    pub fn attachment_with_vendor_and_conforms_to(
486        &self,
487        vendor: Option<&str>,
488        conforms_to: Option<&str>
489    ) -> Result<Self> {
490        let attachments = self.attachments_with_vendor_and_conforms_to(vendor, conforms_to)?;
491        if attachments.is_empty() {
492            bail!(Error::NonexistentAttachment);
493        }
494        if attachments.len() > 1 {
495            bail!(Error::AmbiguousAttachment);
496        }
497        Ok(attachments.first().unwrap().clone())
498    }
499}