Skip to content

Commit 68d49d0

Browse files
authored
feat(v7): Support string[] as condition value for the IN operator (#255)
1 parent a05befa commit 68d49d0

File tree

4 files changed

+353
-226
lines changed

4 files changed

+353
-226
lines changed

flag_engine/context/types.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# generated by datamodel-codegen:
2-
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/features-contexts-in-eval-context-schema/sdk/evaluation-context.json # noqa: E501
3-
# timestamp: 2025-08-11T18:17:29+00:00
2+
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json # noqa: E501
3+
# timestamp: 2025-08-25T11:10:31+00:00
44

55
from __future__ import annotations
66

7-
from typing import Any, Dict, List, Optional, TypedDict, Union
7+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
88

99
from typing_extensions import NotRequired
1010

@@ -27,12 +27,21 @@ class IdentityContext(TypedDict):
2727
traits: NotRequired[Dict[str, Optional[Union[str, float, bool]]]]
2828

2929

30-
class SegmentCondition(TypedDict):
31-
property: NotRequired[str]
30+
class StrValueSegmentCondition(TypedDict):
31+
property: str
3232
operator: ConditionOperator
3333
value: str
3434

3535

36+
class InOperatorSegmentCondition(TypedDict):
37+
property: str
38+
operator: Literal["IN"]
39+
value: List[str]
40+
41+
42+
SegmentCondition = Union[StrValueSegmentCondition, InOperatorSegmentCondition]
43+
44+
3645
class SegmentRule(TypedDict):
3746
type: RuleType
3847
conditions: NotRequired[List[SegmentCondition]]

flag_engine/segments/evaluator.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SegmentCondition,
1717
SegmentContext,
1818
SegmentRule,
19+
StrValueSegmentCondition,
1920
)
2021
from flag_engine.environments.models import EnvironmentModel
2122
from flag_engine.identities.models import IdentityModel
@@ -235,6 +236,29 @@ def context_matches_condition(
235236
else None
236237
)
237238

