Skip to content

Commit ef5082b

Browse files
emyllerclaude
andauthored
feat(Client): Add mapper layer for context-based engine (#177)
* Add mapper layer for context-based engine Co-authored-by: Claude <[email protected]> * Reduce memory allocations in mapper layer Co-authored-by: Claude <[email protected]> * Eliminate deserialization overhead in identity override mapping Co-authored-by: Claude <[email protected]> * Improve type safety for segment metadata Co-authored-by: Claude <[email protected]> * Clarify semantics of empty string values Co-authored-by: Claude <[email protected]> * Add environment name to evaluation context Co-authored-by: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 3a25c02 commit ef5082b

File tree

9 files changed

+555
-6
lines changed

9 files changed

+555
-6
lines changed

Flagsmith.Client.Test/Fixtures.cs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal class Fixtures
1313
public static AnalyticsProcessorTest GetAnalyticalProcessorTest() => new(new HttpClient(), ApiKey, ApiUrl);
1414
public static JObject JsonObject = JObject.Parse(@"{
1515
'api_key': 'test_key',
16+
'name': 'Test Environment',
1617
'project': {
1718
'name': 'Test project',
1819
'organisation': {
@@ -56,14 +57,84 @@ internal class Fixtures
5657
'multivariate_feature_state_values': [],
5758
'feature_state_value': 'some-value',
5859
'id': 1,
59-
'featurestate_uuid': '40eb539d-3713-4720-bbd4-829dbef10d51',
60+
'featurestate_uuid': '00000000-0000-0000-0000-000000000000',
6061
'feature': {
6162
'name': 'some_feature',
6263
'type': 'STANDARD',
6364
'id': 1
6465
},
6566
'segment_id': null,
6667
'enabled': true
68+
},
69+
{
70+
'feature_state_value': 'default_value',
71+
'django_id': 2,
72+
'featurestate_uuid': '11111111-1111-1111-1111-111111111111',
73+
'feature': {
74+
'name': 'mv_feature_with_ids',
75+
'type': 'MULTIVARIATE',
76+
'id': 2
77+
},
78+
'segment_id': null,
79+
'enabled': true,
80+
'multivariate_feature_state_values': [
81+
{
82+
'id': 100,
83+
'multivariate_feature_option': {
84+
'id': 10,
85+
'value': 'variant_a'
86+
},
87+
'mv_fs_value_uuid': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
88+
'percentage_allocation': 30.0
89+
},
90+
{
91+
'id': 200,
92+
'multivariate_feature_option': {
93+
'id': 20,
94+
'value': 'variant_b'
95+
},
96+
'mv_fs_value_uuid': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
97+
'percentage_allocation': 70.0
98+
}
99+
]
100+
},
101+
{
102+
'feature_state_value': 'fallback_value',
103+
'django_id': 3,
104+
'featurestate_uuid': '22222222-2222-2222-2222-222222222222',
105+
'feature': {
106+
'name': 'mv_feature_without_ids',
107+
'type': 'MULTIVARIATE',
108+
'id': 3
109+
},
110+
'segment_id': null,
111+
'enabled': false,
112+
'multivariate_feature_state_values': [
113+
{
114+
'multivariate_feature_option': {
115+
'id': 40,
116+
'value': 'option_y'
117+
},
118+
'mv_fs_value_uuid': 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
119+
'percentage_allocation': 50.0
120+
},
121+
{
122+
'multivariate_feature_option': {
123+
'id': 30,
124+
'value': 'option_x'
125+
},
126+
'mv_fs_value_uuid': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
127+
'percentage_allocation': 25.0
128+
},
129+
{
130+
'multivariate_feature_option': {
131+
'id': 50,
132+
'value': 'option_z'
133+
},
134+
'mv_fs_value_uuid': 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz',
135+
'percentage_allocation': 25.0
136+
}
137+
]
67138
}
68139
],
69140
'identity_overrides': [
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System.Linq;
2+
using FlagsmithEngine;
3+
using FlagsmithEngine.Segment;
4+
using Xunit;
5+
6+
namespace Flagsmith.FlagsmithClientTest
7+
{
8+
public class MappersTest
9+
{
10+
[Fact]
11+
public void MapEnvironmentDocumentToContext_ProducesEvaluationContext()
12+
{
13+
// Given
14+
var environment = Fixtures.Environment;
15+
16+
// When
17+
var context = Mappers.MapEnvironmentDocumentToContext(environment);
18+
19+
// Then
20+
Assert.IsType<EvaluationContext<SegmentMetadata, FeatureMetadata>>(context);
21+
Assert.Equal("test_key", context.Environment.Key);
22+
Assert.Equal("Test Environment", context.Environment.Name);
23+
Assert.Null(context.Identity);
24+
Assert.Equal(2, context.Segments.Count);
25+
26+
// Verify API segment
27+
Assert.True(context.Segments.ContainsKey("1"));
28+
var apiSegment = context.Segments["1"];
29+
Assert.Equal("1", apiSegment.Key);
30+
Assert.Equal("Test segment", apiSegment.Name);
31+
Assert.Single(apiSegment.Rules);
32+
Assert.Empty(apiSegment.Overrides);
33+
Assert.Equal("api", apiSegment.Metadata.Source);
34+
Assert.Equal(1, apiSegment.Metadata.Id);
35+
36+
// Verify segment rule structure
37+
Assert.Equal(TypeEnum.All, apiSegment.Rules[0].Type);
38+
Assert.Empty(apiSegment.Rules[0].Conditions);
39+
Assert.Single(apiSegment.Rules[0].Rules);
40+
41+
Assert.Equal(TypeEnum.All, apiSegment.Rules[0].Rules[0].Type);
42+
Assert.Single(apiSegment.Rules[0].Rules[0].Conditions);
43+
Assert.Empty(apiSegment.Rules[0].Rules[0].Rules);
44+
45+
Assert.Equal("foo", apiSegment.Rules[0].Rules[0].Conditions[0].Property);
46+
Assert.Equal(Operator.Equal, apiSegment.Rules[0].Rules[0].Conditions[0].Operator);
47+
Assert.Equal("bar", apiSegment.Rules[0].Rules[0].Conditions[0].Value.String);
48+
49+
// Verify identity override segment
50+
var overrideKey = "42d7556943d3c6f62b310e40f2252ac29203c20f37e9adffd8f12bd084a87b9d";
51+
Assert.True(context.Segments.ContainsKey(overrideKey));
52+
var overrideSegment = context.Segments[overrideKey];
53+
Assert.Equal("", overrideSegment.Key);
54+
Assert.Equal("identity_overrides", overrideSegment.Name);
55+
Assert.Single(overrideSegment.Rules);
56+
Assert.Single(overrideSegment.Overrides);
57+
58+
Assert.Equal(TypeEnum.All, overrideSegment.Rules[0].Type);
59+
Assert.Single(overrideSegment.Rules[0].Conditions);
60+
Assert.Empty(overrideSegment.Rules[0].Rules);
61+
62+
Assert.Equal("$.identity.identifier", overrideSegment.Rules[0].Conditions[0].Property);
63+
Assert.Equal(Operator.In, overrideSegment.Rules[0].Conditions[0].Operator);
64+
Assert.Equal(new[] { "overridden-id" }, overrideSegment.Rules[0].Conditions[0].Value.StringArray);
65+
66+
Assert.Equal("", overrideSegment.Overrides[0].Key);
67+
Assert.Equal("some_feature", overrideSegment.Overrides[0].Name);
68+
Assert.False(overrideSegment.Overrides[0].Enabled);
69+
Assert.Equal("some-overridden-value", overrideSegment.Overrides[0].Value);
70+
Assert.Equal(Constants.StrongestPriority, overrideSegment.Overrides[0].Priority);
71+
Assert.Null(overrideSegment.Overrides[0].Variants);
72+
Assert.Equal(1, overrideSegment.Overrides[0].Metadata.Id);
73+
74+
// Verify features
75+
Assert.Equal(3, context.Features.Count);
76+
Assert.True(context.Features.ContainsKey("some_feature"));
77+
var someFeature = context.Features["some_feature"];
78+
Assert.Equal("00000000-0000-0000-0000-000000000000", someFeature.Key);
79+
Assert.Equal("some_feature", someFeature.Name);
80+
Assert.True(someFeature.Enabled);
81+
Assert.Equal("some-value", someFeature.Value);
82+
Assert.Null(someFeature.Priority);
83+
Assert.Empty(someFeature.Variants);
84+
Assert.Equal(1, someFeature.Metadata.Id);
85+
86+
// Verify multivariate feature with IDs - priority should be based on ID
87+
Assert.True(context.Features.ContainsKey("mv_feature_with_ids"));
88+
var mvFeatureWithIds = context.Features["mv_feature_with_ids"];
89+
Assert.Equal("2", mvFeatureWithIds.Key);
90+
Assert.Equal("mv_feature_with_ids", mvFeatureWithIds.Name);
91+
Assert.True(mvFeatureWithIds.Enabled);
92+
Assert.Equal("default_value", mvFeatureWithIds.Value);
93+
Assert.Null(mvFeatureWithIds.Priority);
94+
Assert.Equal(2, mvFeatureWithIds.Variants.Length);
95+
Assert.Equal(2, mvFeatureWithIds.Metadata.Id);
96+
97+
// First variant: ID=100, should have priority 100
98+
Assert.Equal("variant_a", mvFeatureWithIds.Variants[0].Value);
99+
Assert.Equal(30.0, mvFeatureWithIds.Variants[0].Weight);
100+
Assert.Equal(100, mvFeatureWithIds.Variants[0].Priority);
101+
102+
// Second variant: ID=200, should have priority 200
103+
Assert.Equal("variant_b", mvFeatureWithIds.Variants[1].Value);
104+
Assert.Equal(70.0, mvFeatureWithIds.Variants[1].Weight);
105+
Assert.Equal(200, mvFeatureWithIds.Variants[1].Priority);
106+
107+
// Verify multivariate feature without IDs - priority should be based on UUID position
108+
Assert.True(context.Features.ContainsKey("mv_feature_without_ids"));
109+
var mvFeatureWithoutIds = context.Features["mv_feature_without_ids"];
110+
Assert.Equal("3", mvFeatureWithoutIds.Key);
111+
Assert.Equal("mv_feature_without_ids", mvFeatureWithoutIds.Name);
112+
Assert.False(mvFeatureWithoutIds.Enabled);
113+
Assert.Equal("fallback_value", mvFeatureWithoutIds.Value);
114+
Assert.Null(mvFeatureWithoutIds.Priority);
115+
Assert.Equal(3, mvFeatureWithoutIds.Variants.Length);
116+
Assert.Equal(3, mvFeatureWithoutIds.Metadata.Id);
117+
118+
// Variants should be ordered by UUID alphabetically
119+
Assert.Equal("option_y", mvFeatureWithoutIds.Variants[0].Value);
120+
Assert.Equal(50.0, mvFeatureWithoutIds.Variants[0].Weight);
121+
Assert.Equal(1, mvFeatureWithoutIds.Variants[0].Priority); // Second in sorted UUID order
122+
123+
Assert.Equal("option_x", mvFeatureWithoutIds.Variants[1].Value);
124+
Assert.Equal(25.0, mvFeatureWithoutIds.Variants[1].Weight);
125+
Assert.Equal(0, mvFeatureWithoutIds.Variants[1].Priority); // First in sorted UUID order
126+
127+
Assert.Equal("option_z", mvFeatureWithoutIds.Variants[2].Value);
128+
Assert.Equal(25.0, mvFeatureWithoutIds.Variants[2].Weight);
129+
Assert.Equal(2, mvFeatureWithoutIds.Variants[2].Priority); // Third in sorted UUID order
130+
}
131+
}
132+
}

Flagsmith.Client.Test/data/offline-environment.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"api_key": "B62qaMZNwfiqT76p38ggrQ",
3+
"name": "Test Environment",
34
"project": {
45
"name": "Test project",
56
"organisation": {

Flagsmith.Engine/Environment/Models/EnvironmentModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public class EnvironmentModel
1414

1515
[JsonProperty(PropertyName = "api_key")]
1616
public string ApiKey { get; set; }
17+
18+
[JsonProperty(PropertyName = "name")]
19+
public string Name { get; set; }
20+
1721
[JsonProperty(PropertyName = "project")]
1822
public ProjectModel Project { get; set; }
1923
[JsonProperty(PropertyName = "feature_states")]

Flagsmith.Engine/Segment/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace FlagsmithEngine.Segment
66
{
77
public static class Constants
88
{
9+
public const double StrongestPriority = float.NegativeInfinity;
910
public const double WeakestPriority = float.PositiveInfinity;
1011

1112
public const string AllRule = "ALL";

Flagsmith.FlagsmithClient/IdentityWrapper.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ public string CacheKey
1919
get
2020
{
2121
var combinedString = Identifier + JsonConvert.SerializeObject(Traits);
22-
using (var sha256 = SHA256.Create())
23-
{
24-
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combinedString));
25-
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
26-
}
22+
return Utils.GetHashString(combinedString);
2723
}
2824
}
2925

0 commit comments

Comments
 (0)