From 4f628948a2ea2f689c1935b3b0ffa53045cc5da1 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:18:44 +0100 Subject: [PATCH 1/3] Update dependencies --- rebar.config | 9 ++------- rebar.lock | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/rebar.config b/rebar.config index 294a8b6..8ae8c59 100644 --- a/rebar.config +++ b/rebar.config @@ -1,11 +1,8 @@ %% -*- mode:erlang -*- {erl_opts, [debug_info, warnings_as_errors]}. -{escript_name, example_model2}. -{escript_emu_args, "%%! -escript main example_model2\n"}. - {deps, [ {snabbkaffe, "1.0.10"} - , {typerefl, {git, "https://github.com/ieQu1/typerefl.git", {tag, "0.9.5"}}} + , {typerefl, {git, "https://github.com/ieQu1/typerefl.git", {tag, "0.9.7"}}} ]}. {cover_enabled, true}. @@ -16,10 +13,8 @@ {eunit_opts, [verbose, {print_depth, 100}]}. -%{dialyzer, [{warnings, [overspecs, underspecs]}]}. - {profiles, [ {test, - [{deps, [{proper, "1.3.0"}]}]} + [{deps, [{proper, "1.5.0"}]}]} , {dev, [ {plugins, [rebar3_hex]} ]} diff --git a/rebar.lock b/rebar.lock index 64e5fb8..cf3cf06 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,12 +1,12 @@ {"1.2.0", [{<<"erlang_qq">>, {git,"https://github.com/k32/erlang_qq.git", - {ref,"beab75239f799240397aee0aec30685b9bbf33bb"}}, + {ref,"21fe46d8c03ce2f65733aadf3ad97b7bb7edea28"}}, 1}, {<<"snabbkaffe">>,{pkg,<<"snabbkaffe">>,<<"1.0.10">>},0}, {<<"typerefl">>, {git,"https://github.com/ieQu1/typerefl.git", - {ref,"90947e191ecc284ff12a193f9c6fe0265863c77e"}}, + {ref,"a202b2a483745373057cc28e04f259a28c4a5c27"}}, 0}]}. [ {pkg_hash,[ From 2b6e4d5754cdcdb34054fbf6cabd330dbf5956f3 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:06:13 +0100 Subject: [PATCH 2/3] Support nested maps in lee_config_file --- src/framework/lee_lib.erl | 114 ++++++++++++++++++++---------- src/framework/lee_model.erl | 41 ++++++----- src/metatypes/lee_config_file.erl | 13 +--- test/data/conf-file-correct.eterm | 10 ++- test/lee_conf_file_tests.erl | 40 +++++++---- 5 files changed, 135 insertions(+), 83 deletions(-) diff --git a/src/framework/lee_lib.erl b/src/framework/lee_lib.erl index 5122fcf..7aeaa86 100644 --- a/src/framework/lee_lib.erl +++ b/src/framework/lee_lib.erl @@ -3,29 +3,22 @@ -include("lee.hrl"). --export([ term_to_string/1 - , format/2 - , patch_key/1 - , make_nested_patch/3 - , splitl/2 - , splitr/2 +-export([ term_to_string/1, format/2 + , patch_key/1, make_nested_patch/3 + , tree_to_patch/3, tree_get/2 + , splitl/2, splitr/2 , inject_error_location/2 - , compose_checks/1 - , perform_checks/3 - , run_cmd/2 - , validate_optional_meta_attr/4 - , validate_optional_meta_attr/3 - , validate_meta_attr/3 - , format_typerefl_error/1 - , report_error/2 - , report_warning/2 - , collect_errors/1 + , compose_checks/1, perform_checks/3 + , validate_optional_meta_attr/4, validate_optional_meta_attr/3, validate_meta_attr/3 + , format_typerefl_error/1, report_error/2, report_warning/2, collect_errors/1 ]). --export_type([check_result/0]). +-export_type([check_result/0, conf_tree/0]). -type check_result() :: {[string()], [string()]}. +-type conf_tree() :: #{atom() | [atom()] => term()}. + -spec format(string(), [term()]) -> string(). format(Fmt, Attrs) -> lists:flatten(io_lib:format(Fmt, Attrs)). @@ -63,6 +56,71 @@ make_nested_patch(Model, Parent, Children) -> [{set, Parent ++ [ChildKey], ?lee_map_placeholder} |[{set, Parent ++ [ChildKey|K], V} || {K, V} <- maps:to_list(Children)]]. +-spec tree_to_patch(lee:model(), conf_tree(), [lee:model_key()]) -> {ok, lee:patch()}. +tree_to_patch(Model, Tree, MKeys) -> + try + {ok, tree_to_patch(Model, [], Tree, [lee_model:key_parts(K) || K <- MKeys])} + catch + %% TODO: better error handling + EC:Err:Stack -> + {error, {EC, Err, Stack}} + end. + +tree_to_patch(Model, Parent, Tree, MKeys) -> + lists:foldl( + fun([Key | Rest], Acc) -> + case tree_get(Key, Tree) of + undefined -> + Acc; + {ok, Value} when Rest =:= [] -> + [{set, Parent ++ Key, Value} | Acc]; + {ok, SubTrees} when is_list(SubTrees) -> + lists:foldl( + fun(SubTree, Acc1) -> + InstKey = make_inst_key(Model, Parent, Key, SubTree), + NewParent = Parent ++ Key ++ [InstKey], + tree_to_patch(Model, NewParent, SubTree, [Rest]) ++ Acc1 + end, + Acc, + SubTrees) + end + end, + [], + MKeys). + +make_inst_key(Model, Parent, MapKey, Conf) -> + ModelKey = lee_model:get_model_key(Parent ++ MapKey), + #mnode{metaparams = MAttrs} = lee_model:get(ModelKey, Model), + KeyElems = [case tree_get(I, Conf) of + {ok, Val} -> + Val; + undefined -> + %% TODO: handle defaults + error({ModelKey, MapKey, I}) + end + || I <- ?m_attr(map, ?key_elements, MAttrs, [])], + list_to_tuple(KeyElems). + +-spec tree_get(lee:key(), conf_tree()) -> {ok, term()} | undefined. +tree_get(Key, Conf) when is_list(Key), is_map(Conf) -> + case Conf of + #{Key := Val} -> + {ok, Val}; + _ -> + case Key of + [Tail] -> + case Conf of + #{Tail := Val} -> {ok, Val}; + _ -> undefined + end; + [Head | Tail] -> + case Conf of + #{Head := Children} -> tree_get(Tail, Children); + _ -> undefined + end + end + end. + -spec splitl(fun((A) -> boolean()), [A]) -> [[A]]. splitl(_, []) -> []; @@ -114,28 +172,6 @@ inject_error_location(Location, {Err, Warn}) -> end, {[Fun(I) || I <- Err], [Fun(I) || I <- Warn]}. --spec run_cmd(string(), [string()]) -> {integer(), binary()} | {error, term()}. -run_cmd(Cmd, Args) -> - case os:find_executable(Cmd) of - false -> - {error, enoent}; - Executable -> - Port = erlang:open_port( {spawn_executable, Executable} - , [ exit_status - , stderr_to_stdout - , {args, Args} - ]), - collect_port_output(Port, []) - end. - -collect_port_output(Port, Acc) -> - receive - {Port, {exit_status, Status}} -> - {Status, iolist_to_binary(Acc)}; - {Port, {data, Data}} -> - [Acc|Data] - end. - -spec compose_checks([check_result()]) -> check_result(). compose_checks(L) -> {Err, Warn} = lists:unzip(L), diff --git a/src/framework/lee_model.erl b/src/framework/lee_model.erl index e65d042..290d4d7 100644 --- a/src/framework/lee_model.erl +++ b/src/framework/lee_model.erl @@ -2,27 +2,15 @@ -module(lee_model). %% API exports --export([ compile/2 +-export([ compile/2, merge/1, merge/2, clone/4 , map_vals/2 - , fold/3 - , fold/4 - , fold_metatypes/3 - , fold_mt_instances/3 - - , get/2 - , get_meta/3 - , get_meta/2 - , patch_meta/2 - , all_metatypes/1 - , get_metatype_index/2 + , fold/3, fold/4, fold_metatypes/3, fold_mt_instances/3 + + , get/2, get_meta/3, get_meta/2, patch_meta/2 + , all_metatypes/1, get_metatype_index/2 , match/2 - , get_model_key/1 - , merge/1 - , merge/2 - , split_key/1 - , full_split_key/1 - , clone/4 + , get_model_key/1, split_key/1, key_parts/1, full_split_key/1 ]). -export_type([ metatype_index/0 @@ -175,6 +163,8 @@ fold(Fun, Acc, Scope, Model) -> %% Fold over cooked module lee_storage:fold(Fun, Acc, Scope, Model). %% @doc Transform instance key to model key +%% +%% WARNING: trailing ?children element is dropped. -spec get_model_key(lee:key()) -> lee:model_key(). get_model_key([]) -> []; @@ -206,6 +196,14 @@ split_key(K) -> ), {lists:reverse(Base0), lists:reverse(Req0)}. +%% @doc +%% ``` +%% [foo, ?children, bar, ?children, baz] -> [[foo], [bar], [baz]] +%% ''' +key_parts(L) -> + key_parts(L, []). + + %% @doc Split a key into a list of child keys. Example: %% ```full_split_key([foo, ?children, bar, baz, {[1]}, quux]) --> %% [[foo, ?children], [bar, baz, {[1]}], [quux]]''' @@ -320,3 +318,10 @@ mk_metatype_index_(Key, #mnode{metatypes = MetaTypes}, Acc0) -> end , Acc0 , MetaTypes). + +key_parts([], Acc) -> + [lists:reverse(Acc)]; +key_parts([?children | Rest], Acc) -> + [lists:reverse(Acc) | key_parts(Rest)]; +key_parts([A|Rest], Acc) -> + key_parts(Rest, [A | Acc]). diff --git a/src/metatypes/lee_config_file.erl b/src/metatypes/lee_config_file.erl index f1ea3ab..86962e5 100644 --- a/src/metatypes/lee_config_file.erl +++ b/src/metatypes/lee_config_file.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2022-2024 k32 All Rights Reserved. +%% Copyright (c) 2022-2025 k32 All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -34,7 +34,8 @@ read(Model, Filename) -> case file:consult(Filename) of {ok, [Term]} -> - {ok, make_patch([], Model, Term, [])}; + Keys = lee_model:get_metatype_index(value, Model), + lee_lib:tree_to_patch(Model, Term, Keys); {error, enoent} -> {ok, []}; {error, Err} -> @@ -77,11 +78,3 @@ read_patch(Tag, Model) -> %%================================================================================ %% Internal functions %%================================================================================ - -make_patch(Path, Model, Term, Acc0) when is_map(Term) -> - Fun = fun(K, V, Acc) -> - make_patch(Path ++ [K], Model, V, Acc) - end, - maps:fold(Fun, Acc0, Term); -make_patch(Path, _Model, Term, Acc) -> - [{set, Path, Term}|Acc]. diff --git a/test/data/conf-file-correct.eterm b/test/data/conf-file-correct.eterm index a4862b5..aba8430 100644 --- a/test/data/conf-file-correct.eterm +++ b/test/data/conf-file-correct.eterm @@ -1,7 +1,11 @@ %% -*- mode:erlang -*- #{ foo => "I am foo" , bar => {"I am bar"} - , baz => - #{ quux => 42 - } + , [baz, quux] => 42 + , map => + [ #{ foo => 2 + , bar => bar + , baz => [#{foo => 22}] + } + ] }. diff --git a/test/lee_conf_file_tests.erl b/test/lee_conf_file_tests.erl index 8c8e927..751b694 100644 --- a/test/lee_conf_file_tests.erl +++ b/test/lee_conf_file_tests.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2022 k32 All Rights Reserved. +%% Copyright (c) 2022, 2025 k32 All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -22,18 +22,26 @@ model() -> - Model0 = #{ foo => {[value], - #{ type => string() - }} - , bar => {[value], - #{ type => {string()} - }} - , baz => - #{ quux => - {[value], - #{ type => integer() - }} - } + Model0 = #{ foo => {[value], #{type => string()}} + , bar => {[value], #{type => {string()}}} + , baz => #{quux => {[value], #{type => integer()}}} + , map => + {[map], + #{ key_elements => [[foo], [bar]] + }, + #{ foo => + {[value], + #{ type => integer() + , default => 1 + }} + , bar => + {[value], #{type => atom()}} + , baz => + {[map], + #{}, + #{ foo => {[value], #{type => integer()}} + }} + }} }, File = "test/data/conf-file-correct.eterm", Meta = [ lee:base_metamodel() @@ -54,4 +62,10 @@ read_test() -> ), ?assertMatch( 42 , lee:get(Model, Data, [baz, quux]) + ), + ?assertMatch( 2 + , lee:get(Model, Data, [map, {2, bar}, foo]) + ), + ?assertMatch( 22 + , lee:get(Model, Data, [map, {2, bar}, baz, {}, foo]) ). From 6c74981d59de1477d2496976efd7e8679fb189c8 Mon Sep 17 00:00:00 2001 From: k32 <10274441+k32@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:08:00 +0100 Subject: [PATCH 3/3] Run CI on OTP 28 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a910359..019d632 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - otp: ['25', '26'] + otp: ['26', '28'] steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} - rebar3-version: '3.16.1' + rebar3-version: '3.25.1' - name: Compile and run tests run: | make