Skip to content

Commit 004b0ed

Browse files
committed
feat: Support string[] as condition value for the IN operator
1 parent a05befa commit 004b0ed

File tree

4 files changed

+213
-83
lines changed

4 files changed

+213
-83
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/chore/support-str-array-for-in-condition/sdk/evaluation-context.json
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 SegmentCondition1(TypedDict):
31+
property: str
3232
operator: ConditionOperator
3333
value: str
3434

3535

36+
class SegmentCondition2(TypedDict):
37+
property: str
38+
operator: Literal["IN"]
39+
value: List[str]
40+
41+
42+
SegmentCondition = Union[SegmentCondition1, SegmentCondition2]
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
@@ -17,6 +17,7 @@
1717
SegmentContext,
1818
SegmentRule,
1919
)
20+
from flag_engine.context.types import SegmentCondition1 as StrValueSegmentCondition
2021
from flag_engine.environments.models import EnvironmentModel
2122
from flag_engine.identities.models import IdentityModel
2223
from flag_engine.identities.traits.types import ContextValue, is_trait_value
@@ -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 any(
255+
context_value is x for x in (False, True)
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: 12 additions & 12 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, SegmentRule
22
from flag_engine.segments import constants
33

44
trait_key_1 = "email"
@@ -19,7 +19,7 @@
1919
SegmentRule(
2020
type=constants.ALL_RULE,
2121
conditions=[
22-
SegmentCondition(
22+
dict(
2323
operator=constants.EQUAL,
2424
property=trait_key_1,
2525
value=trait_value_1,
@@ -36,12 +36,12 @@
3636
SegmentRule(
3737
type=constants.ALL_RULE,
3838
conditions=[
39-
SegmentCondition(
39+
dict(
4040
operator=constants.EQUAL,
4141
property=trait_key_1,
4242
value=trait_value_1,
4343
),
44-
SegmentCondition(
44+
dict(
4545
operator=constants.EQUAL,
4646
property=trait_key_2,
4747
value=trait_value_2,
@@ -58,12 +58,12 @@
5858
SegmentRule(
5959
type=constants.ANY_RULE,
6060
conditions=[
61-
SegmentCondition(
61+
dict(
6262
operator=constants.EQUAL,
6363
property=trait_key_1,
6464
value=trait_value_1,
6565
),
66-
SegmentCondition(
66+
dict(
6767
operator=constants.EQUAL,
6868
property=trait_key_2,
6969
value=trait_value_2,
@@ -83,12 +83,12 @@
8383
SegmentRule(
8484
type=constants.ALL_RULE,
8585
conditions=[
86-
SegmentCondition(
86+
dict(
8787
operator=constants.EQUAL,
8888
property=trait_key_1,
8989
value=trait_value_1,
9090
),
91-
SegmentCondition(
91+
dict(
9292
operator=constants.EQUAL,
9393
property=trait_key_2,
9494
value=trait_value_2,
@@ -98,7 +98,7 @@
9898
SegmentRule(
9999
type=constants.ALL_RULE,
100100
conditions=[
101-
SegmentCondition(
101+
dict(
102102
operator=constants.EQUAL,
103103
property=trait_key_3,
104104
value=trait_value_3,
@@ -117,7 +117,7 @@
117117
SegmentRule(
118118
type=constants.ALL_RULE,
119119
conditions=[
120-
SegmentCondition(
120+
dict(
121121
operator=constants.EQUAL,
122122
property=trait_key_1,
123123
value=trait_value_1,
@@ -127,7 +127,7 @@
127127
SegmentRule(
128128
type=constants.ALL_RULE,
129129
conditions=[
130-
SegmentCondition(
130+
dict(
131131
operator=constants.EQUAL,
132132
property=trait_key_2,
133133
value=trait_value_2,
@@ -137,7 +137,7 @@
137137
SegmentRule(
138138
type=constants.ALL_RULE,
139139
conditions=[
140-
SegmentCondition(
140+
dict(
141141
operator=constants.EQUAL,
142142
property=trait_key_3,
143143
value=trait_value_3,

0 commit comments

Comments
 (0)