bc_envelope/extension/
types.rs

1use anyhow::{Result, bail};
2use bc_components::DigestProvider;
3
4use crate::{Envelope, EnvelopeEncodable, Error, KnownValue, known_values};
5
6/// # Type System for Gordian Envelopes
7///
8/// This module provides functionality for adding, querying, and verifying types
9/// within envelopes. In Gordian Envelope, types are implemented using the
10/// special `'isA'` Known Value predicate, which is semantically equivalent to
11/// the RDF `rdf:type` concept.
12///
13/// Type information enables:
14/// - Semantic classification of envelopes
15/// - Type verification before processing content
16/// - Conversion between domain objects and envelopes
17/// - Schema validation
18///
19/// ## Type Representation
20///
21/// Types are represented as assertions with the `'isA'` predicate (known value
22/// 1) and an object that specifies the type. The type object is typically
23///    either:
24///
25/// 1. A Known Value from the registry (e.g., `known_values::SEED_TYPE`)
26/// 2. A custom type represented as an envelope
27///
28/// ## Usage Patterns
29///
30/// The type system is commonly used in two ways:
31///
32/// 1. **Type Tagging**: Adding type information to envelopes to indicate their
33///    semantic meaning
34///    ```rust
35///    use bc_envelope::prelude::*;
36///
37///    // Create an envelope representing a person
38///    let person = Envelope::new("Alice")
39///        .add_type("Person")
40///        .add_assertion("age", 30);
41///    ```
42///
43/// 2. **Type Checking**: Verifying that an envelope has the expected type
44///    before processing
45///    ```no_run
46///    use bc_envelope::prelude::*;
47///    use anyhow::Result;
48///
49///    fn process_person(envelope: &Envelope) -> Result<()> {
50///        // Verify this is a person before processing
51///        envelope.check_type_envelope("Person")?;
52///
53///        // Now we can safely extract person-specific information
54///        let name: String = envelope.subject().try_into()?;
55///        let age = envelope.extract_object_for_predicate::<u8>("age")?;
56///
57///        println!("{} is {} years old", name, age);
58///        Ok(())
59///    }
60///    ```
61///
62/// ## Domain Object Conversion
63///
64/// The type system also enables conversion between domain objects and
65/// envelopes. The pattern typically involves:
66///
67/// 1. Implementing `From<DomainObject> for Envelope` to convert objects to
68///    envelopes
69/// 2. Implementing `TryFrom<Envelope> for DomainObject` to convert envelopes
70///    back to objects
71/// 3. Using `check_type()` in the TryFrom implementation to verify the envelope
72///    has the correct type
73///
74/// See the `test_seed.rs` file in the tests directory for an example of this
75/// pattern.
76impl Envelope {
77    /// Adds a type assertion to the envelope using the `'isA'` predicate.
78    ///
79    /// This method provides a convenient way to declare the type of an envelope
80    /// using the standard `'isA'` predicate (known value 1). The type can be
81    /// any value that can be converted to an envelope, typically a string
82    /// or a Known Value from the registry.
83    ///
84    /// # Parameters
85    /// - `object`: The type to assign to this envelope
86    ///
87    /// # Returns
88    /// A new envelope with the type assertion added
89    ///
90    /// # Examples
91    ///
92    /// Using a string type:
93    ///
94    /// ```
95    /// use bc_envelope::prelude::*;
96    ///
97    /// // Create a document and declare its type
98    /// let document = Envelope::new("Important Content").add_type("Document");
99    ///
100    /// // Verify the type was added
101    /// assert!(document.has_type_envelope("Document"));
102    /// ```
103    ///
104    /// Using a predefined Known Value type:
105    ///
106    /// ```
107    /// use bc_envelope::prelude::*;
108    ///
109    /// // Create a seed envelope with the standard SEED_TYPE
110    /// let seed_data = "seed data".to_string();
111    /// let seed = Envelope::new(seed_data).add_type(known_values::SEED_TYPE);
112    ///
113    /// // Verify the type was added
114    /// assert!(seed.has_type(&known_values::SEED_TYPE));
115    /// ```
116    pub fn add_type(&self, object: impl EnvelopeEncodable) -> Self {
117        self.add_assertion(known_values::IS_A, object)
118    }
119
120    /// Returns all type objects from the envelope's `'isA'` assertions.
121    ///
122    /// This method retrieves all objects of assertions that use the `'isA'`
123    /// predicate. Each returned envelope represents a type that has been
124    /// assigned to this envelope.
125    ///
126    /// # Returns
127    /// A vector of envelopes, each representing a type assigned to this
128    /// envelope
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use std::convert::TryInto;
134    ///
135    /// use bc_envelope::prelude::*;
136    ///
137    /// // Create an envelope with multiple types
138    /// let multi_typed = Envelope::new("Versatile Entity")
139    ///     .add_type("Person")
140    ///     .add_type("Employee")
141    ///     .add_type("Manager");
142    ///
143    /// // Get all the type objects
144    /// let types = multi_typed.types();
145    ///
146    /// // There should be 3 types
147    /// assert_eq!(types.len(), 3);
148    ///
149    /// // For each type, we could verify its presence directly using digests
150    /// let person_type = Envelope::new("Person");
151    /// let employee_type = Envelope::new("Employee");
152    /// let manager_type = Envelope::new("Manager");
153    ///
154    /// let has_person = types.iter().any(|e| e.digest() == person_type.digest());
155    /// let has_employee =
156    ///     types.iter().any(|e| e.digest() == employee_type.digest());
157    /// let has_manager = types.iter().any(|e| e.digest() == manager_type.digest());
158    ///
159    /// assert!(has_person);
160    /// assert!(has_employee);
161    /// assert!(has_manager);
162    /// ```
163    pub fn types(&self) -> Vec<Self> {
164        self.objects_for_predicate(known_values::IS_A)
165    }
166
167    /// Gets a single type object from the envelope's `'isA'` assertions.
168    ///
169    /// This method is useful when an envelope is expected to have exactly one
170    /// type. It returns an error if the envelope has zero or multiple
171    /// types.
172    ///
173    /// # Returns
174    /// - `Ok(Envelope)`: The single type object if exactly one exists
175    /// - `Err(EnvelopeError::AmbiguousType)`: If multiple types exist
176    ///
177    /// # Examples
178    ///
179    /// With a single type:
180    ///
181    /// ```
182    /// use bc_envelope::prelude::*;
183    ///
184    /// // Create an envelope with a single type
185    /// let person = Envelope::new("Alice").add_type("Person");
186    ///
187    /// // Get the type
188    /// let type_obj = person.get_type().unwrap();
189    /// let type_string: String = type_obj.try_into().unwrap();
190    /// assert_eq!(type_string, "Person");
191    /// ```
192    ///
193    /// With multiple types (results in error):
194    ///
195    /// ```
196    /// use bc_envelope::prelude::*;
197    ///
198    /// // Create an envelope with multiple types
199    /// let multi_typed = Envelope::new("Alice")
200    ///     .add_type("Person")
201    ///     .add_type("Employee");
202    ///
203    /// // Trying to get a single type will fail
204    /// let result = multi_typed.get_type();
205    /// assert!(result.is_err());
206    /// ```
207    pub fn get_type(&self) -> Result<Self> {
208        let t = self.types();
209        if t.len() == 1 {
210            Ok(t[0].clone())
211        } else {
212            bail!(Error::AmbiguousType)
213        }
214    }
215
216    /// Checks if the envelope has a specific type, using an envelope as the
217    /// type identifier.
218    ///
219    /// This method compares the digest of each type object with the digest of
220    /// the provided envelope to determine if the envelope has the specified
221    /// type.
222    ///
223    /// # Parameters
224    /// - `t`: The type to check for, which will be converted to an envelope
225    ///
226    /// # Returns
227    /// `true` if the envelope has the specified type, `false` otherwise
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use bc_envelope::prelude::*;
233    ///
234    /// // Create a typed envelope
235    /// let document = Envelope::new("Contract")
236    ///     .add_type("LegalDocument")
237    ///     .add_assertion("status", "Draft");
238    ///
239    /// // Check for various types
240    /// assert!(document.has_type_envelope("LegalDocument"));
241    /// assert!(!document.has_type_envelope("Spreadsheet"));
242    ///
243    /// // Can also check with an envelope
244    /// let legal_doc_type = Envelope::new("LegalDocument");
245    /// assert!(document.has_type_envelope(legal_doc_type));
246    /// ```
247    pub fn has_type_envelope(&self, t: impl EnvelopeEncodable) -> bool {
248        let e = t.into_envelope();
249        self.types().iter().any(|x| x.digest() == e.digest())
250    }
251
252    /// Checks if the envelope has a specific type, using a Known Value as the
253    /// type identifier.
254    ///
255    /// Similar to `has_type_envelope`, but specifically for checking against
256    /// standard Known Value types from the registry.
257    ///
258    /// # Parameters
259    /// - `t`: The Known Value type to check for
260    ///
261    /// # Returns
262    /// `true` if the envelope has the specified Known Value type, `false`
263    /// otherwise
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use bc_envelope::prelude::*;
269    ///
270    /// // Create a seed envelope
271    /// let seed_data = "seed data".to_string();
272    /// let seed = Envelope::new(seed_data).add_type(known_values::SEED_TYPE);
273    ///
274    /// // Check the type using the Known Value
275    /// assert!(seed.has_type(&known_values::SEED_TYPE));
276    /// assert!(!seed.has_type(&known_values::PRIVATE_KEY_TYPE));
277    /// ```
278    pub fn has_type(&self, t: &KnownValue) -> bool {
279        let type_envelope: Envelope = t.clone().to_envelope();
280        self.types()
281            .iter()
282            .any(|x| x.digest() == type_envelope.digest())
283    }
284
285    /// Verifies that the envelope has a specific Known Value type.
286    ///
287    /// This method is similar to `has_type` but returns a Result, making it
288    /// suitable for use in validation chains with the `?` operator.
289    ///
290    /// # Parameters
291    /// - `t`: The Known Value type to check for
292    ///
293    /// # Returns
294    /// - `Ok(())`: If the envelope has the specified type
295    /// - `Err(EnvelopeError::InvalidType)`: If the envelope does not have the
296    ///   specified type
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use anyhow::Result;
302    /// use bc_envelope::prelude::*;
303    ///
304    /// // Function that processes a seed envelope
305    /// fn process_seed(envelope: &Envelope) -> Result<String> {
306    ///     // Verify this is a seed
307    ///     envelope.check_type(&known_values::SEED_TYPE)?;
308    ///
309    ///     // Extract the seed data
310    ///     let seed_data: String = envelope.subject().try_into()?;
311    ///     Ok(seed_data)
312    /// }
313    ///
314    /// // Create a seed envelope
315    /// let seed_data = "seed data".to_string();
316    /// let seed =
317    ///     Envelope::new(seed_data.clone()).add_type(known_values::SEED_TYPE);
318    ///
319    /// // Process the seed
320    /// let result = process_seed(&seed);
321    /// assert!(result.is_ok());
322    /// assert_eq!(result.unwrap(), seed_data);
323    ///
324    /// // Create a non-seed envelope
325    /// let not_a_seed = Envelope::new("Not a seed").add_type("SomethingElse");
326    ///
327    /// // Processing should fail
328    /// let result = process_seed(&not_a_seed);
329    /// assert!(result.is_err());
330    /// ```
331    pub fn check_type(&self, t: &KnownValue) -> Result<()> {
332        if self.has_type(t) {
333            Ok(())
334        } else {
335            bail!(Error::InvalidType)
336        }
337    }
338
339    /// Verifies that the envelope has a specific type, using an envelope as the
340    /// type identifier.
341    ///
342    /// This method is similar to `has_type_envelope` but returns a Result,
343    /// making it suitable for use in validation chains with the `?`
344    /// operator.
345    ///
346    /// # Parameters
347    /// - `t`: The type to check for, which will be converted to an envelope
348    ///
349    /// # Returns
350    /// - `Ok(())`: If the envelope has the specified type
351    /// - `Err(EnvelopeError::InvalidType)`: If the envelope does not have the
352    ///   specified type
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use anyhow::Result;
358    /// use bc_envelope::prelude::*;
359    ///
360    /// // Function that processes a person
361    /// fn process_person(envelope: &Envelope) -> Result<String> {
362    ///     // Verify this is a person
363    ///     envelope.check_type_envelope("Person")?;
364    ///
365    ///     // Extract the name
366    ///     let name: String = envelope.subject().try_into()?;
367    ///     Ok(name)
368    /// }
369    ///
370    /// // Create a person envelope
371    /// let person = Envelope::new("Alice").add_type("Person");
372    ///
373    /// // Process the person
374    /// let result = process_person(&person);
375    /// assert!(result.is_ok());
376    /// assert_eq!(result.unwrap(), "Alice");
377    ///
378    /// // Create a non-person envelope
379    /// let document = Envelope::new("Contract").add_type("Document");
380    ///
381    /// // Processing should fail
382    /// let result = process_person(&document);
383    /// assert!(result.is_err());
384    /// ```
385    pub fn check_type_envelope(&self, t: impl EnvelopeEncodable) -> Result<()> {
386        if self.has_type_envelope(t) {
387            Ok(())
388        } else {
389            bail!(Error::InvalidType)
390        }
391    }
392}