Skip to content

Commit 968a387

Browse files
committed
add error codes models
1 parent 7c197d8 commit 968a387

4 files changed

Lines changed: 300 additions & 2 deletions

File tree

src/libs/error_code.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use serde::*;
2+
use std::str::FromStr;
23

34
/// `ErrorCode` is a wrapper around `u32` that represents an error code.
45
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -185,4 +186,81 @@ impl ErrorCode {
185186
_ => "CustomError",
186187
}
187188
}
189+
190+
pub fn from_name(name: &str) -> Option<Self> {
191+
let normalized: String = name
192+
.chars()
193+
.filter(|c| c.is_ascii_alphanumeric())
194+
.flat_map(char::to_uppercase)
195+
.collect();
196+
197+
match normalized.as_str() {
198+
"BADREQUEST" => Some(Self::BAD_REQUEST),
199+
"UNAUTHORIZED" => Some(Self::UNAUTHORIZED),
200+
"PAYMENTREQUIRED" => Some(Self::PAYMENT_REQUIRED),
201+
"FORBIDDEN" => Some(Self::FORBIDDEN),
202+
"NOTFOUND" => Some(Self::NOT_FOUND),
203+
"METHODNOTALLOWED" => Some(Self::METHOD_NOT_ALLOWED),
204+
"NOTACCEPTABLE" => Some(Self::NOT_ACCEPTABLE),
205+
"PROXYAUTHENTICATIONREQUIRED" => Some(Self::PROXY_AUTHENTICATION_REQUIRED),
206+
"REQUESTTIMEOUT" => Some(Self::REQUEST_TIMEOUT),
207+
"CONFLICT" => Some(Self::CONFLICT),
208+
"GONE" => Some(Self::GONE),
209+
"LENGTHREQUIRED" => Some(Self::LENGTH_REQUIRED),
210+
"PRECONDITIONFAILED" => Some(Self::PRECONDITION_FAILED),
211+
"PAYLOADTOOLARGE" => Some(Self::PAYLOAD_TOO_LARGE),
212+
"URITOOLONG" => Some(Self::URI_TOO_LONG),
213+
"UNSUPPORTEDMEDIATYPE" => Some(Self::UNSUPPORTED_MEDIA_TYPE),
214+
"RANGENOTSATISFIABLE" => Some(Self::RANGE_NOT_SATISFIABLE),
215+
"EXPECTATIONFAILED" => Some(Self::EXPECTATION_FAILED),
216+
"IMATEAPOT" => Some(Self::IM_A_TEAPOT),
217+
"MISDIRECTEDREQUEST" => Some(Self::MISDIRECTED_REQUEST),
218+
"UNPROCESSABLEENTITY" => Some(Self::UNPROCESSABLE_ENTITY),
219+
"LOCKED" => Some(Self::LOCKED),
220+
"FAILEDDEPENDENCY" => Some(Self::FAILED_DEPENDENCY),
221+
"UPGRADEREQUIRED" => Some(Self::UPGRADE_REQUIRED),
222+
"PRECONDITIONREQUIRED" => Some(Self::PRECONDITION_REQUIRED),
223+
"TOOMANYREQUESTS" => Some(Self::TOO_MANY_REQUESTS),
224+
"REQUESTHEADERFIELDSTOOLARGE" => Some(Self::REQUEST_HEADER_FIELDS_TOO_LARGE),
225+
"UNAVAILABLEFORLEGALREASONS" => Some(Self::UNAVAILABLE_FOR_LEGAL_REASONS),
226+
"INTERNALERROR" => Some(Self::INTERNAL_ERROR),
227+
"NOTIMPLEMENTED" => Some(Self::NOT_IMPLEMENTED),
228+
"BADGATEWAY" => Some(Self::BAD_GATEWAY),
229+
"SERVICEUNAVAILABLE" => Some(Self::SERVICE_UNAVAILABLE),
230+
"GATEWAYTIMEOUT" => Some(Self::GATEWAY_TIMEOUT),
231+
"HTTPVERSIONNOTSUPPORTED" => Some(Self::HTTP_VERSION_NOT_SUPPORTED),
232+
"VARIANTALSONEGOTIATES" => Some(Self::VARIANT_ALSO_NEGOTIATES),
233+
"INSUFFICIENTSTORAGE" => Some(Self::INSUFFICIENT_STORAGE),
234+
"LOOPDETECTED" => Some(Self::LOOP_DETECTED),
235+
"NOTEXTENDED" => Some(Self::NOT_EXTENDED),
236+
"NETWORKAUTHENTICATIONREQUIRED" => Some(Self::NETWORK_AUTHENTICATION_REQUIRED),
237+
_ => None,
238+
}
239+
}
240+
}
241+
242+
impl FromStr for ErrorCode {
243+
type Err = ParseErrorCodeError;
244+
245+
fn from_str(value: &str) -> Result<Self, Self::Err> {
246+
if let Some(code) = Self::from_name(value) {
247+
return Ok(code);
248+
}
249+
250+
value
251+
.parse::<u32>()
252+
.map(Self::new)
253+
.map_err(|_| ParseErrorCodeError)
254+
}
188255
}
256+
257+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258+
pub struct ParseErrorCodeError;
259+
260+
impl std::fmt::Display for ParseErrorCodeError {
261+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262+
f.write_str("invalid error code")
263+
}
264+
}
265+
266+
impl std::error::Error for ParseErrorCodeError {}

