%%------------------------------------------------------------------- %% @doc Flagbuilder %% %% @end %%------------------------------------------------------------------- -module(ldclient_flagbuilder). % Internal functions -export([new/1, key/1, build/2]). % Public API -export([boolean_flag/1, on/2, off_variation/2, fallthrough_variation/2, variations/2, variation_for_all_users/2, value_for_all_users/2, variation_for_user/3, if_match/3, if_not_match/3, and_match/3, and_not_match/3, then_return/2, clear_rules/1, clear_user_targets/1 ]). -export_type([flag_builder/0, flag_rule_builder/0]). -opaque flag_builder() :: #{ key => string(), on => boolean(), variations => [ldclient_flag:variations()], off_variation => non_neg_integer(), fallthrough_variation => non_neg_integer(), rules => [ldclient_rules:rule()], targets => #{ non_neg_integer() => [binary()] } }. %% A builder for feature flag configurations to be used with {@link ldclient_testdata}. %% In the LaunchDarkly model, a flag can have any number of rules, and a rule can have any number of %% clauses. A clause is an individual test such as "name is 'X'". A rule matches a user if all of the %% rule's clauses match the user. %% %% To start defining a rule, use either {@link if_match/3} or {@link if_not_match/3}. %% This defines the first clause for the rule. %% Optionally, you may add more clauses with {@link and_match/3} or {@link and_not_match/3} . %% Finally, call {@link then_return/2} to finish defining the rule. -opaque flag_rule_builder() :: #{ variation => non_neg_integer(), clauses => [ldclient_clause:clause()], flag => flag_builder() }. %% A builder for feature flag rules to be used with {@link flag_builder()}. -ifdef(TEST). -compile(export_all). -endif. %% @doc %% @private %% @end -spec new(FlagName :: string()) -> flag_builder(). new(FlagName) -> #{ key => FlagName, on => true, fallthrough_variation => 0, off_variation => 1, variations => [true, false] }. %% @doc %% @private %% @end -spec build(Flag :: flag_builder(), Version :: non_neg_integer()) -> ldclient_flag:flag(). build(Flag = #{ key := Key, on := On, variations := Variations, off_variation := OffVariation, fallthrough_variation := FallthroughVariation }, Version) -> Rules = maps:get(rules, Flag, []), Targets = lists:map(fun({K, V}) -> #{ variation => K, values => V } end, maps:to_list(maps:get(targets, Flag, #{}))), #{ key => list_to_binary(Key), version => Version, on => On, variations => Variations, offVariation => OffVariation, fallthrough => FallthroughVariation, trackEvents => false, trackEventsFallthrough => false, deleted => false, debugEventsUntilDate => null, prerequisites => [], salt => <<"salt">>, rules => Rules, targets => Targets }. %% @doc %% @private %% @end -spec key(FlagBuilder :: flag_builder()) -> string(). key(#{key := FlagName}) -> FlagName. %% @doc Sets targeting to be on or off for this flag. %% %% The effect of this depends on the rest of the flag configuration, just as it does on the %% real LaunchDarkly dashboard. In the default configuration that you get from calling %% {@link ldclient_testdata:flag/2} with a new flag key, the flag will return `false' %% whenever targeting is off, and `true' when targeting is on. %% %% @param IsOn true if targeting should be on %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% %% @end -spec on(IsOn :: boolean(), FlagBuilder :: flag_builder()) -> flag_builder(). on(IsOn, FlagBuilder) -> FlagBuilder#{on := IsOn}. -spec is_boolean_flag(FlagBuilder :: flag_builder()) -> boolean(). is_boolean_flag(#{ variations := [true, false]}) -> true; is_boolean_flag(_) -> false. %% @doc Removes any existing rules from the flag. %% %% This undoes the effect of {@link if_match/3} and {@link if_not_match/3}. %% %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec clear_rules(FlagBuilder :: flag_builder()) -> flag_builder(). clear_rules(FlagBuilder) -> maps:remove(rules, FlagBuilder). %% @doc Removes any existing user targets from the flag. %% %% This undoes the effect of {@link variation_for_user/3}. %% %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec clear_user_targets(FlagBuilder :: flag_builder()) -> flag_builder(). clear_user_targets(FlagBuilder) -> maps:remove(targets, FlagBuilder). %% @doc A shortcut for setting the flag to use the standard boolean configuration. %% %% This is the default for all new flags created with {@link ldclient_testdata:flag/2}. %% The flag will have two variations, `true' and `false' (in that order); it will return %% `false' whenever targeting is off, and `true' when targeting is on if no other %% settings specify otherwise. %% %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec boolean_flag(FlagBuilder :: flag_builder()) -> flag_builder(). boolean_flag(FlagBuilder) -> case is_boolean_flag(FlagBuilder) of true -> FlagBuilder; false -> fallthrough_variation(0, off_variation(1, variations([true, false], FlagBuilder))) end. -spec variation_for_boolean(Variation :: boolean()) -> non_neg_integer(). variation_for_boolean(true) -> 0; variation_for_boolean(false) -> 1. -type variation() :: boolean() | non_neg_integer(). %% @doc Specifies the off variation for a flag. %% %% The off variation is the value that is returned whenever targeting is off %% %% If the flag was previously configured with other variations and a boolean Variation is specified, %% this also changes the FlagBuilder to a boolean flag. %% %% @param Variation `true', `false', or the index of the desired variation to return: 0 for the first, 1 for the second, etc. %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec off_variation(Variation :: variation(), FlagBuilder :: flag_builder()) -> flag_builder(). off_variation(Variation, FlagBuilder) when is_boolean(Variation) -> off_variation(variation_for_boolean(Variation), boolean_flag(FlagBuilder)); off_variation(Variation, FlagBuilder) when is_integer(Variation) -> FlagBuilder#{off_variation := Variation}. %% @doc Specifies the fallthrough variation for a flag. %% %% The fallthrough is the value that is returned if targeting is on %% and the user was not matched by a more specific target or rule. %% %% If the flag was previously configured with other variations and a boolean variation is specified, %% this also changes the flagbuilder to a boolean flag. %% %% @param Variation `true', `false', or the index of the desired variation to return: 0 for the first, 1 for the second, etc. %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec fallthrough_variation(Variation :: variation(), FlagBuilder :: flag_builder()) -> flag_builder(). fallthrough_variation(Variation, FlagBuilder) when is_boolean(Variation) -> fallthrough_variation(variation_for_boolean(Variation), boolean_flag(FlagBuilder)); fallthrough_variation(Variation, FlagBuilder) when is_integer(Variation) -> FlagBuilder#{fallthrough_variation := Variation}. %% @doc Sets the flag to always return the specified variation value for all users. %% %% The value may be of any JSON type. %% This method changes the flag to have only a single variation, which is this value, %% and to return the same variation regardless of whether targeting is on or off. %% Any existing targets or rules are removed. %% %% @param Value the desired value to be returned for all users %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec variations(Values :: [ldclient_flag:variation_value()], FlagBuilder :: flag_builder() ) -> flag_builder(). variations(Values, FlagBuilder) -> FlagBuilder#{variations := Values}. %% @doc Sets the flag to always return the specified variation for all users. %% %% The variation is set, targeting is switched on, and any existing targets or rules are removed. %% The fallthrough variation is set to the specified value. %% The off variation is left unchanged. %% %% If the flag was previously configured with other variations and a boolean variation is specified, %% this also changes the flagbuilder to a boolean flag. %% %% @param Variation `true', `false', or the index of the desired variation to return: 0 for the first, 1 for the second, etc. %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec variation_for_all_users(Variation :: variation(), FlagBuilder :: flag_builder()) -> flag_builder(). variation_for_all_users(Variation, FlagBuilder) when is_boolean(Variation) -> variation_for_all_users(variation_for_boolean(Variation), boolean_flag(FlagBuilder)); variation_for_all_users(Variation, FlagBuilder) when is_integer(Variation) -> Fallthrough = fallthrough_variation(Variation, FlagBuilder), NoTargets = clear_user_targets(Fallthrough), NoRules = clear_rules(NoTargets), on(true, NoRules). %% @doc Sets the flag to always return the specified variation value for all users. %% %% The value may be of any JSON type, as defined by }. This method changes the %% flag to have only a single variation, which is this value, and to return the same %% variation regardless of whether targeting is on or off. Any existing targets or rules %% are removed. %% %% @param Value the desired value to be returned for all users %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec value_for_all_users(Value :: term(), FlagBuilder :: flag_builder()) -> flag_builder(). value_for_all_users(Value, FlagBuilder) -> variation_for_all_users(0, variations([Value], FlagBuilder)). %% @doc Sets the flag to return the specified variation for a specific user key when %% targeting is on. %% %% This has no effect when targeting is turned off for the flag. %% %% If the flag was previously configured with other variations and a boolean variation is specified, %% this also changes the flagbuilder to a boolean flag. %% %% @param Variation `true', `false', or the index of the desired variation to return: 0 for the first, 1 for the second, etc. %% @param UserKey a user key %% @param FlagBuilder the flag builder to modify %% @returns the modified builder %% @end -spec variation_for_user(Variation :: variation(), UserKey :: string(), FlagBuilder :: flag_builder()) -> flag_builder(). variation_for_user(Variation, UserKey, FlagBuilder) when is_boolean(Variation) -> variation_for_user(variation_for_boolean(Variation), UserKey, boolean_flag(FlagBuilder)); variation_for_user(Variation, UserKey, FlagBuilder) when is_integer(Variation) -> Targets = maps:get(targets, FlagBuilder, #{}), UserKeyBin = list_to_binary(UserKey), FilteredTargets = maps:map(fun(_K, V) -> lists:delete(UserKeyBin, V) end, Targets), UpdatedTargets = maps:update_with(Variation, fun(Users) -> [UserKeyBin|Users] end, [UserKeyBin], FilteredTargets), maps:put(targets, UpdatedTargets, FlagBuilder). %% @doc Starts defining a flag rule, using the "is one of" operator. %% %% For example, this creates a rule that returns `true' if the name is "Patsy" or "Edina": %% %% ``` %% {ok, Flag} = ldclient_testdata:flag(TestData, "flag"), %% RuleBuilder = ldclient_flagbuilder:if_match(<<"name">>, [<<"Patsy">>, <<"Edina">>], Flag), %% UpdatedFlag = ldclient_flagbuilder:then_return(true, RuleBuilder), %% ldclient_testdata:update(TestData, UpdatedFlag). %% ''' %% %% @param UserAttribute the user attribute to match against %% @param Values values to compare to %% @param FlagBuilder the flag builder to modify %% @returns a {@link flag_rule_builder()}; call {@link then_return/2} to finish the rule, %% or add more tests with {@link and_match/3} or {@link and_not_match/3}. %% @end -spec if_match(UserAttribute :: atom() | binary(), Values :: [term()], FlagBuilder :: flag_builder()) -> flag_rule_builder(). if_match(UserAttribute, Values, FlagBuilder) -> and_match(UserAttribute, Values, #{ flag => FlagBuilder }). %% @doc Starts defining a flag rule, using the "is not one of" operator. %% %% For example, this creates a rule that returns `true' if the name is neither "Saffron" nor "Bubble": %% %% ``` %% {ok, Flag} = ldclient_testdata:flag(TestData, "flag"), %% RuleBuilder = ldclient_flagbuilder:if_not_match(<<"name">>, [<<"Saffron">>, <<"Bubble">>], Flag), %% UpdatedFlag = ldclient_flagbuilder:then_return(true, RuleBuilder), %% ldclient_testdata:update(TestData, UpdatedFlag). %% ''' %% %% @param UserAttribute the user attribute to match against %% @param Values values to compare to %% @param FlagBuilder the flag builder to modify %% @returns a {@link flag_rule_builder()}; call {@link then_return/2} to finish the rule, %% or add more tests with {@link and_match/3} or {@link and_not_match/3}. %% @end -spec if_not_match(UserAttribute :: atom() | binary(), Values :: [term()], FlagBuilder :: flag_builder()) -> flag_rule_builder(). if_not_match(UserAttribute, Values, FlagBuilder) -> and_not_match(UserAttribute, Values, #{ flag => FlagBuilder }). %%------------------------------------------------------------------- %% Flag Rule Builder %%------------------------------------------------------------------- %% @doc Adds another clause, using the "is one of" operator. %% %% For example, this creates a rule that returns `true' if the name is "Patsy" and the %% country is "gb": %% %% ``` %% {ok, Flag} = ldclient_testdata:flag(TestData, "flag"), %% RuleBuilder = ldclient_flagbuilder:and_match(<<"country">>, [<<"gb">>], %% ldclient_flagbuilder:if_match(<<"name">>, [<<"Patsy">>], Flag)), %% UpdatedFlag = ldclient_flagbuilder:then_return(true, RuleBuilder), %% ldclient_testdata:update(TestData, UpdatedFlag). %% ''' %% %% @param UserAttribute the user attribute to match against %% @param Values values to compare to %% @param RuleBuilder the rule builder to modify %% @returns the modified rule builder %% @end -spec and_match(UserAttribute :: atom() | binary(), Values :: [term()], RuleBuilder :: flag_rule_builder()) -> flag_rule_builder(). and_match(UserAttribute, Values, RuleBuilder) -> Clauses = maps:get(clauses, RuleBuilder, []), maps:put(clauses, [new_clause(UserAttribute, Values, false)|Clauses], RuleBuilder). %% @doc Adds another clause, using the "is not one of" operator. %% %% For example, this creates a rule that returns `true' if the name is "Patsy" and the %% country is not "gb": %% %% ``` %% {ok, Flag} = ldclient_testdata:flag(TestData, "flag"), %% RuleBuilder = ldclient_flagbuilder:and_not_match(<<"country">>, [<<"gb">>], %% ldclient_flagbuilder:if_match(<<"name">>, [<<"Patsy">>], Flag)), %% UpdatedFlag = ldclient_flagbuilder:then_return(true, RuleBuilder), %% ldclient_testdata:update(TestData, UpdatedFlag). %% ''' %% %% @param UserAttribute the user attribute to match against %% @param Values values to compare to %% @param RuleBuilder the rule builder to modify %% @returns the modified rule builder %% @end -spec and_not_match(UserAttribute :: atom() | binary(), Values :: [term()], RuleBuilder :: flag_rule_builder()) -> flag_rule_builder(). and_not_match(UserAttribute, Values, RuleBuilder) -> Clauses = maps:get(clauses, RuleBuilder, []), maps:put(clauses, [new_clause(UserAttribute, Values, true)|Clauses], RuleBuilder). -spec new_clause(UserAttribute :: atom() | binary(), Values :: [term()], Negate :: boolean()) -> ldclient_clause:clause(). new_clause(UserAttribute, Values, Negate) -> AttributeBinary = if is_binary(UserAttribute) -> UserAttribute; is_atom(UserAttribute) -> atom_to_binary(UserAttribute, utf8); true -> unknown end, #{attribute => AttributeBinary, values => Values, negate => Negate, op => in }. %% @doc Finishes defining the rule, specifying the result variation. %% %% If the flag was previously configured with other variations and a boolean variation is specified, %% this also changes the FlagBuilder to a boolean flag. %% %% @param Variation `true', `false', or the index of the desired variation to return: 0 for the first, 1 for the second, etc. %% @param RuleBuilder the rule builder to use %% @returns the modified flag builder that initially created this rule builder %% @end -spec then_return(Variation :: variation(), RuleBuilder :: flag_rule_builder()) -> flag_builder(). then_return(Variation, RuleBuilder) when is_boolean(Variation) -> Flag = maps:get(flag, RuleBuilder), BooleanRuleBuilder = maps:put(flag, boolean_flag(Flag), RuleBuilder), then_return(variation_for_boolean(Variation), BooleanRuleBuilder); then_return(Variation, RuleBuilder) when is_integer(Variation) -> #{ flag := RuleFlag } = RuleBuilder, ExistingRules = maps:get(rules, RuleFlag, []), RuleBuilderWithVariation = maps:put(variation, Variation, RuleBuilder), Rule = build_rule(length(ExistingRules), RuleBuilderWithVariation), maps:put(rules, [Rule|ExistingRules], RuleFlag). -spec build_rule(Index :: non_neg_integer(), RuleBuilder :: flag_rule_builder()) -> ldclient_rule:rule(). build_rule(Index, #{variation := Variation, clauses := Clauses}) -> #{ id => list_to_binary("rule" ++ integer_to_list(Index)), clauses => Clauses, trackEvents => false, variationOrRollout => Variation }.