239+
if condition["operator"] == constants.IN:
240+
if isinstance(segment_value := condition["value"], list):
241+
in_values = segment_value
242+
else:
243+
try:
244+
in_values = json.loads(segment_value)
245+
# Only accept JSON lists.
246+
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
247+
# but we aim to ditch the pydantic dependency in the future.
248+
if not isinstance(in_values, list):
249+
raise ValueError
250+
except ValueError:
251+
in_values = segment_value.split(",")
252+
in_values = [str(value) for value in in_values]
253+
# Guard against comparing boolean values to numeric strings.
254+
if isinstance(context_value, int) and not (
255+
context_value is True or context_value is False
256+
):
257+
context_value = str(context_value)
258+
return context_value in in_values
259+
260+
condition = typing.cast(StrValueSegmentCondition, condition)
261+
238262
if condition["operator"] == constants.PERCENTAGE_SPLIT:
239263
if context_value is not None:
240264
object_ids = [segment_key, context_value]
@@ -270,7 +294,7 @@ def get_context_value(
270294

271295

272296
def _matches_context_value(
273-
condition: SegmentCondition,
297+
condition: StrValueSegmentCondition,
274298
context_value: ContextValue,
275299
) -> bool:
276300
if matcher := MATCHERS_BY_OPERATOR.get(condition["operator"]):
@@ -316,29 +340,6 @@ def _evaluate_modulo(
316340
return context_value % divisor == remainder
317341

318342

319-
def _evaluate_in(
320-
segment_value: typing.Optional[str], context_value: ContextValue
321-
) -> bool:
322-
if segment_value:
323-
try:
324-
in_values = json.loads(segment_value)
325-
# Only accept JSON lists.
326-
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
327-
# but we aim to ditch the pydantic dependency in the future.
328-
if not isinstance(in_values, list):
329-
raise ValueError
330-
in_values = [str(value) for value in in_values]
331-
except ValueError:
332-
in_values = segment_value.split(",")
333-
# Guard against comparing boolean values to numeric strings.
334-
if isinstance(context_value, int) and not any(
335-
context_value is x for x in (False, True)
336-
):
337-
context_value = str(context_value)
338-
return context_value in in_values
339-
return False
340-
341-
342343
def _context_value_typed(
343344
func: typing.Callable[..., bool],
344345
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
@@ -365,7 +366,6 @@ def inner(
365366
constants.NOT_CONTAINS: _evaluate_not_contains,
366367
constants.REGEX: _evaluate_regex,
367368
constants.MODULO: _evaluate_modulo,
368-
constants.IN: _evaluate_in,
369369
constants.EQUAL: _context_value_typed(operator.eq),
370370
constants.GREATER_THAN: _context_value_typed(operator.gt),
371371
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),

tests/unit/segments/fixtures.py

Lines changed: 119 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flag_engine.context.types import SegmentCondition, SegmentContext, SegmentRule
1+
from flag_engine.context.types import SegmentContext
22
from flag_engine.segments import constants
33

44
trait_key_1 = "email"
@@ -10,141 +10,141 @@
1010
trait_key_3 = "date_joined"
1111
trait_value_3 = "2021-01-01"
1212

13-
empty_segment = SegmentContext(key=str(1), name="empty_segment", rules=[])
13+
empty_segment = {"key": "1", "name": "empty_segment", "rules": []}
1414

15-
segment_single_condition = SegmentContext(
16-
key=str(2),
17-
name="segment_one_condition",
18-
rules=[
19-
SegmentRule(
20-
type=constants.ALL_RULE,
21-
conditions=[
22-
SegmentCondition(
23-
operator=constants.EQUAL,
24-
property=trait_key_1,
25-
value=trait_value_1,
26-
)
15+
segment_single_condition: SegmentContext = {
16+
"key": "2",
17+
"name": "segment_one_condition",
18+
"rules": [
19+
{
20+
"type": constants.ALL_RULE,
21+
"conditions": [
22+
{
23+
"operator": constants.EQUAL,
24+
"property": trait_key_1,
25+
"value": trait_value_1,
26+
}
2727
],
28-
)
28+
}
2929
],
30-
)
30+
}
3131

32-
segment_multiple_conditions_all = SegmentContext(
33-
key=str(3),
34-
name="segment_multiple_conditions_all",
35-
rules=[
36-
SegmentRule(
37-
type=constants.ALL_RULE,
38-
conditions=[
39-
SegmentCondition(
40-
operator=constants.EQUAL,
41-
property=trait_key_1,
42-
value=trait_value_1,
43-
),
44-
SegmentCondition(
45-
operator=constants.EQUAL,
46-
property=trait_key_2,
47-
value=trait_value_2,
48-
),
32+
segment_multiple_conditions_all: SegmentContext = {
33+
"key": "3",
34+
"name": "segment_multiple_conditions_all",
35+
"rules": [
36+
{
37+
"type": constants.ALL_RULE,
38+
"conditions": [
39+
{
40+
"operator": constants.EQUAL,
41+
"property": trait_key_1,
42+
"value": trait_value_1,
43+
},
44+
{
45+
"operator": constants.EQUAL,
46+
"property": trait_key_2,
47+
"value": trait_value_2,
48+
},
4949
],
50-
)
50+
}
5151
],
52-
)
52+
}
5353

54-
segment_multiple_conditions_any = SegmentContext(
55-
key=str(4),
56-
name="segment_multiple_conditions_all",
57-
rules=[
58-
SegmentRule(
59-
type=constants.ANY_RULE,
60-
conditions=[
61-
SegmentCondition(
62-
operator=constants.EQUAL,
63-
property=trait_key_1,
64-
value=trait_value_1,
65-
),
66-
SegmentCondition(
67-
operator=constants.EQUAL,
68-
property=trait_key_2,
69-
value=trait_value_2,
70-
),
54+
segment_multiple_conditions_any: SegmentContext = {
55+
"key": "4",
56+
"name": "segment_multiple_conditions_all",
57+
"rules": [
58+
{
59+
"type": constants.ANY_RULE,
60+
"conditions": [
61+
{
62+
"operator": constants.EQUAL,
63+
"property": trait_key_1,
64+
"value": trait_value_1,
65+
},
66+
{
67+
"operator": constants.EQUAL,
68+
"property": trait_key_2,
69+
"value": trait_value_2,
70+
},
7171
],
72-
)
72+
}
7373
],
74-
)
74+
}
7575

76-
segment_nested_rules = SegmentContext(
77-
key=str(5),
78-
name="segment_nested_rules_all",
79-
rules=[
80-
SegmentRule(
81-
type=constants.ALL_RULE,
82-
rules=[
83-
SegmentRule(
84-
type=constants.ALL_RULE,
85-
conditions=[
86-
SegmentCondition(
87-
operator=constants.EQUAL,
88-
property=trait_key_1,
89-
value=trait_value_1,
90-
),
91-
SegmentCondition(
92-
operator=constants.EQUAL,
93-
property=trait_key_2,
94-
value=trait_value_2,
95-
),
76+
segment_nested_rules: SegmentContext = {
77+
"key": "5",
78+
"name": "segment_nested_rules_all",
79+
"rules": [
80+
{
81+
"type": constants.ALL_RULE,
82+
"rules": [
83+
{
84+
"type": constants.ALL_RULE,
85+
"conditions": [
86+
{
87+
"operator": constants.EQUAL,
88+
"property": trait_key_1,
89+
"value": trait_value_1,
90+
},
91+
{
92+
"operator": constants.EQUAL,
93+
"property": trait_key_2,
94+
"value": trait_value_2,
95+
},
9696
],
97-
),
98-
SegmentRule(
99-
type=constants.ALL_RULE,
100-
conditions=[
101-
SegmentCondition(
102-
operator=constants.EQUAL,
103-
property=trait_key_3,
104-
value=trait_value_3,
105-
)
97+
},
98+
{
99+
"type": constants.ALL_RULE,
100+
"conditions": [
101+
{
102+
"operator": constants.EQUAL,
103+
"property": trait_key_3,
104+
"value": trait_value_3,
105+
}
106106
],
107-
),
107+
},
108108
],
109-
)
109+
}
110110
],
111-
)
111+
}
112112

113-
segment_conditions_and_nested_rules = SegmentContext(
114-
key=str(6),
115-
name="segment_multiple_conditions_all_and_nested_rules",
116-
rules=[
117-
SegmentRule(
118-
type=constants.ALL_RULE,
119-
conditions=[
120-
SegmentCondition(
121-
operator=constants.EQUAL,
122-
property=trait_key_1,
123-
value=trait_value_1,
124-
)
113+
segment_conditions_and_nested_rules: SegmentContext = {
114+
"key": "6",
115+
"name": "segment_multiple_conditions_all_and_nested_rules",
116+
"rules": [
117+
{
118+
"type": constants.ALL_RULE,
119+
"conditions": [
120+
{
121+
"operator": constants.EQUAL,
122+
"property": trait_key_1,
123+
"value": trait_value_1,
124+
}
125125
],
126-
rules=[
127-
SegmentRule(
128-
type=constants.ALL_RULE,
129-
conditions=[
130-
SegmentCondition(
131-
operator=constants.EQUAL,
132-
property=trait_key_2,
133-
value=trait_value_2,
134-
),
126+
"rules": [
127+
{
128+
"type": constants.ALL_RULE,
129+
"conditions": [
130+
{
131+
"operator": constants.EQUAL,
132+
"property": trait_key_2,
133+
"value": trait_value_2,
134+
},
135135
],
136-
),
137-
SegmentRule(
138-
type=constants.ALL_RULE,
139-
conditions=[
140-
SegmentCondition(
141-
operator=constants.EQUAL,
142-
property=trait_key_3,
143-
value=trait_value_3,
144-
)
136+
},
137+
{
138+
"type": constants.ALL_RULE,
139+
"conditions": [
140+
{
141+
"operator": constants.EQUAL,
142+
"property": trait_key_3,
143+
"value": trait_value_3,
144+
}
145145
],
146-
),
146+
},
147147
],
148-
)
148+
}
149149
],
150-
)
150+
}

0 commit comments

Comments
 (0)