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}