Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 795c7f7860 | |||
| 0d77ca0388 | |||
| ed204f8526 | |||
| a949d166f6 | |||
| ba771836fb | |||
| c403fa89a1 | |||
| dd3e731480 | |||
| 6563ef9de7 | |||
| bff07885fb |
@@ -1,21 +1,19 @@
|
||||
GM Serialization
|
||||
=====
|
||||
# GM Serialization
|
||||
|
||||
Serialization helpers for the Gajumaru.
|
||||
|
||||
Build
|
||||
-----
|
||||
For an overview of the static serializer, see [this document](doc/static.md).
|
||||
|
||||
## Build
|
||||
|
||||
$ rebar3 compile
|
||||
|
||||
|
||||
Test
|
||||
----
|
||||
## Test
|
||||
|
||||
$ rebar3 eunit
|
||||
|
||||
Dynamic encoding
|
||||
----
|
||||
## Dynamic encoding
|
||||
|
||||
The module `gmser_dyn` offers dynamic encoding support, encoding most 'regular'
|
||||
Erlang data types into an internal RLP representation.
|
||||
@@ -34,6 +32,8 @@ how the type information is represented. The fully serialized form is
|
||||
produced by the `serialize` functions.
|
||||
|
||||
The basic types supported by the encoder are:
|
||||
* `integer()` (`anyint`, code: 246)
|
||||
* `neg_integer()` (`negint`, code: 247)
|
||||
* `non_neg_integer()` (`int` , code: 248)
|
||||
* `binary()` (`binary`, code: 249)
|
||||
* `boolean()` (`bool` , code: 250)
|
||||
@@ -43,18 +43,39 @@ The basic types supported by the encoder are:
|
||||
* `gmser_id:id()` (`id` , code: 254)
|
||||
* `atom()` (`label` , code: 255)
|
||||
|
||||
(The range of codes is chosen because the `gmser_chain_objects` codes
|
||||
range from 10 to 200, and also to stay within 1 byte.)
|
||||
|
||||
When encoding `map` types, the map elements are first sorted.
|
||||
|
||||
When specifying a map type for template-driven encoding, use
|
||||
the `#{items => [{Key, Value}]}` construct.
|
||||
|
||||
Labels
|
||||
----
|
||||
|
||||
## Labels
|
||||
|
||||
Labels correspond to (existing) atoms in Erlang.
|
||||
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.
|
||||
|
||||
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.
|
||||
Note that when caching labels, the same cache mapping needs to be used on the
|
||||
decoder side.
|
||||
@@ -63,8 +84,7 @@ Labels are encoded as `[<<255>>, << AtomToBinary/binary >>]`.
|
||||
If a cached label is used, the encoding becomes `[<<255>, [Ix]]`, where
|
||||
`Ix` is the integer-encoded index value of the cached label.
|
||||
|
||||
Examples
|
||||
----
|
||||
## Examples
|
||||
|
||||
Dynamically encoded objects have the basic structure `[<<0>>,V,Obj]`, where `V` is the
|
||||
integer-coded version, and `Obj` is the top-level encoding on the form `[Tag,Data]`.
|
||||
@@ -88,8 +108,7 @@ Note that tuples and list are encoded the same way, except for the initial type
|
||||
Maps are encoded as `[<Map>, [KV1, KV2, ...]]`, where `[KV1, KV2, ...]` is the sorted
|
||||
list of key-value tuples from `map:to_list(Map)`, but with the `tuple` type tag omitted.
|
||||
|
||||
Template-driven encoding
|
||||
----
|
||||
## Template-driven encoding
|
||||
|
||||
Templates can be provided to the encoder by either naming an already registered
|
||||
type, or by passing a template directly. In both cases, the encoder will enforce
|
||||
@@ -112,3 +131,32 @@ ET([{int,int}], [{1,2}]) -> [<<0>>,<<1>>,[<<251>>,[[[<<248>>,<<1>>],[<<248>>,<<2
|
||||
gmser_dyn:register_type(1000,lt2i,[{int,int}]).
|
||||
ET(lt2i, [{1,2}]) -> [<<0>>,<<1>>,[<<3,232>>,[[<<1>>,<<2>>]]]]
|
||||
```
|
||||
|
||||
### Alternative types
|
||||
|
||||
The dynamic encoder supports two additions to the `gmserialization` template
|
||||
language: `any` and `#{alt => [AltTypes]}`.
|
||||
|
||||
The `any` type doesn't have an associated code, but enforces dynamic encoding.
|
||||
|
||||
The `#{alt => [Type]}` construct also enforces dynamic encoding, and will try
|
||||
to encode as each type in the list, in the specified order, until one matches.
|
||||
|
||||
```erlang
|
||||
gmser_dyn:encode_typed(#{alt => [negint,int]}, 5) -> [<<0>>,<<1>>,[<<247>>,<<5>>]]
|
||||
gmser_dyn:encode_typed(#{alt => [negint,int]}, 5) -> [<<0>>,<<1>>,[<<248>>,<<5>>]]
|
||||
|
||||
gmser_dyn:encode_typed(anyint,-5) -> [<<0>>,<<1>>,[<<246>>,[<<247>>,<<5>>]]]
|
||||
gmser_dyn:encode_typed(anyint,5) -> [<<0>>,<<1>>,[<<246>>,[<<248>>,<<5>>]]]
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
Note that `anyint` is a standard type. The static serializer supports only
|
||||
positive integers (`int`), as negative numbers are forbidden on-chain.
|
||||
For dynamic encoding e.g. in messaging protocols, handling negative numbers can
|
||||
be useful, so the `negint` type was added as a dynamic type. To encode a full-range
|
||||
integer, the `alt` construct is needed.
|
||||
|
||||
(Floats are not supported, as they are non-deterministic. Rationals and fixed-point
|
||||
numbers could easily be handled as high-level types, e.g. as `{int,int}`.)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
# Static Serialization
|
||||
|
||||
The `gmserialization` and `gmser_chain_objects` modules implement the
|
||||
static serialization support used in the Gajumaru blockchain.
|
||||
|
||||
The purpose is to produce fully deterministic serialization, in order
|
||||
to maintain predictable hashing.
|
||||
|
||||
Example:
|
||||
|
||||
```erlang
|
||||
%% deterministic canonical serialization.
|
||||
-spec serialize_to_binary(signed_tx()) -> binary_signed_tx().
|
||||
serialize_to_binary(#signed_tx{tx = Tx, signatures = Sigs}) ->
|
||||
gmser_chain_objects:serialize(
|
||||
?SIG_TX_TYPE,
|
||||
?SIG_TX_VSN,
|
||||
serialization_template(?SIG_TX_VSN),
|
||||
[ {signatures, lists:sort(Sigs)}
|
||||
, {transaction, aetx:serialize_to_binary(Tx)}
|
||||
]).
|
||||
|
||||
-spec deserialize_from_binary(binary()) -> signed_tx().
|
||||
deserialize_from_binary(SignedTxBin) when is_binary(SignedTxBin) ->
|
||||
[ {signatures, Sigs}
|
||||
, {transaction, TxBin}
|
||||
] = gmser_chain_objects:deserialize(
|
||||
?SIG_TX_TYPE,
|
||||
?SIG_TX_VSN,
|
||||
serialization_template(?SIG_TX_VSN),
|
||||
SignedTxBin),
|
||||
assert_sigs_size(Sigs),
|
||||
#signed_tx{ tx = aetx:deserialize_from_binary(TxBin)
|
||||
, signatures = Sigs
|
||||
}.
|
||||
|
||||
serialization_template(?SIG_TX_VSN) ->
|
||||
[ {signatures, [binary]}
|
||||
, {transaction, binary}
|
||||
].
|
||||
```
|
||||
|
||||
The terms that can be encoded using these templates are given by
|
||||
this type in `gmserialization.erl`:
|
||||
|
||||
```erlang
|
||||
-type encodable_term() :: non_neg_integer()
|
||||
| binary()
|
||||
| boolean()
|
||||
| [encodable_term()] %% Of any length
|
||||
| #{atom() => encodable_term()}
|
||||
| tuple() %% Any arity, containing encodable_term().
|
||||
| gmser_id:id().
|
||||
```
|
||||
|
||||
The template 'language' is defined by these types:
|
||||
|
||||
```erlang
|
||||
-type template() :: [{field_name(), type()}].
|
||||
-type field_name() :: atom().
|
||||
-type type() :: 'int'
|
||||
| 'bool'
|
||||
| 'binary'
|
||||
| 'id' %% As defined in aec_id.erl
|
||||
| [type()] %% Length one in the type. This means a list of any length.
|
||||
| #{items := [{field_name(), type()}]} %% Record with named fields
|
||||
%% represented as a map.
|
||||
%% Encoded as a list in the given
|
||||
%% order.
|
||||
| tuple(). %% Any arity, containing type(). This means a static size array.
|
||||
```
|
||||
|
||||
The `gmser_chain_objects.erl` module specifies a serialization code for each
|
||||
object that can go on-chain. E.g.:
|
||||
|
||||
```erlang
|
||||
tag(signed_tx) -> 11;
|
||||
...
|
||||
rev_tag(11) -> signed_tx;
|
||||
```
|
||||
|
||||
The `tag` and `vsn` are laid out in the beginning of the serialized object.
|
||||
+240
-102
@@ -17,6 +17,9 @@
|
||||
%% register a type schema, inspect existing schema
|
||||
-export([ register_types/1
|
||||
, registered_types/0
|
||||
, get_opts/1
|
||||
, set_opts/1
|
||||
, set_opts/2
|
||||
, types_from_list/1
|
||||
, revert_to_default_types/0
|
||||
, dynamic_types/0 ]).
|
||||
@@ -73,13 +76,12 @@ decode(Fields) ->
|
||||
decode(Fields0, Types) ->
|
||||
case decode_tag_and_vsn(Fields0) of
|
||||
{0, Vsn, Fields} ->
|
||||
[Val] = decode_(Fields, Vsn, Types, []),
|
||||
Val;
|
||||
decode_(Fields, Vsn, Types);
|
||||
Other ->
|
||||
error({illegal_serialization, Other})
|
||||
end.
|
||||
|
||||
decode_tag_and_vsn([TagBin, VsnBin | Fields]) ->
|
||||
decode_tag_and_vsn([TagBin, VsnBin, Fields]) ->
|
||||
{decode_basic(int, TagBin),
|
||||
decode_basic(int, VsnBin),
|
||||
Fields}.
|
||||
@@ -87,7 +89,9 @@ decode_tag_and_vsn([TagBin, VsnBin | Fields]) ->
|
||||
dynamic_types() ->
|
||||
#{ vsn => ?VSN
|
||||
, codes =>
|
||||
#{ 248 => int
|
||||
#{ 246 => anyint
|
||||
, 247 => negint
|
||||
, 248 => int
|
||||
, 249 => binary
|
||||
, 250 => bool
|
||||
, 251 => list
|
||||
@@ -96,7 +100,9 @@ dynamic_types() ->
|
||||
, 254 => id
|
||||
, 255 => label}
|
||||
, rev =>
|
||||
#{ int => 248
|
||||
#{ anyint => 246
|
||||
, negint => 247
|
||||
, int => 248
|
||||
, binary => 249
|
||||
, bool => 250
|
||||
, list => 251
|
||||
@@ -107,7 +113,9 @@ dynamic_types() ->
|
||||
, labels => #{}
|
||||
, rev_labels => #{}
|
||||
, templates =>
|
||||
#{ int => int
|
||||
#{ anyint => #{alt => [negint, int]}
|
||||
, negint => negint
|
||||
, int => int
|
||||
, binary => binary
|
||||
, bool => bool
|
||||
, list => list
|
||||
@@ -116,6 +124,7 @@ dynamic_types() ->
|
||||
, id => id
|
||||
, label => label
|
||||
}
|
||||
, options => #{}
|
||||
}.
|
||||
|
||||
vsn(Types) ->
|
||||
@@ -148,67 +157,65 @@ dyn_template_(F, Vsn) ->
|
||||
find_cached_label(Lbl, #{labels := Lbls}) ->
|
||||
maps:find(Lbl, Lbls).
|
||||
|
||||
decode_(Fields, Vsn, Types, Acc) ->
|
||||
{_Tag, Term, Rest} = decode_field_(Fields, Vsn, Types),
|
||||
Acc1 = [Term | Acc],
|
||||
case Rest of
|
||||
[] ->
|
||||
lists:reverse(Acc1);
|
||||
_ ->
|
||||
decode_(Rest, Vsn, Types, Acc1)
|
||||
end.
|
||||
|
||||
decode_field_([H|T], Vsn, Types) ->
|
||||
{CodeBin, Field, Rest} =
|
||||
case H of
|
||||
[C, F] -> {C, F, T};
|
||||
C when is_binary(C) -> {C, hd(T), tl(T)}
|
||||
end,
|
||||
decode_([CodeBin, Flds], Vsn, Types) ->
|
||||
Code = decode_basic(int, CodeBin),
|
||||
{Tag, Template} = template(Code, Vsn, Types),
|
||||
%% [Fld|Rest] = Fields,
|
||||
Val = decode_from_template(Template, Field, Vsn, Types),
|
||||
{Tag, Val, Rest}.
|
||||
{_Tag, Template} = template(Code, Vsn, Types),
|
||||
decode_from_template(Template, Flds, Vsn, Types).
|
||||
|
||||
encode_(Term, Vsn, Types) ->
|
||||
encode_(Term, true, Vsn, Types).
|
||||
encode_(Term, dyn(emit()), Vsn, Types).
|
||||
|
||||
encode_(Term, Emit, Vsn, Types) ->
|
||||
{Tag, Template} = auto_template(Term),
|
||||
Enc = encode_from_template(Template, Term, Vsn, Types),
|
||||
if Emit ->
|
||||
[emit_code(Tag, Types), Enc];
|
||||
true ->
|
||||
Enc
|
||||
encode_(Term, E, Vsn, Types) ->
|
||||
{_Tag, Template} = auto_template(Term),
|
||||
encode_from_template(Template, Term, E, Vsn, Types).
|
||||
|
||||
%% To control when to emit type codes:
|
||||
%% If the template is predefined, it's 'not dynamic' (nodyn(E)).
|
||||
%% If we are encoding against a type that's part of a predefined template,
|
||||
%% we typically don't emit the type code, except at the very top.
|
||||
%% So: emit type codes if the 'emit' bit is set, or if the 'dyn' bit is set.
|
||||
emit() -> 2#01.
|
||||
dyn() -> 2#10.
|
||||
emit(E) -> E bor 2#01.
|
||||
noemit(E) -> E band 2#10.
|
||||
dyn(E) -> E bor 2#10.
|
||||
nodyn(E) -> E band 2#01.
|
||||
|
||||
encode_typed_(Type, Term, Vsn, #{codes := Codes, rev := Rev} = Types) ->
|
||||
case (is_map_key(Type, Codes) orelse is_map_key(Type, Rev)) of
|
||||
true ->
|
||||
encode_typed_(Type, Term, nodyn(emit()), Vsn, Types);
|
||||
false ->
|
||||
encode_maybe_template(Type, Term, Vsn, Types)
|
||||
end.
|
||||
|
||||
encode_typed_(Type, Term, Vsn, Types) ->
|
||||
encode_typed_(Type, Term, true, Vsn, Types).
|
||||
|
||||
encode_typed_(any, Term, _, Vsn, Types) ->
|
||||
encode_(Term, true, Vsn, Types);
|
||||
encode_typed_(Code, Term, Emit, Vsn, #{codes := Codes} = Types) when is_map_key(Code, Codes) ->
|
||||
encode_(Term, dyn(emit()), Vsn, Types);
|
||||
encode_typed_(Code, Term, E, Vsn, #{codes := Codes} = Types) when is_map_key(Code, Codes) ->
|
||||
{_Tag, Template} = template(Code, Vsn, Types),
|
||||
maybe_emit(Emit, Code, encode_from_template(Template, Term, false, Vsn, Types));
|
||||
encode_typed_(Tag, Term, Emit, Vsn, #{templates := Ts, rev := Rev} = Types)
|
||||
[encode_basic(int,Code),
|
||||
encode_from_template(Template, Term, noemit(nodyn(E)), Vsn, Types)];
|
||||
encode_typed_(Tag, Term, E, Vsn, #{templates := Ts, rev := Rev} = Types)
|
||||
when is_map_key(Tag, Ts) ->
|
||||
Template = dyn_template_(maps:get(Tag, Ts), Vsn),
|
||||
Code = maps:get(Tag, Rev),
|
||||
maybe_emit(Emit, Code, encode_from_template(Template, Term, false, Vsn, Types));
|
||||
[encode_basic(int,Code),
|
||||
encode_from_template(Template, Term, noemit(nodyn(E)), Vsn, Types)];
|
||||
encode_typed_(MaybeTemplate, Term, _, Vsn, Types) ->
|
||||
encode_maybe_template(MaybeTemplate, Term, Vsn, Types).
|
||||
|
||||
maybe_emit(true, Code, Enc) ->
|
||||
[encode_basic(int, Code), Enc];
|
||||
maybe_emit(false, _, Enc) ->
|
||||
Enc.
|
||||
|
||||
encode_maybe_template(#{items := _} = Type, Term, Vsn, Types) ->
|
||||
case is_map(Term) of
|
||||
true ->
|
||||
encode_from_template(Type, Term, emit(dyn()), Vsn, Types);
|
||||
false ->
|
||||
error({invalid, Type, Term})
|
||||
end;
|
||||
encode_maybe_template(#{alt := _} = Type, Term, Vsn, Types) ->
|
||||
encode_from_template(Type, Term, Vsn, emit(dyn()), Types);
|
||||
encode_maybe_template(Pat, Term, Vsn, Types) when is_list(Pat);
|
||||
is_tuple(Pat);
|
||||
is_map(Pat) ->
|
||||
{Tag, _} = auto_template(Pat),
|
||||
[emit_code(Tag, Types),
|
||||
encode_from_template(Pat, Term, true, Vsn, Types)];
|
||||
is_tuple(Pat) ->
|
||||
encode_from_template(Pat, Term, emit(dyn()), Vsn, Types);
|
||||
encode_maybe_template(Other, Term, _Vsn, _Types) ->
|
||||
error({illegal_template, Other, Term}).
|
||||
|
||||
@@ -233,19 +240,32 @@ auto_template(T) ->
|
||||
is_atom(T) -> {label, label}; % binary_to_existing_atom()
|
||||
is_integer(T),
|
||||
T >= 0 -> {int, int};
|
||||
is_integer(T),
|
||||
T < 0 -> {negint, negint};
|
||||
true ->
|
||||
error(invalid_type)
|
||||
error({invalid_type, T})
|
||||
end.
|
||||
|
||||
decode_from_template(list, Fld, Vsn, Types) ->
|
||||
decode_(Fld, Vsn, Types, []);
|
||||
decode_from_template(any, Fld, Vsn, Types) ->
|
||||
decode_(Fld, Vsn, Types);
|
||||
decode_from_template(#{items := Items}, Fld, Vsn, Types) when is_list(Fld) ->
|
||||
Zipped = lists:zip(Items, Fld),
|
||||
lists:foldl(
|
||||
fun({{K, Type}, V}, Map) ->
|
||||
maps:is_key(K, Map) andalso error(badarg, duplicate_field),
|
||||
Map#{K => decode_from_template({any,Type}, V, Vsn, Types)}
|
||||
end, #{}, Zipped);
|
||||
decode_from_template(#{alt := Alts} = T, Fld, Vsn, Types) when is_list(Alts) ->
|
||||
decode_alt(Alts, Fld, T, Vsn, Types);
|
||||
decode_from_template(list, Flds, Vsn, Types) ->
|
||||
[decode_(F, Vsn, Types) || F <- Flds];
|
||||
decode_from_template(map, Fld, Vsn, Types) ->
|
||||
TupleFields = [F || F <- Fld],
|
||||
Items = [decode_from_template(tuple, T, Vsn, Types)
|
||||
Items = [decode_from_template({any,any}, T, Vsn, Types)
|
||||
|| T <- TupleFields],
|
||||
maps:from_list(Items);
|
||||
decode_from_template(tuple, Fld, Vsn, Types) ->
|
||||
Items = decode_(Fld, Vsn, Types, []),
|
||||
Items = [decode_(F, Vsn, Types) || F <- Fld],
|
||||
list_to_tuple(Items);
|
||||
decode_from_template([Type], Fields, Vsn, Types) ->
|
||||
[decode_from_template(Type, F, Vsn, Types)
|
||||
@@ -260,72 +280,109 @@ decode_from_template(label, [C], _, #{rev_labels := RLbls}) ->
|
||||
Code = decode_basic(int, C),
|
||||
maps:get(Code, RLbls);
|
||||
decode_from_template(Type, Fld, _, Types) when Type == int
|
||||
; Type == negint
|
||||
; Type == binary
|
||||
; Type == bool
|
||||
; Type == id
|
||||
; Type == label ->
|
||||
decode_basic(Type, Fld, Types).
|
||||
|
||||
encode_from_template(Type, V, Vsn, Types) ->
|
||||
encode_from_template(Type, V, true, Vsn, Types).
|
||||
|
||||
encode_from_template(any, V, _, Vsn, Types) ->
|
||||
encode_(V, true, Vsn, Types);
|
||||
encode_from_template(list, L, _, Vsn, Types) when is_list(L) ->
|
||||
encode_from_template(any, V, _E, Vsn, Types) ->
|
||||
encode_(V, dyn(emit()), Vsn, Types);
|
||||
encode_from_template(list, L, E, Vsn, Types) when is_list(L) ->
|
||||
assert_type(is_list(L), list, L),
|
||||
[encode_(V, Vsn, Types) || V <- L];
|
||||
encode_from_template(map, M, _, Vsn, Types) ->
|
||||
emit(E, list, Types,
|
||||
[encode_(V, Vsn, Types) || V <- L]);
|
||||
encode_from_template(#{items := Items}, M, E, Vsn, Types) ->
|
||||
assert_type(is_map(M), map, M),
|
||||
[encode_({K,V}, false, Vsn, Types)
|
||||
|| {K, V} <- lists:sort(maps:to_list(M))];
|
||||
encode_from_template(tuple, T, Emit, Vsn, Types) ->
|
||||
Emit = noemit(E),
|
||||
emit(E, map, Types,
|
||||
lists:map(
|
||||
fun({K, Type}) ->
|
||||
V = maps:get(K, M),
|
||||
[encode_from_template(any, K, Emit, Vsn, Types),
|
||||
encode_from_template(Type, V, Emit, Vsn, Types)]
|
||||
end, Items));
|
||||
encode_from_template(#{alt := Alts} = T, Term, E, Vsn, Types) when is_list(Alts) ->
|
||||
encode_alt(Alts, Term, T, E, Vsn, Types);
|
||||
encode_from_template(map, M, E, Vsn, Types) ->
|
||||
assert_type(is_map(M), map, M),
|
||||
Emit = emit(E),
|
||||
emit(E, map, Types,
|
||||
[[encode_from_template(any, K, Emit, Vsn, Types),
|
||||
encode_from_template(any, V, Emit, Vsn, Types)]
|
||||
|| {K, V} <- lists:sort(maps:to_list(M))]);
|
||||
encode_from_template(tuple, T, E, Vsn, Types) ->
|
||||
assert_type(is_tuple(T), tuple, T),
|
||||
[encode_(V, Emit, Vsn, Types) || V <- tuple_to_list(T)];
|
||||
encode_from_template(T, V, Emit, Vsn, Types) when is_tuple(T) ->
|
||||
emit(E, tuple, Types,
|
||||
[encode_(V, noemit(E), Vsn, Types) || V <- tuple_to_list(T)]);
|
||||
encode_from_template(T, V, E, Vsn, Types) when is_tuple(T) ->
|
||||
assert_type(is_tuple(V), T, V),
|
||||
assert_type(tuple_size(T) =:= tuple_size(V), T, V),
|
||||
Zipped = lists:zip(tuple_to_list(T), tuple_to_list(V)),
|
||||
[encode_from_template(T1, V1, Emit, Vsn, Types) || {T1, V1} <- Zipped];
|
||||
encode_from_template([Type] = T, List, Emit, Vsn, Types) ->
|
||||
emit(E, tuple, Types,
|
||||
[encode_from_template(T1, V1, noemit(E), Vsn, Types) || {T1, V1} <- Zipped]);
|
||||
encode_from_template([Type] = T, List, E, Vsn, Types) ->
|
||||
assert_type(is_list(List), T, List),
|
||||
[encode_from_template(Type, V, Emit, Vsn, Types) || V <- List];
|
||||
encode_from_template(Type, List, Emit, Vsn, Types) when is_list(Type), is_list(List) ->
|
||||
encode_fields(Type, List, Emit, Vsn, Types);
|
||||
encode_from_template(label, V, Emit, _, Types) ->
|
||||
emit(E, list, Types,
|
||||
[encode_from_template(Type, V, noemit(E), Vsn, Types) || V <- List]);
|
||||
encode_from_template(Type, List, E, Vsn, Types) when is_list(Type), is_list(List) ->
|
||||
encode_fields(Type, List, E, Vsn, Types);
|
||||
encode_from_template(label, V, E, _, Types) ->
|
||||
assert_type(is_atom(V), label, V),
|
||||
case find_cached_label(V, Types) of
|
||||
error ->
|
||||
encode_basic(label, V, Emit, Types);
|
||||
encode_basic(label, V, E, Types);
|
||||
{ok, Code} when is_integer(Code) ->
|
||||
[encode_basic(int, Code)]
|
||||
emit(E, label, Types,
|
||||
[encode_basic(int, Code)])
|
||||
end;
|
||||
encode_from_template(Type, V, Emit, _, Types) when Type == id
|
||||
; Type == binary
|
||||
; Type == bool
|
||||
; Type == int
|
||||
; Type == label ->
|
||||
encode_basic(Type, V, Emit, Types);
|
||||
encode_from_template(Type, V, Emit, Vsn, Types) ->
|
||||
encode_typed_(Type, V, Emit, Vsn, Types).
|
||||
%% error({illegal, Type, V}).
|
||||
encode_from_template(Type, V, E, _, Types) when Type == id
|
||||
; Type == binary
|
||||
; Type == bool
|
||||
; Type == int
|
||||
; Type == negint
|
||||
; Type == label ->
|
||||
encode_basic(Type, V, E, Types);
|
||||
encode_from_template(Type, V, E, Vsn, Types) ->
|
||||
encode_typed_(Type, V, E, Vsn, Types).
|
||||
|
||||
assert_type(true, _, _) -> ok;
|
||||
assert_type(_, Type, V) -> error({illegal, Type, V}).
|
||||
|
||||
decode_alt([A|Alts], Fld, T, Vsn, Types) ->
|
||||
try decode_from_template(A, Fld, Vsn, Types)
|
||||
catch error:_ ->
|
||||
decode_alt(Alts, Fld, T, Vsn, Types)
|
||||
end;
|
||||
decode_alt([], Fld, T, _, _) ->
|
||||
error({illegal, T, Fld}).
|
||||
|
||||
encode_alt(Alts, Term, T, E, Vsn, Types) ->
|
||||
%% Since we don't know which type may match, treat as dynamic.
|
||||
encode_alt_(Alts, Term, T, dyn(E), Vsn, Types).
|
||||
|
||||
encode_alt_([A|Alts], Term, T, E, Vsn, Types) ->
|
||||
try encode_from_template(A, Term, E, Vsn, Types)
|
||||
catch error:_ ->
|
||||
encode_alt_(Alts, Term, T, E, Vsn, Types)
|
||||
end;
|
||||
encode_alt_([], Term, T, _, _, _) ->
|
||||
error({illegal, T, Term}).
|
||||
|
||||
%% Basically, dynamically encoding a statically defined object
|
||||
encode_fields([{Field, Type}|TypesLeft],
|
||||
[{Field, Val}|FieldsLeft], Emit, Vsn, Types) ->
|
||||
[ encode_from_template(Type, Val, Emit, Vsn, Types)
|
||||
| encode_fields(TypesLeft, FieldsLeft, Emit, Vsn, Types)];
|
||||
[{Field, Val}|FieldsLeft], E, Vsn, Types) ->
|
||||
[ encode_from_template(Type, Val, E, Vsn, Types)
|
||||
| encode_fields(TypesLeft, FieldsLeft, E, Vsn, Types)];
|
||||
encode_fields([{_Field, _Type} = FT|_TypesLeft],
|
||||
[Val |_FieldsLeft], _Emit, _Vsn, _Types) ->
|
||||
[Val |_FieldsLeft], _E, _Vsn, _Types) ->
|
||||
error({illegal_field, FT, Val});
|
||||
encode_fields([Type|TypesLeft],
|
||||
[Val |FieldsLeft], Emit, Vsn, Types) when is_atom(Type) ->
|
||||
[Val |FieldsLeft], E, Vsn, Types) when is_atom(Type) ->
|
||||
%% Not sure about this ...
|
||||
[ encode_from_template(Type, Val, Emit, Vsn, Types)
|
||||
| encode_fields(TypesLeft, FieldsLeft, Emit, Vsn, Types)];
|
||||
[ encode_from_template(Type, Val, E, Vsn, Types)
|
||||
| encode_fields(TypesLeft, FieldsLeft, E, Vsn, Types)];
|
||||
encode_fields([], [], _, _, _) ->
|
||||
[].
|
||||
|
||||
@@ -337,27 +394,52 @@ decode_fields([{Tag, Type}|TypesLeft],
|
||||
decode_fields([], [], _, _) ->
|
||||
[].
|
||||
|
||||
|
||||
emit(E, Tag, Types, Enc) when E > 0 ->
|
||||
[emit_code(Tag, Types), Enc];
|
||||
emit(0, _, _, Enc) ->
|
||||
Enc.
|
||||
|
||||
emit_code(Tag, #{rev := 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
|
||||
Code when map_get(Code, Codes) == Type ->
|
||||
decode_basic(Type, V);
|
||||
decode_basic_(Type, V, Types);
|
||||
_ ->
|
||||
error(illegal)
|
||||
end;
|
||||
decode_basic(Type, V, _) ->
|
||||
decode_basic(Type, V).
|
||||
decode_basic(Type, V, Types) ->
|
||||
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) ->
|
||||
binary_to_existing_atom(decode_basic(binary, Fld), utf8);
|
||||
decode_basic(negint, Fld) ->
|
||||
I = gmserialization:decode_field(int, Fld),
|
||||
-I;
|
||||
decode_basic(Type, Fld) ->
|
||||
gmserialization:decode_field(Type, Fld).
|
||||
|
||||
encode_basic(Tag, V, true, Types) ->
|
||||
encode_basic(negint, I, _, Types) when is_integer(I), I < 0 ->
|
||||
[emit_code(negint, Types), gmserialization:encode_field(int, -I)];
|
||||
encode_basic(Tag, V, E, Types) when E > 0 ->
|
||||
[emit_code(Tag, Types), encode_basic(Tag, V)];
|
||||
encode_basic(Tag, V, false, _) ->
|
||||
encode_basic(Tag, V, 0, _) ->
|
||||
encode_basic(Tag, V).
|
||||
|
||||
encode_basic(label, A) when is_atom(A) ->
|
||||
@@ -379,11 +461,14 @@ register_types(Types) when is_map(Types) ->
|
||||
Rev = rev_codes(Codes),
|
||||
Templates = maps:get(templates, 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(),
|
||||
Merged = #{ codes => maps:merge(Codes0, Codes)
|
||||
, rev => maps:merge(Rev0, Rev)
|
||||
, templates => maps:merge(Templates0, Templates)
|
||||
, options => maps:merge(Options0, Options)
|
||||
, labels => maps:merge(Labels0, Labels) },
|
||||
assert_sizes(Merged),
|
||||
assert_mappings(Merged),
|
||||
@@ -409,6 +494,15 @@ register_type(Code, Tag, Template) when is_integer(Code), Code >= 0 ->
|
||||
{_, true} -> error(tag_exists)
|
||||
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) ->
|
||||
#{labels := Lbls, rev_labels := RevLbls} = Types = registered_types(),
|
||||
case {is_map_key(Label, Lbls), is_map_key(Code, RevLbls)} of
|
||||
@@ -536,6 +630,7 @@ round_trip_test_() ->
|
||||
|
||||
t_sample_types() ->
|
||||
[ 5
|
||||
, -5
|
||||
, <<"a">>
|
||||
, [1,2,3]
|
||||
, {<<"a">>,1}
|
||||
@@ -558,12 +653,25 @@ user_types_test_() ->
|
||||
, ?_test(t_reg_template_vsnd_fun())
|
||||
, ?_test(t_reg_label_cache())
|
||||
, ?_test(t_reg_label_cache2())
|
||||
, ?_test(t_reg_options())
|
||||
]}.
|
||||
|
||||
dynamic_types_test_() ->
|
||||
[ ?_test(revert_to_default_types())
|
||||
, ?_test(t_typed_map())
|
||||
, ?_test(t_alts())
|
||||
, ?_test(t_anyints())
|
||||
, ?_test(t_missing_labels())
|
||||
].
|
||||
|
||||
t_round_trip(T) ->
|
||||
?debugVal(T),
|
||||
?assertMatch({T, T}, {T, decode(encode(T))}).
|
||||
|
||||
t_round_trip_typed(Type, T) ->
|
||||
?debugVal(T),
|
||||
?assertMatch({T, T}, {T, decode(encode_typed(Type, T))}).
|
||||
|
||||
t_reg_typed_tuple() ->
|
||||
Type = {int, int, int},
|
||||
MyTypes = #{ codes => #{ 1001 => int_tup3 }
|
||||
@@ -645,7 +753,37 @@ t_reg_label_cache2() ->
|
||||
Tup = {'1', '1'},
|
||||
Enc = gmser_dyn:encode_typed(lbl_tup2, Tup),
|
||||
[<<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() ->
|
||||
Term = #{a => 13, {key,1} => [a]},
|
||||
Enc = encode_typed(#{items => [{a,int},{{key,1},[label]}]}, Term),
|
||||
?assertEqual(Term, decode(Enc)).
|
||||
|
||||
t_alts() ->
|
||||
t_round_trip_typed(#{alt => [negint, int]}, -4),
|
||||
t_round_trip_typed(#{alt => [negint, int]}, 4),
|
||||
ok.
|
||||
|
||||
t_anyints() ->
|
||||
t_round_trip_typed(anyint, -5),
|
||||
t_round_trip_typed(anyint, 5),
|
||||
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.
|
||||
|
||||
-endif.
|
||||
|
||||
Reference in New Issue
Block a user