From b2e9af0acb084115809bafc096be1e5de132cb02 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Wed, 22 Jul 2026 11:26:49 +0200 Subject: [PATCH] Add ASN.1 schema export from static serialization templates. 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. --- README.md | 4 + doc/schema_export.md | 88 +++++++ src/gmser_chain_objects.erl | 10 +- src/gmser_schema_export.erl | 387 +++++++++++++++++++++++++++++ test/gmser_schema_export_tests.erl | 185 ++++++++++++++ 5 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 doc/schema_export.md create mode 100644 src/gmser_schema_export.erl create mode 100644 test/gmser_schema_export_tests.erl diff --git a/README.md b/README.md index 76b52b1..e773f3a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/schema_export.md b/doc/schema_export.md new file mode 100644 index 0000000..f9ee697 --- /dev/null +++ b/doc/schema_export.md @@ -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` diff --git a/src/gmser_chain_objects.erl b/src/gmser_chain_objects.erl index aa3a7ab..d6d5789 100644 --- a/src/gmser_chain_objects.erl +++ b/src/gmser_chain_objects.erl @@ -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; diff --git a/src/gmser_schema_export.erl b/src/gmser_schema_export.erl new file mode 100644 index 0000000..9b96c80 --- /dev/null +++ b/src/gmser_schema_export.erl @@ -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): +%%%
+%%%   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).
+%%% 
+%%% +%%% Or assemble a full module from several objects: +%%%
+%%%   gmser_schema_export:module_to_asn1(
+%%%       'GajumaruChainObjects',
+%%%       [{spend_tx, Tag, 1, Template},
+%%%        {signed_tx, 11, 1, SignedTemplate}]).
+%%% 
+%%% @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}). diff --git a/test/gmser_schema_export_tests.erl b/test/gmser_schema_export_tests.erl new file mode 100644 index 0000000..98c72ff --- /dev/null +++ b/test/gmser_schema_export_tests.erl @@ -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}])).