bc_envelope/extension/expressions/
request.rs

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