Skip to content

Commit 2151487

Browse files
committed
Add full support for maps
1 parent a2c9fa4 commit 2151487

File tree

7 files changed

+269
-13
lines changed

7 files changed

+269
-13
lines changed

include/proper.hrl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
%%------------------------------------------------------------------------------
4848

4949
-import(proper_types, [integer/2, float/2, atom/0, binary/0, binary/1,
50-
bitstring/0, bitstring/1, list/1, vector/2, union/1,
51-
weighted_union/1, tuple/1, loose_tuple/1, exactly/1,
52-
fixed_list/1, function/2, map/2, any/0]).
50+
bitstring/0, bitstring/1, list/1, map/1, map/2, map_union/2,
51+
vector/2, union/1, weighted_union/1, tuple/1, loose_tuple/1,
52+
exactly/1, fixed_list/1, fixed_map/1, function/2, any/0]).
5353

5454

5555
%%------------------------------------------------------------------------------

src/proper_gen.erl

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
binary_rev/1, binary_len_gen/1, bitstring_gen/1, bitstring_rev/1,
4343
bitstring_len_gen/1, list_gen/2, distlist_gen/3, vector_gen/2,
4444
union_gen/1, weighted_union_gen/1, tuple_gen/1, loose_tuple_gen/2,
45-
loose_tuple_rev/2, exactly_gen/1, fixed_list_gen/1, function_gen/2,
46-
any_gen/1, native_type_gen/2, safe_weighted_union_gen/1,
45+
loose_tuple_rev/2, exactly_gen/1, fixed_list_gen/1, fixed_map_gen/1,
46+
function_gen/2, any_gen/1, native_type_gen/2, safe_weighted_union_gen/1,
4747
safe_union_gen/1]).
4848

4949
%% Public API types
@@ -344,6 +344,12 @@ clean_instance({'$to_part',ImmInstance}) ->
344344
clean_instance(ImmInstance);
345345
clean_instance(ImmInstance) when is_list(ImmInstance) ->
346346
clean_instance_list(ImmInstance);
347+
clean_instance(ImmInstance) when is_map(ImmInstance) ->
348+
%% maps:map only changes the values, this handles both values and keys
349+
maps:from_list([
350+
{clean_instance(Key), clean_instance(Value)}
351+
|| {Key, Value} <- maps:to_list(ImmInstance)
352+
]);
347353
clean_instance(ImmInstance) when is_tuple(ImmInstance) ->
348354
list_to_tuple(clean_instance_list(tuple_to_list(ImmInstance)));
349355
clean_instance(ImmInstance) -> ImmInstance.
@@ -576,6 +582,15 @@ fixed_list_gen({ProperHead,ImproperTail}) ->
576582
fixed_list_gen(ProperFields) ->
577583
[generate(F) || F <- ProperFields].
578584

585+
%% @private
586+
-spec fixed_map_gen(map()) -> imm_instance().
587+
fixed_map_gen(Map) when is_map(Map) ->
588+
maps:from_list([
589+
{generate(KeyOrType), generate(ValueOrType)}
590+
||
591+
{KeyOrType, ValueOrType} <- maps:to_list(Map)
592+
]).
593+
579594
%% @private
580595
-spec function_gen(arity(), proper_types:type()) -> function().
581596
function_gen(Arity, RetType) ->

src/proper_shrink.erl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
-export([number_shrinker/4, union_first_choice_shrinker/3,
3434
union_recursive_shrinker/3]).
3535
-export([split_shrinker/3, remove_shrinker/3]).
36+
-export([map_remove_shrinker/3, map_key_shrinker/3, map_value_shrinker/3]).
3637

3738
-export_type([state/0, shrinker/0]).
3839

