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}])).