11use crate :: model:: { Field , Type } ;
22use convert_case:: { Case , Casing } ;
33use eyre:: { ContextCompat , Result } ;
4+ use serde:: de:: { Error , Unexpected } ;
5+ use serde:: ser:: SerializeStruct ;
46use serde:: * ;
57use 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
3844impl 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
77195pub 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