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, revision_of};
#[cfg(test)]
mod tests;
#[derive(Debug)]
#[must_use]
pub struct CommandAnswer {
revision: Option<Revision>,
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),
fields: Fields::None,
}
}
#[must_use]
pub fn revision(&self) -> Option<Revision> {
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 revision = answer
.revision
.map(|revision| revision.to_string())
.unwrap_or_default();
let _ = body.insert("revision".to_owned(), Value::String(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
}