diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index c4fd79f..ebf1c6a 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -796,6 +796,10 @@ coerce_map_to_record(O, N, MemberTypes, Map) -> case zip_record_fields(MemberTypes, Map) of {ok, Zipped} -> case coerce_zipped_bindings(Zipped, to_fate, field) of + {ok, [SingleElem]} -> + % Singleton records aren't implemented as FATE tuples at + % all. + {ok, SingleElem}; {ok, Converted} -> {ok, {tuple, list_to_tuple(Converted)}}; Errors -> @@ -821,10 +825,18 @@ coerce_record_to_map(O, N, MemberTypes, Tuple) -> single_error({record_too_few_terms, O, N, Tuple}); {error, too_many_terms} -> single_error({record_too_many_terms, O, N, Tuple}); - Errors -> - Errors + {error, Errors} -> + correct_record_error_paths(Names, Errors) end. +correct_record_error_paths(Names, Errors) -> + CorrectOne = fun({Error, [{record_element, N} | Path]}) -> + FieldName = lists:nth(N + 1, Names), + {Error, [{record_element, N, FieldName} | Path]} + end, + Corrected = lists:map(CorrectOne, Errors), + {error, Corrected}. + zip_record_fields(Fields, Map) -> case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of {_, {_, Missing = [_|_]}} -> @@ -915,6 +927,11 @@ 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, [SingleMemberType]}}, Data) -> + % Singleton records aren't implemented as FATE tuples at all. + % Pretend they are, so we can get the full error indexing of the + % non-singletone case. + coerce_record_to_map(O, N, [SingleMemberType], {Data}); 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) -> @@ -927,15 +944,30 @@ fate_to_erlang({O, N, {unknown_type, _}}, Data) -> 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, +fate_to_erlang(Type, Data) -> + TypeStr = type_to_iolist(Type), + io:format("Warning: Could not coerce term into ~s. Using term as is: ~p~n", [TypeStr, Data]), {ok, Data}. +type_to_iolist({O, already_normalized, S}) -> + % Already normalized. Example output: + % type {map, [string, integer]} + opaque_type_to_iolist(O, S); +type_to_iolist({O, N, S}) -> + % Type alias. Print the alias, and then print the normalized version in + % parentheses. Example output: + % type "my_alias" (i.e. record type {"my_record_type", [integer]}) + io_lib:format("type ~p (i.e. ~s)", [O, opaque_type_to_iolist(N, S)]). + +opaque_type_to_iolist(N, {record, _}) -> + % N is the name of a record definition. + io_lib:format("record type ~p", [N]); +opaque_type_to_iolist(N, {variant, _}) -> + % N is the name of a variant definition. + io_lib:format("variant type ~p", [N]); +opaque_type_to_iolist(N, _) -> + % N is some other constructive type. + io_lib:format("type ~p", [N]). %%% AACI Getters @@ -1120,6 +1152,25 @@ record_substitution_test() -> {ok, {[], Output}} = get_function_signature(AACI, "f"), check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}). +singleton_record_substitution_test() -> + Contract = " + contract C = + record single('t) = { it: 't } + entrypoint f(): single(int) = { it = 1 } + entrypoint g(): single(single(int)) = { it = { it = 2 } } + entrypoint h(): single(int * int) = { it = (3, 4) } + ", + {ok, AACI} = aaci_from_string(Contract), + {ok, {[], FOutput}} = get_function_signature(AACI, "f"), + check_roundtrip(FOutput, #{"it" => 123}, 123), + {ok, {[], GOutput}} = get_function_signature(AACI, "g"), + check_roundtrip(GOutput, #{"it" => #{"it" => 123}}, 123), + {ok, {[], HOutput}} = get_function_signature(AACI, "h"), + check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}), + % Also check that records have accurate paths, since the implementation for + % record error paths is a bit fiddly. + {error, [{{tuple_too_many_terms, _, _, _}, [{record_element, 0, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}). + tuple_substitution_test() -> Contract = " contract C =