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 if let Ok(v) = assertion.attachment_vendor() {
414 if v != vendor {
415 return false;
416 }
417 }
418 }
419 if let Some(conforms_to) = conforms_to {
420 if let Ok(c) = assertion.attachment_conforms_to() {
421 if let Some(c) = c {
422 if c != conforms_to {
423 return false;
424 }
425 } else {
426 return false;
427 }
428 }
429 }
430 true
431 })
432 .collect();
433 Result::Ok(matching_assertions)
434 }
435
436 /// Returns all attachments in the envelope.
437 ///
438 /// This is equivalent to calling
439 /// `attachments_with_vendor_and_conforms_to(None, None)`.
440 ///
441 /// # Returns
442 ///
443 /// A vector of all attachment envelopes
444 ///
445 /// # Errors
446 ///
447 /// Returns an error if any of the envelope's attachments are invalid
448 pub fn attachments(&self) -> Result<Vec<Self>> {
449 self.attachments_with_vendor_and_conforms_to(None::<&str>, None::<&str>)
450 }
451
452 /// Validates that an envelope is a proper attachment envelope.
453 ///
454 /// This ensures the envelope is an assertion envelope with the predicate
455 /// `attachment` and the required structure for an attachment.
456 ///
457 /// # Returns
458 ///
459 /// Ok(()) if the envelope is a valid attachment envelope
460 ///
461 /// # Errors
462 ///
463 /// Returns `EnvelopeError::InvalidAttachment` if the envelope is not a
464 /// valid attachment envelope
465 pub fn validate_attachment(&self) -> Result<()> {
466 if let EnvelopeCase::Assertion(assertion) = self.case() {
467 assertion.validate_attachment()?;
468 Ok(())
469 } else {
470 Err(Error::InvalidAttachment)
471 }
472 }
473
474 /// Finds a single attachment matching the given vendor and conformsTo.
475 ///
476 /// This works like `attachments_with_vendor_and_conforms_to` but returns a
477 /// single attachment envelope rather than a vector. It requires that
478 /// exactly one attachment matches the criteria.
479 ///
480 /// # Parameters
481 ///
482 /// * `vendor` - Optional vendor identifier to match
483 /// * `conforms_to` - Optional conformsTo URI to match
484 ///
485 /// # Returns
486 ///
487 /// The matching attachment envelope
488 ///
489 /// # Errors
490 ///
491 /// * Returns `EnvelopeError::NonexistentAttachment` if no attachments match
492 /// * Returns `EnvelopeError::AmbiguousAttachment` if more than one
493 /// attachment matches
494 /// * Returns an error if any of the envelope's attachments are invalid
495 ///
496 /// # Examples
497 ///
498 /// ```
499 /// use bc_envelope::prelude::*;
500 ///
501 /// // Create an envelope with an attachment
502 /// let envelope = Envelope::new("Data").add_attachment(
503 /// "Metadata",
504 /// "com.example",
505 /// Some("https://example.com/format/v1"),
506 /// );
507 ///
508 /// // Find a specific attachment by vendor and format
509 /// let attachment = envelope
510 /// .attachment_with_vendor_and_conforms_to(
511 /// Some("com.example"),
512 /// Some("https://example.com/format/v1"),
513 /// )
514 /// .unwrap();
515 ///
516 /// // Access the attachment payload
517 /// let payload = attachment.attachment_payload().unwrap();
518 /// assert_eq!(payload.extract_subject::<String>().unwrap(), "Metadata");
519 /// ```
520 pub fn attachment_with_vendor_and_conforms_to(
521 &self,
522 vendor: Option<&str>,
523 conforms_to: Option<&str>,
524 ) -> Result<Self> {
525 let attachments =
526 self.attachments_with_vendor_and_conforms_to(vendor, conforms_to)?;
527 if attachments.is_empty() {
528 return Err(Error::NonexistentAttachment);
529 }
530 if attachments.len() > 1 {
531 return Err(Error::AmbiguousAttachment);
532 }
533 Ok(attachments.first().unwrap().clone())
534 }
535}