bc_envelope/extension/expressions/
request.rs

1use anyhow::{ Error, Result };
2use bc_components::{ tags, ARID };
3use dcbor::prelude::*;
4use dcbor::Date;
5
6use crate::{
7    known_values,
8    Envelope,
9    EnvelopeEncodable,
10    Expression,
11    ExpressionBehavior,
12    Function,
13    Parameter,
14};
15
16/// A `Request` represents a message requesting execution of a function with parameters.
17///
18/// Requests are part of the expression system that enables distributed function calls
19/// and communication between systems. Each request:
20/// - Contains a body (an `Expression`) that represents the function to be executed
21/// - Has a unique identifier (ARID) for tracking and correlation
22/// - May include optional metadata like a note and timestamp
23///
24/// Requests are designed to be paired with `Response` objects that contain the results
25/// of executing the requested function.
26///
27/// When serialized to an envelope, requests are tagged with `#6.40010` (TAG_REQUEST).
28///
29/// # Examples
30///
31/// ```
32/// use bc_envelope::prelude::*;
33/// use bc_components::ARID;
34///
35/// // Create a random request ID
36/// let request_id = ARID::new();
37///
38/// // Create a request to execute a function with parameters
39/// let request = Request::new("getBalance", request_id)
40///     .with_parameter("account", "alice")
41///     .with_parameter("currency", "USD")
42///     .with_note("Monthly balance check");
43///
44/// // Convert to an envelope
45/// let envelope = request.into_envelope();
46/// ```
47#[derive(Debug, Clone, PartialEq)]
48pub struct Request {
49    body: Expression,
50    id: ARID,
51    note: String,
52    date: Option<Date>,
53}
54
55impl std::fmt::Display for Request {
56    /// Formats the request for display, showing its ID and body.
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "Request({})", self.summary())
59    }
60}
61
62impl Request {
63    /// Returns a human-readable summary of the request.
64    pub fn summary(&self) -> String {
65        format!(
66            "id: {}, body: {}",
67            self.id.short_description(),
68            self.body.expression_envelope().format_flat()
69        )
70    }
71}
72
73/// Trait that defines the behavior of a request.
74///
75/// This trait extends `ExpressionBehavior` to add methods specific to requests,
76/// including metadata management and access to request properties. Types implementing
77/// this trait can be used in contexts that expect request functionality.
78pub trait RequestBehavior: ExpressionBehavior {
79    //
80    // Composition
81    //
82
83    /// Adds a note to the request.
84    ///
85    /// This provides human-readable context about the request's purpose.
86    fn with_note(self, note: impl Into<String>) -> Self;
87
88    /// Adds a date to the request.
89    ///
90    /// This timestamp typically represents when the request was created.
91    fn with_date(self, date: impl AsRef<Date>) -> Self;
92
93    //
94    // Parsing
95    //
96
97    /// Returns the body of the request, which is the expression to be evaluated.
98    fn body(&self) -> &Expression;
99
100    /// Returns the unique identifier (ARID) of the request.
101    fn id(&self) -> ARID;
102
103    /// Returns the note attached to the request, or an empty string if none exists.
104    fn note(&self) -> &str;
105
106    /// Returns the date attached to the request, if any.
107    fn date(&self) -> Option<&Date>;
108}
109
110impl Request {
111    /// Creates a new request with the specified expression body and ID.
112    ///
113    /// # Arguments
114    ///
115    /// * `body` - The expression to be executed
116    /// * `id` - Unique identifier for the request
117    pub fn new_with_body(body: Expression, id: ARID) -> Self {
118        Self {
119            body,
120            id,
121            note: String::new(),
122            date: None,
123        }
124    }
125
126    /// Creates a new request with a function and ID.
127    ///
128    /// This is a convenience method that creates an expression from the function
129    /// and then creates a request with that expression.
130    ///
131    /// # Arguments
132    ///
133    /// * `function` - The function to be executed
134    /// * `id` - Unique identifier for the request
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use bc_envelope::prelude::*;
140    /// use bc_components::ARID;
141    ///
142    /// let request_id = ARID::new();
143    /// let request = Request::new("transferFunds", request_id)
144    ///     .with_parameter("from", "alice")
145    ///     .with_parameter("to", "bob")
146    ///     .with_parameter("amount", 100);
147    /// ```
148    pub fn new(function: impl Into<Function>, id: ARID) -> Self {
149        Self::new_with_body(Expression::new(function), id)
150    }
151}
152
153/// Implementation of `ExpressionBehavior` for `Request`.
154///
155/// This delegates most operations to the request's body expression.
156impl ExpressionBehavior for Request {
157    /// Adds a parameter to the request.
158    fn with_parameter(
159        mut self,
160        parameter: impl Into<Parameter>,
161        value: impl EnvelopeEncodable
162    ) -> Self {
163        self.body = self.body.with_parameter(parameter, value);
164        self
165    }
166
167    /// Adds an optional parameter to the request.
168    fn with_optional_parameter(
169        mut self,
170        parameter: impl Into<Parameter>,
171        value: Option<impl EnvelopeEncodable>
172    ) -> Self {
173        self.body = self.body.with_optional_parameter(parameter, value);
174        self
175    }
176
177    /// Returns the function of the request.
178    fn function(&self) -> &Function {
179        self.body.function()
180    }
181
182    /// Returns the expression envelope of the request.
183    fn expression_envelope(&self) -> &Envelope {
184        self.body.expression_envelope()
185    }
186
187    /// Returns the object for a parameter in the request.
188    fn object_for_parameter(&self, param: impl Into<Parameter>) -> Result<Envelope> {
189        self.body.object_for_parameter(param)
190    }
191
192    /// Returns all objects for a parameter in the request.
193    fn objects_for_parameter(&self, param: impl Into<Parameter>) -> Vec<Envelope> {
194        self.body.objects_for_parameter(param)
195    }
196
197    /// Extracts a typed object for a parameter in the request.
198    fn extract_object_for_parameter<T>(&self, param: impl Into<Parameter>) -> Result<T>
199        where T: TryFrom<CBOR, Error = dcbor::Error> + 'static
200    {
201        self.body.extract_object_for_parameter(param)
202    }
203
204    /// Extracts an optional typed object for a parameter in the request.
205    fn extract_optional_object_for_parameter<T: TryFrom<CBOR, Error = dcbor::Error> + 'static>(
206        &self,
207        param: impl Into<Parameter>
208    ) -> Result<Option<T>> {
209        self.body.extract_optional_object_for_parameter(param)
210    }
211
212    /// Extracts multiple typed objects for a parameter in the request.
213    fn extract_objects_for_parameter<T>(&self, param: impl Into<Parameter>) -> Result<Vec<T>>
214        where T: TryFrom<CBOR, Error = dcbor::Error> + 'static
215    {
216        self.body.extract_objects_for_parameter(param)
217    }
218}
219
220/// Implementation of `RequestBehavior` for `Request`.
221impl RequestBehavior for Request {
222    /// Adds a note to the request.
223    fn with_note(mut self, note: impl Into<String>) -> Self {
224        self.note = note.into();
225        self
226    }
227
228    /// Adds a date to the request.
229    fn with_date(mut self, date: impl AsRef<Date>) -> Self {
230        self.date = Some(date.as_ref().clone());
231        self
232    }
233
234    /// Returns the body of the request.
235    fn body(&self) -> &Expression {
236        &self.body
237    }
238
239    /// Returns the ID of the request.
240    fn id(&self) -> ARID {
241        self.id
242    }
243
244    /// Returns the note of the request.
245    fn note(&self) -> &str {
246        &self.note
247    }
248
249    /// Returns the date of the request.
250    fn date(&self) -> Option<&Date> {
251        self.date.as_ref()
252    }
253}
254
255/// Converts a `Request` to an `Expression`.
256///
257/// This extracts the request's body expression.
258impl From<Request> for Expression {
259    fn from(request: Request) -> Self {
260        request.body
261    }
262}
263
264/// Converts a `Request` to an `Envelope`.
265///
266/// The envelope's subject is the request's ID tagged with TAG_REQUEST,
267/// and assertions include the request's body, note (if not empty), and date (if present).
268impl From<Request> for Envelope {
269    fn from(request: Request) -> Self {
270        Envelope::new(CBOR::to_tagged_value(tags::TAG_REQUEST, request.id))
271            .add_assertion(known_values::BODY, request.body.into_envelope())
272            .add_assertion_if(!request.note.is_empty(), known_values::NOTE, request.note)
273            .add_optional_assertion(known_values::DATE, request.date)
274    }
275}
276
277/// Converts an envelope and optional expected function to a `Request`.
278///
279/// This constructor is used when parsing an envelope that is expected to contain a request.
280/// The optional function parameter enables validation of the request's function.
281impl TryFrom<(Envelope, Option<&Function>)> for Request {
282    type Error = Error;
283
284    fn try_from((envelope, expected_function): (Envelope, Option<&Function>)) -> Result<Self> {
285        let body_envelope = envelope.object_for_predicate(known_values::BODY)?;
286        Ok(Self {
287            body: Expression::try_from((body_envelope, expected_function))?,
288            id: envelope
289                .subject()
290                .try_leaf()?
291                .try_into_expected_tagged_value(tags::TAG_REQUEST)?
292                .try_into()?,
293            note: envelope.extract_object_for_predicate_with_default(
294                known_values::NOTE,
295                "".to_string()
296            )?,
297            date: envelope.extract_optional_object_for_predicate(known_values::DATE)?,
298        })
299    }
300}
301
302/// Converts an envelope to a `Request`.
303///
304/// This simplified constructor doesn't validate the request's function.
305impl TryFrom<Envelope> for Request {
306    type Error = Error;
307
308    fn try_from(envelope: Envelope) -> Result<Self> {
309        Self::try_from((envelope, None))
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use hex_literal::hex;
317    use indoc::indoc;
318
319    fn request_id() -> ARID {
320        ARID::from_data(hex!("c66be27dbad7cd095ca77647406d07976dc0f35f0d4d654bb0e96dd227a1e9fc"))
321    }
322
323    #[test]
324    fn test_basic_request() -> Result<()> {
325        crate::register_tags();
326
327        let request = Request::new("test", request_id())
328            .with_parameter("param1", 42)
329            .with_parameter("param2", "hello");
330
331        let envelope: Envelope = request.clone().into();
332        #[rustfmt::skip]
333        let expected = indoc!{r#"
334            request(ARID(c66be27d)) [
335                'body': «"test"» [
336                    ❰"param1"❱: 42
337                    ❰"param2"❱: "hello"
338                ]
339            ]
340        "#}.trim();
341        assert_eq!(envelope.format(), expected);
342
343        let parsed_request = Request::try_from(envelope)?;
344        assert_eq!(parsed_request.extract_object_for_parameter::<i32>("param1")?, 42);
345        assert_eq!(parsed_request.extract_object_for_parameter::<String>("param2")?, "hello");
346        assert_eq!(parsed_request.note(), "");
347        assert_eq!(parsed_request.date(), None);
348
349        assert_eq!(request, parsed_request);
350
351        Ok(())
352    }
353
354    #[test]
355    fn test_request_with_metadata() -> Result<()> {
356        crate::register_tags();
357
358        let request_date = Date::try_from("2024-07-04T11:11:11Z")?;
359        let request = Request::new("test", request_id())
360            .with_parameter("param1", 42)
361            .with_parameter("param2", "hello")
362            .with_note("This is a test")
363            .with_date(&request_date);
364
365        let envelope: Envelope = request.clone().into();
366        // println!("{}", envelope.format());
367        #[rustfmt::skip]
368        assert_eq!(envelope.format(), indoc!{r#"
369            request(ARID(c66be27d)) [
370                'body': «"test"» [
371                    ❰"param1"❱: 42
372                    ❰"param2"❱: "hello"
373                ]
374                'date': 2024-07-04T11:11:11Z
375                'note': "This is a test"
376            ]
377        "#}.trim());
378
379        let parsed_request = Request::try_from(envelope)?;
380        assert_eq!(parsed_request.extract_object_for_parameter::<i32>("param1")?, 42);
381        assert_eq!(parsed_request.extract_object_for_parameter::<String>("param2")?, "hello");
382        assert_eq!(parsed_request.note(), "This is a test");
383        assert_eq!(parsed_request.date(), Some(&request_date));
384
385        assert_eq!(request, parsed_request);
386
387        Ok(())
388    }
389}