src/libs/ws/traits.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ pub trait WsUpgrader: Send + Sync {
6868
config: &WsServerConfig,
6969
cached_date: &str,
7070
) -> Result<AsyncRx<Array<UpgradeEvent>>>;
71-
}
71+
}

src/libs/ws/tungstenite.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub mod upgrader;
22

33
pub use upgrader::HyperTungsteniteUpgrader;
4-
pub use upgrader::create_ws_stream;
4+
pub use upgrader::create_ws_stream;

src/model/endpoint.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::model::{Field, Type};
22
use convert_case::{Case, Casing};
33
use eyre::{ContextCompat, Result};
4+
use serde::de::{Error, Unexpected};
5+
use serde::ser::SerializeStruct;
46
use serde::*;
57
use std::fmt::Write;
68

@@ -33,6 +35,10 @@ pub struct EndpointSchema {
3335

3436
// Allowed roles for this endpoint ["EnumRole::EnumVariant"]
3537
pub roles: Vec<String>,
38+
39+
/// Public error variants that handlers may return for this endpoint.
40+
#[serde(default)]
41+
pub errors: Vec<EndpointErrorSchema>,
3642
}
3743

3844
impl EndpointSchema {
@@ -52,6 +58,7 @@ impl EndpointSchema {
5258
description: "".to_string(),
5359
json_schema: Default::default(),
5460
roles: Vec::new(),
61+
errors: Vec::new(),
5562
}
5663
}
5764

@@ -72,6 +79,117 @@ impl EndpointSchema {
7279
self.roles = roles;
7380
self
7481
}
82+
83+
/// Adds public error variants to the endpoint.
84+
pub fn with_errors(mut self, errors: Vec<EndpointErrorSchema>) -> Self {
85+
self.errors = errors;
86+
self
87+
}
88+
}
89+
90+
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)]
91+
pub struct EndpointErrorSchema {
92+
pub name: String,
93+
pub code: EndpointErrorCodeRef,
94+
#[serde(default)]
95+
pub message: String,
96+
#[serde(default)]
97+
pub fields: Vec<Field>,
98+
}
99+
100+
#[derive(Clone, Debug, Hash, PartialEq, PartialOrd, Eq, Ord)]
101+
pub struct EndpointErrorCodeRef {
102+
pub ty: Type,
103+
pub variant: String,
104+
}
105+
106+
impl EndpointErrorCodeRef {
107+
pub const ENUM_NAME: &'static str = "ErrorCode";
108+
109+
pub fn new(variant: impl Into<String>) -> Self {
110+
Self {
111+
ty: Type::enum_ref(Self::ENUM_NAME, true),
112+
variant: variant.into(),
113+
}
114+
}
115+
116+
pub fn variant(&self) -> &str {
117+
&self.variant
118+
}
119+
120+
pub fn path(&self) -> String {
121+
format!("{}::{}", Self::ENUM_NAME, self.variant)
122+
}
123+
124+
fn validate_ty(ty: Type) -> std::result::Result<Type, String> {
125+
match &ty {
126+
Type::EnumRef { name, .. } if name == Self::ENUM_NAME => Ok(ty),
127+
Type::EnumRef { name, .. } => {
128+
Err(format!("expected {} enum ref, got {name}", Self::ENUM_NAME))
129+
}
130+
_ => Err(format!("expected {} enum ref", Self::ENUM_NAME)),
131+
}
132+
}
133+
134+
fn parse_path(path: &str) -> std::result::Result<Self, String> {
135+
let (enum_name, variant) = path
136+
.split_once("::")
137+
.ok_or_else(|| format!("expected {}::Variant", Self::ENUM_NAME))?;
138+
139+
if enum_name != Self::ENUM_NAME {
140+
return Err(format!(
141+
"expected {} enum path, got {enum_name}",
142+
Self::ENUM_NAME
143+
));
144+
}
145+
146+
if variant.is_empty() || variant.contains("::") {
147+
return Err(format!("expected {}::Variant", Self::ENUM_NAME));
148+
}
149+
150+
Ok(Self::new(variant))
151+
}
152+
}
153+
154+
impl std::fmt::Display for EndpointErrorCodeRef {
155+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156+
f.write_str(&self.path())
157+
}
158+
}
159+
160+
impl Serialize for EndpointErrorCodeRef {
161+
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
162+
where
163+
S: Serializer,
164+
{
165+
let mut state = serializer.serialize_struct("EndpointErrorCodeRef", 2)?;
166+
state.serialize_field("ty", &self.ty)?;
167+
state.serialize_field("variant", &self.variant)?;
168+
state.end()
169+
}
170+
}
171+
172+
impl<'de> Deserialize<'de> for EndpointErrorCodeRef {
173+
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
174+
where
175+
D: Deserializer<'de>,
176+
{
177+
#[derive(Deserialize)]
178+
#[serde(untagged)]
179+
enum Helper {
180+
Path(String),
181+
Structured { ty: Type, variant: String },
182+
}
183+
184+
match Helper::deserialize(deserializer)? {
185+
Helper::Path(path) => Self::parse_path(&path)
186+
.map_err(|err| D::Error::invalid_value(Unexpected::Str(&path), &err.as_str())),
187+
Helper::Structured { ty, variant } => {
188+
let ty = Self::validate_ty(ty).map_err(D::Error::custom)?;
189+
Ok(Self { ty, variant })
190+
}
191+
}
192+
}
75193
}
76194

