Current section

Files

Jump to
spectra src spectra_string.erl
Raw

src/spectra_string.erl

-module(spectra_string).
-export([from_string/3, to_string/3]).
-ignore_xref([{spectra_string, from_string, 3}, {spectra_string, to_string, 3}]).
-include("../include/spectra.hrl").
-include("../include/spectra_internal.hrl").
%% API
-doc """
Converts a string value to an Erlang value based on a type specification.
This function validates the given string value against the specified type definition
and converts it to the corresponding Erlang value.
### Returns
{ok, ErlangValue} if conversion succeeds, or {error, Errors} if validation fails
""".
-doc #{
params =>
#{
"String" => "The string value to convert to Erlang format",
"Type" => "The type specification (spectra:sp_type_or_ref())",
"TypeInfo" => "The type information containing type definitions"
}
}.
-spec from_string(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type_or_ref(),
String :: list()
) ->
{ok, dynamic()} | {error, [spectra:error()]}.
from_string(TypeInfo, {type, TypeName, TypeArity}, String) when is_atom(TypeName) ->
Type = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
from_string(TypeInfo, Type, String);
from_string(_TypeInfo, {record, RecordName}, _String) when is_atom(RecordName) ->
erlang:error({type_not_supported, {record, RecordName}});
from_string(_TypeInfo, #sp_simple_type{type = NotSupported} = T, _String) 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});
from_string(_TypeInfo, #sp_simple_type{type = PrimaryType}, String) ->
convert_string_to_type(PrimaryType, String);
from_string(
_TypeInfo,
#sp_range{
type = integer,
lower_bound = Min,
upper_bound = Max
} =
Range,
String
) ->
case convert_string_to_type(integer, String) of
{ok, Value} when Min =< Value, Value =< Max ->
{ok, Value};
{ok, Value} when is_integer(Value) ->
{error, [sp_error:type_mismatch(Range, Value)]};
{error, Reason} ->
{error, Reason}
end;
from_string(_TypeInfo, #sp_remote_type{mfargs = {Module, TypeName, Args}}, String) ->
TypeInfo = spectra_module_types:get(Module),
TypeArity = length(Args),
Type = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, Args),
from_string(TypeInfo, TypeWithoutVars, String);
from_string(_TypeInfo, #sp_literal{value = Literal}, String) ->
try_convert_string_to_literal(Literal, String);
from_string(TypeInfo, #sp_union{} = Type, String) ->
union(fun from_string/3, TypeInfo, Type, String);
from_string(_TypeInfo, Type, String) ->
{error, [sp_error:type_mismatch(Type, String)]}.
-doc """
Converts an Erlang value to a string based on a type specification.
This function validates the given Erlang value against the specified type definition
and converts it to a string representation.
### Returns
{ok, String} if conversion succeeds, or {error, Errors} if validation fails
""".
-doc #{
params =>
#{
"Data" => "The Erlang value to convert to string format",
"Type" => "The type specification (spectra:sp_type_or_ref())",
"TypeInfo" => "The type information containing type definitions"
}
}.
-spec to_string(
TypeInfo :: spectra:type_info(),
Type :: spectra:sp_type_or_ref(),
Data :: dynamic()
) ->
{ok, string()} | {error, [spectra:error()]}.
to_string(TypeInfo, {type, TypeName, TypeArity}, Data) when is_atom(TypeName) ->
Type = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
to_string(TypeInfo, Type, Data);
to_string(_TypeInfo, {record, RecordName}, _Data) when is_atom(RecordName) ->
erlang:error({type_not_supported, {record, RecordName}});
to_string(_TypeInfo, #sp_simple_type{type = NotSupported} = T, _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, T});
to_string(_TypeInfo, #sp_simple_type{type = PrimaryType}, Data) ->
convert_type_to_string(PrimaryType, Data);
to_string(
_TypeInfo,
#sp_range{
type = integer,
lower_bound = Min,
upper_bound = Max
} =
Range,
Data
) ->
case convert_type_to_string(integer, Data) of
{ok, String} when Min =< Data, Data =< Max ->
{ok, String};
{ok, _String} when is_integer(Data) ->
{error, [sp_error:type_mismatch(Range, Data)]};
{error, Reason} ->
{error, Reason}
end;
to_string(_TypeInfo, #sp_remote_type{mfargs = {Module, TypeName, Args}}, Data) ->
TypeInfo = spectra_module_types:get(Module),
TypeArity = length(Args),
Type = spectra_type_info:get_type(TypeInfo, TypeName, TypeArity),
TypeWithoutVars = apply_args(TypeInfo, Type, Args),
to_string(TypeInfo, TypeWithoutVars, Data);
to_string(_TypeInfo, #sp_literal{value = Literal}, Data) ->
try_convert_literal_to_string(Literal, Data);
to_string(TypeInfo, #sp_union{} = Type, Data) ->
union_to_string(TypeInfo, Type, Data);
to_string(_TypeInfo, Type, Data) ->
{error, [sp_error:type_mismatch(Type, Data)]}.
%% INTERNAL
-spec convert_string_to_type(
Type :: spectra:simple_types(),
String :: string()
) ->
{ok, dynamic()} | {error, [spectra:error()]}.
convert_string_to_type(Type, String) when is_atom(Type), is_list(String) ->
do_convert_string_to_type(Type, String);
convert_string_to_type(Type, NotString) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = Type}, NotString)]}.
-spec do_convert_string_to_type(Type :: spectra:simple_types(), String :: string()) ->
{ok, dynamic()} | {error, [spectra:error()]}.
do_convert_string_to_type(integer, String) ->
try
{ok, list_to_integer(String)}
catch
error:badarg ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = integer}, String)]}
end;
do_convert_string_to_type(float, String) ->
try
{ok, list_to_float(String)}
catch
error:badarg ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = float}, String)]}
end;
do_convert_string_to_type(number, String) ->
case do_convert_string_to_type(integer, String) of
{ok, _} = Result ->
Result;
{error, _} ->
do_convert_string_to_type(float, String)
end;
do_convert_string_to_type(boolean, "true") ->
{ok, true};
do_convert_string_to_type(boolean, "false") ->
{ok, false};
do_convert_string_to_type(boolean, String) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = boolean}, String)]};
do_convert_string_to_type(atom, String) ->
try
{ok, list_to_existing_atom(String)}
catch
error:badarg ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = atom}, String)]}
end;
do_convert_string_to_type(string, String) ->
{ok, String};
do_convert_string_to_type(nonempty_string, String) when String =/= [] ->
{ok, String};
do_convert_string_to_type(nonempty_string, []) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = nonempty_string}, [])]};
do_convert_string_to_type(binary, String) ->
{ok, list_to_binary(String)};
do_convert_string_to_type(nonempty_binary, String) when String =/= [] ->
{ok, list_to_binary(String)};
do_convert_string_to_type(nonempty_binary, []) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = nonempty_binary}, [])]};
do_convert_string_to_type(non_neg_integer, String) ->
case do_convert_string_to_type(integer, String) of
{ok, Value} when Value >= 0 ->
{ok, Value};
{ok, Value} ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = non_neg_integer}, Value)]};
{error, Reason} ->
{error, Reason}
end;
do_convert_string_to_type(pos_integer, String) ->
case do_convert_string_to_type(integer, String) of
{ok, Value} when Value > 0 ->
{ok, Value};
{ok, Value} ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = pos_integer}, Value)]};
{error, Reason} ->
{error, Reason}
end;
do_convert_string_to_type(neg_integer, String) ->
case do_convert_string_to_type(integer, String) of
{ok, Value} when Value < 0 ->
{ok, Value};
{ok, Value} ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = neg_integer}, Value)]};
{error, Reason} ->
{error, Reason}
end;
do_convert_string_to_type(Type, String) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = Type}, String)]}.
-spec try_convert_string_to_literal(Literal :: spectra:literal_value(), String :: string()) ->
{ok, dynamic()} | {error, [spectra:error()]}.
try_convert_string_to_literal(Literal, String) when is_boolean(Literal) ->
case convert_string_to_type(boolean, String) of
{ok, Literal} ->
{ok, Literal};
{ok, _Other} ->
{error, [
sp_error:type_mismatch(
#sp_literal{value = Literal, binary_value = atom_to_binary(Literal, utf8)},
String
)
]};
{error, Reason} ->
{error, Reason}
end;
try_convert_string_to_literal(Literal, String) when is_atom(Literal) ->
case convert_string_to_type(atom, String) of
{ok, Literal} ->
{ok, Literal};
{ok, _Other} ->
{error, [
sp_error:type_mismatch(
#sp_literal{value = Literal, binary_value = atom_to_binary(Literal, utf8)},
String
)
]};
{error, Reason} ->
{error, Reason}
end;
try_convert_string_to_literal(Literal, String) when is_integer(Literal) ->
case convert_string_to_type(integer, String) of
{ok, Literal} ->
{ok, Literal};
{ok, _Other} ->
{error, [
sp_error:type_mismatch(
#sp_literal{value = Literal, binary_value = integer_to_binary(Literal)},
String
)
]};
{error, Reason} ->
{error, Reason}
end;
try_convert_string_to_literal(Literal, String) ->
{error, [
sp_error:type_mismatch(
#sp_literal{value = Literal, binary_value = <<>>},
String
)
]}.
union(Fun, TypeInfo, #sp_union{types = Types} = T, String) ->
case do_first(Fun, TypeInfo, Types, String, []) of
{error, UnionErrors} ->
{error, [sp_error:no_match(T, String, UnionErrors)]};
Result ->
Result
end.
do_first(_Fun, _TypeInfo, [], _String, Errors) ->
{error, Errors};
do_first(Fun, TypeInfo, [Type | Rest], String, ErrorsAcc) ->
case Fun(TypeInfo, Type, String) of
{ok, Result} ->
{ok, Result};
{error, Errors} ->
do_first(Fun, TypeInfo, Rest, String, [{Type, Errors} | ErrorsAcc])
end.
apply_args(TypeInfo, Type, TypeArgs) when is_list(TypeArgs) ->
ArgNames = arg_names(Type),
NamedTypes =
maps:from_list(
lists:zip(ArgNames, TypeArgs)
),
spectra_util:type_replace_vars(TypeInfo, Type, NamedTypes).
arg_names(#sp_type_with_variables{vars = Args}) ->
Args;
arg_names(_) ->
[].
convert_type_to_string(integer, Data) when is_integer(Data) ->
{ok, integer_to_list(Data)};
convert_type_to_string(integer, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = integer}, Data)]};
convert_type_to_string(float, Data) when is_float(Data) ->
{ok, float_to_list(Data)};
convert_type_to_string(float, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = float}, Data)]};
convert_type_to_string(number, Data) when is_number(Data) ->
if
is_integer(Data) ->
{ok, integer_to_list(Data)};
is_float(Data) ->
{ok, float_to_list(Data)}
end;
convert_type_to_string(number, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = number}, Data)]};
convert_type_to_string(boolean, true) ->
{ok, "true"};
convert_type_to_string(boolean, false) ->
{ok, "false"};
convert_type_to_string(boolean, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = boolean}, Data)]};
convert_type_to_string(atom, Data) when is_atom(Data) ->
{ok, atom_to_list(Data)};
convert_type_to_string(atom, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = atom}, Data)]};
convert_type_to_string(string, Data) when is_list(Data) ->
list_to_charlist(Data);
convert_type_to_string(string, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = string}, Data)]};
convert_type_to_string(nonempty_string, Data) when is_list(Data), Data =/= [] ->
list_to_charlist(Data);
convert_type_to_string(nonempty_string, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = nonempty_string}, Data)]};
convert_type_to_string(binary, Data) when is_binary(Data) ->
{ok, binary_to_list(Data)};
convert_type_to_string(binary, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = binary}, Data)]};
convert_type_to_string(nonempty_binary, Data) when is_binary(Data), Data =/= <<>> ->
{ok, binary_to_list(Data)};
convert_type_to_string(nonempty_binary, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = nonempty_binary}, Data)]};
convert_type_to_string(non_neg_integer, Data) when is_integer(Data), Data >= 0 ->
{ok, integer_to_list(Data)};
convert_type_to_string(non_neg_integer, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = non_neg_integer}, Data)]};
convert_type_to_string(pos_integer, Data) when is_integer(Data), Data > 0 ->
{ok, integer_to_list(Data)};
convert_type_to_string(pos_integer, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = pos_integer}, Data)]};
convert_type_to_string(neg_integer, Data) when is_integer(Data), Data < 0 ->
{ok, integer_to_list(Data)};
convert_type_to_string(neg_integer, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = neg_integer}, Data)]};
convert_type_to_string(Type, Data) ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = Type}, Data)]}.
list_to_charlist(Data) ->
case application:get_env(spectra, check_unicode, false) of
true ->
case unicode:characters_to_list(Data) of
DataList when is_list(DataList) ->
{ok, DataList};
_Other ->
{error, [sp_error:type_mismatch(#sp_simple_type{type = string}, Data)]}
end;
false ->
{ok, Data}
end.
-spec try_convert_literal_to_string(Literal :: spectra:literal_value(), Data :: dynamic()) ->
{ok, string()} | {error, [spectra:error()]}.
try_convert_literal_to_string(Literal, Literal) when is_atom(Literal) ->
{ok, atom_to_list(Literal)};
try_convert_literal_to_string(Literal, Literal) when is_integer(Literal) ->
{ok, integer_to_list(Literal)};
try_convert_literal_to_string(Literal, Literal) when is_boolean(Literal) ->
if
Literal ->
{ok, "true"};
true ->
{ok, "false"}
end;
try_convert_literal_to_string(Literal, Data) ->
{error, [
sp_error:type_mismatch(
#sp_literal{value = Literal, binary_value = <<>>},
Data
)
]}.
union_to_string(TypeInfo, #sp_union{types = Types} = T, Data) ->
case do_first_to_string(TypeInfo, Types, Data, []) of
{error, UnionErrors} ->
{error, [sp_error:no_match(T, Data, UnionErrors)]};
Result ->
Result
end.
do_first_to_string(_TypeInfo, [], _Data, Errors) ->
{error, Errors};
do_first_to_string(TypeInfo, [Type | Rest], Data, ErrorsAcc) ->
case to_string(TypeInfo, Type, Data) of
{ok, Result} ->
{ok, Result};
{error, Errors} ->
do_first_to_string(TypeInfo, Rest, Data, [{Type, Errors} | ErrorsAcc])
end.