Current section

Files

Jump to
spectra src spectra_json.erl
Raw

src/spectra_json.erl

-module(spectra_json).
-export([to_json/3, from_json/3]).
-ignore_xref([to_json/3, from_json/3]).
-include("../include/spectra.hrl").
-include("../include/spectra_internal.hrl").
%% API
-spec to_json(
spectra:type_info() | module(),
spectra:sp_type_or_ref(),
Data :: dynamic()
) ->
{ok, json:encode_value()} | {error, [spectra:error()]}.
to_json(Module, TypeRef, Data) when is_atom(Module) ->
TypeInfo = spectra_module_types:get(Module),
to_json(TypeInfo, TypeRef, Data);
to_json(TypeInfo, Type, Data) ->
case do_to_json(TypeInfo, Type, Data) of
{ok, Json} ->
{ok, Json};
skip ->
{ok, undefined};
{error, Errs} ->
{error, Errs}
end.
%% INTERNAL
-spec do_to_json(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type_or_ref(),
Data :: dynamic()
) ->
{ok, json:encode_value()} | {error, [spectra:error()]} | skip.
do_to_json(TypeInfo, {record, RecordName}, Record) when is_atom(RecordName) ->
record_to_json(TypeInfo, RecordName, Record, []);
do_to_json(TypeInfo, #sp_rec{} = RecordInfo, Record) when is_tuple(Record) ->
record_to_json(TypeInfo, RecordInfo, Record, []);
do_to_json(
TypeInfo,
#sp_rec_ref{record_name = RecordName, field_types = TypeArgs},
Record
) when
is_atom(RecordName)
->
record_to_json(TypeInfo, RecordName, Record, TypeArgs);
do_to_json(TypeInfo, #sp_user_type_ref{type_name = TypeName, variables = TypeArgs}, Data) when
is_atom(TypeName)
->
TypeArity = length(TypeArgs),
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, TypeArgs),
do_to_json(TypeInfo, TypeWithoutVars, Data);
do_to_json(_TypeInfo, #sp_simple_type{type = NotSupported} = Type, _Data) when
NotSupported =:= pid orelse
NotSupported =:= port orelse
NotSupported =:= reference orelse
NotSupported =:= bitstring orelse
NotSupported =:= nonempty_bitstring orelse
NotSupported =:= none
->
erlang:error({type_not_supported, Type});
do_to_json(_TypeInfo, #sp_simple_type{} = Type, Value) ->
prim_type_to_json(Type, Value);
do_to_json(
_TypeInfo,
#sp_range{
type = integer,
lower_bound = Min,
upper_bound = Max
},
Value
) when
is_integer(Value) andalso Min =< Value, Value =< Max
->
{ok, Value};
do_to_json(_TypeInfo, #sp_literal{value = undefined}, undefined) ->
skip;
do_to_json(_TypeInfo, #sp_literal{value = Value}, Value) ->
{ok, Value};
do_to_json(TypeInfo, #sp_union{} = Type, Data) ->
union(fun do_to_json/3, TypeInfo, Type, Data);
do_to_json(TypeInfo, #sp_nonempty_list{type = Type}, Data) ->
nonempty_list_to_json(TypeInfo, Type, Data);
do_to_json(TypeInfo, #sp_list{type = Type}, Data) when is_list(Data) ->
list_to_json(TypeInfo, Type, Data);
do_to_json(TypeInfo, {type, TypeName, TypeArity}, Data) when is_atom(TypeName) ->
%% FIXME: For simple types without arity, default to 0
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
do_to_json(TypeInfo, Type, Data);
do_to_json(TypeInfo, #sp_map{struct_name = StructName} = Map, Data) ->
case StructName of
undefined ->
map_to_json(TypeInfo, Map, Data);
_ ->
%% For Elixir structs, we expect the data to have the __struct__ field
case maps:get('__struct__', Data, undefined) of
StructName ->
map_to_json(TypeInfo, Map, Data);
_ ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{expected_struct => StructName, value => Data}
}
]}
end
end;
do_to_json(_TypeInfo, #sp_remote_type{mfargs = {Module, TypeName, Args}}, Data) ->
TypeInfo = spectra_module_types:get(Module),
TypeArity = length(Args),
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, Args),
do_to_json(TypeInfo, TypeWithoutVars, Data);
do_to_json(_TypeInfo, #sp_maybe_improper_list{} = Type, _Data) ->
erlang:error({type_not_implemented, Type});
do_to_json(_TypeInfo, #sp_nonempty_improper_list{} = Type, _Data) ->
erlang:error({type_not_implemented, Type});
%% Not supported types
do_to_json(_TypeInfo, #sp_tuple{} = Type, _Data) ->
erlang:error({type_not_supported, Type});
do_to_json(_TypeInfo, #sp_function{} = Type, _Data) ->
erlang:error({type_not_supported, Type});
do_to_json(_TypeInfo, Type, OtherValue) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => Type, value => OtherValue}
}
]}.
-spec prim_type_to_json(Type :: spectra:sp_type(), Value :: term()) ->
{ok, json:encode_value()} | {error, [spectra:error()]}.
prim_type_to_json(#sp_simple_type{type = Type} = T, Value) ->
case check_type_to_json(Type, Value) of
{true, NewValue} ->
{ok, NewValue};
{error, Reason} ->
{error, Reason};
false ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => T, value => Value}
}
]}
end.
nonempty_list_to_json(TypeInfo, Type, Data) when is_list(Data) andalso Data =/= [] ->
list_to_json(TypeInfo, Type, Data);
nonempty_list_to_json(_TypeInfo, Type, Data) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => {nonempty_list, Type}, value => Data}
}
]}.
-spec list_to_json(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type_or_ref(),
Data :: [term()]
) ->
{ok, [json:encode_value()]} | {error, [spectra:error()]}.
list_to_json(TypeInfo, Type, Data) when is_list(Data) ->
spectra_util:map_until_error(
fun({Nr, Item}) ->
case do_to_json(TypeInfo, Type, Item) of
{ok, Json} ->
{ok, Json};
skip ->
{ok, undefined};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, Nr) end,
Errs
),
{error, Errs2}
end
end,
lists:enumerate(Data)
).
map_to_json(TypeInfo, #sp_map{fields = Fields}, Data) when is_map(Data) ->
%% Check if this is an Elixir struct and remove __struct__ field for JSON serialization
DataWithoutStruct =
case maps:take('__struct__', Data) of
{_StructName, CleanData} ->
CleanData;
error ->
Data
end,
case map_fields_to_json(TypeInfo, Fields, DataWithoutStruct) of
{ok, MapFields} ->
{ok, maps:from_list(MapFields)};
{error, Errors} ->
{error, Errors}
end;
map_to_json(_TypeInfo, _MapFieldTypes, Data) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => #sp_simple_type{type = map}, value => Data}
}
]}.
map_fields_to_json(TypeInfo, MapFieldTypes, Data) ->
Fun = fun
({map_field_assoc, FieldName, FieldType}, {FieldsAcc, DataAcc}) ->
case maps:take(FieldName, DataAcc) of
{FieldData, NewDataAcc} ->
case do_to_json(TypeInfo, FieldType, FieldData) of
{ok, FieldJson} ->
{ok, {
[{FieldName, FieldJson}] ++ FieldsAcc,
maps:remove(FieldName, DataAcc)
}};
skip ->
{ok, {FieldsAcc, NewDataAcc}};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, FieldName) end,
Errs
),
{error, Errs2}
end;
error ->
{ok, {FieldsAcc, DataAcc}}
end;
({map_field_type_assoc, KeyType, ValueType}, {FieldsAcc, DataAcc}) ->
case map_field_type(TypeInfo, KeyType, ValueType, DataAcc) of
{ok, {NewFields, NewDataAcc}} ->
{ok, {NewFields ++ FieldsAcc, NewDataAcc}};
{error, _} = Err ->
Err
end;
({map_field_type_exact, KeyType, ValueType}, {FieldsAcc, DataAcc}) ->
case map_field_type(TypeInfo, KeyType, ValueType, DataAcc) of
{ok, {[], _}} ->
NoExactMatch =
#sp_error{
type = not_matched_fields,
location = [],
ctx =
#{type => {map_field_type_exact, KeyType, ValueType}}
},
{error, [NoExactMatch]};
{ok, {NewFields, NewDataAcc}} ->
{ok, {NewFields ++ FieldsAcc, NewDataAcc}};
{error, _} = Err ->
Err
end;
({map_field_exact, FieldName, FieldType}, {FieldsAcc, DataAcc}) ->
case maps:take(FieldName, DataAcc) of
{FieldData, NewDataAcc} ->
case do_to_json(TypeInfo, FieldType, FieldData) of
{ok, FieldJson} ->
{ok, {[{FieldName, FieldJson}] ++ FieldsAcc, NewDataAcc}};
skip ->
%% FIXME: Warn about weird type def??
{ok, {FieldsAcc, NewDataAcc}};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, FieldName) end,
Errs
),
{error, Errs2}
end;
error ->
case spectra_type:can_be_undefined(TypeInfo, FieldType) of
true ->
{ok, {FieldsAcc, DataAcc}};
false ->
{error, [#sp_error{type = missing_data, location = [FieldName]}]}
end
end
end,
case spectra_util:fold_until_error(Fun, {[], Data}, MapFieldTypes) of
{ok, {MapFields, FinalData}} ->
case maps:to_list(FinalData) of
[] ->
{ok, MapFields};
L ->
{error,
lists:map(
fun({Key, Value}) ->
#sp_error{
type = not_matched_fields,
location = [],
ctx = #{key => Key, value => Value}
}
end,
L
)}
end;
{error, _} = Err ->
Err
end.
-spec map_field_type(
TypeInfo :: spectra:type_info(),
KeyType :: spectra:sp_type(),
ValueType :: spectra:sp_type(),
Data :: map()
) ->
{ok, {[{json:encode_value(), json:encode_value()}], map()}}
| {error, [spectra:error()]}.
map_field_type(TypeInfo, KeyType, ValueType, Data) ->
Fun = fun({Key, Value}, {FieldsAcc, DataAcc}) ->
case do_to_json(TypeInfo, KeyType, Key) of
{ok, KeyJson} ->
case do_to_json(TypeInfo, ValueType, Value) of
{ok, ValueJson} ->
{ok, {FieldsAcc ++ [{KeyJson, ValueJson}], maps:remove(Key, DataAcc)}};
skip ->
{ok, {FieldsAcc, DataAcc}};
{error, Errs} ->
Errs2 = lists:map(fun(Err) -> err_append_location(Err, Key) end, Errs),
{error, Errs2}
end;
{error, _Errs} ->
{ok, {FieldsAcc, DataAcc}}
end
end,
spectra_util:fold_until_error(Fun, {[], Data}, maps:to_list(Data)).
-spec record_to_json(
TypeInfo :: spectra:type_info(),
RecordName :: atom() | #sp_rec{},
Record :: term(),
TypeArgs :: [{atom(), spectra:sp_type()}]
) ->
{ok, #{atom() => json:encode_value()}} | {error, [spectra:error()]}.
record_to_json(TypeInfo, RecordName, Record, TypeArgs) when is_atom(RecordName) ->
{ok, RecordInfo} = spectra_type_info:get_record(TypeInfo, RecordName),
record_to_json(TypeInfo, RecordInfo, Record, TypeArgs);
record_to_json(
TypeInfo,
#sp_rec{
name = RecordName,
fields = Fields,
arity = Arity
},
Record,
TypeArgs
) when
is_tuple(Record) andalso
element(1, Record) =:= RecordName andalso
tuple_size(Record) =:= Arity
->
[RecordName | FieldsData] = tuple_to_list(Record),
RecFieldTypes = record_replace_vars(Fields, TypeArgs),
RecFieldTypesWithData = lists:zip(RecFieldTypes, FieldsData),
do_record_to_json(TypeInfo, RecFieldTypesWithData);
record_to_json(_TypeInfo, RecordName, Record, TypeArgs) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx =
#{
record_name => RecordName,
record => Record,
type_args => TypeArgs
}
}
]}.
record_replace_vars(RecordInfo, TypeArgs) ->
lists:foldl(
fun({Field, Type}, Fields) ->
lists:keyreplace(Field, 1, Fields, {Field, Type})
end,
RecordInfo,
TypeArgs
).
-spec do_record_to_json(
spectra:type_info(),
[{{atom(), spectra:sp_type()}, term()}]
) ->
{ok, #{atom() => json}} | {error, [spectra:error()]}.
do_record_to_json(TypeInfo, RecFieldTypesWithData) ->
Fun = fun({{FieldName, FieldType}, RecordFieldData}, FieldsAcc) when is_atom(FieldName) ->
case do_to_json(TypeInfo, FieldType, RecordFieldData) of
{ok, FieldJson} ->
{ok, [{FieldName, FieldJson}] ++ FieldsAcc};
skip ->
{ok, FieldsAcc};
{error, Errors} ->
{error, lists:map(fun(Error) -> err_append_location(Error, FieldName) end, Errors)}
end
end,
case spectra_util:fold_until_error(Fun, [], RecFieldTypesWithData) of
{ok, Fields} ->
{ok, maps:from_list(Fields)};
{error, _} = Err ->
Err
end.
err_append_location(Err, FieldName) ->
Err#sp_error{location = [FieldName | Err#sp_error.location]}.
-spec from_json(
TypeInfo :: spectra:type_info() | module(),
Type :: spectra:sp_type_or_ref(),
Json :: json:decode_value()
) ->
{ok, term()} | {error, [spectra:error()]}.
from_json(Module, Type, Json) when is_atom(Module) ->
TypeInfo = spectra_module_types:get(Module),
do_from_json(TypeInfo, Type, Json);
from_json(TypeInfo, Type, Json) ->
do_from_json(TypeInfo, Type, Json).
-spec do_from_json(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type_or_ref(),
Json :: json:decode_value()
) ->
{ok, term()} | {error, [spectra:error()]}.
do_from_json(TypeInfo, {record, RecordName}, Json) when is_atom(RecordName) ->
record_from_json(TypeInfo, RecordName, Json, []);
do_from_json(TypeInfo, #sp_rec{} = Rec, Json) ->
record_from_json(TypeInfo, Rec, Json, []);
do_from_json(_TypeInfo, #sp_remote_type{mfargs = {Module, TypeName, Args}}, Data) ->
TypeInfo = spectra_module_types:get(Module),
TypeArity = length(Args),
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, Args),
do_from_json(TypeInfo, TypeWithoutVars, Data);
do_from_json(
TypeInfo,
#sp_rec_ref{record_name = RecordName, field_types = TypeArgs},
Json
) when
is_atom(RecordName)
->
record_from_json(TypeInfo, RecordName, Json, TypeArgs);
do_from_json(TypeInfo, #sp_map{fields = Fields, struct_name = StructName}, Json) ->
case map_from_json(TypeInfo, Fields, Json) of
{ok, MapResult} when StructName =/= undefined ->
%% Add back the __struct__ field for Elixir structs
{ok, maps:put('__struct__', StructName, MapResult)};
Result ->
Result
end;
do_from_json(
TypeInfo,
#sp_user_type_ref{type_name = TypeName, variables = TypeArgs},
Json
) when
is_atom(TypeName)
->
type_from_json(TypeInfo, TypeName, length(TypeArgs), TypeArgs, Json);
do_from_json(TypeInfo, #sp_nonempty_list{type = Type}, Data) ->
nonempty_list_from_json(TypeInfo, Type, Data);
do_from_json(TypeInfo, #sp_list{type = Type}, Data) ->
list_from_json(TypeInfo, Type, Data);
do_from_json(_TypeInfo, #sp_simple_type{type = NotSupported} = T, _Value) when
NotSupported =:= pid orelse
NotSupported =:= port orelse
NotSupported =:= reference orelse
NotSupported =:= bitstring orelse
NotSupported =:= nonempty_bitstring orelse
NotSupported =:= none
->
erlang:error({type_not_supported, T});
do_from_json(_TypeInfo, #sp_simple_type{type = PrimaryType} = T, Json) ->
case check_type_from_json(PrimaryType, Json) of
{true, NewValue} ->
{ok, NewValue};
{error, Reason} ->
{error, Reason};
false ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => T, value => Json}
}
]}
end;
do_from_json(_TypeInfo, #sp_literal{value = Literal}, Literal) ->
{ok, Literal};
do_from_json(_TypeInfo, #sp_literal{value = Literal} = Type, Value) ->
case try_convert_to_literal(Literal, Value) of
{ok, Literal} ->
{ok, Literal};
false ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => Type, value => Value}
}
]}
end;
do_from_json(TypeInfo, {type, TypeName, TypeArity}, Json) when is_atom(TypeName) ->
type_from_json(TypeInfo, TypeName, TypeArity, [], Json);
do_from_json(TypeInfo, #sp_union{} = Type, Json) ->
union(fun do_from_json/3, TypeInfo, Type, Json);
do_from_json(
_TypeInfo,
#sp_range{
type = integer,
lower_bound = Min,
upper_bound = Max
},
Value
) when
Min =< Value, Value =< Max, is_integer(Value)
->
{ok, Value};
do_from_json(
_TypeInfo,
#sp_range{
type = integer,
lower_bound = _Min,
upper_bound = _Max
} =
Range,
Value
) when
is_integer(Value)
->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => Range, value => Value}
}
]};
do_from_json(_TypeInfo, #sp_maybe_improper_list{} = Type, _Value) ->
erlang:error({type_not_implemented, Type});
do_from_json(_TypeInfo, #sp_nonempty_improper_list{} = Type, _Value) ->
erlang:error({type_not_implemented, Type});
do_from_json(_TypeInfo, #sp_function{} = Type, _Value) ->
erlang:error({type_not_supported, Type});
do_from_json(_TypeInfo, #sp_tuple{} = Type, _Value) ->
erlang:error({type_not_supported, Type});
do_from_json(_TypeInfo, Type, Value) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => Type, value => Value}
}
]}.
try_convert_to_literal(Literal, Value) when is_atom(Literal) andalso is_binary(Value) ->
try binary_to_existing_atom(Value, utf8) of
Literal when is_atom(Literal) ->
{ok, Literal};
_ ->
false
catch
error:badarg ->
false
end;
try_convert_to_literal(_Literal, _Value) ->
false.
nonempty_list_from_json(TypeInfo, Type, Data) when is_list(Data) andalso Data =/= [] ->
list_from_json(TypeInfo, Type, Data);
nonempty_list_from_json(_TypeInfo, Type, Data) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => {nonempty_list, Type}, value => Data}
}
]}.
list_from_json(TypeInfo, Type, Data) when is_list(Data) ->
Fun = fun({Nr, Item}) ->
case do_from_json(TypeInfo, Type, Item) of
{ok, Json} ->
{ok, Json};
{error, Errs} ->
Errs2 = lists:map(fun(Err) -> err_append_location(Err, Nr) end, Errs),
{error, Errs2}
end
end,
spectra_util:map_until_error(Fun, lists:enumerate(Data));
list_from_json(_TypeInfo, Type, Data) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => {list, Type}, value => Data}
}
]}.
string_from_json(Type, Json) ->
case unicode:characters_to_list(Json) of
StringValue when is_list(StringValue) ->
{true, StringValue};
_Other ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx =
#{
type => #sp_simple_type{type = Type},
value => Json,
comment => "unicode conversion failed"
}
}
]}
end.
check_type_from_json(string, Json) when is_binary(Json) ->
string_from_json(string, Json);
check_type_from_json(nonempty_string, Json) when is_binary(Json), byte_size(Json) > 0 ->
string_from_json(nonempty_string, Json);
check_type_from_json(iodata, Json) when is_binary(Json) ->
{true, Json};
check_type_from_json(iolist, Json) when is_binary(Json) ->
{true, [Json]};
check_type_from_json(atom, Json) when is_binary(Json) ->
try
{true, binary_to_existing_atom(Json, utf8)}
catch
error:badarg ->
false
end;
check_type_from_json(Type, Json) ->
check_type(Type, Json).
check_type_to_json(iodata, Json) when is_binary(Json) ->
{true, Json};
check_type_to_json(iodata, Json) when is_list(Json) ->
{true, iolist_to_binary(Json)};
check_type_to_json(iolist, Json) when is_list(Json) ->
{true, iolist_to_binary(Json)};
check_type_to_json(nonempty_string, Json) when is_list(Json), Json =/= [] ->
case unicode:characters_to_binary(Json) of
{Err, _, _} when Err =:= error orelse Err =:= incomplete ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx =
#{
type => #sp_simple_type{type = string},
value => Json,
comment => "non printable"
}
}
]};
Bin when is_binary(Bin) ->
{true, Bin}
end;
check_type_to_json(string, Json) when is_list(Json) ->
case unicode:characters_to_binary(Json) of
{Err, _, _} when Err =:= error orelse Err =:= incomplete ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx =
#{
type => #sp_simple_type{type = string},
value => Json,
comment => "non printable"
}
}
]};
Bin when is_binary(Bin) ->
{true, Bin}
end;
check_type_to_json(Type, Json) ->
check_type(Type, Json).
check_type(integer, Json) when is_integer(Json) ->
{true, Json};
check_type(boolean, Json) when is_boolean(Json) ->
{true, Json};
check_type(float, Json) when is_float(Json) ->
{true, Json};
check_type(number, Json) when is_integer(Json) orelse is_float(Json) ->
{true, Json};
check_type(non_neg_integer, Json) when is_integer(Json) andalso Json >= 0 ->
{true, Json};
check_type(pos_integer, Json) when is_integer(Json) andalso Json > 0 ->
{true, Json};
check_type(neg_integer, Json) when is_integer(Json) andalso Json < 0 ->
{true, Json};
check_type(binary, Json) when is_binary(Json) ->
{true, Json};
check_type(nonempty_binary, Json) when is_binary(Json), byte_size(Json) > 0 ->
{true, Json};
check_type(atom, Json) when is_atom(Json) ->
{true, Json};
check_type(term, Json) ->
{true, Json};
check_type(_Type, _Json) ->
false.
union(Fun, TypeInfo, #sp_union{types = Types} = T, Json) ->
case do_first(Fun, TypeInfo, Types, Json) of
{error, no_match} ->
{error, [
#sp_error{
type = no_match,
location = [],
ctx = #{type => T, value => Json}
}
]};
Result ->
Result
end.
do_first(_Fun, _TypeInfo, [], _Json) ->
{error, no_match};
do_first(Fun, TypeInfo, [Type | Rest], Json) ->
case Fun(TypeInfo, Type, Json) of
skip ->
skip;
{ok, Result} ->
{ok, Result};
{error, _} ->
do_first(Fun, TypeInfo, Rest, Json)
end.
-spec type_from_json(
TypeInfo :: spectra:type_info(),
TypeName :: atom(),
TypeArity :: non_neg_integer(),
TypeArgs :: [spectra:sp_type()],
Json :: json:decode_value()
) ->
{ok, term()} | {error, [spectra:error()]}.
type_from_json(TypeInfo, TypeName, TypeArity, TypeArgs, Json) ->
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, TypeArgs),
do_from_json(TypeInfo, TypeWithoutVars, Json).
apply_args(TypeInfo, Type, TypeArgs) when is_list(TypeArgs) ->
ArgNames = arg_names(Type),
NamedTypes =
maps:from_list(
lists:zip(ArgNames, TypeArgs)
),
type_replace_vars(TypeInfo, Type, NamedTypes).
arg_names(#sp_type_with_variables{vars = Args}) ->
Args;
arg_names(_) ->
[].
-spec type_replace_vars(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type(),
NamedTypes :: #{atom() => spectra:sp_type()}
) ->
spectra:sp_type().
type_replace_vars(_TypeInfo, #sp_var{name = Name}, NamedTypes) ->
maps:get(Name, NamedTypes);
type_replace_vars(TypeInfo, #sp_type_with_variables{type = Type}, NamedTypes) ->
case Type of
#sp_union{types = UnionTypes} ->
#sp_union{
types =
lists:map(
fun(UnionType) ->
type_replace_vars(TypeInfo, UnionType, NamedTypes)
end,
UnionTypes
)
};
#sp_map{fields = Fields, struct_name = StructName} ->
#sp_map{
fields =
lists:map(
fun
({map_field_assoc, FieldName, FieldType}) ->
{map_field_assoc, FieldName,
type_replace_vars(TypeInfo, FieldType, NamedTypes)};
({map_field_exact, FieldName, FieldType}) ->
{map_field_exact, FieldName,
type_replace_vars(TypeInfo, FieldType, NamedTypes)};
({map_field_type_assoc, KeyType, ValueType}) ->
%% ADD TESTS
{map_field_type_assoc,
type_replace_vars(TypeInfo, KeyType, NamedTypes),
type_replace_vars(TypeInfo, ValueType, NamedTypes)};
({map_field_type_exact, KeyType, ValueType}) ->
%% ADD TESTS
{map_field_type_exact,
type_replace_vars(TypeInfo, KeyType, NamedTypes),
type_replace_vars(TypeInfo, ValueType, NamedTypes)}
end,
Fields
),
struct_name = StructName
};
#sp_rec_ref{record_name = RecordName, field_types = RefFieldTypes} ->
{ok, #sp_rec{fields = Fields} = Rec} = spectra_type_info:get_record(
TypeInfo, RecordName
),
NewRec = Rec#sp_rec{fields = record_replace_vars(Fields, RefFieldTypes)},
type_replace_vars(TypeInfo, NewRec, NamedTypes);
#sp_remote_type{mfargs = {Module, TypeName, Args}} ->
TypeInfo = spectra_module_types:get(Module),
TypeArity = length(Args),
{ok, Type} = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
type_replace_vars(TypeInfo, Type, NamedTypes);
#sp_list{type = ListType} ->
#sp_list{type = type_replace_vars(TypeInfo, ListType, NamedTypes)}
end;
type_replace_vars(TypeInfo, #sp_rec{fields = Fields} = Rec, NamedTypes) ->
Rec#sp_rec{
fields =
lists:map(
fun({Name, NType}) ->
{Name, type_replace_vars(TypeInfo, NType, NamedTypes)}
end,
Fields
)
};
type_replace_vars(_TypeInfo, Type, _NamedTypes) ->
Type.
-spec map_from_json(
spectra:type_info(),
[spectra:map_field()],
json:decode_value()
) ->
{ok, #{json:encode_value() => json:encode_value()}}
| {error, [spectra:error()]}.
map_from_json(TypeInfo, MapFieldType, Json) when is_map(Json) ->
Fun = fun
({map_field_assoc, FieldName, FieldType}, {FieldsAcc, JsonAcc}) ->
case maps:take(atom_to_binary(FieldName), JsonAcc) of
{FieldData, NewJsonAcc} ->
case do_from_json(TypeInfo, FieldType, FieldData) of
{ok, FieldJson} ->
{ok, {[{FieldName, FieldJson}] ++ FieldsAcc, NewJsonAcc}};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, FieldName) end,
Errs
),
{error, Errs2}
end;
error ->
{ok, {FieldsAcc, JsonAcc}}
end;
({map_field_exact, FieldName, FieldType}, {FieldsAcc, JsonAcc}) ->
case maps:take(atom_to_binary(FieldName), JsonAcc) of
{FieldData, NewJsonAcc} ->
case do_from_json(TypeInfo, FieldType, FieldData) of
{ok, FieldJson} ->
{ok, {[{FieldName, FieldJson}] ++ FieldsAcc, NewJsonAcc}};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, FieldName) end,
Errs
),
{error, Errs2}
end;
error ->
case spectra_type:can_be_undefined(TypeInfo, FieldType) of
true ->
{ok, {[{FieldName, undefined}] ++ FieldsAcc, JsonAcc}};
false ->
{error, [#sp_error{type = missing_data, location = [FieldName]}]}
end
end;
({map_field_type_assoc, KeyType, ValueType}, {FieldsAcc, JsonAcc}) ->
case map_field_type_from_json(TypeInfo, KeyType, ValueType, JsonAcc) of
{ok, {NewFields, NewJsonAcc}} ->
{ok, {NewFields ++ FieldsAcc, NewJsonAcc}};
{error, Reason} ->
{error, Reason}
end;
({map_field_type_exact, KeyType, ValueType}, {FieldsAcc, JsonAcc}) ->
case map_field_type_from_json(TypeInfo, KeyType, ValueType, JsonAcc) of
{ok, {NewFields, NewJsonAcc}} ->
case NewFields of
[] ->
NoExactMatch =
#sp_error{
type = not_matched_fields,
location = [],
ctx =
#{
type =>
{map_field_type_exact, KeyType, ValueType}
}
},
{error, [NoExactMatch]};
_ ->
{ok, {NewFields ++ FieldsAcc, NewJsonAcc}}
end;
{error, _} = Err ->
Err
end
end,
case spectra_util:fold_until_error(Fun, {[], Json}, MapFieldType) of
{ok, {Fields, NotMapped}} ->
case maps:size(NotMapped) of
0 ->
{ok, maps:from_list(Fields)};
_ ->
{error,
lists:map(
fun({Key, Value}) ->
#sp_error{
type = not_matched_fields,
location = [],
ctx = #{key => Key, value => Value}
}
end,
maps:to_list(NotMapped)
)}
end;
{error, _} = Err ->
Err
end;
map_from_json(_TypeInfo, _MapFieldType, Json) ->
%% Return error when Json is not a map
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{type => #sp_simple_type{type = map}, value => Json}
}
]}.
map_field_type_from_json(TypeInfo, KeyType, ValueType, Json) ->
spectra_util:fold_until_error(
fun({Key, Value}, {FieldsAcc, JsonAcc}) ->
case do_from_json(TypeInfo, KeyType, Key) of
{ok, KeyJson} ->
case do_from_json(TypeInfo, ValueType, Value) of
{ok, ValueJson} ->
{ok, {FieldsAcc ++ [{KeyJson, ValueJson}], maps:remove(Key, JsonAcc)}};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) ->
err_append_location(
Err,
Key
)
end,
Errs
),
{error, Errs2}
end;
{error, _Errs} ->
{ok, {FieldsAcc, JsonAcc}}
end
end,
{[], Json},
maps:to_list(Json)
).
-spec record_from_json(
TypeInfo :: spectra:type_info(),
RecordName :: atom() | #sp_rec{},
Json :: json:decode_value(),
TypeArgs :: [spectra:record_field()]
) ->
{ok, term()} | {error, list()}.
record_from_json(TypeInfo, RecordName, Json, TypeArgs) when is_atom(RecordName) ->
{ok, Record} = spectra_type_info:get_record(TypeInfo, RecordName),
record_from_json(TypeInfo, Record, Json, TypeArgs);
record_from_json(TypeInfo, #sp_rec{name = RecordName} = ARec, Json, TypeArgs) ->
RecordInfo = record_replace_vars(ARec#sp_rec.fields, TypeArgs),
do_record_from_json(TypeInfo, RecordName, RecordInfo, Json).
-spec do_record_from_json(
TypeInfo :: spectra:type_info(),
RecordName :: atom(),
RecordInfo :: list(),
Json :: json:decode_value()
) ->
{ok, term()} | {error, list()}.
do_record_from_json(TypeInfo, RecordName, RecordInfo, Json) when is_map(Json) ->
Fun = fun({FieldName, FieldType}) when is_atom(FieldName) ->
case maps:find(atom_to_binary(FieldName), Json) of
{ok, RecordFieldData} ->
case do_from_json(TypeInfo, FieldType, RecordFieldData) of
{ok, FieldJson} ->
{ok, FieldJson};
{error, Errs} ->
Errs2 =
lists:map(
fun(Err) -> err_append_location(Err, FieldName) end,
Errs
),
{error, Errs2}
end;
error ->
case spectra_type:can_be_undefined(TypeInfo, FieldType) of
true ->
{ok, undefined};
false ->
{error, [#sp_error{type = missing_data, location = [FieldName]}]}
end
end
end,
case spectra_util:map_until_error(Fun, RecordInfo) of
{ok, Fields} ->
{ok, list_to_tuple([RecordName | Fields])};
{error, Errs} ->
{error, Errs}
end;
do_record_from_json(_TypeInfo, RecordName, _RecordInfo, Json) ->
{error, [
#sp_error{
type = type_mismatch,
location = [],
ctx = #{record_name => RecordName, record => Json}
}
]}.