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