Move AACI and coerce logic to hz_aaci.erl #17

Open
spivee wants to merge 3 commits from hz_aaci into master
2 changed files with 204 additions and 165 deletions
Showing only changes of commit 3da9bd570b - Show all commits

View File

@ -716,7 +716,7 @@ decode_bytearray_fate(EncodedStr) ->
decode_bytearray(Type, EncodedStr) ->
case decode_bytearray_fate(EncodedStr) of
{ok, none} -> {ok, none};
{ok, Object} -> hz_aaci:coerce(Type, Object, from_fate);
{ok, Object} -> hz_aaci:fate_to_erlang(Type, Object);
{error, Reason} -> {error, Reason}
end.
@ -1518,7 +1518,7 @@ encode_call_data({aaci, Label}, Fun, Args) ->
end.
encode_call_data2(ArgDef, Fun, Args) ->
case hz_aaci:coerce_bindings(ArgDef, Args, to_fate) of
case hz_aaci:erlang_args_to_fate(ArgDef, Args) of
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced);
Errors -> Errors
end.

View File

@ -18,8 +18,9 @@
% Contract call and serialization interface functions
-export([prepare_contract/1,
prepare_aaci/1,
coerce/3,
coerce_bindings/3,
erlang_to_fate/2,
fate_to_erlang/2,
erlang_args_to_fate/2,
aaci_get_function_signature/2]).
%%% Types
@ -441,97 +442,51 @@ substitute_opaque_types(Bindings, Types) ->
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types).
%%% Coerce
%%% Erlang to FATE
coerce_bindings(VarTypes, Terms, Direction) ->
erlang_args_to_fate(VarTypes, Terms) ->
DefLength = length(VarTypes),
ArgLength = length(Terms),
if
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg);
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg);
DefLength > ArgLength -> {error, too_few_args};
DefLength < ArgLength -> {error, too_many_args}
end.
coerce_zipped_bindings(Bindings, Direction, Tag) ->
coerce_zipped_bindings(Bindings, Direction, Tag, [], []).
coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) ->
{{ArgName, Type}, Term} = Next,
case coerce(Type, Term, Direction) of
{ok, NewTerm} ->
coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({Tag, ArgName}, Errors),
coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken])
end;
coerce_zipped_bindings([], _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_zipped_bindings([], _, _, _, Broken) ->
{error, combine_errors(Broken)}.
wrap_errors(Location, Errors) ->
F = fun({Error, Path}) ->
{Error, [Location | Path]}
end,
lists:map(F, Errors).
combine_errors(Broken) ->
F = fun(NextErrors, Acc) ->
NextErrors ++ Acc
end,
lists:foldl(F, [], Broken).
coerce({_, _, integer}, S, _) when is_integer(S) ->
erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
{ok, S};
coerce({O, N, integer}, S, to_fate) when is_list(S) ->
erlang_to_fate({O, N, integer}, S) when is_list(S) ->
try
Val = list_to_integer(S),
{ok, Val}
catch
error:badarg -> single_error({invalid, O, N, S})
end;
coerce({O, N, address}, S, to_fate) ->
erlang_to_fate({O, N, address}, S) ->
coerce_chain_object(O, N, address, account_pubkey, S);
coerce({_, _, address}, {address, Bin}, from_fate) ->
Address = gmser_api_encoder:encode(account_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
coerce({O, N, contract}, S, to_fate) ->
erlang_to_fate({O, N, contract}, S) ->
coerce_chain_object(O, N, contract, contract_pubkey, S);
coerce({_, _, contract}, {contract, Bin}, from_fate) ->
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) ->
erlang_to_fate({_, _, signature}, S) when is_binary(S) andalso (byte_size(S) =:= 64) ->
% Usually to pass a binary in, you need to wrap it as {raw, Binary}, but
% since sg_... strings OR hex blobs can be used as signatures in Sophia, we
% special case this case based on the length. Even if a binary starts with
% "sg_", 64 characters is not enough to represent a 64 byte signature, so
% the most optimistic interpretation is to use the binary directly.
{ok, S};
coerce({O, N, signature}, S, to_fate) ->
erlang_to_fate({O, N, signature}, S) ->
coerce_chain_object(O, N, signature, signature, S);
coerce({_, _, signature}, Bin, from_fate) ->
Address = gmser_api_encoder:encode(signature, Bin),
{ok, unicode:characters_to_list(Address)};
%coerce({_, _, channel}, S, to_fate) when is_binary(S) ->
%erlang_to_fate({_, _, channel}, S) when is_binary(S) ->
%{ok, {channel, S}};
%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) ->
%{ok, S};
coerce({_, _, boolean}, true, _) ->
erlang_to_fate({_, _, boolean}, true) ->
{ok, true};
coerce({_, _, boolean}, "true", _) ->
erlang_to_fate({_, _, boolean}, "true") ->
{ok, true};
coerce({_, _, boolean}, false, _) ->
erlang_to_fate({_, _, boolean}, false) ->
{ok, false};
coerce({_, _, boolean}, "false", _) ->
erlang_to_fate({_, _, boolean}, "false") ->
{ok, false};
coerce({O, N, boolean}, S, _) ->
single_error({invalid, O, N, S});
coerce({O, N, string}, Str, Direction) ->
Result = case Direction of
to_fate -> unicode:characters_to_binary(Str);
from_fate -> unicode:characters_to_list(Str)
end,
case Result of
erlang_to_fate({O, N, string}, Str) ->
case unicode:characters_to_binary(Str) of
{error, _, _} ->
single_error({invalid, O, N, Str});
{incomplete, _, _} ->
@ -539,9 +494,9 @@ coerce({O, N, string}, Str, Direction) ->
StrBin ->
{ok, StrBin}
end;
coerce({_, _, char}, Val, _Direction) when is_integer(Val) ->
erlang_to_fate({_, _, char}, Val) when is_integer(Val) ->
{ok, Val};
coerce({O, N, char}, Str, to_fate) ->
erlang_to_fate({O, N, char}, Str) ->
Result = unicode:characters_to_list(Str),
case Result of
{error, _, _} ->
@ -553,27 +508,24 @@ coerce({O, N, char}, Str, to_fate) ->
_ ->
single_error({invalid, O, N, Str})
end;
coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) ->
erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) ->
coerce_bytes(O, N, Count, Bytes);
coerce({_, _, bits}, {bits, Num}, from_fate) ->
{ok, Num};
coerce({_, _, bits}, Num, to_fate) when is_integer(Num) ->
erlang_to_fate({_, _, bits}, Num) when is_integer(Num) ->
{ok, {bits, Num}};
coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) ->
erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) ->
Size = bit_size(Bits),
<<IntValue:Size>> = Bits,
{ok, {bits, IntValue}};
coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) ->
coerce_list(Type, Data, Direction);
coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, Direction);
coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) ->
erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) ->
coerce_list(Type, Data, to_fate);
erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, to_fate);
erlang_to_fate({O, N, {tuple, ElementTypes}}, Data) when is_tuple(Data) ->
ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, to_fate);
coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) ->
ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, from_fate);
coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_size(Data) > 0 ->
erlang_to_fate({O, N, {variant, Variants}}, Name) when is_list(Name) ->
erlang_to_fate({O, N, {variant, Variants}}, {Name});
erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_size(Data) > 0 ->
[Name | Terms] = tuple_to_list(Data),
case lookup_variant(Name, Variants) of
{Tag, TermTypes} ->
@ -582,17 +534,9 @@ coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_si
ValidNames = [Valid || {Valid, _} <- Variants],
single_error({invalid_variant, O, N, Name, ValidNames})
end;
coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) ->
coerce({O, N, {variant, Variants}}, {Name}, to_fate);
coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) ->
erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) ->
coerce_map_to_record(O, N, MemberTypes, Map);
coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
coerce({O, N, {unknown_type, _}}, Data, _) ->
erlang_to_fate({O, N, {unknown_type, _}}, Data) ->
case N of
already_normalized ->
Message = "Warning: Unknown type ~p. Using term ~p as is.~n",
@ -602,24 +546,7 @@ coerce({O, N, {unknown_type, _}}, Data, _) ->
io:format(Message, [O, N, Data])
end,
{ok, Data};
coerce({O, N, _}, Data, from_fate) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
{ok, Data};
coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}).
coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
single_error({partial_bytes, O, N, bit_size(Bytes)});
coerce_bytes(_, _, any, Bytes) ->
{ok, Bytes};
coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
single_error({incorrect_size, O, N, Bytes});
coerce_bytes(_, _, _, Bytes) ->
{ok, Bytes}.
erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}).
coerce_chain_object(_, _, _, _, {raw, Binary}) ->
{ok, Binary};
@ -644,12 +571,38 @@ decode_chain_object(Tag, S) ->
error:incorrect_size -> {error, incorrect_size}
end.
coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
single_error({partial_bytes, O, N, bit_size(Bytes)});
coerce_bytes(_, _, any, Bytes) ->
{ok, Bytes};
coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
single_error({incorrect_size, O, N, Bytes});
coerce_bytes(_, _, _, Bytes) ->
{ok, Bytes}.
coerce_zipped_bindings(Bindings, Direction, Tag) ->
coerce_zipped_bindings(Bindings, Direction, Tag, [], []).
coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) ->
{{ArgName, Type}, Term} = Next,
case coerce_direction(Type, Term, Direction) of
{ok, NewTerm} ->
coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({Tag, ArgName}, Errors),
coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken])
end;
coerce_zipped_bindings([], _, _, Good, []) ->
{ok, lists:reverse(Good)};
coerce_zipped_bindings([], _, _, _, Broken) ->
{error, combine_errors(Broken)}.
coerce_list(Type, Elements, Direction) ->
% 0 index since it represents a sophia list
coerce_list(Type, Elements, Direction, 0, [], []).
coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) ->
case coerce(Type, Next, Direction) of
case coerce_direction(Type, Next, Direction) of
{ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken);
{error, Errors} ->
Wrapped = wrap_errors({index, Index}, Errors),
@ -672,7 +625,7 @@ coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) ->
end.
coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
case coerce(KeyType, K, Direction) of
case coerce_direction(KeyType, K, Direction) of
{ok, KFATE} ->
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE);
{error, Errors} ->
@ -683,7 +636,7 @@ coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
end.
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) ->
case coerce(ValType, V, Direction) of
case coerce_direction(ValType, V, Direction) of
{ok, VFATE} ->
NewGood = Good#{KFATE => VFATE},
coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken);
@ -720,11 +673,6 @@ coerce_tuple(O, N, TermTypes, Terms, Direction) ->
Errors -> Errors
end.
% Wraps a single error in a list, along with an empty path, so that other
% accumulating error handlers can work with it.
single_error(Reason) ->
{error, [{Reason, []}]}.
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) ->
% FIXME: we could go through and add the variant tag to the adt_element
% paths?
@ -751,7 +699,7 @@ coerce_tuple_elements(Types, Terms, Direction, Tag) ->
coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []).
coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) ->
case coerce(Type, Term, Direction) of
case coerce_direction(Type, Term, Direction) of
{ok, Value} ->
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken);
{error, Errors} ->
@ -820,6 +768,91 @@ zip_record_field({Name, Type}, {Remaining, Missing}) ->
{missing, {Remaining, [Name | Missing]}}
end.
% Wraps a single error in a list, along with an empty path, so that other
% accumulating error handlers can work with it.
single_error(Reason) ->
{error, [{Reason, []}]}.
wrap_errors(Location, Errors) ->
F = fun({Error, Path}) ->
{Error, [Location | Path]}
end,
lists:map(F, Errors).
combine_errors(Broken) ->
F = fun(NextErrors, Acc) ->
NextErrors ++ Acc
end,
lists:foldl(F, [], Broken).
%%% FATE to Erlang
% Not sure if this is needed... fate_to_erlang shouldn't fail.
coerce_direction(Type, Term, to_fate) ->
erlang_to_fate(Type, Term);
coerce_direction(Type, Term, from_fate) ->
fate_to_erlang(Type, Term).
fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
{ok, S};
fate_to_erlang({_, _, address}, {address, Bin}) ->
Address = gmser_api_encoder:encode(account_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
fate_to_erlang({_, _, contract}, {contract, Bin}) ->
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
{ok, unicode:characters_to_list(Address)};
fate_to_erlang({_, _, signature}, Bin) ->
Address = gmser_api_encoder:encode(signature, Bin),
{ok, unicode:characters_to_list(Address)};
%fate_to_erlang({_, _, channel}, {channel, S}) when is_binary(S) ->
%{ok, S};
fate_to_erlang({_, _, boolean}, true) ->
{ok, true};
fate_to_erlang({_, _, boolean}, false) ->
{ok, false};
fate_to_erlang({_, _, string}, Bin) ->
Str = binary_to_list(Bin),
{ok, Str};
fate_to_erlang({_, _, char}, Val) ->
{ok, Val};
fate_to_erlang({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) ->
coerce_bytes(O, N, Count, Bytes);
fate_to_erlang({_, _, bits}, {bits, Num}) ->
{ok, Num};
fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) ->
coerce_list(Type, Data, from_fate);
fate_to_erlang({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) ->
coerce_map(KeyType, ValType, Data, from_fate);
fate_to_erlang({O, N, {tuple, ElementTypes}}, {tuple, Data}) ->
ElementList = tuple_to_list(Data),
coerce_tuple(O, N, ElementTypes, ElementList, from_fate);
fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
case N of
already_normalized ->
Message = "Warning: Unknown type ~p. Using term ~p as is.~n",
io:format(Message, [O, Data]);
_ ->
Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n",
io:format(Message, [O, N, Data])
end,
{ok, Data};
fate_to_erlang({O, N, _}, Data) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
{ok, Data}.
%%% AACI Getters
@ -841,38 +874,44 @@ aaci_get_function_signature({aaci, _, FunDefs, _}, Fun) ->
end.
%%% Simple coerce/3 tests
%%% Simple FATE/erlang tests
% Round trip coerce run for the eunit tests below. If these results don't match
% then the test should fail.
try_coerce(Type, Sophia, Fate) ->
% Run both first, to see if they fail to produce any result.
{ok, FateActual} = coerce(Type, Sophia, to_fate),
{ok, SophiaActual} = coerce(Type, Fate, from_fate),
% Now check that the results were what we expected.
check_erlang_to_fate(Type, Sophia, Fate) ->
{ok, FateActual} = erlang_to_fate(Type, Sophia),
case FateActual of
Fate ->
ok;
_ ->
erlang:error({to_fate_failed, Fate, FateActual})
end,
end.
check_fate_to_erlang(Type, Fate, Sophia) ->
{ok, SophiaActual} = fate_to_erlang(Type, Fate),
% Now check that the results were what we expected.
case SophiaActual of
Sophia ->
ok;
_ ->
erlang:error({from_fate_failed, Sophia, SophiaActual})
end,
end.
% Round trip coerce run for the eunit tests below. If these results don't match
% then the test should fail.
check_roundtrip(Type, Sophia, Fate) ->
check_erlang_to_fate(Type, Sophia, Fate),
check_fate_to_erlang(Type, Fate, Sophia),
% Finally, check that the FATE result is something that gmb understands.
gmb_fate_encoding:serialize(Fate),
ok.
coerce_int_test() ->
{ok, Type} = annotate_type(integer, #{}),
try_coerce(Type, 123, 123).
check_roundtrip(Type, 123, 123).
coerce_address_test() ->
{ok, Type} = annotate_type(address, #{}),
try_coerce(Type,
check_roundtrip(Type,
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
@ -880,7 +919,7 @@ coerce_address_test() ->
coerce_contract_test() ->
{ok, Type} = annotate_type(contract, #{}),
try_coerce(Type,
check_roundtrip(Type,
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
{contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
@ -888,7 +927,7 @@ coerce_contract_test() ->
coerce_signature_test() ->
{ok, Type} = annotate_type(signature, #{}),
try_coerce(Type,
check_roundtrip(Type,
"sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n",
<<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
@ -901,69 +940,69 @@ coerce_signature_binary_test() ->
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
233,110,119,143,206,137,124,36,63,154,85,7>>,
{ok, Binary} = coerce(Type, {raw, Binary}, to_fate),
{ok, Binary} = coerce(Type, Binary, to_fate),
{ok, Binary} = erlang_to_fate(Type, {raw, Binary}),
{ok, Binary} = erlang_to_fate(Type, Binary),
ok.
coerce_bool_test() ->
{ok, Type} = annotate_type(boolean, #{}),
try_coerce(Type, true, true),
try_coerce(Type, false, false).
check_roundtrip(Type, true, true),
check_roundtrip(Type, false, false).
coerce_string_test() ->
{ok, Type} = annotate_type(string, #{}),
try_coerce(Type, "hello world", <<"hello world">>).
check_roundtrip(Type, "hello world", <<"hello world">>).
coerce_list_test() ->
{ok, Type} = annotate_type({list, [string]}, #{}),
try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
check_roundtrip(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
coerce_map_test() ->
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}),
try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
check_roundtrip(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
coerce_tuple_test() ->
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}),
try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
coerce_variant_test() ->
{ok, Type} = annotate_type({variant, [{"A", [integer]},
{"B", [integer, integer]}]},
#{}),
try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
check_roundtrip(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
check_roundtrip(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
coerce_option_test() ->
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}),
try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
check_roundtrip(Type, {"None"}, {variant, [0, 1], 0, {}}),
check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
coerce_record_test() ->
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
coerce_bytes_test() ->
{ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}),
try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}).
check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}).
coerce_bits_test() ->
{ok, Type} = annotate_type(bits, #{}),
try_coerce(Type, 5, {bits, 5}).
check_roundtrip(Type, 5, {bits, 5}).
coerce_char_test() ->
{ok, Type} = annotate_type(char, #{}),
try_coerce(Type, $?, $?).
check_roundtrip(Type, $?, $?).
coerce_unicode_test() ->
{ok, Type} = annotate_type(char, #{}),
% Latin Small Letter C with cedilla and acute
{ok, $ḉ} = coerce(Type, <<""/utf8>>, to_fate),
{ok, $ḉ} = erlang_to_fate(Type, <<""/utf8>>),
ok.
coerce_hash_test() ->
{ok, Type} = annotate_type("hash", builtin_typedefs()),
Hash = list_to_binary(lists:seq(1,32)),
try_coerce(Type, Hash, Hash),
check_roundtrip(Type, Hash, Hash),
ok.
@ -985,7 +1024,7 @@ namespace_coerce_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
record_substitution_test() ->
Contract = "
@ -995,7 +1034,7 @@ record_substitution_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
tuple_substitution_test() ->
Contract = "
@ -1005,7 +1044,7 @@ tuple_substitution_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
check_roundtrip(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
variant_substitution_test() ->
Contract = "
@ -1015,8 +1054,8 @@ variant_substitution_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
try_coerce(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
check_roundtrip(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
check_roundtrip(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
nested_coerce_test() ->
Contract = "
@ -1027,7 +1066,7 @@ nested_coerce_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "f"),
try_coerce(Output,
check_roundtrip(Output,
#{ "f1" => {1, 2}, "f2" => {"a", "b"}},
{tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}).
@ -1039,7 +1078,7 @@ state_coerce_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], Output}} = aaci_get_function_signature(AACI, "init"),
try_coerce(Output, 0, 0).
check_roundtrip(Output, 0, 0).
param_test() ->
Contract = "
@ -1049,8 +1088,8 @@ param_test() ->
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[{"x", Input}], Output}} = aaci_get_function_signature(AACI, "init"),
try_coerce(Input, 0, 0),
try_coerce(Output, 0, 0).
check_roundtrip(Input, 0, 0),
check_roundtrip(Output, 0, 0).
%%% Obscure Sophia types where we should check the AACI as well
@ -1126,21 +1165,21 @@ name_coerce_test() ->
{ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()),
TTLSoph = {"FixedTTL", 0},
TTLFate = {variant, [1, 1], 0, {0}},
try_coerce(TTL, TTLSoph, TTLFate),
check_roundtrip(TTL, TTLSoph, TTLFate),
{ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()),
PointeeSoph = {"AccountPt", AddrSoph},
PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}},
try_coerce(Pointee, PointeeSoph, PointeeFate),
check_roundtrip(Pointee, PointeeSoph, PointeeFate),
{ok, Name} = annotate_type("AENS.name", builtin_typedefs()),
NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}},
NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}},
try_coerce(Name, NameSoph, NameFate).
check_roundtrip(Name, NameSoph, NameFate).
void_coerce_test() ->
% Void itself can't be represented, but other types built out of void are
% valid.
{ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()),
try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
check_roundtrip(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
{ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()),
try_coerce(NonList, [], []).
check_roundtrip(NonList, [], []).