bc_envelope/extension/attachment/
attachment_impl.rs

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