@@ -397,6 +398,110 @@ elements_shrinker(Instance, Type,
397398
elements_shrinker(Instance, Type,
398399
{inner,Indices,GetElemType,{shrunk,N,InnerState}}).
399400

401+
-spec map_remove_shrinker(
402+
proper_gen:imm_instance(), proper_types:type(), state()
403+
) -> {[proper_gen:imm_instance()], state()}.
404+
map_remove_shrinker(Instance, Type, init) when is_map(Instance) ->
405+
GetKeys = proper_types:get_prop(get_keys, Type),
406+
Keys = GetKeys(Instance),
407+
map_remove_shrinker(Instance, Type, {shrunk, 1, {keys, ordsets:new(), Keys}});
408+
map_remove_shrinker(Instance, _Type, {keys, _Checked, []}) when is_map(Instance) ->
409+
{[], done};
410+
map_remove_shrinker(Instance, Type, {keys, Checked, [Key | Rest]}) when is_map(Instance) ->
411+
Remove = proper_types:get_prop(remove, Type),
412+
{[Remove(Key, Instance)], {keys, ordsets:add_element(Key, Checked), Rest}};
413+
map_remove_shrinker(Instance, Type, {shrunk, 1, {keys, Checked, ToCheck}}) when is_map(Instance) ->
414+
%% GetKeys = proper_types:get_prop(get_keys, Type),
415+
%% Keys = ordsets:from_list(GetKeys(Instance)),
416+
%% NewToCheck = ordsets:subtract(Keys, Checked),
417+
map_remove_shrinker(Instance, Type, {keys, Checked, ToCheck}).
418+
419+
-spec map_value_shrinker(
420+
proper_gen:imm_instance(), proper_types:type(), state()
421+
) -> {[proper_gen:imm_instance()], state()}.
422+
map_value_shrinker(Instance, _Type, init) when map_size(Instance) =:= 0 ->
423+
{[], done};
424+
map_value_shrinker(Instance, Type, init) when is_map(Instance) ->
425+
GetKeys = proper_types:get_prop(get_keys, Type),
426+
TypeMap = proper_types:get_prop(internal_types, Type),
427+
Keys = GetKeys(Instance),
428+
ValueTypeMap = maps:map(fun(Key, Value) ->
429+
{_KeyType, ValueType} = get_map_field_candidates(Key, Value, TypeMap),
430+
ValueType
431+
end, Instance),
432+
map_value_shrinker(Instance, Type, {inner, Keys, ValueTypeMap, init});
433+
map_value_shrinker(Instance, _Type, {inner, [], _ValueTypeMap, init}) when is_map(Instance) ->
434+
{[], done};
435+
map_value_shrinker(
436+
Instance, Type, {inner, [_Key | Rest], ValueTypeMap, done}
437+
) when is_map(Instance) ->
438+
map_value_shrinker(Instance, Type, {inner, Rest, ValueTypeMap, init});
439+
map_value_shrinker(
440+
Instance, Type, {inner, Keys = [Key | _], ValueTypeMap, InnerState}
441+
) when is_map(Instance) ->
442+
Retrieve = proper_types:get_prop(retrieve, Type),
443+
Update = proper_types:get_prop(update, Type),
444+
Value = Retrieve(Key, Instance),
445+
ValueType = Retrieve(Key, ValueTypeMap),
446+
{NewValues, NewInnerState} = shrink(Value, ValueType, InnerState),
447+
NewInstances = [Update(Key, NewValue, Instance) || NewValue <- NewValues],
448+
{NewInstances, {inner, Keys, ValueTypeMap, NewInnerState}};
449+
map_value_shrinker(
450+
Instance, Type, {shrunk, N, {inner, ToCheck, ValueTypeMap, InnerState}}
451+
) when is_map(Instance) ->
452+
map_value_shrinker(
453+
Instance, Type, {inner, ToCheck, ValueTypeMap, {shrunk, N, InnerState}}
454+
).
455+
456+
-spec map_key_shrinker(
457+
proper_gen:imm_instance(), proper_types:type(), state()
458+
) -> {[proper_gen:imm_instance()], state()}.
459+
map_key_shrinker(Instance, Type, init) when is_map(Instance) ->
460+
GetKeys = proper_types:get_prop(get_keys, Type),
461+
TypeMap = proper_types:get_prop(internal_types, Type),
462+
Keys = GetKeys(Instance),
463+
KeyTypeMap = maps:map(fun(Key, Value) ->
464+
{KeyType, _ValueType} = get_map_field_candidates(Key, Value, TypeMap),
465+
KeyType
466+
end, Instance),
467+
map_key_shrinker(Instance, Type, {inner, Keys, KeyTypeMap, init});
468+
map_key_shrinker(Instance, _Type, {inner, [], _ValueTypeMap, init}) when is_map(Instance) ->
469+
{[], done};
470+
map_key_shrinker(
471+
Instance, Type, {inner, [_Key | Rest], KeyTypeMap, done}
472+
) when is_map(Instance) ->
473+
map_key_shrinker(Instance, Type, {inner, Rest, KeyTypeMap, init});
474+
map_key_shrinker(
475+
Instance, Type, {inner, Keys = [Key | _], KeyTypeMap, InnerState}
476+
) when is_map(Instance) ->
477+
Retrieve = proper_types:get_prop(retrieve, Type),
478+
Update = proper_types:get_prop(update, Type),
479+
Remove = proper_types:get_prop(remove, Type),
480+
Value = Retrieve(Key, Instance),
481+
KeyType = Retrieve(Key, KeyTypeMap),
482+
{NewKeys, NewInnerState} = shrink(Key, KeyType, InnerState),
483+
InstanceWithoutKey = Remove(Key, Instance),
484+
NewInstances = [
485+
Update(NewKey, Value, InstanceWithoutKey) || NewKey <- NewKeys
486+
],
487+
{NewInstances, {inner, Keys, KeyTypeMap, NewInnerState}};
488+
map_key_shrinker(
489+
Instance, Type, {shrunk, N, {inner, ToCheck, KeyTypeMap, InnerState}}
490+
) when is_map(Instance) ->
491+
map_key_shrinker(
492+
Instance, Type, {inner, ToCheck, KeyTypeMap, {shrunk, N, InnerState}}
493+
).
494+
495+
get_map_field_candidates(Key, Value, TypeMap) ->
496+
Candidates = maps:filter(fun(KeyType, ValueType) ->
497+
(not proper_types:is_type(Key) andalso Key =:= KeyType
498+
orelse proper_types:is_instance(Key, KeyType))
499+
andalso
500+
(not proper_types:is_type(Value) andalso Value =:= ValueType
501+
orelse proper_types:is_instance(Value, ValueType))
502+
end, TypeMap),
503+
{KeyType, ValueType, _} = maps:next(maps:iterator(Candidates)),
504+
{KeyType, ValueType}.
400505

401506
%%------------------------------------------------------------------------------
402507
%% Custom shrinkers

src/proper_types.erl

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@
141141

142142
-export([integer/2, float/2, atom/0, binary/0, binary/1, bitstring/0,
143143
bitstring/1, list/1, vector/2, union/1, weighted_union/1, tuple/1,
144-
loose_tuple/1, exactly/1, fixed_list/1, function/2, map/0, map/2,
145-
any/0, shrink_list/1, safe_union/1, safe_weighted_union/1]).
144+
loose_tuple/1, exactly/1, fixed_list/1, fixed_map/1, function/2, map/0,
145+
map/1, map/2, any/0, shrink_list/1, safe_union/1, safe_weighted_union/1]).
146146
-export([integer/0, non_neg_integer/0, pos_integer/0, neg_integer/0, range/2,
147147
float/0, non_neg_float/0, number/0, boolean/0, byte/0, char/0, nil/0,
148148
list/0, tuple/0, string/0, wunion/1, term/0, timeout/0, arity/0]).
149149
-export([int/0, nat/0, largeint/0, real/0, bool/0, choose/2, elements/1,
150150
oneof/1, frequency/1, return/1, default/2, orderedlist/1, function0/1,
151-
function1/1, function2/1, function3/1, function4/1,
151+
function1/1, function2/1, function3/1, function4/1, map_union/1,
152152
weighted_default/2]).
153153
-export([resize/2, non_empty/1, noshrink/1]).
154154

