1use std::collections::BTreeMap;
4use std::fmt;
5
6use lino_objects_codec::LinoValue;
7
8use crate::value::{int, object, string};
9
10pub const PROBLEM_TYPE_BASE: &str = "https://link-foundation.github.io/lino-rest-api/errors/";
12
13pub fn reason_phrase(status: u16) -> &'static str {
15 match status {
16 400 => "Bad Request",
17 401 => "Unauthorized",
18 403 => "Forbidden",
19 404 => "Not Found",
20 405 => "Method Not Allowed",
21 406 => "Not Acceptable",
22 409 => "Conflict",
23 410 => "Gone",
24 412 => "Precondition Failed",
25 413 => "Content Too Large",
26 415 => "Unsupported Media Type",
27 422 => "Unprocessable Content",
28 428 => "Precondition Required",
29 429 => "Too Many Requests",
30 500 => "Internal Server Error",
31 501 => "Not Implemented",
32 502 => "Bad Gateway",
33 503 => "Service Unavailable",
34 504 => "Gateway Timeout",
35 status if status >= 500 => "Server Error",
36 _ => "Request Error",
37 }
38}
39
40pub fn problem_slug(title: &str) -> String {
50 let mut slug = String::new();
51 let mut pending_dash = false;
52 for character in title.to_lowercase().chars() {
53 if character.is_ascii_alphanumeric() {
54 if pending_dash && !slug.is_empty() {
55 slug.push('-');
56 }
57 pending_dash = false;
58 slug.push(character);
59 } else {
60 pending_dash = true;
61 }
62 }
63 slug
64}
65
66#[derive(Debug, Clone, PartialEq)]
71pub struct LinoHttpError {
72 pub status: u16,
74 pub title: String,
76 pub detail: Option<String>,
78 pub type_uri: String,
80 pub instance: Option<String>,
82 pub headers: BTreeMap<String, String>,
84 pub extensions: Vec<(String, LinoValue)>,
86}
87
88impl LinoHttpError {
89 pub fn new(status: u16, detail: Option<&str>) -> Self {
101 let title = reason_phrase(status).to_string();
102 Self {
103 status,
104 type_uri: format!("{PROBLEM_TYPE_BASE}{}", problem_slug(&title)),
105 title,
106 detail: detail.map(str::to_string),
107 instance: None,
108 headers: BTreeMap::new(),
109 extensions: Vec::new(),
110 }
111 }
112
113 pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
115 self.title = title.into();
116 self.type_uri = format!("{PROBLEM_TYPE_BASE}{}", problem_slug(&self.title));
117 self
118 }
119
120 pub fn with_type<S: Into<String>>(mut self, type_uri: S) -> Self {
122 self.type_uri = type_uri.into();
123 self
124 }
125
126 pub fn with_instance<S: Into<String>>(mut self, instance: S) -> Self {
128 self.instance = Some(instance.into());
129 self
130 }
131
132 pub fn with_header<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
134 self.headers.insert(name.into(), value.into());
135 self
136 }
137
138 pub fn with_extension<S: Into<String>>(mut self, key: S, value: LinoValue) -> Self {
140 self.extensions.push((key.into(), value));
141 self
142 }
143
144 pub fn message(&self) -> &str {
146 self.detail.as_deref().unwrap_or(&self.title)
147 }
148
149 pub fn to_problem(&self, instance: Option<&str>) -> LinoValue {
151 let mut problem = object([
152 ("type", string(self.type_uri.clone())),
153 ("title", string(self.title.clone())),
154 ("status", int(i64::from(self.status))),
155 ]);
156 if let LinoValue::Object(members) = &mut problem {
157 if let Some(detail) = &self.detail {
158 members.push(("detail".to_string(), string(detail.clone())));
159 }
160 let resolved = self
161 .instance
162 .as_deref()
163 .filter(|text| !text.is_empty())
164 .or(instance)
165 .filter(|text| !text.is_empty());
166 if let Some(resolved) = resolved {
167 members.push(("instance".to_string(), string(resolved)));
168 }
169 for (key, value) in &self.extensions {
170 members.push((key.clone(), value.clone()));
171 }
172 }
173 problem
174 }
175}
176
177impl fmt::Display for LinoHttpError {
178 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179 formatter.write_str(self.message())
180 }
181}
182
183impl std::error::Error for LinoHttpError {}
184
185pub fn problem_details(status: u16, detail: Option<&str>, instance: Option<&str>) -> LinoValue {
187 LinoHttpError::new(status, detail).to_problem(instance)
188}
189
190pub fn validation_error(errors: Vec<(String, String)>, detail: &str) -> LinoHttpError {
195 let items = errors
196 .into_iter()
197 .map(|(field, message)| object([("field", string(field)), ("message", string(message))]))
198 .collect::<Vec<_>>();
199 LinoHttpError::new(422, Some(detail))
200 .with_title("Unprocessable Content")
201 .with_type(format!("{PROBLEM_TYPE_BASE}validation-failed"))
202 .with_extension("errors", LinoValue::Array(items))
203}