Add ASN.1 schema export from static serialization templates.
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:
Ulf Wiger
2026-07-22 11:26:49 +02:00
parent 4036133476
commit b2e9af0acb
5 changed files with 673 additions and 1 deletions
+4
View File
@@ -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
+88
View File
@@ -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`
+9 -1
View File
@@ -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;
+387
View File
@@ -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}).
+185
View File
@@ -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}])).