Compare commits

...

12 Commits

Author SHA1 Message Date
dda5cac7a9 Merge pull request 'Serialization got broken by previous PR' (#54) from uw-fix-serialization into master
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m58s
Reviewed-on: #54
2025-04-29 17:22:22 +09:00
Ulf Wiger
07d61722b4 Serialization got broken by previous PR
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m58s
2025-04-29 10:18:59 +02:00
4ac7531351 Merge pull request 'uw-switch-semantics' (#53) from uw-switch-semantics into master
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m56s
Reviewed-on: #53
2025-04-29 03:57:16 +09:00
Ulf Wiger
f996253e6b Add forgotten exports, expand(Types) function
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m55s
2025-04-28 12:12:43 +02:00
Ulf Wiger
b9a51acf55 Add gmser_dyn_types.erl
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m54s
2025-04-28 11:59:27 +02:00
Ulf Wiger
5df23c05c1 test case for 'switch'
Some checks failed
Gajumaru Serialization Tests / tests (push) Failing after 49m55s
2025-04-28 11:51:23 +02:00
Ulf Wiger
b358dfe914 Add switch semantics
Some checks failed
Gajumaru Serialization Tests / tests (push) Failing after 49m54s
2025-04-28 11:36:02 +02:00
0288719ae1 Merge pull request 'Save options, test cases for missing labels' (#52) from uw-save-options into master
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m42s
Reviewed-on: #52
2025-04-24 06:46:26 +09:00
Ulf Wiger
795c7f7860 Save options, test cases for missing labels
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m42s
2025-04-23 23:36:03 +02:00
0d77ca0388 Fix function_clause bug (#51)
All checks were successful
Gajumaru Serialization Tests / tests (push) Successful in 49m16s
Co-authored-by: Ulf Wiger <ulf@wiger.net>
Reviewed-on: #51
2025-04-14 19:10:58 +09:00
ed204f8526 Merge pull request 'uw-dyn-options' (#50) from uw-dyn-options into master
Some checks failed
Gajumaru Serialization Tests / tests (push) Failing after 49m16s
Reviewed-on: #50
2025-04-14 18:59:38 +09:00
Ulf Wiger
a949d166f6 Add options for deserialization of missing labels
Some checks failed
Gajumaru Serialization Tests / tests (push) Failing after 49m16s
2025-04-14 11:54:48 +02:00
3 changed files with 440 additions and 105 deletions

View File

@ -58,6 +58,24 @@ Labels correspond to (existing) atoms in Erlang.
Decoding of a label results in a call to `binary_to_existing_atom/2`, so will Decoding of a label results in a call to `binary_to_existing_atom/2`, so will
fail if the corresponding atom does not already exist. fail if the corresponding atom does not already exist.
This behavior can be modified using the option `#{missing_labels => fail | create | convert}`,
where `fail` is the default, as described above, `convert` means that missing atoms are
converted to binaries, and `create` means that the atom is created dynamically.
The option can be passed e.g.:
```erlang
gmser_dyn:deserialize(Binary, set_opts(#{missing_labels => convert}))
```
or
```erlang
gmser_dyn:deserialize(Binary, set_opts(#{missing_labels => convert}, Types))
```
By calling `gmser_dyn:register_types/1`, after having added options to the type map,
the options can be made to take effect automatically.
It's possible to cache labels for more compact encoding. It's possible to cache labels for more compact encoding.
Note that when caching labels, the same cache mapping needs to be used on the Note that when caching labels, the same cache mapping needs to be used on the
decoder side. decoder side.

View File

@ -1,29 +1,44 @@
-module(gmser_dyn). -module(gmser_dyn).
-export([ encode/1 -export([ encode/1 %% (Term) -> rlp()
, encode/2 , encode/2 %% (Term, Types) -> rlp()
, encode_typed/2 , encode/3 %% (Term, Vsn, Types) -> rlp()
, encode_typed/3 , encode_typed/2 %% (Type, Term) -> rlp()
, decode/1 , encode_typed/3 %% (Type, Term, Types) -> rlp()
, decode/2 ]). , encode_typed/4 %% (Type, Term, Vsn, Types) -> rlp()
, decode/1 %% (RLP) -> Term
, decode/2 %% (RLP, Types) -> Term
, decode/3 %% (RLP, Vsn, Types) -> Term
, decode_typed/2 %% (Type, RLP) -> Term
, decode_typed/3 %% (Type, RLP, Types) -> Term
, decode_typed/4 ]). %% (Type, RLP, Vsn, Types) -> Term
-export([ serialize/1 -export([ serialize/1 %% (Term) -> Bin
, serialize/2 , serialize/2 %% (Term, Types) -> Bin
, serialize_typed/2 , serialize/3 %% (Term, Vsn, Types) -> Bin
, serialize_typed/3 , serialize_typed/2 %% (Type, Term) -> Bin
, deserialize/1 , serialize_typed/3 %% (Type, Term, Types) -> Bin
, deserialize/2 ]). , serialize_typed/4 %% (Type, Term, Vsn, Types) -> Bin
, deserialize/1 %% (Bin) -> Term
, deserialize/2 %% (Bin, Types) -> Term
, deserialize/3 ]). %% (Bin, Vsn, Types) -> Term
%% register a type schema, inspect existing schema %% register a type schema, inspect existing schema
-export([ register_types/1 -export([ register_types/1
, registered_types/0 , registered_types/0
, registered_types/1
, latest_vsn/0
, get_opts/1
, set_opts/1
, set_opts/2
, types_from_list/1 , types_from_list/1
, revert_to_default_types/0 , revert_to_default_types/0
, dynamic_types/0 ]). , dynamic_types/0 ]).
%% Register individual types, or cache labels %% Register individual types, or cache labels
-export([ register_type/3 -export([ register_type/3
, cache_label/2 ]). , cache_label/2
]).
-import(gmserialization, [ decode_field/2 ]). -import(gmserialization, [ decode_field/2 ]).
@ -36,41 +51,53 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-endif. -endif.
serialize(Term) -> rlp_encode(encode(Term)). serialize(Term) ->
serialize(Term, Types) -> rlp_encode(encode(Term, Types)). Vsn = latest_vsn(),
serialize_typed(Type, Term) -> rlp_encode(encode_typed(Type, Term)). rlp_encode(encode(Term, Vsn, registered_types(Vsn))).
serialize_typed(Type, Term, Types) -> rlp_encode(encode_typed(Type, Term, Types)).
deserialize(Binary) -> decode(rlp_decode(Binary)). serialize(Term, Types0) ->
deserialize(Binary, Types) -> decode(rlp_decode(Binary), Types). Types = proper_types(Types0),
Vsn = types_vsn(Types),
rlp_encode(encode(Term, Vsn, Types)).
serialize(Term, Vsn, Types) ->
rlp_encode(encode(Term, Vsn, proper_types(Types, Vsn))).
encode(Term) -> serialize_typed(Type, Term) ->
encode(Term, registered_types()). Vsn = latest_vsn(),
rlp_encode(encode_typed(Type, Term, Vsn, registered_types(Vsn))).
encode(Term, Types) -> serialize_typed(Type, Term, Types0) ->
encode(Term, vsn(Types), Types). Types = proper_types(Types0),
Vsn = types_vsn(Types),
rlp_encode(encode_typed(Type, Term, Vsn, Types)).
encode(Term, Vsn, Types) -> serialize_typed(Type, Term, Vsn, Types) ->
[ encode_basic(int, 0) rlp_encode(encode_typed(Type, Term, Vsn, proper_types(Types, Vsn))).
, encode_basic(int, Vsn)
, encode_(Term, Vsn, Types) ].
encode_typed(Type, Term) -> deserialize(Binary) ->
encode_typed(Type, Term, registered_types()). Fields0 = rlp_decode(Binary),
case decode_tag_and_vsn(Fields0) of
{0, Vsn, Fields} ->
decode_(Fields, Vsn, registered_types(Vsn));
Other ->
error({illegal_serialization, Other})
end.
encode_typed(Type, Term, Types) -> deserialize(Binary, Types0) ->
encode_typed(Type, Term, vsn(Types), Types). Types = proper_types(Types0),
Vsn0 = types_vsn(Types),
encode_typed(Type, Term, Vsn, Types) -> Fields0 = rlp_decode(Binary),
[ encode_basic(int, 0) case decode_tag_and_vsn(Fields0) of
, encode_basic(int, Vsn) {0, Vsn, Fields} when Vsn0 == undefined; Vsn0 == Vsn ->
, encode_typed_(Type, Term, Vsn, Types) ]. decode_(Fields, Vsn, Types);
Other ->
decode(Fields) -> error({illegal_serialization, Other})
decode(Fields, registered_types()). end.
decode(Fields0, Types) -> deserialize(Binary, Vsn, Types0) ->
Types = proper_types(Types0),
Fields0 = rlp_decode(Binary),
case decode_tag_and_vsn(Fields0) of case decode_tag_and_vsn(Fields0) of
{0, Vsn, Fields} -> {0, Vsn, Fields} ->
decode_(Fields, Vsn, Types); decode_(Fields, Vsn, Types);
@ -78,11 +105,90 @@ decode(Fields0, Types) ->
error({illegal_serialization, Other}) error({illegal_serialization, Other})
end. end.
encode(Term) ->
Vsn = latest_vsn(),
encode(Term, Vsn, registered_types(Vsn)).
encode(Term, Types0) ->
Types = proper_types(Types0),
encode(Term, types_vsn(Types), Types).
encode(Term, Vsn, Types0) ->
Types = proper_types(Types0, Vsn),
[ encode_basic(int, 0)
, encode_basic(int, Vsn)
, encode_(Term, Vsn, Types) ].
encode_typed(Type, Term) ->
Vsn = latest_vsn(),
encode_typed(Type, Term, Vsn, registered_types(Vsn)).
encode_typed(Type, Term, Types0) ->
Types = proper_types(Types0),
encode_typed(Type, Term, types_vsn(Types), Types).
encode_typed(Type, Term, Vsn, Types0) ->
Types = proper_types(Types0, Vsn),
[ encode_basic(int, 0)
, encode_basic(int, Vsn)
, encode_typed_(Type, Term, Vsn, Types) ].
decode(Fields) ->
Vsn = latest_vsn(),
decode(Fields, Vsn, registered_types(Vsn)).
decode(Fields, Types0) ->
Types = proper_types(Types0),
decode(Fields, types_vsn(Types), Types).
decode(Fields0, Vsn, Types0) ->
Types = proper_types(Types0, Vsn),
case decode_tag_and_vsn(Fields0) of
{0, Vsn, Fields} ->
decode_(Fields, Vsn, Types);
Other ->
error({illegal_encoding, Other})
end.
decode_typed(Type, Fields) ->
Vsn = latest_vsn(),
decode_typed(Type, Fields, Vsn, registered_types(Vsn)).
decode_typed(Type, Fields, Types0) ->
Types = proper_types(Types0),
decode_typed(Type, Fields, types_vsn(Types), Types).
decode_typed(Type, Fields0, Vsn, Types0) ->
Types = proper_types(Types0, Vsn),
case decode_tag_and_vsn(Fields0) of
{0, Vsn, Fields} ->
decode_from_template(Type, Fields, Vsn, Types);
Other ->
error({illegal_encoding, Other})
end.
decode_tag_and_vsn([TagBin, VsnBin, Fields]) -> decode_tag_and_vsn([TagBin, VsnBin, Fields]) ->
{decode_basic(int, TagBin), {decode_basic(int, TagBin),
decode_basic(int, VsnBin), decode_basic(int, VsnBin),
Fields}. Fields}.
proper_types(undefined) ->
registered_types(latest_vsn());
proper_types(#{} = Types) ->
Types.
proper_types(undefined, Vsn) ->
registered_types(Vsn);
proper_types(#{} = Types, Vsn) ->
assert_vsn(Vsn, Types).
types_vsn(#{vsn := Vsn}) -> Vsn;
types_vsn(_) -> latest_vsn().
assert_vsn(V, #{vsn := V} = Types) -> Types;
assert_vsn(V, #{vsn := Other} ) -> error({version_mismatch, V, Other});
assert_vsn(V, #{} = Types ) -> Types#{vsn => V}.
dynamic_types() -> dynamic_types() ->
#{ vsn => ?VSN #{ vsn => ?VSN
, codes => , codes =>
@ -121,17 +227,20 @@ dynamic_types() ->
, id => id , id => id
, label => label , label => label
} }
, options => #{}
}. }.
vsn(Types) ->
maps:get(vsn, Types, ?VSN).
registered_types() -> registered_types() ->
case persistent_term:get({?MODULE, types}, undefined) of registered_types(latest_vsn()).
registered_types(Vsn) ->
case persistent_term:get(pt_key(), undefined) of
undefined -> undefined ->
dynamic_types(); dynamic_types();
Types when is_map(Types) -> #{latest_vsn := _, types := #{Vsn := Types}} ->
Types Types;
#{latest_vsn := _, types := _} ->
dynamic_types()
end. end.
template(TagOrCode, Vsn, Types) -> template(TagOrCode, Vsn, Types) ->
@ -209,6 +318,8 @@ encode_maybe_template(#{items := _} = Type, Term, Vsn, Types) ->
end; end;
encode_maybe_template(#{alt := _} = Type, Term, Vsn, Types) -> encode_maybe_template(#{alt := _} = Type, Term, Vsn, Types) ->
encode_from_template(Type, Term, Vsn, emit(dyn()), Types); encode_from_template(Type, Term, Vsn, emit(dyn()), Types);
encode_maybe_template(#{switch := _} = Type, Term, Vsn, Types) ->
encode_from_template(Type, Term, Vsn, emit(dyn()), Types);
encode_maybe_template(Pat, Term, Vsn, Types) when is_list(Pat); encode_maybe_template(Pat, Term, Vsn, Types) when is_list(Pat);
is_tuple(Pat) -> is_tuple(Pat) ->
encode_from_template(Pat, Term, emit(dyn()), Vsn, Types); encode_from_template(Pat, Term, emit(dyn()), Vsn, Types);
@ -245,14 +356,19 @@ auto_template(T) ->
decode_from_template(any, Fld, Vsn, Types) -> decode_from_template(any, Fld, Vsn, Types) ->
decode_(Fld, Vsn, Types); decode_(Fld, Vsn, Types);
decode_from_template(#{items := Items}, Fld, Vsn, Types) when is_list(Fld) -> decode_from_template(#{items := Items}, Fld, Vsn, Types) when is_list(Fld) ->
Zipped = lists:zip(Items, Fld), Zipped = lists:zipwith(
fun({{K, T}, V}) -> {K, T, V};
({{opt,K,T}, V}) -> {K, T, V}
end, Items, Fld),
lists:foldl( lists:foldl(
fun({{K, Type}, V}, Map) -> fun({K, Type, V}, Map) ->
maps:is_key(K, Map) andalso error(badarg, duplicate_field), maps:is_key(K, Map) andalso error(badarg, duplicate_field),
Map#{K => decode_from_template({any,Type}, V, Vsn, Types)} Map#{K => decode_from_template({any,Type}, V, Vsn, Types)}
end, #{}, Zipped); end, #{}, Zipped);
decode_from_template(#{alt := Alts} = T, Fld, Vsn, Types) when is_list(Alts) -> decode_from_template(#{alt := Alts} = T, Fld, Vsn, Types) when is_list(Alts) ->
decode_alt(Alts, Fld, T, Vsn, Types); decode_alt(Alts, Fld, T, Vsn, Types);
decode_from_template(#{switch := Alts} = T, Fld, Vsn, Types) when is_map(Alts) ->
decode_switch(Alts, Fld, T, Vsn, Types);
decode_from_template(list, Flds, Vsn, Types) -> decode_from_template(list, Flds, Vsn, Types) ->
[decode_(F, Vsn, Types) || F <- Flds]; [decode_(F, Vsn, Types) || F <- Flds];
decode_from_template(map, Fld, Vsn, Types) -> decode_from_template(map, Fld, Vsn, Types) ->
@ -292,15 +408,28 @@ encode_from_template(list, L, E, Vsn, Types) when is_list(L) ->
encode_from_template(#{items := Items}, M, E, Vsn, Types) -> encode_from_template(#{items := Items}, M, E, Vsn, Types) ->
assert_type(is_map(M), map, M), assert_type(is_map(M), map, M),
Emit = noemit(E), Emit = noemit(E),
Encode = fun(K, Type, V) ->
[encode_from_template(any, K, Emit, Vsn, Types),
encode_from_template(Type, V, Emit, Vsn, Types)]
end,
emit(E, map, Types, emit(E, map, Types,
lists:map( lists:foldr(
fun({K, Type}) -> fun({K, Type}, Acc) ->
V = maps:get(K, M), V = maps:get(K, M),
[encode_from_template(any, K, Emit, Vsn, Types), [Encode(K, Type, V) | Acc];
encode_from_template(Type, V, Emit, Vsn, Types)] ({opt, K, Type}, Acc) ->
end, Items)); case maps:find(K, M) of
{ok, V} ->
[Encode(K, Type, V) | Acc];
error ->
Acc
end
end, [], Items));
encode_from_template(#{alt := Alts} = T, Term, E, Vsn, Types) when is_list(Alts) -> encode_from_template(#{alt := Alts} = T, Term, E, Vsn, Types) when is_list(Alts) ->
encode_alt(Alts, Term, T, E, Vsn, Types); encode_alt(Alts, Term, T, E, Vsn, Types);
encode_from_template(#{switch := Alts} = T, Term, E, Vsn, Types) when is_map(Alts),
is_map(Term) ->
encode_switch(Alts, Term, T, E, Vsn, Types);
encode_from_template(map, M, E, Vsn, Types) -> encode_from_template(map, M, E, Vsn, Types) ->
assert_type(is_map(M), map, M), assert_type(is_map(M), map, M),
Emit = emit(E), Emit = emit(E),
@ -326,13 +455,13 @@ encode_from_template(Type, List, E, Vsn, Types) when is_list(Type), is_list(List
encode_fields(Type, List, E, Vsn, Types); encode_fields(Type, List, E, Vsn, Types);
encode_from_template(label, V, E, _, Types) -> encode_from_template(label, V, E, _, Types) ->
assert_type(is_atom(V), label, V), assert_type(is_atom(V), label, V),
emit(E, label, Types, case find_cached_label(V, Types) of
case find_cached_label(V, Types) of error ->
error -> encode_basic(label, V, E, Types);
encode_basic(label, V, E, Types); {ok, Code} when is_integer(Code) ->
{ok, Code} when is_integer(Code) -> emit(E, label, Types,
[encode_basic(int, Code)] [encode_basic(int, Code)])
end); end;
encode_from_template(Type, V, E, _, Types) when Type == id encode_from_template(Type, V, E, _, Types) when Type == id
; Type == binary ; Type == binary
; Type == bool ; Type == bool
@ -366,6 +495,30 @@ encode_alt_([A|Alts], Term, T, E, Vsn, Types) ->
encode_alt_([], Term, T, _, _, _) -> encode_alt_([], Term, T, _, _, _) ->
error({illegal, T, Term}). error({illegal, T, Term}).
decode_switch(Alts, Fld, T, Vsn, Types) ->
[KFld, VFld] = Fld,
Key = decode_(KFld, Vsn, Types),
case maps:find(Key, Alts) of
{ok, SubType} ->
SubTerm = decode_from_template(SubType, VFld, Vsn, Types),
#{Key => SubTerm};
error ->
error({illegal, T, Fld})
end.
encode_switch(Alts, Term, T, E, Vsn, Types) ->
assert_type(map_size(Term) == 1, singleton_map, Term),
[{Key, Subterm}] = maps:to_list(Term),
case maps:find(Key, Alts) of
{ok, SubType} ->
Enc = encode_from_template(SubType, Subterm, E, Vsn, Types),
emit(E, map, Types,
[[encode_from_template(any, Key, E, Vsn, Types),
Enc]]);
error ->
error({illegal, T, Term})
end.
%% Basically, dynamically encoding a statically defined object %% Basically, dynamically encoding a statically defined object
encode_fields([{Field, Type}|TypesLeft], encode_fields([{Field, Type}|TypesLeft],
[{Field, Val}|FieldsLeft], E, Vsn, Types) -> [{Field, Val}|FieldsLeft], E, Vsn, Types) ->
@ -399,15 +552,29 @@ emit(0, _, _, Enc) ->
emit_code(Tag, #{rev := Tags}) -> emit_code(Tag, #{rev := Tags}) ->
encode_basic(int, maps:get(Tag, Tags)). encode_basic(int, maps:get(Tag, Tags)).
decode_basic(Type, [Tag,V], #{codes := Codes}) -> decode_basic(Type, [Tag,V], #{codes := Codes} = Types) ->
case decode_basic(int, Tag) of case decode_basic(int, Tag) of
Code when map_get(Code, Codes) == Type -> Code when map_get(Code, Codes) == Type ->
decode_basic(Type, V); decode_basic_(Type, V, Types);
_ -> _ ->
error(illegal) error(illegal)
end; end;
decode_basic(Type, V, _) -> decode_basic(Type, V, Types) ->
decode_basic(Type, V). decode_basic_(Type, V, Types).
decode_basic_(label, Fld, #{options := #{missing_labels := Opt}}) ->
Bin = decode_basic(binary, Fld),
case Opt of
create -> binary_to_atom(Bin, utf8);
fail -> binary_to_existing_atom(Bin, utf8);
convert ->
try binary_to_existing_atom(Bin, utf8)
catch
error:_ -> Bin
end
end;
decode_basic_(Type, Fld, _) ->
decode_basic(Type, Fld).
decode_basic(label, Fld) -> decode_basic(label, Fld) ->
binary_to_existing_atom(decode_basic(binary, Fld), utf8); binary_to_existing_atom(decode_basic(binary, Fld), utf8);
@ -439,29 +606,68 @@ rlp_encode(Fields) ->
%% Type registration and validation code %% Type registration and validation code
register_types(Types) when is_map(Types) -> register_types(Types) when is_map(Types) ->
register_types(latest_vsn(), Types).
register_types(Vsn, Types) ->
Codes = maps:get(codes, Types, #{}), Codes = maps:get(codes, Types, #{}),
Rev = rev_codes(Codes), Rev = rev_codes(Codes),
Templates = maps:get(templates, Types, #{}), Templates = maps:get(templates, Types, #{}),
Labels = maps:get(labels, Types, #{}), Labels = maps:get(labels, Types, #{}),
#{codes := Codes0, rev := Rev0, labels := Labels0, templates := Templates0} = Options = maps:get(options, Types, #{}),
#{codes := Codes0, rev := Rev0, labels := Labels0,
templates := Templates0, options := Options0} =
dynamic_types(), dynamic_types(),
Merged = #{ codes => maps:merge(Codes0, Codes) Merged = #{ codes => maps:merge(Codes0, Codes)
, rev => maps:merge(Rev0, Rev) , rev => maps:merge(Rev0, Rev)
, templates => maps:merge(Templates0, Templates) , templates => maps:merge(Templates0, Templates)
, options => maps:merge(Options0, Options)
, labels => maps:merge(Labels0, Labels) }, , labels => maps:merge(Labels0, Labels) },
assert_sizes(Merged), assert_sizes(Merged),
assert_mappings(Merged), assert_mappings(Merged),
Merged1 = assert_label_cache(Merged), Merged1 = assert_label_cache(Merged),
put_types(Merged1). put_types(Vsn, Merged1).
latest_vsn() ->
case persistent_term:get(pt_key(), undefined) of
undefined -> ?VSN;
#{latest_vsn := V} ->
V
end.
pt_key() -> {?MODULE, types}.
put_types(Types) -> put_types(Types) ->
persistent_term:put({?MODULE, types}, Types). put_types(types_vsn(Types), Types).
put_types(V, Types) ->
K = pt_key(),
Old = case persistent_term:get(K, undefined) of
undefined -> default_types_pt();
Existing -> Existing
end,
put_types_(K, V, Types, Old).
put_types_(K, V, Types, #{latest_vsn := V0, types := Types0} = Old) ->
New = case V > V0 of
true ->
Old#{latest_vsn := V,
types := Types0#{V => Types#{vsn => V}}};
false ->
Old#{types := Types0#{V => Types#{vsn => V}}}
end,
persistent_term:put(K, New).
types_from_list(L) -> types_from_list(L) ->
lists:foldl(fun elem_to_type/2, dynamic_types(), L). types_from_list(L, registered_types()).
register_type(Code, Tag, Template) when is_integer(Code), Code >= 0 -> types_from_list(L, Types) ->
#{codes := Codes, rev := Rev, templates := Temps} = Types = registered_types(), gmser_dyn_types:from_list(L, Types).
register_type(Code, Tag, Template) ->
register_type(latest_vsn(), Code, Tag, Template).
register_type(Vsn, Code, Tag, Template) when is_integer(Code), Code >= 0 ->
#{codes := Codes, rev := Rev, templates := Temps} = Types = registered_types(Vsn),
case {is_map_key(Code, Codes), is_map_key(Tag, Rev)} of case {is_map_key(Code, Codes), is_map_key(Tag, Rev)} of
{false, false} -> {false, false} ->
New = Types#{ codes := Codes#{Code => Tag} New = Types#{ codes := Codes#{Code => Tag}
@ -473,6 +679,15 @@ register_type(Code, Tag, Template) when is_integer(Code), Code >= 0 ->
{_, true} -> error(tag_exists) {_, true} -> error(tag_exists)
end. end.
set_opts(Opts) ->
set_opts(Opts, registered_types()).
set_opts(Opts, Types) ->
Types#{options => Opts}.
get_opts(#{options := Opts}) ->
Opts.
cache_label(Code, Label) when is_integer(Code), Code >= 0, is_atom(Label) -> cache_label(Code, Label) when is_integer(Code), Code >= 0, is_atom(Label) ->
#{labels := Lbls, rev_labels := RevLbls} = Types = registered_types(), #{labels := Lbls, rev_labels := RevLbls} = Types = registered_types(),
case {is_map_key(Label, Lbls), is_map_key(Code, RevLbls)} of case {is_map_key(Label, Lbls), is_map_key(Code, RevLbls)} of
@ -485,37 +700,11 @@ cache_label(Code, Label) when is_integer(Code), Code >= 0, is_atom(Label) ->
{_,true} -> error(code_exists) {_,true} -> error(code_exists)
end. end.
elem_to_type({Tag, Code, Template}, Acc) when is_atom(Tag), is_integer(Code) ->
#{codes := Codes, rev := Rev, templates := Temps} = Acc,
case {is_map_key(Tag, Rev), is_map_key(Code, Codes)} of
{false, false} ->
Acc#{ codes := Codes#{Code => Tag}
, rev := Rev#{Tag => Code}
, templates => Temps#{Tag => Template}
};
{true, _} -> error({duplicate_tag, Tag});
{_, true} -> error({duplicate_code, Code})
end;
elem_to_type({labels, Lbls}, Acc) ->
lists:foldl(fun add_label/2, Acc, Lbls);
elem_to_type(Elem, _) ->
error({invalid_type_list_element, Elem}).
add_label({L, Code}, #{labels := Lbls, rev_labels := RevLbls} = Acc)
when is_atom(L), is_integer(Code), Code > 0 ->
case {is_map_key(L, Lbls), is_map_key(Code, RevLbls)} of
{false, false} ->
Acc#{labels := Lbls#{L => Code},
rev_labels := RevLbls#{Code => L}};
{true, _} -> error({duplicate_label, L});
{_, true} -> error({duplicate_label_code, Code})
end;
add_label(Elem, _) ->
error({invalid_label_elem, Elem}).
revert_to_default_types() -> revert_to_default_types() ->
persistent_term:put({?MODULE, types}, dynamic_types()). persistent_term:put(pt_key(), default_types_pt()).
default_types_pt() ->
#{latest_vsn => ?VSN, types => #{?VSN => dynamic_types()}}.
assert_sizes(#{codes := Codes, rev := Rev, templates := Ts} = Types) -> assert_sizes(#{codes := Codes, rev := Rev, templates := Ts} = Types) ->
assert_sizes(map_size(Codes), map_size(Rev), map_size(Ts), Types). assert_sizes(map_size(Codes), map_size(Rev), map_size(Ts), Types).
@ -598,6 +787,11 @@ round_trip_test_() ->
T <- t_sample_types() T <- t_sample_types()
]. ].
ser_round_trip_test_() ->
[?_test(t_ser_round_trip(T)) ||
T <- t_sample_types()
].
t_sample_types() -> t_sample_types() ->
[ 5 [ 5
, -5 , -5
@ -623,19 +817,31 @@ user_types_test_() ->
, ?_test(t_reg_template_vsnd_fun()) , ?_test(t_reg_template_vsnd_fun())
, ?_test(t_reg_label_cache()) , ?_test(t_reg_label_cache())
, ?_test(t_reg_label_cache2()) , ?_test(t_reg_label_cache2())
, ?_test(t_reg_options())
]}. ]}.
dynamic_types_test_() -> dynamic_types_test_() ->
[ ?_test(revert_to_default_types()) [ ?_test(revert_to_default_types())
, ?_test(t_typed_map()) , ?_test(t_typed_map())
, ?_test(t_alts()) , ?_test(t_alts())
, ?_test(t_switch())
, ?_test(t_anyints()) , ?_test(t_anyints())
, ?_test(t_missing_labels())
].
versioned_types_test_() ->
[ ?_test(t_new_version())
]. ].
t_round_trip(T) -> t_round_trip(T) ->
?debugVal(T), ?debugVal(T),
?assertMatch({T, T}, {T, decode(encode(T))}). ?assertMatch({T, T}, {T, decode(encode(T))}).
t_ser_round_trip(T) ->
Data = serialize(T),
?debugFmt("Data (~p) = ~p~n", [T, Data]),
?assertMatch({T, T}, {T, deserialize(Data)}).
t_round_trip_typed(Type, T) -> t_round_trip_typed(Type, T) ->
?debugVal(T), ?debugVal(T),
?assertMatch({T, T}, {T, decode(encode_typed(Type, T))}). ?assertMatch({T, T}, {T, decode(encode_typed(Type, T))}).
@ -687,7 +893,6 @@ t_reg_template_vsnd_fun() ->
E = encode_typed(tup2f1, {3,4}), E = encode_typed(tup2f1, {3,4}),
{3,4} = decode(E), {3,4} = decode(E),
ok. ok.
t_reg_label_cache() -> t_reg_label_cache() ->
Enc0 = gmser_dyn:encode('1'), Enc0 = gmser_dyn:encode('1'),
@ -711,7 +916,7 @@ t_reg_label_cache() ->
?assertNotEqual(Enc0a, Enc1a). ?assertNotEqual(Enc0a, Enc1a).
t_reg_label_cache2() -> t_reg_label_cache2() ->
TFromL = gmser_dyn:types_from_list( TFromL = types_from_list(
[ {lbl_tup2, 1003, {label, label}} [ {lbl_tup2, 1003, {label, label}}
, {labels, , {labels,
[{'1', 49}]} [{'1', 49}]}
@ -723,19 +928,69 @@ t_reg_label_cache2() ->
[<<0>>,<<1>>,[<<3,235>>,[[<<49>>],[<<49>>]]]] = Enc, [<<0>>,<<1>>,[<<3,235>>,[[<<49>>],[<<49>>]]]] = Enc,
_Tup = gmser_dyn:decode(Enc). _Tup = gmser_dyn:decode(Enc).
t_reg_options() ->
register_types(set_opts(#{missing_labels => convert})),
[Dyn,Vsn,[Am,<<"random">>]] = gmser_dyn:encode(random),
EncNewAm = [Dyn,Vsn,[Am,<<"foo12345">>]],
<<"foo12345">> = gmser_dyn:decode(EncNewAm),
ok.
t_typed_map() -> t_typed_map() ->
Term = #{a => 13, {key,1} => [a]}, Term = #{a => 13, {key,1} => [a]},
Enc = encode_typed(#{items => [{a,int},{{key,1},[label]}]}, Term), Items = [{a,int},{{key,1},[label]}],
?assertEqual(Term, decode(Enc)). OptItems = [{opt, b, int} | Items],
Enc = encode_typed(#{items => Items}, Term),
?assertEqual(Term, decode(Enc)),
?assertEqual(Enc, encode_typed(#{items => Items}, Term)),
?assertEqual(Enc, encode_typed(#{items => OptItems}, Term)),
Term1 = Term#{b => 4},
Enc1 = encode_typed(#{items => OptItems}, Term1),
?assertEqual(Term1, decode(Enc1)),
?assertEqual(Enc, encode_typed(#{items => Items}, Term1)).
t_alts() -> t_alts() ->
t_round_trip_typed(#{alt => [negint, int]}, -4), t_round_trip_typed(#{alt => [negint, int]}, -4),
t_round_trip_typed(#{alt => [negint, int]}, 4), t_round_trip_typed(#{alt => [negint, int]}, 4),
ok. ok.
t_switch() ->
T = #{switch => #{a => int, b => binary}},
t_round_trip_typed(T, #{a => 17}),
t_round_trip_typed(T, #{b => <<"foo">>}),
?assertError({illegal,int,<<"foo">>}, encode_typed(T, #{a => <<"foo">>})),
MMap = #{a => 17, b => <<"foo">>},
?assertError({illegal, singleton_map, MMap}, encode_typed(T, MMap)).
t_anyints() -> t_anyints() ->
t_round_trip_typed(anyint, -5), t_round_trip_typed(anyint, -5),
t_round_trip_typed(anyint, 5), t_round_trip_typed(anyint, 5),
ok. ok.
t_missing_labels() ->
[Dyn,Vsn,[Am,<<"random">>]] = gmser_dyn:encode(random),
EncNewAm = [Dyn,Vsn,[Am,<<"flurbee">>]],
?assertError(badarg, gmser_dyn:decode(EncNewAm)),
?assertError(badarg, gmser_dyn:decode(EncNewAm, set_opts(#{missing_labels => fail}))),
<<"flurbee">> = gmser_dyn:decode(EncNewAm, set_opts(#{missing_labels => convert})),
true = is_atom(gmser_dyn:decode(EncNewAm, set_opts(#{missing_labels => create}))),
ok.
t_new_version() ->
V = latest_vsn(),
Types0 = registered_types(V),
V1 = V+1,
Types1 = types_from_list([{vsn, V1},
{msg1, 300, {int, int}}], Types0),
T2 = {3,5},
Enc21 = encode_typed(msg1, T2, Types1),
T2 = decode(Enc21, Types1),
V2 = V1+1,
Types2 = types_from_list([{vsn, V2},
{modify, {msg1, {int, int, int}}}], Types1),
Enc21 = encode_typed(msg1, T2, Types1),
?assertError({illegal,{int,int,int},T2}, encode_typed(msg1, T2, Types2)),
T3 = {3,5,7},
Enc32 = encode_typed(msg1, T3, Types2),
T3 = decode(Enc32, Types2).
-endif. -endif.

62
src/gmser_dyn_types.erl Normal file
View File

@ -0,0 +1,62 @@
-module(gmser_dyn_types).
-export([ add_type/4
, from_list/2
, expand/1 ]).
-export([ next_code/1 ]).
next_code(#{codes := Codes}) ->
lists:max(maps:keys(Codes)) + 1.
add_type(Tag, Code, Template, Types) ->
elem_to_type({Tag, Code, Template}, Types).
from_list(L, Types) ->
lists:foldl(fun elem_to_type/2, Types, L).
expand(#{vsn := V, templates := Templates0} = Types) ->
Templates =
maps:map(
fun(_, F) when is_function(F, 0) ->
F();
(_, F) when is_function(F, 1) ->
F(V);
(_, T) ->
T
end, Templates0),
Types#{templates := Templates}.
elem_to_type({Tag, Code, Template}, Acc) when is_atom(Tag), is_integer(Code) ->
#{codes := Codes, rev := Rev, templates := Temps} = Acc,
case {is_map_key(Tag, Rev), is_map_key(Code, Codes)} of
{false, false} ->
Acc#{ codes := Codes#{Code => Tag}
, rev := Rev#{Tag => Code}
, templates => Temps#{Tag => Template}
};
{true, _} -> error({duplicate_tag, Tag});
{_, true} -> error({duplicate_code, Code})
end;
elem_to_type({modify, {Tag, Template}}, Acc) ->
#{codes := _, rev := Rev, templates := Templates} = Acc,
_ = maps:get(Tag, Rev),
Templates1 = Templates#{Tag := Template},
Acc#{templates := Templates1};
elem_to_type({labels, Lbls}, Acc) ->
lists:foldl(fun add_label/2, Acc, Lbls);
elem_to_type({vsn, V}, Acc) ->
Acc#{vsn => V};
elem_to_type(Elem, _) ->
error({invalid_type, Elem}).
add_label({L, Code}, #{labels := Lbls, rev_labels := RevLbls} = Acc)
when is_atom(L), is_integer(Code), Code > 0 ->
case {is_map_key(L, Lbls), is_map_key(Code, RevLbls)} of
{false, false} ->
Acc#{labels := Lbls#{L => Code},
rev_labels := RevLbls#{Code => L}};
{true, _} -> error({duplicate_label, L});
{_, true} -> error({duplicate_label_code, Code})
end;
add_label(Elem, _) ->
error({invalid_label, Elem}).