Skip to content

Commit dea6db6

Browse files
authored
Merge pull request #304 from thalesmg/computed-fields
feat: add computed fields
2 parents 05ff1a0 + e5e3e5c commit dea6db6

File tree

3 files changed

+195
-3
lines changed

3 files changed

+195
-3
lines changed

include/hocon.hrl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
-ifndef(HOCON_HRL).
2020
-define(HOCON_HRL, true).
2121

22+
-define(COMPUTED, '_computed').
23+
2224
-define(IS_VALUE_LIST(T), (T =:= array orelse T =:= concat orelse T =:= object)).
2325
-define(IS_FIELD(F), (is_tuple(F) andalso size(F) =:= 2)).
2426

src/hocon_tconf.erl

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
-include("hoconsc.hrl").
3232
-include("hocon_private.hrl").
33+
-include("hocon.hrl").
3334

3435
-export_type([opts/0]).
3536

@@ -486,7 +487,9 @@ map_one_field_non_hidden(FieldType, FieldSchema, FieldValue0, Opts) ->
486487
IsMakeSerializable = is_make_serializable(Opts),
487488
{MaybeLog, FieldValue} = resolve_field_value(FieldSchema, FieldValue0, Opts),
488489
Converter = upgrade_converter(field_schema(FieldSchema, converter)),
489-
{Acc0, NewValue} = map_field_maybe_convert(FieldType, FieldSchema, FieldValue, Opts, Converter),
490+
{Acc0, NewValue0} = map_field_maybe_convert(
491+
FieldType, FieldSchema, FieldValue, Opts, Converter
492+
),
490493
Acc = MaybeLog ++ Acc0,
491494
Validators =
492495
case IsMakeSerializable orelse is_primitive_type(FieldType) of
@@ -500,7 +503,7 @@ map_one_field_non_hidden(FieldType, FieldSchema, FieldValue0, Opts) ->
500503
end,
501504
case find_errors(Acc) of
502505
ok ->
503-
Pv = ensure_plain(NewValue),
506+
Pv = ensure_plain(NewValue0),
504507
ValidationResult = validate(Opts, FieldSchema, Pv, Validators),
505508
Mapping =
506509
case is_make_serializable(Opts) of
@@ -510,14 +513,26 @@ map_one_field_non_hidden(FieldType, FieldSchema, FieldValue0, Opts) ->
510513
case ValidationResult of
511514
[] ->
512515
Mapped = maybe_mapping(Mapping, Pv),
516+
NewValue = maybe_computed(FieldSchema, NewValue0, Opts),
513517
{Acc ++ Mapped, NewValue};
514518
Errors ->
515-
{Acc ++ Errors, NewValue}
519+
{Acc ++ Errors, NewValue0}
516520
end;
517521
_ ->
518522
{Acc, FieldValue}
519523
end.
520524

