Compare commits
2 Commits
dda5cac7a9
...
00699b08b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00699b08b7 | ||
|
|
8734e67c66 |
141
README.md
141
README.md
@ -49,8 +49,25 @@ 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.
|
||||
the `#{items => [{Key, ValueType} | {opt, Key, ValueType}]}` construct.
|
||||
The key names are included in the encoding, and are match against the item
|
||||
specs during decoding. If the key names don't match, the decoding fails, unless
|
||||
for an `{opt, K, V}` item, in which case that item spec is skipped.
|
||||
|
||||
```erlang
|
||||
T = #{items => [{a,int},{opt,b,int},{c,int}]}
|
||||
E1 = gmser_dyn:encode_typed(T, #{a => 1, b => 2, c => 3}) ->
|
||||
[<<0>>,<<1>>,[<<252>>,
|
||||
[[[<<255>>,<<97>>],[<<248>>,<<1>>]],
|
||||
[[<<255>>,<<98>>],[<<248>>,<<2>>]],
|
||||
[[<<255>>,<<99>>],[<<248>>,<<3>>]]]]]
|
||||
E2 = gmser_dyn:encode_typed(T, #{a => 1, c => 3}) ->
|
||||
[<<0>>,<<1>>,[<<252>>,
|
||||
[[[<<255>>,<<97>>],[<<248>>,<<1>>]],
|
||||
[[<<255>>,<<99>>],[<<248>>,<<3>>]]]]]
|
||||
gmser_dyn:decode_typed(T,E2) ->
|
||||
#{c => 3,a => 1}
|
||||
```
|
||||
|
||||
## Labels
|
||||
|
||||
@ -64,12 +81,12 @@ 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}))
|
||||
gmser_dyn:deserialize(Binary, gmser_dyn:set_opts(#{missing_labels => convert}))
|
||||
```
|
||||
|
||||
or
|
||||
```erlang
|
||||
gmser_dyn:deserialize(Binary, set_opts(#{missing_labels => convert}, Types))
|
||||
gmser_dyn:deserialize(Binary, gmser_dyn:set_opts(#{missing_labels => convert}, Types))
|
||||
```
|
||||
|
||||
By calling `gmser_dyn:register_types/1`, after having added options to the type map,
|
||||
@ -114,31 +131,70 @@ 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
|
||||
the type information in the template.
|
||||
|
||||
If the template has been registered, the encoder omits inner type tags (still
|
||||
inserting the top-level tag), leading to some compression of the output.
|
||||
This also means that the serialized term cannot be decoded without the same
|
||||
schema information on the decoder side.
|
||||
|
||||
In the case of a directly provided template, all type information is inserted,
|
||||
such that the serialized term can be decoded without any added type information.
|
||||
The template types are still enforced during encoding.
|
||||
If the template has been registered, the encoder uses the registered type specification
|
||||
to drive the encoding. The code of the registered template is embedded in the encoded
|
||||
output:
|
||||
|
||||
```erlang
|
||||
ET = fun(Type,Term) -> io:fwrite("~w~n", [gmser_dyn:encode_typed(Type,Term)]) end.
|
||||
gmser_dyn:encode_typed({int,int,int}, {1,2,3}) ->
|
||||
[<<0>>,<<1>>,[<<253>>,
|
||||
[[<<248>>,<<1>>],[<<248>>,<<2>>],[<<248>>,<<3>>]]]]
|
||||
|
||||
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>>]]]]
|
||||
Types = gmser_dyn_types:add_type(t3,1013,{int,int,int}).
|
||||
gmser_dyn:encode_typed(t3, {1,2,3}, Types) ->
|
||||
[<<0>>,<<1>>,[[<<3,245>>,<<253>>],
|
||||
[[<<248>>,<<1>>],[<<248>>,<<2>>],[<<248>>,<<3>>]]]]
|
||||
```
|
||||
|
||||
Note that the original `<<253>>` type code is wrapped as `[<<3,245>>,<<253>>]`,
|
||||
where `<<3,245>>` corresponds to the custom code `1013`.
|
||||
|
||||
Using the default option `#{strict => true}`, the decoder will extract the custom
|
||||
type spec, and validate the encoded data against it. If the custom code is missing,
|
||||
the decoder aborts. Using `#{strict => false}`, the custom code is used if it exists,
|
||||
but otherwise, it's ignored, and the encoded data is decoded using the dynamic type
|
||||
info.
|
||||
|
||||
### Alternative types
|
||||
|
||||
The dynamic encoder supports two additions to the `gmserialization` template
|
||||
language: `any` and `#{alt => [AltTypes]}`.
|
||||
The dynamic encoder supports a few additions to the `gmserialization` template
|
||||
language: `any`, `#{list => Type}`, `#{alt => [AltTypes]}` and `#{switch => [AltTypes]}`.
|
||||
|
||||
#### `any`
|
||||
|
||||
The `any` type doesn't have an associated code, but enforces dynamic encoding.
|
||||
|
||||
#### `list`
|
||||
|
||||
The original list type notation expects a key-value list, e.g.
|
||||
|
||||
`[{name, binary}, {age, int}]`
|
||||
|
||||
```erlang
|
||||
EL = gmser_dyn:encode_typed([{name,binary},{age,int}], [{name,<<"Ulf">>},{age,29}]) ->
|
||||
[<<0>>,<<1>>,[<<251>>,
|
||||
[[<<253>>,[[<<255>>,<<110,97,109,101>>],[<<249>>,<<85,108,102>>]]],
|
||||
[<<253>>,[[<<255>>,<<97,103,101>>],[<<248>>,<<29>>]]]]]]
|
||||
```
|
||||
Note that the encoding explicitly lays out a `[{Key, Value}]` structure, all
|
||||
dynamically typed. This means it can be dynamically decoded without templates.
|
||||
|
||||
```erlang
|
||||
gmser_dyn:decode(EL).
|
||||
[{name,<<"Ulf">>},{age,29}]
|
||||
```
|
||||
|
||||
In order to specify something like Erlang's `[integer()]` type, we can use
|
||||
the following:
|
||||
|
||||
```erlang
|
||||
gmser_dyn:encode_typed(#{list => int}, [1,2,3,4]) ->
|
||||
[<<0>>,<<1>>,[<<251>>,
|
||||
[[<<248>>,<<1>>],[<<248>>,<<2>>],[<<248>>,<<3>>],[<<248>>,<<4>>]]]]
|
||||
```
|
||||
|
||||
#### `alt`
|
||||
|
||||
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.
|
||||
|
||||
@ -150,6 +206,55 @@ gmser_dyn:encode_typed(anyint,-5) -> [<<0>>,<<1>>,[<<246>>,[<<247>>,<<5>>]]]
|
||||
gmser_dyn:encode_typed(anyint,5) -> [<<0>>,<<1>>,[<<246>>,[<<248>>,<<5>>]]]
|
||||
```
|
||||
|
||||
#### `switch`
|
||||
|
||||
The `switch` type allows for encoding a 'tagged' object, where the tag determines
|
||||
the type.
|
||||
|
||||
```erlang
|
||||
E1 = gmser_dyn:encode_typed(#{switch => #{name => binary, age => int}}, #{age => 29}) ->
|
||||
[<<0>>,<<1>>,[<<252>>,[[[<<255>>,<<97,103,101>>],[<<248>>,<<29>>]]]]]
|
||||
gmser_dyn:decode_typed(#{switch => #{name => binary, age => int}}, E1) ->
|
||||
#{age => 29}
|
||||
E2 = gmser_dyn:encode_typed(#{switch => #{name => binary, age => int}}, #{name => <<"Ulf">>}) ->
|
||||
[<<0>>,<<1>>,[<<252>>,[[[<<255>>,<<110,97,109,101>>],[<<249>>,<<85,108,102>>]]]]]
|
||||
gmser_dyn:decode_typed(#{switch => #{name => binary, age => int}}, E1) ->
|
||||
#{name => <<"Ulf">>}
|
||||
```
|
||||
|
||||
A practical use of `switch` would be in a protocol schema:
|
||||
|
||||
```erlang
|
||||
t_msg(_) ->
|
||||
#{switch => #{ call => t_call
|
||||
, reply => t_reply
|
||||
, notification => t_notification }}.
|
||||
|
||||
t_call(_) ->
|
||||
#{items => [ {id, anyint}
|
||||
, {req, t_req} ]}.
|
||||
|
||||
t_reply(_) ->
|
||||
#{alt => [#{items => [ {id, anyint}
|
||||
, {result, t_result} ]},
|
||||
#{items => [ {id, anyint}
|
||||
, {code, anyint}
|
||||
, {message, binary} ]}
|
||||
]}.
|
||||
```
|
||||
|
||||
In this scenario, messages are 'taggged' as 1-element maps, e.g.:
|
||||
|
||||
```erlang
|
||||
async_request(Msg) ->
|
||||
Id = erlang:unique_integer(),
|
||||
gmmp_cp:to_server(
|
||||
whereis(gmmp_core_connector),
|
||||
#{call => #{ id => Id
|
||||
, req => Msg }}),
|
||||
Id.
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
Note that `anyint` is a standard type. The static serializer supports only
|
||||
|
||||
@ -7,4 +7,11 @@
|
||||
{enacl,
|
||||
{git,
|
||||
"https://git.qpq.swiss/QPQ-AG/enacl.git",
|
||||
{ref, "4eb7ec70084ba7c87b1af8797c4c4e90c84f95a2"}}}]}.
|
||||
{ref, "4eb7ec70084ba7c87b1af8797c4c4e90c84f95a2"}}},
|
||||
{eblake2, "1.0.0"}
|
||||
]}.
|
||||
|
||||
{dialyzer,
|
||||
[ {plt_apps, all_deps},
|
||||
{base_plt_apps, [erts, kernel, stdlib, enacl, base58, eblake2]}
|
||||
]}.
|
||||
10
rebar.lock
10
rebar.lock
@ -1,8 +1,16 @@
|
||||
{"1.2.0",
|
||||
[{<<"base58">>,
|
||||
{git,"https://git.qpq.swiss/QPQ-AG/erl-base58.git",
|
||||
{ref,"e6aa62eeae3d4388311401f06e4b939bf4e94b9c"}},
|
||||
0},
|
||||
{<<"eblake2">>,{pkg,<<"eblake2">>,<<"1.0.0">>},0},
|
||||
{<<"enacl">>,
|
||||
{git,"https://git.qpq.swiss/QPQ-AG/enacl.git",
|
||||
{ref,"4eb7ec70084ba7c87b1af8797c4c4e90c84f95a2"}},
|
||||
0}].
|
||||
0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"eblake2">>, <<"EC8AD20E438AAB3F2E8D5D118C366A0754219195F8A0F536587440F8F9BCF2EF">>}]},
|
||||
{pkg_hash_ext,[
|
||||
{<<"eblake2">>, <<"3C4D300A91845B25D501929A26AC2E6F7157480846FAB2347A4C11AE52E08A99">>}]}
|
||||
].
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
-module(gmser_dyn_types).
|
||||
|
||||
-export([ add_type/4
|
||||
-export([ add_type/3 %% (Tag, Code, Template) -> Types1
|
||||
, add_type/4 %% (Tag, Code, Template, Types) -> Types1
|
||||
, from_list/2
|
||||
, expand/1 ]).
|
||||
-export([ next_code/1 ]).
|
||||
@ -8,6 +9,14 @@
|
||||
next_code(#{codes := Codes}) ->
|
||||
lists:max(maps:keys(Codes)) + 1.
|
||||
|
||||
-spec add_type(Tag, Code, Template) -> Types
|
||||
when Tag :: gmser_dyn:tag()
|
||||
, Code :: gmser_dyn:code()
|
||||
, Template :: gmser_dyn:template()
|
||||
, Types :: gmser_dyn:types().
|
||||
add_type(Tag, Code, Template) ->
|
||||
add_type(Tag, Code, Template, gmser_dyn:registered_types()).
|
||||
|
||||
add_type(Tag, Code, Template, Types) ->
|
||||
elem_to_type({Tag, Code, Template}, Types).
|
||||
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
, is_id/1
|
||||
]).
|
||||
|
||||
-export([ t_id/1
|
||||
]).
|
||||
|
||||
%% For aec_serialization
|
||||
-export([ encode/1
|
||||
, decode/1
|
||||
@ -26,11 +29,18 @@
|
||||
, val
|
||||
}).
|
||||
|
||||
-type tag() :: 'account' | 'name'
|
||||
| 'commitment' | 'contract' | 'channel'
|
||||
| 'associate_chain' | 'entry' .
|
||||
-type tag() :: 'account'
|
||||
| 'associate_chain'
|
||||
| 'channel'
|
||||
| 'commitment'
|
||||
| 'contract'
|
||||
| 'contract_source'
|
||||
| 'name'
|
||||
| 'native_token'
|
||||
| 'entry'.
|
||||
|
||||
-type val() :: <<_:256>>.
|
||||
-opaque(id() :: #id{}).
|
||||
-type id() :: #id{}.
|
||||
|
||||
-export_type([ id/0
|
||||
, tag/0
|
||||
@ -94,6 +104,9 @@ decode(<<Tag:?TAG_SIZE/unit:8, Val:?PUB_SIZE/binary>>) ->
|
||||
#id{ tag = decode_tag(Tag)
|
||||
, val = Val}.
|
||||
|
||||
-spec t_id(any()) -> id().
|
||||
t_id(#id{} = Id) -> Id.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user