use serde::Serialize;
use trusty_common::uds::server::RpcError;
pub use crate::daemon::error::{
CODE_CONFLICT, CODE_FORBIDDEN, CODE_NOT_FOUND, CODE_PANE_GONE, CODE_UNPROCESSABLE,
CODE_WORKSPACE_GONE,
};
#[derive(Debug, Clone, PartialEq)]
pub enum RouteBody {
Json(serde_json::Value),
Text(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct RouteOutcome {
pub status: u16,
pub body: RouteBody,
pub rpc_code: Option<i64>,
}
impl RouteOutcome {
pub fn ok(value: &impl Serialize) -> Self {
Self::json(200, value)
}
pub fn created(value: &impl Serialize) -> Self {
Self::json(201, value)
}
pub fn json(status: u16, value: &impl Serialize) -> Self {
match serde_json::to_value(value) {
Ok(v) => Self {
status,
body: RouteBody::Json(v),
rpc_code: None,
},
Err(e) => Self::text(500, format!("serialize response: {e}")),
}
}
pub fn text(status: u16, message: impl Into<String>) -> Self {
Self {
status,
body: RouteBody::Text(message.into()),
rpc_code: None,
}
}
pub fn with_rpc_code(mut self, code: i64) -> Self {
self.rpc_code = Some(code);
self
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn into_rpc(self) -> Result<serde_json::Value, RpcError> {
let success = self.is_success();
let code = self
.rpc_code
.unwrap_or_else(|| status_to_rpc_code(self.status));
match (success, self.body) {
(true, RouteBody::Json(v)) => Ok(v),
(true, RouteBody::Text(s)) => Ok(serde_json::Value::String(s)),
(false, RouteBody::Text(s)) => Err(RpcError::new(code, s)),
(false, RouteBody::Json(v)) => Err(RpcError::new(code, v.to_string())),
}
}
}
pub use crate::daemon::error::rpc_code_for_status as status_to_rpc_code;
#[cfg(test)]
mod outcome_tests {
use trusty_common::uds::server::{CODE_INTERNAL_ERROR, CODE_INVALID_PARAMS};
use crate::daemon::error::CODE_UPSTREAM_FAILED;
use super::*;
#[test]
fn success_projects_the_body_verbatim() {
let outcome = RouteOutcome::ok(&serde_json::json!({"id": "abc", "n": 3}));
let value = outcome.into_rpc().expect("2xx is Ok");
assert_eq!(value, serde_json::json!({"id": "abc", "n": 3}));
}
#[test]
fn created_is_a_success_like_ok() {
let outcome = RouteOutcome::created(&serde_json::json!({"id": "abc"}));
assert!(outcome.is_success());
assert!(outcome.into_rpc().is_ok());
}
#[test]
fn refusal_keeps_the_http_message_and_maps_the_status() {
let outcome = RouteOutcome::text(404, "session zzz not found");
let err = outcome.into_rpc().expect_err("4xx is Err");
assert_eq!(err.code, CODE_NOT_FOUND);
assert_eq!(
err.message, "session zzz not found",
"the socket must carry the HTTP body verbatim, not a paraphrase"
);
}
#[test]
fn status_to_rpc_code_maps_each_refusal_class() {
assert_eq!(status_to_rpc_code(400), CODE_INVALID_PARAMS);
assert_eq!(status_to_rpc_code(403), CODE_FORBIDDEN);
assert_eq!(status_to_rpc_code(404), CODE_NOT_FOUND);
assert_eq!(status_to_rpc_code(409), CODE_CONFLICT);
assert_eq!(status_to_rpc_code(422), CODE_UNPROCESSABLE);
assert_eq!(status_to_rpc_code(500), CODE_INTERNAL_ERROR);
assert_eq!(status_to_rpc_code(502), CODE_UPSTREAM_FAILED);
}
#[test]
fn an_rpc_code_override_replaces_the_status_projection() {
let err = RouteOutcome::text(422, "workspace /gone no longer exists")
.with_rpc_code(CODE_WORKSPACE_GONE)
.into_rpc()
.expect_err("422 is Err");
assert_eq!(err.code, CODE_WORKSPACE_GONE);
assert_eq!(err.message, "workspace /gone no longer exists");
let pane = RouteOutcome::text(422, "pane %42 no longer exists")
.with_rpc_code(CODE_PANE_GONE)
.into_rpc()
.expect_err("422 is Err");
assert_ne!(
pane.code, err.code,
"the two 422 classes must stay distinguishable without a header"
);
}
#[test]
fn an_unserializable_body_is_a_500_not_a_panic() {
let bad: std::collections::HashMap<(u8, u8), u8> =
std::collections::HashMap::from([((1, 2), 3)]);
let outcome = RouteOutcome::ok(&bad);
assert_eq!(outcome.status, 500);
assert!(outcome.into_rpc().is_err());
}
}