use std::error::Error;
use bytes::Bytes;
use http::{HeaderValue, Response, StatusCode, header};
use serde::Serialize;
use serde_json::{Map, Value};
use super::{HttpError, error::Chain};
use crate::revision_of;
#[cfg(test)]
mod tests;
#[derive(Debug)]
#[must_use]
pub struct CommandAnswer {
revision: String,
fields: Fields,
}
#[derive(Debug)]
enum Fields {
None,
Added(Value),
Unencodable(serde_json::Error),
Incomplete(Box<dyn Error + Send + Sync + 'static>),
}
impl CommandAnswer {
pub fn new(written: &[eventsourcingdb::Event]) -> Self {
Self {
revision: revision_of(written).unwrap_or_default(),
fields: Fields::None,
}
}
#[must_use]
pub fn revision(&self) -> &str {
&self.revision
}
pub fn with(self, fields: impl Serialize) -> Self {
let fields = match serde_json::to_value(fields) {
Ok(value) => Fields::Added(value),
Err(failure) => Fields::Unencodable(failure),
};
Self { fields, ..self }
}
pub fn with_result<F, E>(self, fields: Result<F, E>) -> Self
where
F: Serialize,
E: Into<Box<dyn Error + Send + Sync + 'static>>,
{
match fields {
Ok(fields) => self.with(fields),
Err(failure) => Self {
fields: Fields::Incomplete(failure.into()),
..self
},
}
}
}
impl From<CommandAnswer> for Response<Bytes> {
fn from(answer: CommandAnswer) -> Self {
let mut body = match answer.fields {
Fields::None | Fields::Added(Value::Null) => Map::new(),
Fields::Added(Value::Object(fields)) if fields.contains_key("revision") => {
return HttpError::internal(
"the fields of the answer to a command hold a revision, which the kit adds itself",
)
.into();
}
Fields::Added(Value::Object(fields)) => fields,
Fields::Added(other) => {
return HttpError::internal(format!(
"the fields of the answer to a command have to be a JSON object, not {other}"
))
.into();
}
Fields::Unencodable(failure) => return HttpError::internal(failure).into(),
Fields::Incomplete(failure) => {
log::error!(
"architecturekit: incomplete answer, the command has succeeded, but its fields could not be added: {}",
Chain(Some(&*failure)),
);
Map::new()
}
};
let _ = body.insert("revision".to_owned(), Value::String(answer.revision));
json(StatusCode::OK, &Value::Object(body))
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for CommandAnswer {
fn into_response(self) -> axum::response::Response {
Response::<Bytes>::from(self).map(axum::body::Body::from)
}
}
pub(super) fn json(status: StatusCode, value: &Value) -> Response<Bytes> {
let mut response = Response::new(Bytes::from(format!("{value}\n")));
*response.status_mut() = status;
let headers = response.headers_mut();
let _ = headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let _ = headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}