@@ -103,19 +103,24 @@ defmodule Atex.Lexicon do
103103 end
104104
105105 struct_def =
106- if schema_key == :main do
107- quoted_struct
108- else
109- nested_module_name =
110- schema_key
111- |> Recase . to_pascal ( )
112- |> atomise ( )
113-
114- quote do
115- defmodule unquote ( { :__aliases__ , [ alias: false ] , [ nested_module_name ] } ) do
116- unquote ( quoted_struct )
106+ cond do
107+ schema_key == :main ->
108+ quoted_struct
109+
110+ schema_key == :errors ->
111+ quoted_struct
112+
113+ true ->
114+ nested_module_name =
115+ schema_key
116+ |> Recase . to_pascal ( )
117+ |> atomise ( )
118+
119+ quote do
120+ defmodule unquote ( { :__aliases__ , [ alias: false ] , [ nested_module_name ] } ) do
121+ unquote ( quoted_struct )
122+ end
117123 end
118- end
119124 end
120125
121126 quote do
@@ -265,7 +270,7 @@ defmodule Atex.Lexicon do
265270 |> Map . from_struct ( )
266271 |> Enum . reject ( fn { k , v } -> k in @ optional_if_nil_keys && v == nil end )
267272 |> Enum . into ( % { } )
268- |> Jason .Encoder. encode ( encoder )
273+ |> JSON .Encoder. encode ( encoder )
269274 end
270275 end
271276
@@ -305,6 +310,8 @@ defmodule Atex.Lexicon do
305310 schema
306311 end
307312
313+ errors = build_errors_module ( def [ :errors ] )
314+
308315 # Root struct containing `params`
309316 main =
310317 if params do
@@ -317,6 +324,8 @@ defmodule Atex.Lexicon do
317324 quote do
318325 @ enforce_keys [ :params ]
319326 defstruct params: nil
327+
328+ unquote ( coerce_error_function ( errors ) )
320329 end
321330 }
322331 else
@@ -328,11 +337,13 @@ defmodule Atex.Lexicon do
328337 end ,
329338 quote do
330339 defstruct [ ]
340+
341+ unquote ( coerce_error_function ( errors ) )
331342 end
332343 }
333344 end
334345
335- [ main , params , output ]
346+ [ main , params , output , errors ]
336347 |> Enum . reject ( & is_nil / 1 )
337348 end
338349
@@ -380,6 +391,8 @@ defmodule Atex.Lexicon do
380391 def . input [ :encoding ]
381392 end
382393
394+ errors = build_errors_module ( def [ :errors ] )
395+
383396 # Root struct containing `input`, `raw_input`, and `params`
384397 main =
385398 {
@@ -410,11 +423,15 @@ defmodule Atex.Lexicon do
410423 params && input ->
411424 quote do
412425 defstruct input: nil , params: nil
426+
427+ unquote ( coerce_error_function ( errors ) )
413428 end
414429
415430 input ->
416431 quote do
417432 defstruct input: nil
433+
434+ unquote ( coerce_error_function ( errors ) )
418435 end
419436
420437 params && raw_input_encoding ->
@@ -423,6 +440,8 @@ defmodule Atex.Lexicon do
423440
424441 @ spec content_type ( ) :: String . t ( )
425442 def content_type , do: unquote ( raw_input_encoding )
443+
444+ unquote ( coerce_error_function ( errors ) )
426445 end
427446
428447 raw_input_encoding ->
@@ -431,21 +450,27 @@ defmodule Atex.Lexicon do
431450
432451 @ spec content_type ( ) :: String . t ( )
433452 def content_type , do: unquote ( raw_input_encoding )
453+
454+ unquote ( coerce_error_function ( errors ) )
434455 end
435456
436457 params ->
437458 quote do
438459 defstruct raw_input: nil , params: nil
460+
461+ unquote ( coerce_error_function ( errors ) )
439462 end
440463
441464 true ->
442465 quote do
443466 defstruct raw_input: nil
467+
468+ unquote ( coerce_error_function ( errors ) )
444469 end
445470 end
446471 }
447472
448- [ main , params , output , input ]
473+ [ main , params , output , input , errors ]
449474 |> Enum . reject ( & is_nil / 1 )
450475 end
451476
@@ -766,4 +791,204 @@ defmodule Atex.Lexicon do
766791 defp do_join_with_pipe ( [ head ] ) , do: [ head ]
767792 defp do_join_with_pipe ( [ head | tail ] ) , do: [ { :| , [ ] , [ head | do_join_with_pipe ( tail ) ] } ]
768793 defp do_join_with_pipe ( [ ] ) , do: [ ]
794+
795+ @ spec build_errors_module ( errors :: list ( map ( ) ) | nil ) ::
796+ { atom ( ) , term ( ) , term ( ) , term ( ) } | nil
797+ defp build_errors_module ( errors ) when errors == nil or errors == [ ] , do: nil
798+
799+ defp build_errors_module ( errors ) do
800+ error_name_atoms = Enum . map ( errors , fn % { name: name } -> atomise ( name ) end )
801+
802+ error_names_type =
803+ case error_name_atoms do
804+ [ ] ->
805+ quote ( do: nil )
806+
807+ [ single ] ->
808+ { { :. , [ ] , [ { :__aliases__ , [ alias: false ] , [ single ] } , :t ] } , [ ] , [ ] }
809+
810+ multiple ->
811+ { :| , [ ] ,
812+ [
813+ { :| , [ ] ,
814+ Enum . map ( multiple , fn atom ->
815+ { { :. , [ ] , [ { :__aliases__ , [ alias: false ] , [ atom ] } , :t ] } , [ ] , [ ] }
816+ end ) } ,
817+ nil
818+ ] }
819+ end
820+
821+ error_structs =
822+ Enum . map ( errors , fn % { name: name } = error_def ->
823+ error_name = atomise ( name )
824+ description = Map . get ( error_def , :description )
825+
826+ quoted_struct =
827+ quote do
828+ defmodule unquote ( { :__aliases__ , [ alias: false ] , [ error_name ] } ) do
829+ @ moduledoc false
830+ @ enforce_keys [ ]
831+ defstruct message: nil
832+
833+ @ type t :: % __MODULE__ { message: String . t ( ) | nil }
834+
835+ @ spec from_json ( map ( ) ) :: { :ok , t ( ) } | { :error , :not_this_error }
836+ def from_json ( % { "error" => unquote ( name ) , "message" => msg } )
837+ when is_binary ( msg ) or is_nil ( msg ) do
838+ { :ok , % __MODULE__ { message: msg } }
839+ end
840+
841+ def from_json ( % { "error" => unquote ( name ) } ) ,
842+ do: { :ok , % __MODULE__ { message: nil } }
843+
844+ def from_json ( _ ) , do: { :error , :not_this_error }
845+
846+ defimpl JSON.Encoder do
847+ def encode ( % { mesage: message } , encoder ) do
848+ % { "error" => unquote ( name ) }
849+ |> then ( & if ( message , do: Map . put ( & 1 , "message" , message ) , else: & 1 ) )
850+ |> JSON.Encoder . encode ( encoder )
851+ end
852+ end
853+
854+ defimpl Jason.Encoder do
855+ def encode ( % { message: message } , options ) do
856+ % { "error" => unquote ( name ) }
857+ |> then ( & if ( message , do: Map . put ( & 1 , "message" , message ) , else: & 1 ) )
858+ |> Jason.Encode . map ( options )
859+ end
860+ end
861+
862+ unquote ( if ( description , do: quote ( do: @ doc ( unquote ( description ) ) ) , else: nil ) )
863+
864+ def error_name , do: unquote ( name )
865+ end
866+ end
867+
868+ quoted_name =
869+ quote do
870+ unquote ( error_name )
871+ end
872+
873+ { quoted_name , quoted_struct }
874+ end )
875+
876+ coerce_function_body =
877+ if error_name_atoms == [ ] do
878+ quote do: nil
879+ else
880+ error_module_refs =
881+ Enum . map ( error_name_atoms , fn name ->
882+ { :__aliases__ , [ alias: false ] , [ name ] }
883+ end )
884+
885+ quoted_error_name_atoms =
886+ Enum . map ( error_name_atoms , fn name ->
887+ quote do
888+ unquote ( name )
889+ end
890+ end )
891+
892+ quote do
893+ @ type error_struct :: unquote ( error_names_type )
894+
895+ @ spec coerce ( map ( ) ) ::
896+ { :ok , error_struct ( ) , String . t ( ) } | { :error , :no_matching_error }
897+ def coerce ( body ) when is_map ( body ) do
898+ result =
899+ Enum . find_value ( unquote ( error_module_refs ) , fn error_module ->
900+ case apply ( error_module , :from_json , [ body ] ) do
901+ { :ok , _ } = ok -> ok
902+ { :error , :not_this_error } -> nil
903+ end
904+ end )
905+
906+ case result do
907+ { :ok , struct } ->
908+ error_name =
909+ Enum . find_value ( unquote ( quoted_error_name_atoms ) , fn error_name ->
910+ error_module = Module . concat ( __MODULE__ , error_name )
911+
912+ case error_module . from_json ( body ) do
913+ { :ok , _ } -> error_name
914+ { :error , :not_this_error } -> nil
915+ end
916+ end )
917+
918+ { :ok , struct , error_name }
919+
920+ nil ->
921+ { :error , :no_matching_error }
922+ end
923+ end
924+
925+ def coerce ( _ ) , do: { :error , :no_matching_error }
926+ end
927+ end
928+
929+ errors_module =
930+ if error_name_atoms == [ ] do
931+ nil
932+ else
933+ quoted_structs = Enum . map ( error_structs , fn { _ , quoted } -> quoted end )
934+
935+ quote do
936+ defmodule Errors do
937+ @ moduledoc false
938+
939+ unquote_splicing ( quoted_structs )
940+
941+ unquote ( coerce_function_body )
942+ end
943+ end
944+ end
945+
946+ { :errors , nil , nil , errors_module }
947+ end
948+
949+ @ spec coerce_error_function ( { atom ( ) , term ( ) , term ( ) , term ( ) } | nil ) :: term ( )
950+ defp coerce_error_function ( nil ) do
951+ quote do
952+ @ spec coerce_error ( map ( ) ) :: { :error , :no_errors_defined }
953+ def coerce_error ( % { } ) , do: { :error , :no_errors_defined }
954+ def coerce_error ( _ ) , do: { :error , :no_errors_defined }
955+ end
956+ end
957+
958+ defp coerce_error_function ( { :errors , _ , _ , _ } ) do
959+ quote do
960+ @ spec coerce_error ( map ( ) ) ::
961+ { :ok , Atex.XRPC.Error . t ( ) } | { :error , :unknown_error | :not_an_error }
962+ def coerce_error ( % { "error" => _ } = body ) do
963+ errors_module = Module . concat ( __MODULE__ , Errors )
964+
965+ case errors_module . coerce ( body ) do
966+ { :ok , error_struct , error_name } ->
967+ { :ok ,
968+ % Atex.XRPC.Error {
969+ error: to_string ( error_name ) ,
970+ message: error_struct . message ,
971+ error_struct: error_struct
972+ } }
973+
974+ { :error , :no_matching_error } ->
975+ error_name =
976+ body
977+ |> Map . take ( [ "error" , "message" ] )
978+ |> Enum . map ( fn { k , v } -> { String . to_atom ( k ) , v } end )
979+ |> Keyword . get_values ( :error )
980+ |> List . first ( )
981+
982+ { :error ,
983+ % Atex.XRPC.Error {
984+ error: to_string ( error_name ) ,
985+ message: Map . get ( body , "message" ) ,
986+ error_struct: nil
987+ } }
988+ end
989+ end
990+
991+ def coerce_error ( _ ) , do: { :error , :not_an_error }
992+ end
993+ end
769994end
0 commit comments