use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::{Value, json};
pub(crate) const MAX_ID_LEN: usize = 128;
#[derive(Debug)]
pub(crate) enum ActionVerdict {
Succeeded { id: String, detail: Value },
Refused {
id: String,
reason: String,
detail: Value,
},
Unreachable { id: String, reason: String },
Invalid { id: String, reason: String },
}
impl ActionVerdict {
pub(crate) fn status(&self) -> StatusCode {
match self {
Self::Succeeded { .. } => StatusCode::OK,
Self::Refused { .. } => StatusCode::CONFLICT,
Self::Unreachable { .. } => StatusCode::SERVICE_UNAVAILABLE,
Self::Invalid { .. } => StatusCode::BAD_REQUEST,
}
}
pub(crate) fn succeeded(&self) -> bool {
matches!(self, Self::Succeeded { .. })
}
pub(crate) fn reason(&self) -> &str {
match self {
Self::Succeeded { .. } => "",
Self::Refused { reason, .. }
| Self::Unreachable { reason, .. }
| Self::Invalid { reason, .. } => reason,
}
}
pub(crate) fn id(&self) -> &str {
match self {
Self::Succeeded { id, .. }
| Self::Refused { id, .. }
| Self::Unreachable { id, .. }
| Self::Invalid { id, .. } => id,
}
}
}
impl IntoResponse for ActionVerdict {
fn into_response(self) -> Response {
let status = self.status();
let body = match self {
Self::Succeeded { id, detail } => json!({ "ok": true, "id": id, "detail": detail }),
Self::Refused { id, reason, detail } => {
json!({ "ok": false, "id": id, "error": reason, "detail": detail })
}
Self::Unreachable { id, reason } | Self::Invalid { id, reason } => {
json!({ "ok": false, "id": id, "error": reason })
}
};
(status, axum::Json(body)).into_response()
}
}
pub(crate) fn validate_id(id: &str) -> Result<(), String> {
if id.is_empty() {
return Err("the resource id is empty".to_string());
}
if id.len() > MAX_ID_LEN {
return Err(format!(
"the resource id is longer than the {MAX_ID_LEN}-byte limit"
));
}
if id.contains("..") {
return Err("the resource id contains '..'".to_string());
}
if let Some(bad) = id
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')))
{
return Err(format!(
"the resource id contains {bad:?}; only letters, digits, '.', '_' and '-' are accepted"
));
}
Ok(())
}
pub(crate) fn first_line(body: &str) -> String {
const MAX: usize = 300;
let line = body.lines().next().unwrap_or("").trim();
if line.len() <= MAX {
return line.to_string();
}
let cut = line
.char_indices()
.map(|(i, _)| i)
.take_while(|i| *i <= MAX)
.last()
.unwrap_or(0);
format!("{}…", &line[..cut])
}