use std::collections::BTreeMap;
use std::fmt;
use lino_objects_codec::LinoValue;
use crate::value::{int, object, string};
pub const PROBLEM_TYPE_BASE: &str = "https://link-foundation.github.io/lino-rest-api/errors/";
pub fn reason_phrase(status: u16) -> &'static str {
match status {
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
409 => "Conflict",
410 => "Gone",
412 => "Precondition Failed",
413 => "Content Too Large",
415 => "Unsupported Media Type",
422 => "Unprocessable Content",
428 => "Precondition Required",
429 => "Too Many Requests",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
status if status >= 500 => "Server Error",
_ => "Request Error",
}
}
pub fn problem_slug(title: &str) -> String {
let mut slug = String::new();
let mut pending_dash = false;
for character in title.to_lowercase().chars() {
if character.is_ascii_alphanumeric() {
if pending_dash && !slug.is_empty() {
slug.push('-');
}
pending_dash = false;
slug.push(character);
} else {
pending_dash = true;
}
}
slug
}
#[derive(Debug, Clone, PartialEq)]
pub struct LinoHttpError {
pub status: u16,
pub title: String,
pub detail: Option<String>,
pub type_uri: String,
pub instance: Option<String>,
pub headers: BTreeMap<String, String>,
pub extensions: Vec<(String, LinoValue)>,
}
impl LinoHttpError {
pub fn new(status: u16, detail: Option<&str>) -> Self {
let title = reason_phrase(status).to_string();
Self {
status,
type_uri: format!("{PROBLEM_TYPE_BASE}{}", problem_slug(&title)),
title,
detail: detail.map(str::to_string),
instance: None,
headers: BTreeMap::new(),
extensions: Vec::new(),
}
}
pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
self.title = title.into();
self.type_uri = format!("{PROBLEM_TYPE_BASE}{}", problem_slug(&self.title));
self
}
pub fn with_type<S: Into<String>>(mut self, type_uri: S) -> Self {
self.type_uri = type_uri.into();
self
}
pub fn with_instance<S: Into<String>>(mut self, instance: S) -> Self {
self.instance = Some(instance.into());
self
}
pub fn with_header<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn with_extension<S: Into<String>>(mut self, key: S, value: LinoValue) -> Self {
self.extensions.push((key.into(), value));
self
}
pub fn message(&self) -> &str {
self.detail.as_deref().unwrap_or(&self.title)
}
pub fn to_problem(&self, instance: Option<&str>) -> LinoValue {
let mut problem = object([
("type", string(self.type_uri.clone())),
("title", string(self.title.clone())),
("status", int(i64::from(self.status))),
]);
if let LinoValue::Object(members) = &mut problem {
if let Some(detail) = &self.detail {
members.push(("detail".to_string(), string(detail.clone())));
}
let resolved = self
.instance
.as_deref()
.filter(|text| !text.is_empty())
.or(instance)
.filter(|text| !text.is_empty());
if let Some(resolved) = resolved {
members.push(("instance".to_string(), string(resolved)));
}
for (key, value) in &self.extensions {
members.push((key.clone(), value.clone()));
}
}
problem
}
}
impl fmt::Display for LinoHttpError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.message())
}
}
impl std::error::Error for LinoHttpError {}
pub fn problem_details(status: u16, detail: Option<&str>, instance: Option<&str>) -> LinoValue {
LinoHttpError::new(status, detail).to_problem(instance)
}
pub fn validation_error(errors: Vec<(String, String)>, detail: &str) -> LinoHttpError {
let items = errors
.into_iter()
.map(|(field, message)| object([("field", string(field)), ("message", string(message))]))
.collect::<Vec<_>>();
LinoHttpError::new(422, Some(detail))
.with_title("Unprocessable Content")
.with_type(format!("{PROBLEM_TYPE_BASE}validation-failed"))
.with_extension("errors", LinoValue::Array(items))
}