Skip to content

Commit 4b6c1ff

Browse files
Merge pull request #19 from Flagsmith/release/1.1.0
Release 1.1.0
2 parents 310ecea + 881c0bf commit 4b6c1ff

File tree

6 files changed

+236
-6
lines changed

6 files changed

+236
-6
lines changed

lib/flagsmith_engine.ex

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule Flagsmith.Engine do
2+
require Logger
3+
24
alias Flagsmith.Schemas.{
35
Environment,
46
Traits,
@@ -69,7 +71,8 @@ defmodule Flagsmith.Engine do
6971
override_traits \\ []
7072
) do
7173
with identity <- Identity.set_env_key(identity, env),
72-
segment_features <- get_segment_features(segments, identity, override_traits),
74+
segment_features <-
75+
get_identity_applicable_segments(segments, identity, override_traits),
7376
prioritized <- clean_segments_by_priority(segment_features),
7477
replaced <- replace_segment_features(fs, prioritized),
7578
pre_features <- replace_identity_features(replaced, identity_features),
@@ -78,6 +81,30 @@ defmodule Flagsmith.Engine do
7881
end
7982
end
8083

84+
@doc """
85+
Get list of segments for a given `t:Flagsmith.Schemas.Identity.t/0` in a
86+
given `t:Flagsmith.Schemas.Environment.t/0`.
87+
"""
88+
@spec get_identity_segments(
89+
Environment.t(),
90+
Identity.t(),
91+
override_traits :: list(Traits.Trait.t())
92+
) :: list(Segments.IdentitySegment.t())
93+
def get_identity_segments(
94+
%Environment{
95+
project: %Environment.Project{segments: segments}
96+
} = env,
97+
identity,
98+
override_traits \\ []
99+
) do
100+
with identity <- Identity.set_env_key(identity, env),
101+
segments <-
102+
get_identity_applicable_segments(segments, identity, override_traits),
103+
replaced <- Enum.map(segments, &Segments.IdentitySegment.from_segment/1) do
104+
replaced
105+
end
106+
end
107+
81108
defp clean_segments_by_priority(segments) do
82109
# keep an ordered table by index so we can retrieve the segments in order
83110
# at the end when manipulating them
@@ -244,12 +271,12 @@ defmodule Flagsmith.Engine do
244271
Filters a list of segments accordingly to if they match an identity and traits
245272
(optionally using a list of traits to override those in the identity)
246273
"""
247-
@spec get_segment_features(
274+
@spec get_identity_applicable_segments(
248275
segments :: list(Segments.Segment.t()),
249276
Identity.t(),
250277
override_traits :: list(Traits.Trait.t())
251278
) :: list(Segments.Segment.t())
252-
def get_segment_features(segments, identity, override_traits) do
279+
def get_identity_applicable_segments(segments, identity, override_traits) do
253280
Enum.filter(segments, fn segment ->
254281
evaluate_identity_in_segment(identity, segment, override_traits)
255282
end)
@@ -386,6 +413,24 @@ defmodule Flagsmith.Engine do
386413
end
387414
end
388415

416+
def traits_match_segment_condition(
417+
traits,
418+
%Segments.Segment.Condition{operator: :IS_SET, property_: prop},
419+
_segment_id,
420+
_identifier
421+
) do
422+
Enum.any?(traits, fn %Traits.Trait{trait_key: t_key} -> t_key == prop end)
423+
end
424+
425+
def traits_match_segment_condition(
426+
traits,
427+
%Segments.Segment.Condition{operator: :IS_NOT_SET, property_: prop},
428+
_segment_id,
429+
_identifier
430+
) do
431+
Enum.all?(traits, fn %Traits.Trait{trait_key: t_key} -> t_key != prop end)
432+
end
433+
389434
def traits_match_segment_condition(
390435
traits,
391436
%Segments.Segment.Condition{operator: operator, value: value, property_: prop},
@@ -535,6 +580,27 @@ defmodule Flagsmith.Engine do
535580
end
536581
end
537582

583+
def trait_match(:MODULO, trait, %Trait.Value{value: value}) do
584+
with true <- is_binary(trait),
585+
%Decimal{} <- value,
586+
[mod, result] <- String.split(trait, "|"),
587+
%Decimal{} = mod_val <- Decimal.new(mod),
588+
%Decimal{} = result_val <- Decimal.new(result),
589+
%Decimal{} = remainder <- Decimal.rem(value, mod_val) do
590+
Decimal.equal?(remainder, result_val)
591+
else
592+
_ ->
593+
false
594+
end
595+
rescue
596+
Decimal.Error ->
597+
Logger.warn(
598+
"invalid MODULO segment rule or trait value :: rule: #{inspect(trait)} :: value: #{inspect(value)}"
599+
)
600+
601+
false
602+
end
603+
538604
def trait_match(condition, not_cast, %Trait.Value{} = t_value_struct)
539605
when condition in @condition_operators and not is_struct(not_cast) and not is_map(not_cast) do
540606
case Trait.Value.is_semver(not_cast) do

lib/schemas/identity_segment.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Flagsmith.Schemas.Segments.IdentitySegment do
2+
use TypedEctoSchema
3+
4+
alias Flagsmith.Schemas.Segments
5+
6+
@moduledoc """
7+
Ecto schema representing a Flagsmith Identity Segment.
8+
"""
9+
10+
@primary_key {:id, :id, autogenerate: false}
11+
typed_embedded_schema do
12+
field(:name, :string)
13+
end
14+
15+
@doc false
16+
@spec from_segment(Segments.Segment.t()) :: __MODULE__.t()
17+
def from_segment(%Segments.Segment{id: id, name: name}),
18+
do: %__MODULE__{id: id, name: name}
19+
end

lib/schemas/types/operator.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ defmodule Flagsmith.Schemas.Types.Operator do
1515
:LESS_THAN_INCLUSIVE,
1616
:NOT_EQUAL,
1717
:CONTAINS,
18+
:IS_SET,
19+
:IS_NOT_SET,
20+
:MODULO,
1821
:PERCENTAGE_SPLIT
1922
]
2023
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule FlagsmithEngine.MixProject do
44
def project do
55
[
66
app: :flagsmith_engine,
7-
version: "1.0.1",
7+
version: "1.1.0",
88
elixir: "~> 1.12",
99
start_permanent: Mix.env() == :prod,
1010
deps: deps(),

test/flagsmith_engine_test.exs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Flagsmith.EngineTest do
22
use ExUnit.Case, async: true
33

4-
alias Flagsmith.Schemas.{Environment, Features, Segments}
4+
alias Flagsmith.Schemas.{Environment, Features, Segments, Traits}
55
alias Flagsmith.Engine.Test
66

77
# stub the mock so that it calls the normal module as it would under regular usage
@@ -253,6 +253,34 @@ defmodule Flagsmith.EngineTest do
253253
] = Flagsmith.Engine.get_identity_feature_states(env, identity, [])
254254
end
255255

256+
test "get_identity_segments/3", %{env: env, identity: identity} do
257+
# the identity we're using has `show_popup` trait as false by default so
258+
# it should evaluate as this segment being for this identity when no traits
259+
# are passed
260+
assert [%Flagsmith.Schemas.Segments.IdentitySegment{id: 5241, name: "test_segment"}] =
261+
Flagsmith.Engine.get_identity_segments(env, identity, [])
262+
263+
# passing the trait as `true` should make this segment no longer match since
264+
# the condition is `show_popup` to be false
265+
assert [] =
266+
Flagsmith.Engine.get_identity_segments(env, identity, [
267+
%Traits.Trait{
268+
trait_key: "show_popup",
269+
trait_value: %Traits.Trait.Value{value: true, type: :boolean}
270+
}
271+
])
272+
273+
# and passing the trait as `false` (as is the default) should make it match just
274+
# the same as initially
275+
assert [%Flagsmith.Schemas.Segments.IdentitySegment{id: 5241, name: "test_segment"}] =
276+
Flagsmith.Engine.get_identity_segments(env, identity, [
277+
%Traits.Trait{
278+
trait_key: "show_popup",
279+
trait_value: %Traits.Trait.Value{value: false, type: :boolean}
280+
}
281+
])
282+
end
283+
256284
test "get_identity_feature_state/4", %{env: env, identity: identity} do
257285
assert %Features.FeatureState{
258286
id: 72267,

test/unit/segment_conditions_test.exs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ defmodule Flagsmith.Engine.SegmentConditionsTest do
5555
{:NOT_CONTAINS, "bar", "baz", true},
5656
{:REGEX, "foo", "[a-z]+", true},
5757
{:REGEX, "FOO", "[a-z]+", false},
58-
{:REGEX, "1.2.3", "\\d", true}
58+
{:REGEX, "1.2.3", "\\d", true},
59+
{:MODULO, 1, "2|0", false},
60+
{:MODULO, 2, "2|0", true},
61+
{:MODULO, 3, "2|0", false},
62+
{:MODULO, 34.2, "4|3", false},
63+
{:MODULO, 35.0, "4|3", true},
64+
{:MODULO, "dummy", "3|0", false},
65+
{:MODULO, "1.0.0", "3|0", false},
66+
{:MODULO, false, "1|3", false},
67+
{:MODULO, 3.5, "1.5|0.5", true},
68+
{:MODULO, 4, "1.5|0.5", false}
5969
]
6070

6171
test "all conditions" do
@@ -354,4 +364,108 @@ defmodule Flagsmith.Engine.SegmentConditionsTest do
354364

355365
refute Flagsmith.Engine.traits_match_segment_rule(traits_3, @segment_rule_nested_any, 1, 1)
356366
end
367+
368+
@segment_rule_all_is_or_not_set %Segment.Rule{
369+
conditions: [],
370+
rules: [
371+
%Segment.Rule{
372+
conditions: [
373+
%Segment.Condition{
374+
operator: :IS_SET,
375+
property_: "test_is_set"
376+
},
377+
%Segment.Condition{
378+
operator: :IS_NOT_SET,
379+
property_: "test_is_not_set"
380+
}
381+
],
382+
rules: [],
383+
type: :ALL
384+
}
385+
],
386+
type: :ALL
387+
}
388+
389+
test "Segment.Rule IS_SET and IS_NOT_SET" do
390+
# test that the segment matches (both conditions, IS_SET is true, and
391+
# IS_NOT_SET true)
392+
traits_1 = [
393+
%Trait{
394+
trait_key: "test_is_set",
395+
trait_value: %Trait.Value{value: true, type: :boolean}
396+
}
397+
]
398+
399+
assert Flagsmith.Engine.traits_match_segment_rule(
400+
traits_1,
401+
@segment_rule_all_is_or_not_set,
402+
1,
403+
1
404+
)
405+
406+
# test that the segment matches even is the test_is_set trait value is false
407+
traits_2 = [
408+
%Trait{
409+
trait_key: "test_is_set",
410+
trait_value: %Trait.Value{value: false, type: :boolean}
411+
}
412+
]
413+
414+
assert Flagsmith.Engine.traits_match_segment_rule(
415+
traits_2,
416+
@segment_rule_all_is_or_not_set,
417+
1,
418+
1
419+
)
420+
421+
# refute because `test_is_not_set` is passed as a trait and so the segment
422+
# condition IS_NOT_SET should fail since the segment specifies :ALL for rules
423+
# validations
424+
traits_3 = [
425+
%Trait{
426+
trait_key: "test_is_set",
427+
trait_value: %Trait.Value{value: true, type: :boolean}
428+
},
429+
%Trait{
430+
trait_key: "test_is_not_set",
431+
trait_value: %Trait.Value{value: true, type: :boolean}
432+
}
433+
]
434+
435+
refute Flagsmith.Engine.traits_match_segment_rule(
436+
traits_3,
437+
@segment_rule_all_is_or_not_set,
438+
1,
439+
1
440+
)
441+
442+
# :IS_NOT_SET should still evaluate to false even if the trait value is `false`
443+
# since it's still set
444+
traits_4 = [
445+
%Trait{
446+
trait_key: "test_is_set",
447+
trait_value: %Trait.Value{value: true, type: :boolean}
448+
},
449+
%Trait{
450+
trait_key: "test_is_not_set",
451+
trait_value: %Trait.Value{value: false, type: :boolean}
452+
}
453+
]
454+
455+
refute Flagsmith.Engine.traits_match_segment_rule(
456+
traits_4,
457+
@segment_rule_all_is_or_not_set,
458+
1,
459+
1
460+
)
461+
462+
# :IS_SET should fail since no trait is being passed to match it
463+
464+
refute Flagsmith.Engine.traits_match_segment_rule(
465+
[],
466+
@segment_rule_all_is_or_not_set,
467+
1,
468+
1
469+
)
470+
end
357471
end

0 commit comments

Comments
 (0)