dev_codec_flat.erl - Flat Map Codec for Path-Based Messages
Overview
Purpose: Convert between TABMs and flat maps with path-based keys
Module: dev_codec_flat
Format: flat@1.0
Pattern: Nested structures ↔ Flat path-based keys
This module provides a codec for converting TABMs (Type Annotated Binary Messages) to and from flat Erlang maps where keys are multi-layer paths and values are TABM binaries. It enables simple key-value representations of deeply nested message structures.
Dependencies
- Testing:
eunit - HyperBEAM:
hb_message,hb_util,hb_path,hb_maps - Codecs:
dev_codec_httpsig(for commitment functions) - Includes:
include/hb.hrl
Public Functions Overview
%% Codec Interface
-spec from(Input, Req, Opts) -> {ok, TABM}.
-spec to(TABM, Req, Opts) -> {ok, FlatMap}.
%% Commitment Interface (delegates to httpsig)
-spec commit(Msg, Req, Opts) -> {ok, SignedMsg}.
-spec verify(Msg, Req, Opts) -> {ok, boolean()}.
%% Serialization Utilities
-spec serialize(Map) -> {ok, Binary}.
-spec serialize(Map, Opts) -> {ok, Binary}.
-spec deserialize(Binary) -> {ok, StructuredMap}.Public Functions
1. from/3
-spec from(Input, Req, Opts) -> {ok, TABM}
when
Input :: map() | binary(),
Req :: map(),
Opts :: map(),
TABM :: map() | binary().Description: Convert a flat map with path-based keys into a nested TABM structure. Binary values pass through unchanged.
Path Format:<<"a/b/c">>→ Nested:#{<<"a">> => #{<<"b">> => #{<<"c">> => Value}}}[<<"a">>, <<"b">>]→ Same as above<<"x">>→ Single level:#{<<"x">> => Value}
-module(dev_codec_flat_from_test).
-include_lib("eunit/include/eunit.hrl").
from_simple_path_test() ->
Flat = #{<<"a">> => <<"value">>},
{ok, Result} = dev_codec_flat:from(Flat, #{}, #{}),
?assertEqual(#{<<"a">> => <<"value">>}, Result).
from_nested_path_test() ->
Flat = #{<<"a/b">> => <<"value">>},
{ok, Result} = dev_codec_flat:from(Flat, #{}, #{}),
Expected = #{<<"a">> => #{<<"b">> => <<"value">>}},
?assert(hb_message:match(Expected, Result)).
from_multiple_paths_test() ->
Flat = #{
<<"x/y">> => <<"1">>,
<<"x/z">> => <<"2">>,
<<"a">> => <<"3">>
},
{ok, Result} = dev_codec_flat:from(Flat, #{}, #{}),
Expected = #{
<<"x">> => #{
<<"y">> => <<"1">>,
<<"z">> => <<"2">>
},
<<"a">> => <<"3">>
},
?assert(hb_message:match(Expected, Result)).
from_binary_passthrough_test() ->
Bin = <<"raw binary">>,
{ok, Result} = dev_codec_flat:from(Bin, #{}, #{}),
?assertEqual(Bin, Result).
from_deep_nesting_test() ->
Flat = #{<<"a/b/c/d">> => <<"deep">>},
{ok, Result} = dev_codec_flat:from(Flat, #{}, #{}),
Expected = #{<<"a">> => #{<<"b">> => #{<<"c">> => #{<<"d">> => <<"deep">>}}}},
?assert(hb_message:match(Expected, Result)).2. to/3
-spec to(TABM, Req, Opts) -> {ok, FlatMap}
when
TABM :: map() | binary(),
Req :: map(),
Opts :: map(),
FlatMap :: map() | binary().Description: Convert a nested TABM structure into a flat map with path-based keys. Binary values pass through unchanged.
Test Code:-module(dev_codec_flat_to_test).
-include_lib("eunit/include/eunit.hrl").
to_simple_test() ->
Nested = #{<<"a">> => <<"value">>},
{ok, Result} = dev_codec_flat:to(Nested, #{}, #{}),
?assertEqual(#{<<"a">> => <<"value">>}, Result).
to_nested_test() ->
Nested = #{<<"a">> => #{<<"b">> => <<"value">>}},
{ok, Result} = dev_codec_flat:to(Nested, #{}, #{}),
Expected = #{<<"a/b">> => <<"value">>},
?assert(hb_message:match(Expected, Result)).
to_multiple_paths_test() ->
Nested = #{
<<"x">> => #{
<<"y">> => <<"1">>,
<<"z">> => <<"2">>
},
<<"a">> => <<"3">>
},
{ok, Result} = dev_codec_flat:to(Nested, #{}, #{}),
Expected = #{
<<"x/y">> => <<"1">>,
<<"x/z">> => <<"2">>,
<<"a">> => <<"3">>
},
?assert(hb_message:match(Expected, Result)).
to_binary_passthrough_test() ->
Bin = <<"raw binary">>,
{ok, Result} = dev_codec_flat:to(Bin, #{}, #{}),
?assertEqual(Bin, Result).
to_empty_map_test() ->
{ok, Result} = dev_codec_flat:to(#{}, #{}, #{}),
?assertEqual(#{}, Result).3. commit/3
-spec commit(Msg, Req, Opts) -> {ok, SignedMsg}
when
Msg :: map(),
Req :: map(),
Opts :: map(),
SignedMsg :: map().Description: Delegates to dev_codec_httpsig:commit/3 for message signing. The flat codec uses HTTP signature commitment.
-module(dev_codec_flat_commit_test).
-include_lib("eunit/include/eunit.hrl").
commit_test() ->
% commit/3 delegates to dev_codec_httpsig:commit/3
% Need to provide a wallet for signing
Wallet = ar_wallet:new(),
Msg = #{<<"key">> => <<"value">>},
Committed = hb_message:commit(Msg, #{ priv_wallet => Wallet }, #{}),
?assert(is_map(Committed)),
?assert(maps:is_key(<<"commitments">>, Committed)).4. verify/3
-spec verify(Msg, Req, Opts) -> {ok, boolean()}
when
Msg :: map(),
Req :: map(),
Opts :: map().Description: Delegates to dev_codec_httpsig:verify/3 for signature verification.
-module(dev_codec_flat_verify_test).
-include_lib("eunit/include/eunit.hrl").
verify_test() ->
% verify/3 delegates to dev_codec_httpsig:verify/3
% Commit then verify using hb_message functions
Wallet = ar_wallet:new(),
Msg = #{<<"key">> => <<"value">>},
Committed = hb_message:commit(Msg, #{ priv_wallet => Wallet }, #{}),
Signers = hb_message:signers(Committed, #{}),
?assert(length(Signers) > 0).5. serialize/1, serialize/2
-spec serialize(Map, Opts) -> {ok, Binary}
when
Map :: map(),
Opts :: map(),
Binary :: binary().Description: Serialize a map to a text format with key: value lines. First converts to flat format, then serializes.
key1: value1
key2/nested: value2
key3/deep/path: value3-module(dev_codec_flat_serialize_test).
-include_lib("eunit/include/eunit.hrl").
serialize_simple_test() ->
Map = #{<<"key">> => <<"value">>},
{ok, Binary} = dev_codec_flat:serialize(Map),
?assert(is_binary(Binary)),
?assert(binary:match(Binary, <<"key: value">>) =/= nomatch).
serialize_nested_test() ->
Map = #{
<<"x">> => #{<<"y">> => <<"1">>},
<<"a">> => <<"2">>
},
{ok, Binary} = dev_codec_flat:serialize(Map),
?assert(is_binary(Binary)),
% Nested map serializes to flat format with path keys
?assert(byte_size(Binary) > 0).
serialize_sorted_test() ->
Map = #{
<<"z">> => <<"last">>,
<<"a">> => <<"first">>
},
{ok, Binary} = dev_codec_flat:serialize(Map),
Lines = binary:split(Binary, <<"\n">>, [global]),
% Keys should be sorted
?assertEqual(2, length([L || L <- Lines, L =/= <<>>])).6. deserialize/1
-spec deserialize(Binary) -> {ok, StructuredMap}
when
Binary :: binary(),
StructuredMap :: map().Description: Deserialize text format back to a structured map. Parses key: value lines and converts from flat to structured format.
-module(dev_codec_flat_deserialize_test).
-include_lib("eunit/include/eunit.hrl").
deserialize_simple_test() ->
Binary = <<"key: value\n">>,
{ok, Result} = dev_codec_flat:deserialize(Binary),
?assertEqual(#{<<"key">> => <<"value">>}, Result).
deserialize_nested_test() ->
Binary = <<"x/y: 1\na: 2\n">>,
{ok, Result} = dev_codec_flat:deserialize(Binary),
Expected = #{
<<"x">> => #{<<"y">> => <<"1">>},
<<"a">> => <<"2">>
},
?assert(hb_message:match(Expected, Result)).
deserialize_roundtrip_test() ->
Original = #{
<<"x">> => #{<<"y">> => <<"1">>},
<<"a">> => <<"2">>
},
{ok, Binary} = dev_codec_flat:serialize(Original),
{ok, Deserialized} = dev_codec_flat:deserialize(Binary),
?assert(hb_message:match(Original, Deserialized)).Common Patterns
%% Convert flat to nested
FlatMsg = #{
<<"user/name">> => <<"Alice">>,
<<"user/email">> => <<"alice@example.com">>,
<<"settings/theme">> => <<"dark">>
},
{ok, Nested} = dev_codec_flat:from(FlatMsg, #{}, #{}).
% Result: #{
% <<"user">> => #{
% <<"name">> => <<"Alice">>,
% <<"email">> => <<"alice@example.com">>
% },
% <<"settings">> => #{
% <<"theme">> => <<"dark">>
% }
% }
%% Convert nested to flat
NestedMsg = #{
<<"config">> => #{
<<"database">> => #{
<<"host">> => <<"localhost">>,
<<"port">> => <<"5432">>
}
}
},
{ok, Flat} = dev_codec_flat:to(NestedMsg, #{}, #{}).
% Result: #{
% <<"config/database/host">> => <<"localhost">>,
% <<"config/database/port">> => <<"5432">>
% }
%% Use with hb_message:convert
Structured = #{<<"a">> => #{<<"b">> => <<"value">>}},
Flat = hb_message:convert(Structured, <<"flat@1.0">>, #{}),
BackToStructured = hb_message:convert(Flat, <<"structured@1.0">>, <<"flat@1.0">>, #{}).
%% Serialize to text
Map = #{
<<"path1">> => <<"value1">>,
<<"path2/nested">> => <<"value2">>
},
{ok, Text} = dev_codec_flat:serialize(Map).
% Result: <<"path1: value1\npath2/nested: value2\n">>
%% Deserialize from text
Text = <<"key: value\npath/to/data: result\n">>,
{ok, Map} = dev_codec_flat:deserialize(Text).
% Result: #{
% <<"key">> => <<"value">>,
% <<"path">> => #{
% <<"to">> => #{
% <<"data">> => <<"result">>
% }
% }
% }Path Handling
Path Separators
% Slash separator
<<"a/b/c">> → #{<<"a">> => #{<<"b">> => #{<<"c">> => Value}}}
% List of parts
[<<"a">>, <<"b">>, <<"c">>] → Same as above
% Single key
<<"key">> → #{<<"key">> => Value}Path Lists as Keys
Nested = #{
<<"x">> => #{
[<<"y">>, <<"z">>] => #{<<"a">> => <<"2">>}
}
},
{ok, Flat} = dev_codec_flat:to(Nested, #{}, #{}).
% Path lists are converted to flat keys without newlinesConversion Examples
Example 1: User Profile
% Flat format
Flat = #{
<<"user/id">> => <<"123">>,
<<"user/profile/name">> => <<"Alice">>,
<<"user/profile/age">> => <<"30">>,
<<"user/settings/theme">> => <<"dark">>
}
% Nested format
Nested = #{
<<"user">> => #{
<<"id">> => <<"123">>,
<<"profile">> => #{
<<"name">> => <<"Alice">>,
<<"age">> => <<"30">>
},
<<"settings">> => #{
<<"theme">> => <<"dark">>
}
}
}Example 2: Configuration
% Flat format
Flat = #{
<<"db/host">> => <<"localhost">>,
<<"db/port">> => <<"5432">>,
<<"cache/enabled">> => <<"true">>,
<<"cache/ttl">> => <<"3600">>
}
% Nested format
Nested = #{
<<"db">> => #{
<<"host">> => <<"localhost">>,
<<"port">> => <<"5432">>
},
<<"cache">> => #{
<<"enabled">> => <<"true">>,
<<"ttl">> => <<"3600">>
}
}Serialization Format
Text Format
key1: value1
key2: value2
nested/path: value3
deep/nested/path: value4Properties
- Line-based: One key-value pair per line
- Separator: Colon and space (
:) - Sorted: Keys are sorted alphabetically
- UTF-8: Binary content is preserved as-is
- Newline: Each line ends with
\n
References
- Message System -
hb_message.erl - Path Utilities -
hb_path.erl - Deep Operations -
hb_util:deep_set/4 - HTTPSig Codec -
dev_codec_httpsig.erl - Structured Codec -
dev_codec_structured.erl
Notes
- Binary Passthrough: Binary values pass through
from/3andto/3unchanged - Path Normalization: Uses
hb_path:term_to_path_parts/2for path parsing - Deep Set: Uses
hb_util:deep_set/4for nested structure creation - Commitment: Delegates to
dev_codec_httpsigfor signing/verification - Empty Lists: Empty list values trigger error events but continue processing
- Format Agnostic: Works with any TABM-compatible message structure
- Idempotent: Converting back and forth preserves structure
- Sorted Output: Serialization produces sorted keys for consistency
- Round-trip Safe:
serialize → deserialize → serializeproduces same output - Integration: Works with
hb_message:convert/3for format conversion - Path Flexibility: Supports both string and list path formats
- Newline Handling: Path keys never contain newlines in serialized form