bc_envelope/extension/expressions/
request.rs

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