Skip to content

Commit e603c08

Browse files
authored
feat: Update in-process resolver to support flag metadata #305 (#309)
Signed-off-by: christian.lutnik <[email protected]>
1 parent 2f4907e commit e603c08

File tree

4 files changed

+323
-68
lines changed

4 files changed

+323
-68
lines changed

.gitmodules

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"]
22
path = src/OpenFeature.Contrib.Providers.Flagd/schemas
3-
url = git@github.com:open-feature/schemas.git
3+
url = https://github.com/open-feature/schemas.git
44
[submodule "spec"]
55
path = spec
66
url = https://github.com/open-feature/spec.git

src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

+135-48
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,32 @@
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
44
using System.Linq;
5+
using System.Text.RegularExpressions;
56
using JsonLogic.Net;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Linq;
89
using OpenFeature.Constant;
910
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
1011
using OpenFeature.Error;
1112
using OpenFeature.Model;
12-
using System.Text.RegularExpressions;
1313

1414
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess
1515
{
16-
1716
internal class FlagConfiguration
1817
{
19-
[JsonProperty("state")]
20-
internal string State { get; set; }
21-
[JsonProperty("defaultVariant")]
22-
internal string DefaultVariant { get; set; }
23-
[JsonProperty("variants")]
24-
internal Dictionary<string, object> Variants { get; set; }
25-
[JsonProperty("targeting")]
26-
internal object Targeting { get; set; }
27-
[JsonProperty("source")]
28-
internal string Source { get; set; }
18+
[JsonProperty("state")] internal string State { get; set; }
19+
[JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; }
20+
[JsonProperty("variants")] internal Dictionary<string, object> Variants { get; set; }
21+
[JsonProperty("targeting")] internal object Targeting { get; set; }
22+
[JsonProperty("source")] internal string Source { get; set; }
23+
[JsonProperty("metadata")] internal Dictionary<string, object> Metadata { get; set; }
2924
}
3025

3126
internal class FlagSyncData
3227
{
33-
[JsonProperty("flags")]
34-
internal Dictionary<string, FlagConfiguration> Flags { get; set; }
35-
[JsonProperty("$evaluators")]
36-
internal Dictionary<string, object> Evaluators { get; set; }
28+
[JsonProperty("flags")] internal Dictionary<string, FlagConfiguration> Flags { get; set; }
29+
[JsonProperty("$evaluators")] internal Dictionary<string, object> Evaluators { get; set; }
30+
[JsonProperty("metadata")] internal Dictionary<string, object> Metadata { get; set; }
3731
}
3832

3933
internal class FlagConfigurationSync
@@ -53,6 +47,7 @@ internal enum FlagConfigurationUpdateType
5347
internal class JsonEvaluator
5448
{
5549
private Dictionary<string, FlagConfiguration> _flags = new Dictionary<string, FlagConfiguration>();
50+
private Dictionary<string, object> _flagSetMetadata = new Dictionary<string, object>();
5651

5752
private string _selector;
5853

@@ -88,7 +83,57 @@ internal FlagSyncData Parse(string flagConfigurations)
8883
});
8984
}
9085

91-
return JsonConvert.DeserializeObject<FlagSyncData>(transformed);
86+
87+
var data = JsonConvert.DeserializeObject<FlagSyncData>(transformed);
88+
if (data.Metadata == null)
89+
{
90+
data.Metadata = new Dictionary<string, object>();
91+
}
92+
else
93+
{
94+
foreach (var key in new List<string>(data.Metadata.Keys))
95+
{
96+
var value = data.Metadata[key];
97+
if (value is long longValue)
98+
{
99+
value = data.Metadata[key] = (int)longValue;
100+
}
101+
102+
VerifyMetadataValue(key, value);
103+
}
104+
}
105+
106+
foreach (var flagConfig in data.Flags)
107+
{
108+
if (flagConfig.Value.Metadata == null)
109+
{
110+
continue;
111+
}
112+
113+
foreach (var key in new List<string>(flagConfig.Value.Metadata.Keys))
114+
{
115+
var value = flagConfig.Value.Metadata[key];
116+
if (value is long longValue)
117+
{
118+
value = flagConfig.Value.Metadata[key] = (int)longValue;
119+
}
120+
121+
VerifyMetadataValue(key, value);
122+
}
123+
}
124+
125+
return data;
126+
}
127+
128+
private static void VerifyMetadataValue(string key, object value)
129+
{
130+
if (value is int || value is double || value is string || value is bool)
131+
{
132+
return;
133+
}
134+
135+
throw new ParseErrorException("Metadata entry for key " + key + " and value " + value +
136+
" is of unknown type");
92137
}
93138