@@ -258,7 +258,7 @@
258258
| {'shrinkers', [proper_shrink:shrinker()]}
259259
| {'noshrink', boolean()}
260260
| {'internal_type', raw_type()}
261-
| {'internal_types', tuple() | maybe_improper_list(type(),type() | [])}
261+
| {'internal_types', tuple() | map() | maybe_improper_list(type(),type() | [])}
262262
%% The items returned by 'remove' must be of this type.
263263
| {'get_length', fun((proper_gen:imm_instance()) -> length())}
264264
%% If this is a container type, this should return the number of elements
@@ -312,6 +312,8 @@ cook_outer(RawType) when is_tuple(RawType) ->
312312
tuple(tuple_to_list(RawType));
313313
cook_outer(RawType) when is_list(RawType) ->
314314
fixed_list(RawType); %% CAUTION: this must handle improper lists
315+
cook_outer(RawType) when is_map(RawType) ->
316+
fixed_map(RawType);
315317
cook_outer(RawType) -> %% default case (integers, floats, atoms, binaries, ...)
316318
exactly(RawType).
317319

@@ -1113,12 +1115,91 @@ function_is_instance(Type, X) ->
11131115
%% TODO: what if it's not a function we produced?
11141116
andalso equal_types(RetType, proper_gen:get_ret_type(X)).
11151117

