bc_envelope/extension/attachment/
attachment_impl.rs

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