use bc_components::{ARID, tags};
use dcbor::{Date, prelude::*};
use crate::{
Envelope, EnvelopeEncodable, Error, Expression, ExpressionBehavior,
Function, Parameter, Result, known_values,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Request {
body: Expression,
id: ARID,
note: String,
date: Option<Date>,
}
impl std::fmt::Display for Request {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Request({})", self.summary())
}
}
impl Request {
pub fn summary(&self) -> String {
format!(
"id: {}, body: {}",
self.id.short_description(),
self.body.expression_envelope().format_flat()
)
}
}
pub trait RequestBehavior: ExpressionBehavior {
fn with_note(self, note: impl Into<String>) -> Self;
fn with_date(self, date: Date) -> Self;
fn body(&self) -> &Expression;
fn id(&self) -> ARID;
fn note(&self) -> &str;
fn date(&self) -> Option<Date>;
}
impl Request {
pub fn new_with_body(body: Expression, id: ARID) -> Self {
Self { body, id, note: String::new(), date: None }
}
pub fn new(function: impl Into<Function>, id: ARID) -> Self {
Self::new_with_body(Expression::new(function), id)
}
}
impl ExpressionBehavior for Request {
fn with_parameter(
mut self,
parameter: impl Into<Parameter>,
value: impl EnvelopeEncodable,
) -> Self {
self.body = self.body.with_parameter(parameter, value);
self
}
fn with_optional_parameter(
mut self,
parameter: impl Into<Parameter>,
value: Option<impl EnvelopeEncodable>,
) -> Self {
self.body = self.body.with_optional_parameter(parameter, value);
self
}
fn function(&self) -> &Function { self.body.function() }
fn expression_envelope(&self) -> &Envelope {
self.body.expression_envelope()
}
fn object_for_parameter(
&self,
param: impl Into<Parameter>,
) -> Result<Envelope> {
self.body.object_for_parameter(param)
}
fn objects_for_parameter(
&self,
param: impl Into<Parameter>,
) -> Vec<Envelope> {
self.body.objects_for_parameter(param)
}
fn extract_object_for_parameter<T>(
&self,
param: impl Into<Parameter>,
) -> Result<T>
where
T: TryFrom<CBOR, Error = dcbor::Error> + 'static,
{
self.body.extract_object_for_parameter(param)
}
fn extract_optional_object_for_parameter<
T: TryFrom<CBOR, Error = dcbor::Error> + 'static,
>(
&self,
param: impl Into<Parameter>,
) -> Result<Option<T>> {
self.body.extract_optional_object_for_parameter(param)
}
fn extract_objects_for_parameter<T>(
&self,
param: impl Into<Parameter>,
) -> Result<Vec<T>>
where
T: TryFrom<CBOR, Error = dcbor::Error> + 'static,
{
self.body.extract_objects_for_parameter(param)
}
}
impl RequestBehavior for Request {
fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = note.into();
self
}
fn with_date(mut self, date: Date) -> Self {
self.date = Some(date);
self
}
fn body(&self) -> &Expression { &self.body }
fn id(&self) -> ARID { self.id }
fn note(&self) -> &str { &self.note }
fn date(&self) -> Option<Date> { self.date }
}
impl From<Request> for Expression {
fn from(request: Request) -> Self { request.body }
}
impl From<Request> for Envelope {
fn from(request: Request) -> Self {
Envelope::new(CBOR::to_tagged_value(tags::TAG_REQUEST, request.id))
.add_assertion(known_values::BODY, request.body.into_envelope())
.add_assertion_if(
!request.note.is_empty(),
known_values::NOTE,
request.note,
)
.add_optional_assertion(known_values::DATE, request.date)
}
}
impl TryFrom<(Envelope, Option<&Function>)> for Request {
type Error = Error;
fn try_from(
(envelope, expected_function): (Envelope, Option<&Function>),
) -> Result<Self> {
let body_envelope =
envelope.object_for_predicate(known_values::BODY)?;
Ok(Self {
body: Expression::try_from((body_envelope, expected_function))?,
id: envelope
.subject()
.try_leaf()?
.try_into_expected_tagged_value(tags::TAG_REQUEST)?
.try_into()?,
note: envelope.extract_object_for_predicate_with_default(
known_values::NOTE,
"".to_string(),
)?,
date: envelope
.extract_optional_object_for_predicate(known_values::DATE)?,
})
}
}
impl TryFrom<Envelope> for Request {
type Error = Error;
fn try_from(envelope: Envelope) -> Result<Self> {
Self::try_from((envelope, None))
}
}
#[cfg(test)]
mod tests {
use hex_literal::hex;
use indoc::indoc;
use super::*;
fn request_id() -> ARID {
ARID::from_data(hex!(
"c66be27dbad7cd095ca77647406d07976dc0f35f0d4d654bb0e96dd227a1e9fc"
))
}
#[test]
fn test_basic_request() -> Result<()> {
crate::register_tags();
let request = Request::new("test", request_id())
.with_parameter("param1", 42)
.with_parameter("param2", "hello");
let envelope: Envelope = request.clone().into();
#[rustfmt::skip]
let expected = indoc!{r#"
request(ARID(c66be27d)) [
'body': «"test"» [
❰"param1"❱: 42
❰"param2"❱: "hello"
]
]
"#}.trim();
assert_eq!(envelope.format(), expected);
let parsed_request = Request::try_from(envelope)?;
assert_eq!(
parsed_request.extract_object_for_parameter::<i32>("param1")?,
42
);
assert_eq!(
parsed_request.extract_object_for_parameter::<String>("param2")?,
"hello"
);
assert_eq!(parsed_request.note(), "");
assert_eq!(parsed_request.date(), None);
assert_eq!(request, parsed_request);
Ok(())
}
#[test]
fn test_request_with_metadata() -> Result<()> {
crate::register_tags();
let request_date = Date::try_from("2024-07-04T11:11:11Z")?;
let request = Request::new("test", request_id())
.with_parameter("param1", 42)
.with_parameter("param2", "hello")
.with_note("This is a test")
.with_date(request_date);
let envelope: Envelope = request.clone().into();
#[rustfmt::skip]
assert_eq!(envelope.format(), indoc!{r#"
request(ARID(c66be27d)) [
'body': «"test"» [
❰"param1"❱: 42
❰"param2"❱: "hello"
]
'date': 2024-07-04T11:11:11Z
'note': "This is a test"
]
"#}.trim());
let parsed_request = Request::try_from(envelope)?;
assert_eq!(
parsed_request.extract_object_for_parameter::<i32>("param1")?,
42
);
assert_eq!(
parsed_request.extract_object_for_parameter::<String>("param2")?,
"hello"
);
assert_eq!(parsed_request.note(), "This is a test");
assert_eq!(parsed_request.date(), Some(request_date));
assert_eq!(request, parsed_request);
Ok(())
}
#[test]
fn test_parameter_format() {
crate::register_tags();
let parameter = Parameter::new_named("testParam");
let envelope = parameter.into_envelope();
let expected = r#"❰"testParam"❱"#;
assert_eq!(envelope.format(), expected);
}
}