1118+
%% @doc A map whose keys and values are defined by the given `Map'.
1119+
%%
1120+
%% Shrinks towards the empty map. That is, all keys are assumed to be optional.
1121+
%%
1122+
%% Also written simply as a {@link maps. map}.
1123+
-spec map(#{Key::raw_type() => Value::raw_type()}) -> proper_types:type().
1124+
map(Map) when is_map(Map) ->
1125+
MapType = fixed_map(Map),
1126+
Shrinkers = get_prop(shrinkers, MapType),
1127+
add_props([
1128+
{remove,fun maps:remove/2},
1129+
{shrinkers, [
1130+
fun proper_shrink:map_remove_shrinker/3,
1131+
fun proper_shrink:map_key_shrinker/3
1132+
| Shrinkers
1133+
]}
1134+
], MapType).
1135+
11161136
%% @doc A map whose keys are defined by the generator `K' and values
11171137
%% by the generator `V'.
11181138
-spec map(K::raw_type(), V::raw_type()) -> proper_types:type().
11191139
map(K, V) ->
11201140
?LET(L, list({K, V}), maps:from_list(L)).
11211141

1142+
%% @doc A map merged from the given map generators.
1143+
-spec map_union([Map::raw_type()]) -> proper_types:type().
1144+
map_union(RawMaps) when is_list(RawMaps) ->
1145+
?LET(Maps, RawMaps, lists:foldl(fun maps:merge/2, #{}, Maps)).
1146+
1147+
%% @doc A map whose keys and values are defined by the given `Map'.
1148+
%% Also written simply as a {@link maps. map}.
1149+
-spec fixed_map(#{Key::raw_type() => Value::raw_type()}) -> proper_types:type().
1150+
fixed_map(Map) when is_map(Map) ->
1151+
WithValueTypes = maps:map(fun(_Key, Value) -> cook_outer(Value) end, Map),
1152+
?CONTAINER([
1153+
{generator, {typed, fun map_gen/1}},
1154+
{is_instance, {typed, fun map_is_instance/2}},
1155+
{shrinkers, [
1156+
fun proper_shrink:map_value_shrinker/3
1157+
]},
1158+
{internal_types, WithValueTypes},
1159+
{get_length, fun maps:size/1},
1160+
{join, fun maps:merge/2},
1161+
{get_keys, fun maps:keys/1},
1162+
{retrieve, fun maps:get/2},
1163+
{update, fun maps:put/3}
1164+
]).
1165+
1166+
map_gen(Type) ->
1167+
Map = get_prop(internal_types, Type),
1168+
proper_gen:fixed_map_gen(Map).
1169+
1170+
map_is_instance(Type, X) when is_map(X) ->
1171+
Map = get_prop(internal_types, Type),
1172+
map_all(
1173+
fun (Key, ValueType) when is_map_key(Key, X) ->
1174+
is_instance(maps:get(Key, X), ValueType);
1175+
(KeyOrType, ValueType) ->
1176+
case is_raw_type(KeyOrType) of
1177+
true ->
1178+
map_all(fun(Key, Value) ->
1179+
case is_instance(Key, KeyOrType) of
1180+
true -> is_instance(Value, ValueType);
1181+
false -> true %% Ignore other keys
1182+
end
1183+
end, X);
1184+
false ->
1185+
%% The key not a type and not in `X'
1186+
false
1187+
end
1188+
end,
1189+
Map
1190+
);
1191+
map_is_instance(_Type, _X) ->
1192+
false.
1193+
1194+
map_all(Fun, Map) when is_function(Fun, 2) andalso is_map(Map) ->
1195+
map_all_internal(Fun, maps:next(maps:iterator(Map)), true).
1196+
1197+
map_all_internal(Fun, _, false) when is_function(Fun, 2) ->
1198+
false;
1199+
map_all_internal(Fun, none, Result) when is_function(Fun, 2) andalso is_boolean(Result) ->
1200+
Result;
1201+
map_all_internal(Fun, {Key, Value, NextIterator}, true) when is_function(Fun, 2) ->
1202+
map_all_internal(Fun, NextIterator, Fun(Key, Value)).
11221203

11231204
%% @doc All Erlang terms (that PropEr can produce). For reasons of efficiency,
11241205
%% functions are never produced as instances of this type.<br />

src/proper_typeserver.erl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,8 @@ convert(_Mod, {type,_,nonempty_string,[]}, State, _Stack, _VarDict) ->
16481648
{ok, {simple,proper_types:non_empty(proper_types:string())}, State};
16491649
convert(_Mod, {type,_,map,any}, State, _Stack, _VarDict) ->
16501650
{ok, {simple,proper_types:map()}, State};
1651+
convert(Mod, {type,_,map,Fields}, State, Stack, VarDict) ->
1652+
convert_map(Mod, Fields, State, Stack, VarDict);
16511653
convert(_Mod, {type,_,tuple,any}, State, _Stack, _VarDict) ->
16521654
{ok, {simple,proper_types:tuple()}, State};
16531655
convert(Mod, {type,_,tuple,ElemForms}, State, Stack, VarDict) ->
@@ -1787,6 +1789,57 @@ convert_normal_rec_list(RecFun, RecArgs, NonEmpty) ->
17871789
NewRecArgs = clean_rec_args(RecArgs),
17881790
{NewRecFun, NewRecArgs}.
17891791

1792+
-spec convert_map(mod_name(), [Field], state(), stack(), var_dict()) ->
1793+
rich_result2(ret_type(), state())
1794+
when
1795+
Field :: {type, erl_anno:anno(), map_field_assoc, [abs_type()]}
1796+
| {type, erl_anno:anno(), map_field_exact, [abs_type()]}.
1797+
convert_map(Mod, Fields, State1, Stack, VarDict) ->
1798+
{AbstractRequiredFields, AbstractOptionalFields} = lists:partition(
1799+
fun ({type, _, map_field_exact, _FieldType}) ->
1800+
true;
1801+
({type, _, map_field_assoc, _FieldType}) ->
1802+
false
1803+
end,
1804+
Fields
1805+
),
1806+
case process_map_fields(Mod, AbstractRequiredFields, State1, Stack, VarDict) of
1807+
{ok, RequiredFields, State2} ->
1808+
case process_map_fields(Mod, AbstractOptionalFields, State2, Stack, VarDict) of
1809+
{ok, OptionalFields, State3} ->
1810+
Required = proper_types:fixed_map(maps:from_list(RequiredFields)),
1811+
Optional = proper_types:map(maps:from_list(OptionalFields)),
1812+
{ok, {simple, proper_types:map_union([Required, Optional])}, State3};
1813+
{error, Reason} ->
1814+
{error, Reason}
1815+
end;
1816+
{error, Reason} ->
1817+
{error, Reason}
1818+
end.
1819+
1820+
process_map_fields(Mod, AbstractFields, State, Stack, VarDict) ->
1821+
Process =
1822+
fun ({type, _, _, RawFieldTypes}, {ok, Fields, State1}) when
1823+
length(RawFieldTypes) =:= 2
1824+
->
1825+
case process_list(
1826+
Mod, RawFieldTypes, State1, [map | Stack], VarDict
1827+
) of
1828+
{ok, FieldTypes, State2} ->
1829+
{ok, [list_to_tuple(FieldTypes) | Fields], State2};
1830+
{error, Reason} ->
1831+
{error, Reason}
1832+
end;
1833+
(_FieldTypes, {error, Reason}) ->
1834+
{error, Reason}
1835+
end,
1836+
case lists:foldl(Process, {ok, [], State}, AbstractFields) of
1837+
{ok, ReverseFields, NewState} ->
1838+
{ok, lists:reverse(ReverseFields), NewState};
1839+
{error, Reason} ->
1840+
{error, Reason}
1841+
end.
1842+
17901843
-spec convert_tuple(mod_name(), [abs_type()], boolean(), state(), stack(),
17911844
var_dict()) -> rich_result2(ret_type(),state()).
17921845
convert_tuple(Mod, ElemForms, ToList, State, Stack, VarDict) ->

test/proper_exported_types_test.erl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@
4040
%%
4141
%% Still, the test is currently not 100% there.
4242
%% TODOs:
43-
%% - Eliminate the 12 errors that `proper_typeserver:demo_translate_type/2`
44-
%% currently returns. (Three of these errors are due to the incomplete
45-
%% handling of maps.)
4643
%% - Handle symbolic instances (the {'$call', ...} case below).
4744
%%
4845

test/proper_tests.erl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ simple_types_with_data() ->
410410
{[], [[]], [], [[a],[1,2,3]], "[]"},
411411
{fixed_list([neg_integer(),pos_integer()]), [[-12,32],[-1,1]], [-1,1],
412412
[[0,0]], none},
413+
{map(#{key => value, pos_integer() => neg_integer()}),
414+
[#{key => value, 1 => -1}], #{}, [not_a_map], none},
415+
{fixed_map(#{key => value, some_number => neg_integer()}),
416+
[#{key => value, some_number => -3}], #{key => value, some_number => -1},
417+
[not_a_map], none},
413418
{[atom(),integer(),atom(),float()], [[forty_two,42,forty_two,42.0]],
414419
['',0,'',0.0], [[proper,is,licensed],[under,the,gpl]], none},
415420
{[42 | list(integer())], [[42],[42,44,22]], [42], [[],[11,12]], none},
@@ -773,7 +778,7 @@ cant_generate_test_() ->
773778
[?_test(assert_cant_generate(Type)) || Type <- impossible_types()].
774779

775780
proper_exported_types_test_() ->
776-
[?_assertEqual({[],12}, proper_exported_types_test:not_handled())].
781+
[?_assertEqual({[],0}, proper_exported_types_test:not_handled())].
777782

778783
%%------------------------------------------------------------------------------
779784
%% Verify that failing constraints are correctly reported

0 commit comments

Comments
 (0)