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}