94139
internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations)
@@ -99,71 +144,100 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat
99144
{
100145
case FlagConfigurationUpdateType.ALL:
101146
_flags = flagConfigsMap.Flags;
147+
_flagSetMetadata = flagConfigsMap.Metadata;
148+
102149
break;
103150
case FlagConfigurationUpdateType.ADD:
151+
case FlagConfigurationUpdateType.UPDATE:
104152
foreach (var keyAndValue in flagConfigsMap.Flags)
105153
{
106154
_flags[keyAndValue.Key] = keyAndValue.Value;
107155
}
108-
break;
109-
case FlagConfigurationUpdateType.UPDATE:
110-
foreach (var keyAndValue in flagConfigsMap.Flags)
156+
157+
foreach (var metadata in flagConfigsMap.Metadata)
111158
{
112-
_flags[keyAndValue.Key] = keyAndValue.Value;
159+
_flagSetMetadata[metadata.Key] = metadata.Value;
113160
}
161+
114162
break;
115163
case FlagConfigurationUpdateType.DELETE:
116164
foreach (var keyAndValue in flagConfigsMap.Flags)
117165
{
118166
_flags.Remove(keyAndValue.Key);
119167
}
120-
break;
121168

169+
foreach (var keyValuePair in flagConfigsMap.Metadata)
170+
{
171+
_flagSetMetadata.Remove(keyValuePair.Key);
172+
}
173+
174+
break;
122175
}
123176
}
124177

125-
public ResolutionDetails<bool> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null)
178+
public ResolutionDetails<bool> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
179+
EvaluationContext context = null)
126180
{
127181
return ResolveValue(flagKey, defaultValue, context);
128182
}
129183

130-
public ResolutionDetails<string> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null)
184+
public ResolutionDetails<string> ResolveStringValueAsync(string flagKey, string defaultValue,
185+
EvaluationContext context = null)
131186
{
132187
return ResolveValue(flagKey, defaultValue, context);
133188
}
134189

135-
public ResolutionDetails<int> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null)
190+
public ResolutionDetails<int> ResolveIntegerValueAsync(string flagKey, int defaultValue,
191+
EvaluationContext context = null)
136192
{
137193
return ResolveValue(flagKey, defaultValue, context);
138194
}
139195

140-
public ResolutionDetails<double> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null)
196+
public ResolutionDetails<double> ResolveDoubleValueAsync(string flagKey, double defaultValue,
197+
EvaluationContext context = null)
141198
{
142199
return ResolveValue(flagKey, defaultValue, context);
143200
}
144201

145-
public ResolutionDetails<Value> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null)
202+
public ResolutionDetails<Value> ResolveStructureValueAsync(string flagKey, Value defaultValue,
203+
EvaluationContext context = null)
146204
{
147205
return ResolveValue(flagKey, defaultValue, context);
148206
}
149207

