Add ASN.1 schema export from static serialization templates.
Gajumaru Serialization Tests / tests (push) Successful in 48m24s
Gajumaru Serialization Tests / tests (push) Successful in 48m24s
Introduce gmser_schema_export to emit portable ASN.1 type definitions from tag/version/template triples (RLP remains the wire format). Export tag/1 and rev_tag/1 for discovery and naming, with docs and eunit coverage.
This commit is contained in:
@@ -4,6 +4,10 @@ Serialization helpers for the Gajumaru.
|
||||
|
||||
For an overview of the static serializer, see [this document](doc/static.md).
|
||||
|
||||
To export static templates as portable ASN.1 type definitions (for codegen
|
||||
in other languages; wire format remains RLP), see
|
||||
[doc/schema_export.md](doc/schema_export.md) and module `gmser_schema_export`.
|
||||
|
||||
## Build
|
||||
|
||||
$ rebar3 compile
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Schema export (ASN.1)
|
||||
|
||||
`gmser_schema_export` turns static serialization templates into ASN.1 type
|
||||
definitions. ASN.1 is the **portable abstract model** (headers / codegen for
|
||||
other languages). The on-chain wire format remains RLP via `gmserialization`
|
||||
and `gmser_rlp`.
|
||||
|
||||
## Core API
|
||||
|
||||
Callers supply the three facts that fully define a static object layout:
|
||||
|
||||
```erlang
|
||||
Tag = gmser_chain_objects:tag(spend_tx), %% 12
|
||||
Vsn = 1,
|
||||
Template = [ {sender_id, id}
|
||||
, {recipient_id, id}
|
||||
, {amount, int}
|
||||
, {gas_price, int}
|
||||
, {gas, int}
|
||||
, {ttl, int}
|
||||
, {nonce, int}
|
||||
, {payload, binary}
|
||||
], %% e.g. aec_spend_tx:serialization_template(1)
|
||||
|
||||
{ok, TypeName, Defs} =
|
||||
gmser_schema_export:object_to_asn1(Tag, Vsn, Template).
|
||||
%% TypeName = "SpendTxV1"
|
||||
%% Defs = ASN.1 SEQUENCE body for that type
|
||||
```
|
||||
|
||||
Assemble a full module:
|
||||
|
||||
```erlang
|
||||
{ok, Asn1} =
|
||||
gmser_schema_export:module_to_asn1(
|
||||
'GajumaruChainObjects',
|
||||
[ {spend_tx, Tag, Vsn, Template}
|
||||
, {signed_tx, 11, 1, SignedTemplate}
|
||||
]).
|
||||
|
||||
ok = gmser_schema_export:write_module(
|
||||
"asn1_generated/GajumaruChainObjects.asn",
|
||||
'GajumaruChainObjects',
|
||||
Objects).
|
||||
```
|
||||
|
||||
Object specs are either `{Tag, Vsn, Template}` or
|
||||
`{TypeName, Tag, Vsn, Template}`. When the name is omitted, it is derived
|
||||
from `gmser_chain_objects:rev_tag(Tag)` (e.g. `spend_tx` → `SpendTxV1`).
|
||||
|
||||
## Options
|
||||
|
||||
```erlang
|
||||
#{ type_name => atom() | string() | binary() %% override derived name
|
||||
, include_tag_vsn => boolean() %% default true:
|
||||
%% first fields are
|
||||
%% tag INTEGER (Tag),
|
||||
%% vsn INTEGER (Vsn)
|
||||
}
|
||||
```
|
||||
|
||||
## Type mapping
|
||||
|
||||
| Template type | ASN.1 |
|
||||
|---------------|--------|
|
||||
| `int` | `BigInt` (`INTEGER (0..MAX)`) |
|
||||
| `uint8` … `uint128` | `Uint8` … `Uint128` |
|
||||
| `bool` | `BOOLEAN` |
|
||||
| `binary` | `OCTET STRING` |
|
||||
| `id` | `Id` |
|
||||
| `[T]` | `SEQUENCE OF T` |
|
||||
| `{T1,...,Tn}` | anonymous `SEQUENCE { c1 T1, ... }` |
|
||||
| `#{items := [{f,T},...]}` | anonymous `SEQUENCE { f T, ... }` |
|
||||
|
||||
Field names are lowerCamelCase (`gas_price` → `gasPrice`). Type names are
|
||||
UpperCamelCase with a version suffix (`spend_tx` + vsn 1 → `SpendTxV1`).
|
||||
|
||||
## Discovery of templates
|
||||
|
||||
This module does **not** scan the tree for all templates. Callers (or a later
|
||||
harvester in gajumaru) are expected to gather `{Tag, Vsn, Template}` triples
|
||||
from `serialization_template/1` and `gmser_chain_objects:tag/1`.
|
||||
|
||||
## Related
|
||||
|
||||
- Template language: `doc/static.md`, `gmserialization.erl`
|
||||
- Tag registry: `gmser_chain_objects:tag/1`, `rev_tag/1`
|
||||
- Earlier ASN.1 experiments: `asn1/`, `doc/asn1_compact.md`
|
||||
@@ -13,6 +13,8 @@
|
||||
-export([ serialize/4
|
||||
, deserialize/4
|
||||
, deserialize_type_and_vsn/1
|
||||
, tag/1
|
||||
, rev_tag/1
|
||||
]).
|
||||
|
||||
-type template() :: gmserialization:template().
|
||||
@@ -34,8 +36,14 @@ deserialize_type_and_vsn(Binary) ->
|
||||
deserialize(Type, Vsn, Template, Binary) ->
|
||||
gmserialization:deserialize(Type, tag(Type), Vsn, Template, Binary).
|
||||
|
||||
%% Numeric wire tag for a chain-object type atom (e.g. spend_tx -> 12).
|
||||
-spec tag(atom()) -> non_neg_integer().
|
||||
|
||||
%% Inverse of tag/1.
|
||||
-spec rev_tag(non_neg_integer()) -> atom().
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%% Tag registry
|
||||
%%%===================================================================
|
||||
|
||||
tag(account) -> 10;
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2026, QPQ AG
|
||||
%%% @doc
|
||||
%%% Export static serialization templates as ASN.1 type definitions.
|
||||
%%%
|
||||
%%% This is the portable schema surface for the template language in
|
||||
%%% {@link gmserialization}: given a chain-object tag, version and
|
||||
%%% field template, emit ASN.1 that other languages can compile to
|
||||
%%% typed headers. The wire format remains RLP; ASN.1 is the abstract
|
||||
%%% model, not BER/DER on the chain.
|
||||
%%%
|
||||
%%% Minimal API (discovery of all templates is left to callers):
|
||||
%%% <pre>
|
||||
%%% Template = aec_spend_tx:serialization_template(1),
|
||||
%%% Tag = gmser_chain_objects:tag(spend_tx),
|
||||
%%% {ok, Asn1} = gmser_schema_export:object_to_asn1(Tag, 1, Template).
|
||||
%%% </pre>
|
||||
%%%
|
||||
%%% Or assemble a full module from several objects:
|
||||
%%% <pre>
|
||||
%%% gmser_schema_export:module_to_asn1(
|
||||
%%% 'GajumaruChainObjects',
|
||||
%%% [{spend_tx, Tag, 1, Template},
|
||||
%%% {signed_tx, 11, 1, SignedTemplate}]).
|
||||
%%% </pre>
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(gmser_schema_export).
|
||||
-vsn("0.1.0").
|
||||
|
||||
-export([ object_to_asn1/3
|
||||
, object_to_asn1/4
|
||||
, module_to_asn1/2
|
||||
, module_to_asn1/3
|
||||
, write_module/3
|
||||
, write_module/4
|
||||
, type_name/2
|
||||
, type_name/3
|
||||
]).
|
||||
|
||||
-export_type([ object_spec/0
|
||||
, export_opts/0
|
||||
]).
|
||||
|
||||
-type template() :: gmserialization:template().
|
||||
-type type_name() :: atom() | string() | binary().
|
||||
|
||||
%% {Tag, Vsn, Template} | {TypeName, Tag, Vsn, Template}
|
||||
-type object_spec() ::
|
||||
{non_neg_integer(), non_neg_integer(), template()}
|
||||
| {type_name(), non_neg_integer(), non_neg_integer(), template()}.
|
||||
|
||||
-type export_opts() ::
|
||||
#{ type_name => type_name()
|
||||
, include_tag_vsn => boolean() %% default true
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Emit ASN.1 type definitions for one object (no module wrapper).
|
||||
-spec object_to_asn1(non_neg_integer(), non_neg_integer(), template()) ->
|
||||
{ok, TypeName :: string(), Defs :: iodata()}.
|
||||
object_to_asn1(Tag, Vsn, Template) ->
|
||||
object_to_asn1(Tag, Vsn, Template, #{}).
|
||||
|
||||
-spec object_to_asn1(non_neg_integer(), non_neg_integer(), template(),
|
||||
export_opts()) ->
|
||||
{ok, TypeName :: string(), Defs :: iodata()}.
|
||||
object_to_asn1(Tag, Vsn, Template, Opts) when is_map(Opts) ->
|
||||
assert_template(Template),
|
||||
TypeName = case maps:get(type_name, Opts, undefined) of
|
||||
undefined -> type_name(Tag, Vsn);
|
||||
Name -> normalize_type_name(Name, Vsn)
|
||||
end,
|
||||
IncludeTagVsn = maps:get(include_tag_vsn, Opts, true),
|
||||
Fields = case IncludeTagVsn of
|
||||
true ->
|
||||
[{tag, {fixed_int, Tag}},
|
||||
{vsn, {fixed_int, Vsn}}
|
||||
| Template];
|
||||
false ->
|
||||
Template
|
||||
end,
|
||||
{ok, Body} = emit_sequence(TypeName, Fields),
|
||||
{ok, TypeName, Body}.
|
||||
|
||||
%% @doc Emit a complete ASN.1 module for a list of objects.
|
||||
-spec module_to_asn1(type_name(), [object_spec()]) -> {ok, iodata()}.
|
||||
module_to_asn1(ModuleName, Objects) ->
|
||||
module_to_asn1(ModuleName, Objects, #{}).
|
||||
|
||||
-spec module_to_asn1(type_name(), [object_spec()], export_opts()) ->
|
||||
{ok, iodata()}.
|
||||
module_to_asn1(ModuleName, Objects, Opts) when is_list(Objects), is_map(Opts) ->
|
||||
Mod = asn1_type_name(ModuleName),
|
||||
{TypeParts, IndexLines} =
|
||||
lists:mapfoldl(
|
||||
fun(Spec, Acc) ->
|
||||
{Tag, Vsn, Template, NameOpt} = expand_spec(Spec),
|
||||
ObjOpts = case NameOpt of
|
||||
undefined -> Opts;
|
||||
N -> Opts#{type_name => N}
|
||||
end,
|
||||
{ok, TN, Defs} = object_to_asn1(Tag, Vsn, Template, ObjOpts),
|
||||
Index = io_lib:format(
|
||||
"-- tag ~w vsn ~w -> ~s~n", [Tag, Vsn, TN]),
|
||||
{[Defs, $\n], [Index | Acc]}
|
||||
end,
|
||||
[],
|
||||
Objects),
|
||||
Header = [
|
||||
"-- Generated by gmser_schema_export.\n",
|
||||
"-- Abstract syntax for static gmserialization templates.\n",
|
||||
"-- Wire encoding remains RLP (see gmser_rlp / gmserialization);\n",
|
||||
"-- this module is for portable types / codegen, not BER on-chain.\n",
|
||||
"\n",
|
||||
Mod, " DEFINITIONS\n",
|
||||
" AUTOMATIC TAGS ::=\n",
|
||||
"BEGIN\n",
|
||||
"\n",
|
||||
"EXPORTS ALL;\n",
|
||||
"\n",
|
||||
common_types(),
|
||||
"\n",
|
||||
"-- ============================================================\n",
|
||||
"-- Generated object types\n",
|
||||
"-- ============================================================\n",
|
||||
"\n"
|
||||
],
|
||||
Index = [
|
||||
"-- ============================================================\n",
|
||||
"-- Tag / version index\n",
|
||||
"-- ============================================================\n",
|
||||
lists:reverse(IndexLines),
|
||||
"\n"
|
||||
],
|
||||
Footer = "END\n",
|
||||
{ok, [Header, TypeParts, Index, Footer]}.
|
||||
|
||||
%% @doc Write a complete ASN.1 module to a file.
|
||||
-spec write_module(file:filename(), type_name(), [object_spec()]) ->
|
||||
ok | {error, term()}.
|
||||
write_module(Filename, ModuleName, Objects) ->
|
||||
write_module(Filename, ModuleName, Objects, #{}).
|
||||
|
||||
-spec write_module(file:filename(), type_name(), [object_spec()],
|
||||
export_opts()) ->
|
||||
ok | {error, term()}.
|
||||
write_module(Filename, ModuleName, Objects, Opts) ->
|
||||
{ok, Iodata} = module_to_asn1(ModuleName, Objects, Opts),
|
||||
file:write_file(Filename, Iodata).
|
||||
|
||||
%% @doc Derive an ASN.1 type name from tag and version.
|
||||
%% Uses {@link gmser_chain_objects:rev_tag/1} when the tag is registered,
|
||||
%% otherwise `Object{Tag}V{Vsn}`.
|
||||
-spec type_name(non_neg_integer(), non_neg_integer()) -> string().
|
||||
type_name(Tag, Vsn) ->
|
||||
Base =
|
||||
try gmser_chain_objects:rev_tag(Tag) of
|
||||
Atom when is_atom(Atom) -> atom_to_list(Atom)
|
||||
catch
|
||||
error:function_clause ->
|
||||
"Object" ++ integer_to_list(Tag);
|
||||
error:{case_clause, _} ->
|
||||
"Object" ++ integer_to_list(Tag)
|
||||
end,
|
||||
normalize_type_name(Base, Vsn).
|
||||
|
||||
-spec type_name(type_name(), non_neg_integer(), non_neg_integer()) -> string().
|
||||
type_name(Name, _Tag, Vsn) ->
|
||||
normalize_type_name(Name, Vsn).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Spec expansion
|
||||
%%%===================================================================
|
||||
|
||||
expand_spec({Tag, Vsn, Template})
|
||||
when is_integer(Tag), is_integer(Vsn), is_list(Template) ->
|
||||
{Tag, Vsn, Template, undefined};
|
||||
expand_spec({Name, Tag, Vsn, Template})
|
||||
when is_integer(Tag), is_integer(Vsn), is_list(Template) ->
|
||||
{Tag, Vsn, Template, Name};
|
||||
expand_spec(Other) ->
|
||||
error({bad_object_spec, Other}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Template → ASN.1 SEQUENCE
|
||||
%%%===================================================================
|
||||
|
||||
emit_sequence(TypeName, Fields) ->
|
||||
Lines =
|
||||
[io_lib:format("~s ::= SEQUENCE {~n", [TypeName])] ++
|
||||
field_lines(Fields) ++
|
||||
["}\n"],
|
||||
{ok, Lines}.
|
||||
|
||||
field_lines([]) ->
|
||||
[];
|
||||
field_lines([{Name, Type}]) ->
|
||||
[io_lib:format(" ~s ~s~n",
|
||||
[asn1_field_name(Name), type_to_asn1(Type)])];
|
||||
field_lines([{Name, Type} | Rest]) ->
|
||||
[io_lib:format(" ~s ~s,~n",
|
||||
[asn1_field_name(Name), type_to_asn1(Type)])
|
||||
| field_lines(Rest)].
|
||||
|
||||
%% Map one template type term to an ASN.1 type expression (string/iodata).
|
||||
type_to_asn1(int) -> "BigInt";
|
||||
type_to_asn1(uint128) -> "Uint128";
|
||||
type_to_asn1(uint64) -> "Uint64";
|
||||
type_to_asn1(uint32) -> "Uint32";
|
||||
type_to_asn1(uint16) -> "Uint16";
|
||||
type_to_asn1(uint8) -> "Uint8";
|
||||
type_to_asn1(bool) -> "BOOLEAN";
|
||||
type_to_asn1(binary) -> "OCTET STRING";
|
||||
type_to_asn1(id) -> "Id";
|
||||
type_to_asn1({fixed_int, N}) when is_integer(N), N >= 0 ->
|
||||
io_lib:format("INTEGER (~w)", [N]);
|
||||
type_to_asn1([ElemType]) ->
|
||||
%% SEQUENCE OF T (list of any length)
|
||||
io_lib:format("SEQUENCE OF ~s", [type_to_asn1(ElemType)]);
|
||||
type_to_asn1(Type) when is_tuple(Type) ->
|
||||
%% Fixed-arity tuple → anonymous SEQUENCE with c1..cN
|
||||
Types = tuple_to_list(Type),
|
||||
Components =
|
||||
lists:map(
|
||||
fun({I, T}) ->
|
||||
io_lib:format("~sc~w ~s",
|
||||
[indent_spacer(), I, type_to_asn1(T)])
|
||||
end,
|
||||
lists:zip(lists:seq(1, length(Types)), Types)),
|
||||
["SEQUENCE {\n",
|
||||
join_components(Components),
|
||||
"\n }"];
|
||||
type_to_asn1(#{items := Items}) when is_list(Items) ->
|
||||
%% Static map / record: named fields, order preserved, no keys on wire
|
||||
Components =
|
||||
[io_lib:format("~s~s ~s",
|
||||
[indent_spacer(),
|
||||
asn1_field_name(Name),
|
||||
type_to_asn1(T)])
|
||||
|| {Name, T} <- Items],
|
||||
["SEQUENCE {\n",
|
||||
join_components(Components),
|
||||
"\n }"];
|
||||
type_to_asn1(Other) ->
|
||||
error({unsupported_template_type, Other}).
|
||||
|
||||
indent_spacer() ->
|
||||
" ".
|
||||
|
||||
join_components([]) ->
|
||||
"";
|
||||
join_components([C]) ->
|
||||
C;
|
||||
join_components([C | Rest]) ->
|
||||
[C, ",\n", join_components(Rest)].
|
||||
|
||||
%%%===================================================================
|
||||
%%% Naming
|
||||
%%%===================================================================
|
||||
|
||||
%% "spend_tx" / spend_tx / <<"spend_tx">> + vsn 1 -> "SpendTxV1"
|
||||
normalize_type_name(Name, Vsn) when is_atom(Name) ->
|
||||
normalize_type_name(atom_to_list(Name), Vsn);
|
||||
normalize_type_name(Name, Vsn) when is_binary(Name) ->
|
||||
normalize_type_name(binary_to_list(Name), Vsn);
|
||||
normalize_type_name(Name, Vsn) when is_list(Name), is_integer(Vsn), Vsn >= 0 ->
|
||||
Base = asn1_type_name(Name),
|
||||
%% Avoid double-appending version if caller already passed SpendTxV1
|
||||
case lists:suffix("V" ++ integer_to_list(Vsn), Base) of
|
||||
true -> Base;
|
||||
false -> Base ++ "V" ++ integer_to_list(Vsn)
|
||||
end.
|
||||
|
||||
%% Upper camel case, strip non alphanumerics: spend_tx -> SpendTx
|
||||
asn1_type_name(Name) when is_atom(Name) ->
|
||||
asn1_type_name(atom_to_list(Name));
|
||||
asn1_type_name(Name) when is_binary(Name) ->
|
||||
asn1_type_name(binary_to_list(Name));
|
||||
asn1_type_name(Name) when is_list(Name) ->
|
||||
Parts = split_name(Name),
|
||||
lists:flatten([uppercase_first(P) || P <- Parts, P =/= ""]).
|
||||
|
||||
%% Field names: lower camel case (ASN.1 values start with lowercase;
|
||||
%% hyphens/underscores avoided for Java friendliness).
|
||||
asn1_field_name(Name) when is_atom(Name) ->
|
||||
asn1_field_name(atom_to_list(Name));
|
||||
asn1_field_name(Name) when is_binary(Name) ->
|
||||
asn1_field_name(binary_to_list(Name));
|
||||
asn1_field_name(Name) when is_list(Name) ->
|
||||
case split_name(Name) of
|
||||
[] ->
|
||||
error({bad_field_name, Name});
|
||||
[First | Rest] ->
|
||||
lists:flatten([lowercase_first(First)
|
||||
| [uppercase_first(P) || P <- Rest, P =/= ""]])
|
||||
end.
|
||||
|
||||
split_name(Name) ->
|
||||
%% Split on '_' or '-'
|
||||
split_name(Name, [], []).
|
||||
|
||||
split_name([], Acc, Cur) ->
|
||||
lists:reverse(case Cur of
|
||||
[] -> Acc;
|
||||
_ -> [lists:reverse(Cur) | Acc]
|
||||
end);
|
||||
split_name([C | Rest], Acc, Cur) when C =:= $_; C =:= $- ->
|
||||
case Cur of
|
||||
[] -> split_name(Rest, Acc, []);
|
||||
_ -> split_name(Rest, [lists:reverse(Cur) | Acc], [])
|
||||
end;
|
||||
split_name([C | Rest], Acc, Cur) ->
|
||||
split_name(Rest, Acc, [C | Cur]).
|
||||
|
||||
uppercase_first([C | Rest]) when C >= $a, C =< $z ->
|
||||
[C - ($a - $A) | Rest];
|
||||
uppercase_first(Other) ->
|
||||
Other.
|
||||
|
||||
lowercase_first([C | Rest]) when C >= $A, C =< $Z ->
|
||||
[C + ($a - $A) | Rest];
|
||||
lowercase_first(Other) ->
|
||||
Other.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Common ASN.1 types (shared vocabulary)
|
||||
%%%===================================================================
|
||||
|
||||
common_types() ->
|
||||
"-- ============================================================\n"
|
||||
"-- Common types (gmserialization template vocabulary)\n"
|
||||
"-- ============================================================\n"
|
||||
"\n"
|
||||
"Id ::= SEQUENCE {\n"
|
||||
" type INTEGER (0..255),\n"
|
||||
" value OCTET STRING (SIZE (32))\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"-- Non-negative bignum (template type 'int', e.g. Pucks amounts)\n"
|
||||
"BigInt ::= INTEGER (0..MAX)\n"
|
||||
"\n"
|
||||
"Uint8 ::= INTEGER (0..255)\n"
|
||||
"Uint16 ::= INTEGER (0..65535)\n"
|
||||
"Uint32 ::= INTEGER (0..4294967295)\n"
|
||||
"Uint64 ::= INTEGER (0..18446744073709551615)\n"
|
||||
"Uint128 ::= INTEGER (0..340282366920938463463374607431768211455)\n"
|
||||
"\n".
|
||||
|
||||
%%%===================================================================
|
||||
%%% Validation
|
||||
%%%===================================================================
|
||||
|
||||
assert_template(Template) when is_list(Template) ->
|
||||
lists:foreach(
|
||||
fun({Name, Type}) when is_atom(Name) ->
|
||||
assert_type(Type);
|
||||
(Other) ->
|
||||
error({bad_template_field, Other})
|
||||
end,
|
||||
Template);
|
||||
assert_template(Other) ->
|
||||
error({bad_template, Other}).
|
||||
|
||||
assert_type(int) -> ok;
|
||||
assert_type(uint128) -> ok;
|
||||
assert_type(uint64) -> ok;
|
||||
assert_type(uint32) -> ok;
|
||||
assert_type(uint16) -> ok;
|
||||
assert_type(uint8) -> ok;
|
||||
assert_type(bool) -> ok;
|
||||
assert_type(binary) -> ok;
|
||||
assert_type(id) -> ok;
|
||||
assert_type([T]) -> assert_type(T);
|
||||
assert_type(T) when is_tuple(T) ->
|
||||
lists:foreach(fun assert_type/1, tuple_to_list(T));
|
||||
assert_type(#{items := Items}) when is_list(Items) ->
|
||||
lists:foreach(
|
||||
fun({Name, T}) when is_atom(Name) -> assert_type(T);
|
||||
(Other) -> error({bad_map_item, Other})
|
||||
end,
|
||||
Items);
|
||||
assert_type(Other) ->
|
||||
error({unsupported_template_type, Other}).
|
||||
@@ -0,0 +1,185 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2026, QPQ AG
|
||||
%%% @doc
|
||||
%%% EUnit tests for gmser_schema_export.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(gmser_schema_export_tests).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
%% Spend template as in aec_spend_tx (kept local so tests do not
|
||||
%% depend on aecore).
|
||||
spend_template() ->
|
||||
[ {sender_id, id}
|
||||
, {recipient_id, id}
|
||||
, {amount, int}
|
||||
, {gas_price, int}
|
||||
, {gas, int}
|
||||
, {ttl, int}
|
||||
, {nonce, int}
|
||||
, {payload, binary}
|
||||
].
|
||||
|
||||
signed_tx_template() ->
|
||||
[ {signatures, [binary]}
|
||||
, {transaction, binary}
|
||||
].
|
||||
|
||||
name_update_v1_template() ->
|
||||
[ {account_id, id}
|
||||
, {nonce, int}
|
||||
, {name_id, id}
|
||||
, {name_ttl, int}
|
||||
, {pointers, [{binary, id}]}
|
||||
, {client_ttl, int}
|
||||
, {gas_price, int}
|
||||
, {gas, int}
|
||||
, {ttl, int}
|
||||
].
|
||||
|
||||
match(Bin, Re) when is_binary(Bin) ->
|
||||
re:run(Bin, Re, [{capture, none}]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Naming
|
||||
%%%===================================================================
|
||||
|
||||
type_name_from_registered_tag_test() ->
|
||||
Tag = gmser_chain_objects:tag(spend_tx),
|
||||
?assertEqual(12, Tag),
|
||||
?assertEqual("SpendTxV1", gmser_schema_export:type_name(Tag, 1)).
|
||||
|
||||
type_name_from_atom_test() ->
|
||||
?assertEqual("SpendTxV1",
|
||||
gmser_schema_export:type_name(spend_tx, 12, 1)),
|
||||
?assertEqual("SignedTxV1",
|
||||
gmser_schema_export:type_name(<<"signed_tx">>, 11, 1)).
|
||||
|
||||
type_name_unknown_tag_test() ->
|
||||
?assertEqual("Object9999V2", gmser_schema_export:type_name(9999, 2)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Single object export
|
||||
%%%===================================================================
|
||||
|
||||
spend_object_export_test() ->
|
||||
Tag = gmser_chain_objects:tag(spend_tx),
|
||||
{ok, "SpendTxV1", Defs} =
|
||||
gmser_schema_export:object_to_asn1(Tag, 1, spend_template()),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"SpendTxV1 ::= SEQUENCE \\{">>)),
|
||||
?assertEqual(match, match(Text, <<"tag INTEGER \\(12\\)">>)),
|
||||
?assertEqual(match, match(Text, <<"vsn INTEGER \\(1\\)">>)),
|
||||
?assertEqual(match, match(Text, <<"senderId Id">>)),
|
||||
?assertEqual(match, match(Text, <<"recipientId Id">>)),
|
||||
?assertEqual(match, match(Text, <<"amount BigInt">>)),
|
||||
?assertEqual(match, match(Text, <<"gasPrice BigInt">>)),
|
||||
?assertEqual(match, match(Text, <<"payload OCTET STRING">>)),
|
||||
%% last field must not have a trailing comma before closing brace
|
||||
?assertEqual(match, match(Text, <<"payload OCTET STRING\n}">>)).
|
||||
|
||||
spend_without_tag_vsn_fields_test() ->
|
||||
Tag = gmser_chain_objects:tag(spend_tx),
|
||||
{ok, "SpendTxV1", Defs} =
|
||||
gmser_schema_export:object_to_asn1(
|
||||
Tag, 1, spend_template(), #{include_tag_vsn => false}),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(nomatch, match(Text, <<"tag INTEGER">>)),
|
||||
?assertEqual(match, match(Text, <<"senderId Id">>)).
|
||||
|
||||
explicit_type_name_test() ->
|
||||
{ok, "MySpendV1", Defs} =
|
||||
gmser_schema_export:object_to_asn1(
|
||||
12, 1, spend_template(), #{type_name => "MySpend"}),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"MySpendV1 ::= SEQUENCE">>)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Nested types
|
||||
%%%===================================================================
|
||||
|
||||
list_and_tuple_export_test() ->
|
||||
Tag = gmser_chain_objects:tag(name_update_tx),
|
||||
{ok, TypeName, Defs} =
|
||||
gmser_schema_export:object_to_asn1(Tag, 1, name_update_v1_template()),
|
||||
?assertEqual("NameUpdateTxV1", TypeName),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"pointers SEQUENCE OF SEQUENCE \\{">>)),
|
||||
?assertEqual(match, match(Text, <<"c1 OCTET STRING">>)),
|
||||
?assertEqual(match, match(Text, <<"c2 Id">>)).
|
||||
|
||||
signed_tx_list_field_test() ->
|
||||
Tag = gmser_chain_objects:tag(signed_tx),
|
||||
{ok, "SignedTxV1", Defs} =
|
||||
gmser_schema_export:object_to_asn1(Tag, 1, signed_tx_template()),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"signatures SEQUENCE OF OCTET STRING">>)),
|
||||
?assertEqual(match, match(Text, <<"transaction OCTET STRING">>)).
|
||||
|
||||
map_items_export_test() ->
|
||||
Template = [{body, #{items => [{foo, int}, {bar, binary}]}}],
|
||||
{ok, _, Defs} =
|
||||
gmser_schema_export:object_to_asn1(
|
||||
10, 1, Template, #{type_name => account, include_tag_vsn => false}),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"body SEQUENCE \\{">>)),
|
||||
?assertEqual(match, match(Text, <<"foo BigInt">>)),
|
||||
?assertEqual(match, match(Text, <<"bar OCTET STRING">>)).
|
||||
|
||||
uint_types_export_test() ->
|
||||
Template = [{a, uint8}, {b, uint16}, {c, uint32},
|
||||
{d, uint64}, {e, uint128}, {f, bool}],
|
||||
{ok, _, Defs} =
|
||||
gmser_schema_export:object_to_asn1(
|
||||
1, 1, Template,
|
||||
#{type_name => "Uints", include_tag_vsn => false}),
|
||||
Text = iolist_to_binary(Defs),
|
||||
?assertEqual(match, match(Text, <<"a Uint8">>)),
|
||||
?assertEqual(match, match(Text, <<"b Uint16">>)),
|
||||
?assertEqual(match, match(Text, <<"c Uint32">>)),
|
||||
?assertEqual(match, match(Text, <<"d Uint64">>)),
|
||||
?assertEqual(match, match(Text, <<"e Uint128">>)),
|
||||
?assertEqual(match, match(Text, <<"f BOOLEAN">>)).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Full module
|
||||
%%%===================================================================
|
||||
|
||||
module_export_test() ->
|
||||
Objects =
|
||||
[ {spend_tx,
|
||||
gmser_chain_objects:tag(spend_tx),
|
||||
1,
|
||||
spend_template()}
|
||||
, {gmser_chain_objects:tag(signed_tx), 1, signed_tx_template()}
|
||||
],
|
||||
{ok, Iodata} =
|
||||
gmser_schema_export:module_to_asn1('GajumaruChainObjects', Objects),
|
||||
Text = iolist_to_binary(Iodata),
|
||||
?assertEqual(match, match(Text, <<"GajumaruChainObjects DEFINITIONS">>)),
|
||||
?assertEqual(match, match(Text, <<"Id ::= SEQUENCE">>)),
|
||||
?assertEqual(match, match(Text, <<"BigInt ::= INTEGER">>)),
|
||||
?assertEqual(match, match(Text, <<"SpendTxV1 ::= SEQUENCE">>)),
|
||||
?assertEqual(match, match(Text, <<"SignedTxV1 ::= SEQUENCE">>)),
|
||||
?assertEqual(match, match(Text, <<"tag 12 vsn 1 -> SpendTxV1">>)),
|
||||
?assertEqual(match, match(Text, <<"tag 11 vsn 1 -> SignedTxV1">>)),
|
||||
?assertEqual(match, re:run(Text, <<"^END">>, [{capture, none}, multiline])).
|
||||
|
||||
write_module_test() ->
|
||||
Tmp = filename:join(
|
||||
filename:basedir(user_cache, "gmser_schema_export"),
|
||||
"gmser_schema_export_test.asn"),
|
||||
ok = filelib:ensure_dir(Tmp),
|
||||
Objects =
|
||||
[{gmser_chain_objects:tag(spend_tx), 1, spend_template()}],
|
||||
?assertEqual(ok, gmser_schema_export:write_module(
|
||||
Tmp, 'GajumaruChainObjects', Objects)),
|
||||
{ok, Bin} = file:read_file(Tmp),
|
||||
?assertEqual(match, match(Bin, <<"SpendTxV1 ::= SEQUENCE">>)),
|
||||
ok = file:delete(Tmp).
|
||||
|
||||
bad_template_test() ->
|
||||
?assertError({unsupported_template_type, float},
|
||||
gmser_schema_export:object_to_asn1(
|
||||
1, 1, [{x, float}])).
|
||||
Reference in New Issue
Block a user