77195
pub fn encode_header<T: Serialize>(v: T, schema: EndpointSchema) -> Result<String> {
@@ -94,3 +212,105 @@ pub fn encode_header<T: Serialize>(v: T, schema: EndpointSchema) -> Result<Strin
94212
}
95213
Ok(s)
96214
}
215+
216+
#[cfg(test)]
217+
mod tests {
218+
use super::*;
219+
use crate::libs::error_code::ErrorCode;
220+
use std::str::FromStr;
221+
222+
#[test]
223+
fn endpoint_schema_defaults_errors() {
224+
let schema: EndpointSchema = serde_json::from_value(serde_json::json!({
225+
"name": "Login",
226+
"code": 10001,
227+
"parameters": [],
228+
"returns": [],
229+
"roles": []
230+
}))
231+
.unwrap();
232+
233+
assert!(schema.errors.is_empty());
234+
}
235+
236+
#[test]
237+
fn endpoint_schema_reads_error_definitions() {
238+
let schema: EndpointSchema = serde_json::from_value(serde_json::json!({
239+
"name": "Login",
240+
"code": 10001,
241+
"parameters": [],
242+
"returns": [],
243+
"roles": [],
244+
"errors": [
245+
{
246+
"name": "WrongPassword",
247+
"code": "ErrorCode::Unauthorized",
248+
"message": "Wrong password",
249+
"fields": []
250+
},
251+
{
252+
"name": "PasswordTooShort",
253+
"code": "ErrorCode::BadRequest",
254+
"message": "Password too short",
255+
"fields": [
256+
{ "name": "min_length", "ty": "Int32" }
257+
]
258+
}
259+
]
260+
}))
261+
.unwrap();
262+
263+
assert_eq!(schema.errors.len(), 2);
264+
assert_eq!(schema.errors[0].code.variant(), "Unauthorized");
265+
assert_eq!(schema.errors[1].fields[0].name, "min_length");
266+
}
267+
268+
#[test]
269+
fn endpoint_error_code_ref_serializes_as_typed_ref() {
270+
let value = serde_json::to_value(EndpointErrorCodeRef::new("BadRequest")).unwrap();
271+
272+
assert_eq!(
273+
value,
274+
serde_json::json!({
275+
"ty": {
276+
"EnumRef": {
277+
"name": "ErrorCode"
278+
}
279+
},
280+
"variant": "BadRequest"
281+
})
282+
);
283+
284+
let parsed: EndpointErrorCodeRef = serde_json::from_value(value).unwrap();
285+
assert_eq!(parsed.variant(), "BadRequest");
286+
}
287+
288+
#[test]
289+
fn endpoint_error_code_ref_rejects_wrong_paths() {
290+
assert!(
291+
serde_json::from_value::<EndpointErrorCodeRef>(serde_json::json!("BadRequest"))
292+
.is_err()
293+
);
294+
assert!(
295+
serde_json::from_value::<EndpointErrorCodeRef>(serde_json::json!("UserRole::Admin"))
296+
.is_err()
297+
);
298+
}
299+
300+
#[test]
301+
fn error_code_parses_names_and_numeric_codes() {
302+
assert_eq!(
303+
ErrorCode::from_str("BadRequest").unwrap(),
304+
ErrorCode::BAD_REQUEST
305+
);
306+
assert_eq!(
307+
ErrorCode::from_str("BAD_REQUEST").unwrap(),
308+
ErrorCode::BAD_REQUEST
309+
);
310+
assert_eq!(
311+
ErrorCode::from_str("100777").unwrap(),
312+
ErrorCode::new(100777)
313+
);
314+
assert!(ErrorCode::from_str("not a code").is_err());
315+
}
316+
}

0 commit comments

Comments
 (0)