Current section

Files

Jump to
launchdarkly_server_sdk src ldclient_clause.erl
Raw

src/ldclient_clause.erl

%%-------------------------------------------------------------------
%% @doc Rule clause data type
%% @private
%% @end
%%-------------------------------------------------------------------
-module(ldclient_clause).
%% API
-export([new/1]).
-export([match_context/4]).
-export([match_context/5]).
%% Types
-type clause() :: #{
attribute => ldclient_attribute_reference:attribute_reference(),
op => operator(),
values => [ldclient_flag:variation_value()],
negate => boolean(),
context_kind => binary()
}.
%% Describes an individual clause within a targeting rule
-type operator() :: in | endsWith | startsWith | matches | contains
| lessThan | lessThanOrEqual | greaterThan | greaterThanOrEqual
| before | 'after' | segmentMatch | semVerEqual | semVerLessThan
| semVerGreaterThan | none.
%% List of available operators
-export_type([clause/0]).
-ifdef(TEST).
-compile(export_all).
-endif.
-define(CLAUSE_TEMPLATE, #{
<<"attribute">> => <<>>,
<<"negate">> => false,
<<"op">> => <<>>,
<<"values">> => [],
<<"contextKind">> => <<"user">>
}).
%%===================================================================
%% API
%%===================================================================
-spec new(map()) -> clause().
new(RawClauseMap) ->
ContextKind = maps:get(<<"contextKind">>, RawClauseMap, undefined),
ClauseMap = maps:merge(?CLAUSE_TEMPLATE, RawClauseMap),
%% Clauses with no context kind are legacy.
new_from_template(ClauseMap, ContextKind == undefined).
%% @doc Match all clauses to the context.
%%
%% @end
-spec match_context(
Clause :: clause(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom()
) -> match | no_match | malformed_flag.
match_context(Clause, Context, FeatureStore, Tag) ->
check_clause(Clause, Context, FeatureStore, Tag, []).
%% @doc Match all clauses to the context.
%%
%% Allows providing of already visited segments to allow detection of cyclic dependencies.
%% @end
-spec match_context(
Clause :: clause(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
VisitedSegments :: [binary()]
) -> match | no_match | malformed_flag.
match_context(Clause, Context, FeatureStore, Tag, VisitedSegments) ->
check_clause(Clause, Context, FeatureStore, Tag, VisitedSegments).
%%===================================================================
%% Internal functions
%%===================================================================
-spec new_from_template(map(), IsLegacy :: boolean()) -> clause().
new_from_template(#{
<<"attribute">> := Attribute,
<<"negate">> := Negate,
<<"op">> := Op,
<<"values">> := Values,
<<"contextKind">> := ContextKind
}, IsLegacy) ->
#{
attribute => parse_attribute(Attribute, IsLegacy),
negate => Negate,
op => parse_operator(Op),
values => Values,
context_kind => ContextKind
}.
-spec parse_attribute(Attribute :: binary(), Legacy :: boolean()) -> ldclient_attribute_reference:attribute_reference().
parse_attribute(Attribute, true) -> ldclient_attribute_reference:new_from_legacy(Attribute);
parse_attribute(Attribute, false) -> ldclient_attribute_reference:new(Attribute).
-spec parse_operator(binary()) -> operator().
parse_operator(<<"in">>) -> in;
parse_operator(<<"endsWith">>) -> endsWith;
parse_operator(<<"startsWith">>) -> startsWith;
parse_operator(<<"matches">>) -> matches;
parse_operator(<<"contains">>) -> contains;
parse_operator(<<"lessThan">>) -> lessThan;
parse_operator(<<"lessThanOrEqual">>) -> lessThanOrEqual;
parse_operator(<<"greaterThan">>) -> greaterThan;
parse_operator(<<"greaterThanOrEqual">>) -> greaterThanOrEqual;
parse_operator(<<"before">>) -> before;
parse_operator(<<"after">>) -> 'after';
parse_operator(<<"segmentMatch">>) -> segmentMatch;
parse_operator(<<"semVerEqual">>) -> semVerEqual;
parse_operator(<<"semVerLessThan">>) -> semVerLessThan;
parse_operator(<<"semVerGreaterThan">>) -> semVerGreaterThan;
parse_operator(_) -> none.
-spec check_clause(Clause :: clause(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
VisitedSegments :: [binary()]
) -> match | no_match | malformed_flag.
check_clause(#{op := segmentMatch, values := SegmentKeys} = Clause, Context, FeatureStore, Tag, VisitedSegments) ->
maybe_negate_match(Clause, check_segment_keys_match(SegmentKeys, Context, FeatureStore, Tag, VisitedSegments));
check_clause(#{op := none}, _Context, _FeatureStore, _Tag, _VisitedSegments) -> no_match;
check_clause(Clause, Context, _FeatureStore, _Tag, _VisitedSegments) ->
check_clause(Clause, Context).
-spec check_clause(
Clause :: clause(),
Context :: ldclient_context:context()
) -> match | no_match | malformed_flag.
check_clause(#{attribute := #{valid := false}, context_kind := _ContextKind} = _Clause, _Context) ->
%% The attribute reference is not valid. The flag is malformed.
malformed_flag;
check_clause(#{attribute := #{components := [<<"kind">>]}, context_kind := _ContextKind} = Clause, Context) ->
%% If the reference is to the kind of the context, then we handle it special and apply the checks
%% to the kinds of the context (versus the kind attribute specifically).
check_context_value_null(ldclient_context:get_kinds(Context), Clause);
check_clause(#{attribute := Attribute, context_kind := ContextKind} = Clause, Context) ->
ContextValue = ldclient_context:get(ContextKind, Attribute, Context),
check_context_value_null(ContextValue, Clause).
check_context_value_null(null, _Clause) -> no_match;
check_context_value_null(ContextValue, Clause) ->
maybe_negate_match(Clause, check_attribute(ContextValue, Clause)).
check_attribute([] = ContextValues, _Clause) when is_list(ContextValues) -> no_match;
check_attribute([ContextValue|Rest] = ContextValues, Clause) when is_list(ContextValues) ->
Result = check_attribute(ContextValue, Clause),
check_attribute_result(Result, Rest, Clause);
check_attribute(ContextValue, #{values := ClauseValues, op := Operator}) ->
check_attribute_against_clause_values(ContextValue, Operator, ClauseValues).
check_attribute_against_clause_values(_ContextValue, _Operator, []) -> no_match;
check_attribute_against_clause_values(ContextValue, Operator, [ClauseValue|Rest]) ->
Result = check_attribute_against_clause_value(ContextValue, Operator, ClauseValue),
check_attribute_against_clause_value_result(Result, ContextValue, Operator, Rest).
check_attribute_against_clause_value_result(true, _ContextValue, _Operator, _Rest) -> match;
check_attribute_against_clause_value_result(false, ContextValue, Operator, Rest) ->
check_attribute_against_clause_values(ContextValue, Operator, Rest).
check_attribute_against_clause_value(null, _Operator, _ClauseValue) -> false;
check_attribute_against_clause_value(_ContextValue, _Operator, null) -> false;
check_attribute_against_clause_value(Value, in, Value) -> true;
check_attribute_against_clause_value(_ContextValue, in, _ClauseValue) -> false;
check_attribute_against_clause_value(ContextValue, endsWith, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
binary:longest_common_suffix([ContextValue, ClauseValue]) == byte_size(ClauseValue);
check_attribute_against_clause_value(ContextValue, startsWith, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
binary:longest_common_prefix([ContextValue, ClauseValue]) == byte_size(ClauseValue);
check_attribute_against_clause_value(ContextValue, matches, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
try
re:run(ContextValue, ClauseValue) =/= nomatch
catch _:_ ->
%% If the regex was not valid, then it isn't a match.
false
end;
check_attribute_against_clause_value(ContextValue, contains, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
binary:match(ContextValue, ClauseValue) =/= nomatch;
check_attribute_against_clause_value(ContextValue, lessThan, ClauseValue)
when is_number(ContextValue), is_number(ClauseValue) ->
ContextValue < ClauseValue;
check_attribute_against_clause_value(ContextValue, lessThanOrEqual, ClauseValue)
when is_number(ContextValue), is_number(ClauseValue) ->
ContextValue =< ClauseValue;
check_attribute_against_clause_value(ContextValue, greaterThan, ClauseValue)
when is_number(ContextValue), is_number(ClauseValue) ->
ContextValue > ClauseValue;
check_attribute_against_clause_value(ContextValue, greaterThanOrEqual, ClauseValue)
when is_number(ContextValue), is_number(ClauseValue) ->
ContextValue >= ClauseValue;
check_attribute_against_clause_value(ContextValue, before, ClauseValue)
when
is_binary(ContextValue) =/= true, is_integer(ContextValue) =/= true;
is_binary(ClauseValue) =/= true, is_integer(ClauseValue) =/= true ->
% One of the values is neither binary nor integer
false;
check_attribute_against_clause_value(ContextValue, before, ClauseValue) ->
try
ContextDate = parse_date_to_int(ContextValue),
ClauseDate = parse_date_to_int(ClauseValue),
ContextDate < ClauseDate
catch _:_ ->
false
end;
check_attribute_against_clause_value(ContextValue, 'after', ClauseValue)
when
is_binary(ContextValue) =/= true, is_integer(ContextValue) =/= true;
is_binary(ClauseValue) =/= true, is_integer(ClauseValue) =/= true ->
% One of the values is neither binary nor integer
false;
check_attribute_against_clause_value(ContextValue, 'after', ClauseValue) ->
try
ContextDate = parse_date_to_int(ContextValue),
ClauseDate = parse_date_to_int(ClauseValue),
ContextDate > ClauseDate
catch _:_ ->
false
end;
check_attribute_against_clause_value(ContextValue, semVerEqual, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
check_semver_equal(parse_semver(ContextValue), parse_semver(ClauseValue));
check_attribute_against_clause_value(ContextValue, semVerLessThan, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
check_semver_less_than(parse_semver(ContextValue), parse_semver(ClauseValue));
check_attribute_against_clause_value(ContextValue, semVerGreaterThan, ClauseValue)
when is_binary(ContextValue), is_binary(ClauseValue) ->
check_semver_greater_than(parse_semver(ContextValue), parse_semver(ClauseValue));
check_attribute_against_clause_value(_ContextValue, _Operator, _ClauseValue) -> false.
-spec parse_date_to_int(binary()|integer()) -> integer().
parse_date_to_int(Value) when is_binary(Value) ->
calendar:rfc3339_to_system_time(binary_to_list(Value), [{unit, nanosecond}]);
parse_date_to_int(Value) when is_integer(Value) ->
% Convert milliseconds to nanoseconds
Value * 1000000.
-spec parse_semver(binary()) -> binary().
parse_semver(S) ->
case re:run(S, <<"^\\d+(\\.\\d+)?(\\.\\d)?">>, [{capture, all, binary}]) of
{match, [M0|_] = Matches} when length(Matches) =:= 1 ->
Rest = binary:part(S, byte_size(M0), byte_size(S)-byte_size(M0)),
<<M0/binary, $., $0, $., $0, Rest/binary>>;
{match, [M0|_] = Matches} when length(Matches) =:= 2 ->
Rest = binary:part(S, byte_size(M0), byte_size(S)-byte_size(M0)),
<<M0/binary, $., $0, Rest/binary>>;
{match, _Matches} -> S;
nomatch -> S
end.
-spec check_semver_equal(binary(), binary()) -> boolean().
check_semver_equal(ContextSemVer, ClauseSemVer) ->
case verl:compare(ContextSemVer, ClauseSemVer) of
eq -> true;
_ -> false
end.
-spec check_semver_less_than(binary(), binary()) -> boolean().
check_semver_less_than(ContextSemVer, ClauseSemVer) ->
case verl:compare(ContextSemVer, ClauseSemVer) of
lt -> true;
_ -> false
end.
-spec check_semver_greater_than(binary(), binary()) -> boolean().
check_semver_greater_than(ContextSemVer, ClauseSemVer) ->
case verl:compare(ContextSemVer, ClauseSemVer) of
gt -> true;
_ -> false
end.
check_attribute_result(match, _Rest, _Clause) -> match;
check_attribute_result(no_match, Rest, Clause) ->
check_attribute(Rest, Clause).
-spec check_segment_keys_match(
SegmentKeys :: [binary()],
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
VisitedSegments :: [binary()]
) -> match | no_match.
check_segment_keys_match([], _Context, _FeatureStore, _Tag, _VisitedSegments) -> no_match;
check_segment_keys_match([SegmentKey|Rest], Context, FeatureStore, Tag, VisitedSegments) ->
case lists:member(SegmentKey, VisitedSegments) of
%% Detected a segment cycle, so the flag is malformed.
true -> malformed_flag;
%% No cycle, evaluate as normal.
false ->
Result = check_segment_key_match(SegmentKey, Context, FeatureStore, Tag, [SegmentKey | VisitedSegments]),
check_segment_key_match_result(Result, Rest, Context, FeatureStore, Tag, [SegmentKey | VisitedSegments])
end.
-spec check_segment_key_match_result(Result :: match | no_match | malformed_flag,
SegmentKeys :: [binary()],
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
VisitedSegments :: [binary()]
) -> match | no_match | malformed_flag.
check_segment_key_match_result(malformed_flag, _Rest, _Context, _FeatureStore, _Tag, _VisitedSegments) -> malformed_flag;
check_segment_key_match_result(match, _Rest, _Context, _FeatureStore, _Tag, _VisitedSegments) -> match;
check_segment_key_match_result(no_match, Rest, Context, FeatureStore, Tag, VisitedSegments) ->
check_segment_keys_match(Rest, Context, FeatureStore, Tag, VisitedSegments).
check_segment_key_match(SegmentKey, Context, FeatureStore, Tag, VisitedSegments) ->
Segments = get_segment(Tag, FeatureStore, SegmentKey),
check_segments_match(Segments, Context, FeatureStore, Tag, VisitedSegments).
check_segments_match([], _Context, _FeatureStore, _Tag, _VisitedSegments) -> no_match;
check_segments_match([{_SegmentKey, Segment}|_], Context, FeatureStore, Tag, VisitedSegments) ->
ldclient_segment:match_context(Segment, Context, FeatureStore, Tag, VisitedSegments).
-spec maybe_negate_match(clause(), match | no_match) -> match | no_match.
maybe_negate_match(#{negate := false}, Match) -> Match;
maybe_negate_match(#{negate := true}, match) -> no_match;
maybe_negate_match(#{negate := true}, no_match) -> match.
-spec is_not_deleted(Item :: map()) -> boolean().
is_not_deleted(#{deleted := true}) -> false;
is_not_deleted(_) -> true.
%% @doc Get the segment for the specified segment key. If there is no segment matching the SegmentKey, then return
%% an empty list.
%%
%% @end
-spec get_segment(Tag :: atom(), FeatureStore :: atom(), SegmentKey :: binary()) ->
[{SegmentKey :: binary(), SegmentValue :: ldclient_segment:segment()}].
get_segment(Tag, FeatureStore, SegmentKey) ->
[Segment || Segment = {_, SegmentValue} <- FeatureStore:get(Tag, segments, SegmentKey), is_not_deleted(SegmentValue)].