150-
private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, EvaluationContext context = null)
208+
private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue,
209+
EvaluationContext context = null)
151210
{
152211
// check if we find the flag key
153212
var reason = Reason.Static;
154213
if (_flags.TryGetValue(flagKey, out var flagConfiguration))
155214
{
156215
if ("DISABLED" == flagConfiguration.State)
157216
{
158-
throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
217+
throw new FeatureProviderException(ErrorType.FlagNotFound,
218+
"FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
219+
}
220+
221+
Dictionary<string, object> combinedMetadata = new Dictionary<string, object>(_flagSetMetadata);
222+
if (flagConfiguration.Metadata != null)
223+
{
224+
foreach (var metadataEntry in flagConfiguration.Metadata)
225+
{
226+
combinedMetadata[metadataEntry.Key] = metadataEntry.Value;
227+
}
159228
}
229+
230+
var flagMetadata = new ImmutableMetadata(combinedMetadata);
160231
var variant = flagConfiguration.DefaultVariant;
161-
if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}")
232+
if (flagConfiguration.Targeting != null &&
233+
!String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) &&
234+
flagConfiguration.Targeting.ToString() != "{}")
162235
{
163236
reason = Reason.TargetingMatch;
164237
var flagdProperties = new Dictionary<string, Value>();
165238
flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey));
166-
flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
239+
flagdProperties.Add(FlagdProperties.TimestampKey,
240+
new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
167241

168242
if (context == null)
169243
{
@@ -173,7 +247,7 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
173247
var targetingContext = context.AsDictionary().Add(
174248
FlagdProperties.FlagdPropertiesKey,
175249
new Value(new Structure(flagdProperties))
176-
);
250+
);
177251

178252
var targetingString = flagConfiguration.Targeting.ToString();
179253
// Parse json into hierarchical structure
@@ -202,32 +276,39 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
202276
{
203277
// if variant is null, revert to default
204278
reason = Reason.Default;
205-
flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue);
279+
flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant,
280+
out var defaultVariantValue);
206281
if (defaultVariantValue == null)
207282
{
208-
throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
283+
throw new FeatureProviderException(ErrorType.ParseError,
284+
"PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
209285
}
286+
210287
var value = ExtractFoundVariant<T>(defaultVariantValue, flagKey);
211288
return new ResolutionDetails<T>(
212-
flagKey: flagKey,
213-
value,
214-
reason: reason,
215-
variant: variant
216-
);
289+
flagKey: flagKey,
290+
value,
291+
reason: reason,
292+
variant: variant,
293+
flagMetadata: flagMetadata
294+
);
217295
}
218296
else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue))
219297
{
220298
// if variant can be found, return it - this could be TARGETING_MATCH or STATIC.
221299
var value = ExtractFoundVariant<T>(foundVariantValue, flagKey);
222300
return new ResolutionDetails<T>(
223-
flagKey: flagKey,
224-
value,
225-
reason: reason,
226-
variant: variant
227-
);
301+
flagKey: flagKey,
302+
value,
303+
reason: reason,
304+
variant: variant,
305+
flagMetadata: flagMetadata
306+
);
228307
}
229308
}
230-
throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found");
309+
310+
throw new FeatureProviderException(ErrorType.FlagNotFound,
311+
"FLAG_NOT_FOUND: flag '" + flagKey + "' not found");
231312
}
232313

233314
static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
@@ -236,6 +317,7 @@ static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
236317
{
237318
foundVariantValue = Convert.ToInt32(foundVariantValue);
238319
}
320+
239321
if (typeof(T) == typeof(double))
240322
{
241323
foundVariantValue = Convert.ToDouble(foundVariantValue);
@@ -244,11 +326,14 @@ static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
244326
{
245327
foundVariantValue = ConvertJObjectToOpenFeatureValue(value);
246328
}
329+
247330
if (foundVariantValue is T castValue)
248331
{
249332
return castValue;
250333
}
251-
throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type");
334+
335+
throw new FeatureProviderException(ErrorType.TypeMismatch,
336+
"TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type");
252337
}
253338

254339
static dynamic ConvertToDynamicObject(IImmutableDictionary<string, Value> dictionary)
@@ -259,7 +344,9 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary<string, Value> dictio
259344
foreach (var kvp in dictionary)
260345
{
261346
expandoDict.Add(kvp.Key,
262-
kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject);
347+
kvp.Value.IsStructure
348+
? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary())
349+
: kvp.Value.AsObject);
263350
}
264351

265352
return expandoObject;
@@ -302,4 +389,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue)
302389
return new Value(new Structure(result));
303390
}
304391
}
305-
}
392+
}

0 commit comments

Comments
 (0)