525+
maybe_computed(FieldSchema, #{} = CheckedValue, Opts) ->
526+
case field_schema(FieldSchema, computed) of
527+
Fn when is_function(Fn, 2) ->
528+
Computed = Fn(CheckedValue, Opts),
529+
CheckedValue#{?COMPUTED => Computed};
530+
_ ->
531+
CheckedValue
532+
end;
533+
maybe_computed(_FieldSchema, CheckedValue, _Opts) ->
534+
CheckedValue.
535+
521536
map_field_maybe_convert(Type, Schema, Value0, Opts, undefined) ->
522537
map_field(Type, Schema, Value0, Opts);
523538
map_field_maybe_convert(Type, Schema, Value0, Opts, Converter) ->

test/hocon_tconf_tests.erl

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
-include_lib("eunit/include/eunit.hrl").
2020
-include("hocon_private.hrl").
2121
-include("hoconsc.hrl").
22+
-include("hocon.hrl").
2223

2324
-export([roots/0, fields/1, validations/0, desc/1, namespace/0]).
2425

@@ -2586,3 +2587,177 @@ map_type_with_alias_test() ->
25862587
?assertEqual(NormalRecord, hocon_tconf:check_plain(Sc, NormalRecord)),
25872588
?assertEqual(NormalRecord, hocon_tconf:check_plain(Sc, AliasedRecord)),
25882589
ok.
2590+
2591+
computed_fields_test() ->
2592+
Size = 3,
2593+
Counter = counters:new(Size, []),
2594+
Reset = fun() ->
2595+
lists:foreach(
2596+
fun(Ix) ->
2597+
counters:put(Counter, Ix, 0)
2598+
end,
2599+
lists:seq(1, Size)
2600+
)
2601+
end,
2602+
ComputedRoot = fun
2603+
(
2604+
#{<<"bag">> := #{?COMPUTED := ComputedBag}, <<"baz">> := #{?COMPUTED := ComputedBaz}},
2605+
_HoconOpts
2606+
) ->
2607+
counters:add(Counter, 1, 1),
2608+
[ComputedBag, ComputedBaz];
2609+
(#{bag := #{?COMPUTED := ComputedBag}, baz := #{?COMPUTED := ComputedBaz}}, _HoconOpts) ->
2610+
counters:add(Counter, 1, 1),
2611+
[ComputedBaz, ComputedBag]
2612+
end,
2613+
ComputedBaz = fun
2614+
(#{<<"quux">> := Val}, _HoconOpts) ->
2615+
counters:add(Counter, 2, 1),
2616+
Val + 333;
2617+
(#{quux := Val}, _HoconOpts) ->
2618+
counters:add(Counter, 2, 1),
2619+
Val - 333
2620+
end,
2621+
%% This one is an open map without schema, so keys are not converted to atoms.
2622+
ComputedBag = fun(#{<<"key">> := Val}, _HoconOpts) ->
2623+
counters:add(Counter, 3, 1),
2624+
<<"computed ", Val/binary>>
2625+
end,
2626+
BadComputed = fun(X, _HoconOpts) -> error({shouldnt_be_called, X}) end,
2627+
QuuxValidator = fun(X) -> X > 300 end,
2628+
Sc = #{
2629+
roots => [
2630+
{"root", hoconsc:mk(hoconsc:ref(foo), #{computed => ComputedRoot})}
2631+
],
2632+
fields =>
2633+
#{
2634+
foo => [
2635+
{bar, hoconsc:mk(integer(), #{computed => BadComputed})},
2636+
{baz, hoconsc:mk(hoconsc:ref(qux), #{computed => ComputedBaz})},
2637+
{bag, hoconsc:mk(map(), #{computed => ComputedBag})}
2638+
],
2639+
qux => [
2640+
{quux,
2641+
hoconsc:mk(integer(), #{
2642+
computed => BadComputed,
2643+
validator => QuuxValidator
2644+
})}
2645+
]
2646+
}
2647+
},
2648+
Data = #{
2649+
<<"root">> =>
2650+
#{
2651+
<<"bar">> => 123,
2652+
<<"baz">> =>
2653+
#{<<"quux">> => 666},
2654+
<<"bag">> => #{<<"key">> => <<"value">>}
2655+
}
2656+
},
2657+
Res1 = hocon_tconf:check_plain(Sc, Data, #{}),
2658+
?assertMatch(
2659+
#{
2660+
<<"root">> :=
2661+
#{
2662+
?COMPUTED := [<<"computed value">>, 999],
2663+
<<"bar">> := 123,
2664+
<<"baz">> :=
2665+
#{
2666+
?COMPUTED := 999,
2667+
<<"quux">> := 666
2668+
},
2669+
<<"bag">> :=
2670+
#{
2671+
?COMPUTED := <<"computed value">>,
2672+
<<"key">> := <<"value">>
2673+
}
2674+
}
2675+
},
2676+
Res1
2677+
),
2678+
?assertEqual(1, counters:get(Counter, 1)),
2679+
?assertEqual(1, counters:get(Counter, 2)),
2680+
?assertEqual(1, counters:get(Counter, 3)),
2681+
Reset(),
2682+
Res2 = hocon_tconf:check_plain(Sc, Data, #{atom_key => true}),
2683+
?assertMatch(
2684+
#{
2685+
root :=
2686+
#{
2687+
?COMPUTED := [333, <<"computed value">>],
2688+
bar := 123,
2689+
baz :=
2690+
#{
2691+
?COMPUTED := 333,
2692+
quux := 666
2693+
},
2694+
bag :=
2695+
#{
2696+
?COMPUTED := <<"computed value">>,
2697+
<<"key">> := <<"value">>
2698+
}
2699+
}
2700+
},
2701+
Res2
2702+
),
2703+
?assertEqual(1, counters:get(Counter, 1)),
2704+
?assertEqual(1, counters:get(Counter, 2)),
2705+
?assertEqual(1, counters:get(Counter, 3)),
2706+
Reset(),
2707+
%% Tests that fail validation/type check do not trigger computation
2708+
BadData1 = #{
2709+
<<"root">> =>
2710+
#{
2711+
<<"bar">> => 123,
2712+
<<"baz">> =>
2713+
#{
2714+
<<"quux">> =>
2715+
%% bad value: fails validation
2716+
200
2717+
},
2718+
<<"bag">> => #{<<"key">> => <<"value">>}
2719+
}
2720+
},
2721+
?assertThrow(
2722+
{_, [
2723+
#{
2724+
reason := returned_false,
2725+
kind := validation_error,
2726+
path := "root.baz.quux"
2727+
}
2728+
]},
2729+
hocon_tconf:check_plain(Sc, BadData1, #{})
2730+
),
2731+
%% Root is not called
2732+
?assertEqual(0, counters:get(Counter, 1)),
2733+
%% Baz is not called
2734+
?assertEqual(0, counters:get(Counter, 2)),
2735+
%% Bag is called
2736+
?assertEqual(1, counters:get(Counter, 3)),
2737+
Reset(),
2738+
BadData2 = #{
2739+
<<"root">> =>
2740+
#{
2741+
<<"bar">> => 123,
2742+
<<"baz">> =>
2743+
#{<<"quux">> => <<"wrong type">>},
2744+
<<"bag">> => #{<<"key">> => <<"value">>}
2745+
}
2746+
},
2747+
?assertThrow(
2748+
{_, [
2749+
#{
2750+
reason := "Unable to parse integer value",
2751+
kind := validation_error,
2752+
path := "root.baz.quux"
2753+
}
2754+
]},
2755+
hocon_tconf:check_plain(Sc, BadData2, #{})
2756+
),
2757+
%% Root is not called
2758+
?assertEqual(0, counters:get(Counter, 1)),
2759+
%% Baz is not called
2760+
?assertEqual(0, counters:get(Counter, 2)),
2761+
%% Bag is called
2762+
?assertEqual(1, counters:get(Counter, 3)),
2763+
ok.

0 commit comments

Comments
 (0)