diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index c4fd79f..4aa095d 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, 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) -> @@ -1120,6 +1137,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, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}). + tuple_substitution_test() -> Contract = " contract C =