Skip to content

Commit 4b27e98

Browse files
committed
Fix issue with flag metadata not being mapped in RpcResolver
Signed-off-by: Kyle Julian <[email protected]>
1 parent 06c698c commit 4b27e98

File tree

2 files changed

+154
-11
lines changed

2 files changed

+154
-11
lines changed

src/OpenFeature.Contrib.Providers.Flagd/Resolver/Rpc/RpcResolver.cs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,11 @@ public async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(string flagK
8989

9090
return new ResolutionDetails<bool>(
9191
flagKey: flagKey,
92-
value: (bool)resolveBooleanResponse.Value,
92+
value: resolveBooleanResponse.Value,
9393
reason: resolveBooleanResponse.Reason,
94-
variant: resolveBooleanResponse.Variant
95-
);
94+
variant: resolveBooleanResponse.Variant,
95+
flagMetadata: BuildFlagMetadata(resolveBooleanResponse.Metadata)
96+
);
9697
}, context).ConfigureAwait(false);
9798
}
9899

@@ -110,8 +111,9 @@ public async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flag
110111
flagKey: flagKey,
111112
value: resolveStringResponse.Value,
112113
reason: resolveStringResponse.Reason,
113-
variant: resolveStringResponse.Variant
114-
);
114+
variant: resolveStringResponse.Variant,
115+
flagMetadata: BuildFlagMetadata(resolveStringResponse.Metadata)
116+
);
115117
}, context).ConfigureAwait(false);
116118
}
117119

@@ -129,8 +131,9 @@ public async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(string flagKe
129131
flagKey: flagKey,
130132
value: (int)resolveIntResponse.Value,
131133
reason: resolveIntResponse.Reason,
132-
variant: resolveIntResponse.Variant
133-
);
134+
variant: resolveIntResponse.Variant,
135+
flagMetadata: BuildFlagMetadata(resolveIntResponse.Metadata)
136+
);
134137
}, context).ConfigureAwait(false);
135138
}
136139

@@ -148,8 +151,9 @@ public async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flag
148151
flagKey: flagKey,
149152
value: resolveDoubleResponse.Value,
150153
reason: resolveDoubleResponse.Reason,
151-
variant: resolveDoubleResponse.Variant
152-
);
154+
variant: resolveDoubleResponse.Variant,
155+
flagMetadata: BuildFlagMetadata(resolveDoubleResponse.Metadata)
156+
);
153157
}, context).ConfigureAwait(false);
154158
}
155159

@@ -167,8 +171,9 @@ public async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string fl
167171
flagKey: flagKey,
168172
value: ConvertObjectToValue(resolveObjectResponse.Value),
169173
reason: resolveObjectResponse.Reason,
170-
variant: resolveObjectResponse.Variant
171-
);
174+
variant: resolveObjectResponse.Variant,
175+
flagMetadata: BuildFlagMetadata(resolveObjectResponse.Metadata)
176+
);
172177
}, context).ConfigureAwait(false);
173178
}
174179

@@ -451,6 +456,44 @@ private FeatureProviderException GetOFException(RpcException e)
451456
}
452457
}
453458

459+
#nullable enable
460+
private static ImmutableMetadata? BuildFlagMetadata(Struct? metadata)
461+
{
462+
if (metadata == null || metadata.Fields.Count == 0)
463+
{
464+
return null;
465+
}
466+
467+
var items = new Dictionary<string, object>();
468+
469+
foreach (var entry in metadata.Fields)
470+
{
471+
switch (entry.Value.KindCase)
472+
{
473+
case ProtoValue.KindOneofCase.NumberValue:
474+
items.Add(entry.Key, entry.Value.NumberValue);
475+
break;
476+
case ProtoValue.KindOneofCase.StringValue:
477+
items.Add(entry.Key, entry.Value.StringValue);
478+
break;
479+
case ProtoValue.KindOneofCase.BoolValue:
480+
items.Add(entry.Key, entry.Value.BoolValue);
481+
break;
482+
483+
// Unsupported types for metadata
484+
case ProtoValue.KindOneofCase.None:
485+
case ProtoValue.KindOneofCase.NullValue:
486+
case ProtoValue.KindOneofCase.StructValue:
487+
case ProtoValue.KindOneofCase.ListValue:
488+
default:
489+
break;
490+
}
491+
}
492+
493+
return new ImmutableMetadata(items);
494+
}
495+
#nullable disable
496+
454497
private Service.ServiceClient BuildClientForPlatform(FlagdConfig config)
455498
{
456499
var useUnixSocket = config.GetUri().ToString().StartsWith("unix://");

test/OpenFeature.Contrib.Providers.Flagd.Test/Resolver/Rpc/RpcResolverTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Threading;
44
using System.Threading.Tasks;
5+
using Google.Protobuf;
56
using Grpc.Core;
67
using NSubstitute;
78
using NSubstitute.ExceptionExtensions;
@@ -301,6 +302,105 @@ public static IEnumerable<object[]> ResolveValueDataLossData()
301302
};
302303
}
303304

305+
[Theory]
306+
[MemberData(nameof(ResolveValueFlagdMetadata))]
307+
internal async Task ResolveValueAsync_AddsFlagMetadata<T>(Func<RpcResolver, Task<ResolutionDetails<T>>> act,
308+
Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct> setup)
309+
{
310+
// Arrange
311+
var mockGrpcClient = Substitute.For<Service.ServiceClient>();
312+
313+
var setupMetadata = new Google.Protobuf.WellKnownTypes.Struct()
314+
{
315+
Fields =
316+
{
317+
{ "key1", Google.Protobuf.WellKnownTypes.Value.ForString("value1") },
318+
{ "key2", Google.Protobuf.WellKnownTypes.Value.ForString(string.Empty) },
319+
{ "key3", Google.Protobuf.WellKnownTypes.Value.ForBool(true) },
320+
{ "key4", Google.Protobuf.WellKnownTypes.Value.ForBool(false) },
321+
{ "key5", Google.Protobuf.WellKnownTypes.Value.ForNumber(1) },
322+
{ "key6", Google.Protobuf.WellKnownTypes.Value.ForNumber(3.14) },
323+
{ "key7", Google.Protobuf.WellKnownTypes.Value.ForNumber(-0.531921) },
324+
{ "key8", Google.Protobuf.WellKnownTypes.Value.ForList(Google.Protobuf.WellKnownTypes.Value.ForString("1"), Google.Protobuf.WellKnownTypes.Value.ForString("2")) },
325+
{ "key9", Google.Protobuf.WellKnownTypes.Value.ForNull() },
326+
{ "key10", Google.Protobuf.WellKnownTypes.Value.ForStruct(new Google.Protobuf.WellKnownTypes.Struct()
327+
{
328+
Fields = { { "innerkey", Google.Protobuf.WellKnownTypes.Value.ForBool(true) } }
329+
}) },
330+
}
331+
};
332+
333+
setup(mockGrpcClient, setupMetadata);
334+
335+
var config = new FlagdConfig();
336+
var resolver = new RpcResolver(mockGrpcClient, config, null);
337+
338+
// Act
339+
var value = await act(resolver);
340+
341+
// Assert
342+
var metadata = value.FlagMetadata;
343+
Assert.NotNull(metadata);
344+
Assert.Equal("value1", metadata.GetString("key1"));
345+
Assert.Equal(string.Empty, metadata.GetString("key2"));
346+
Assert.True(metadata.GetBool("key3"));
347+
Assert.False(metadata.GetBool("key4"));
348+
Assert.Equal(1, metadata.GetInt("key5"));
349+
Assert.Equal(3.14, metadata.GetDouble("key6"));
350+
Assert.Equal(-0.531921, metadata.GetDouble("key7"));
351+
Assert.Null(metadata.GetString("key8"));
352+
Assert.Null(metadata.GetString("key9"));
353+
Assert.Null(metadata.GetString("key10"));
354+
}
355+
356+
public static IEnumerable<object[]> ResolveValueFlagdMetadata()
357+
{
358+
const string flagKey = "test-key";
359+
360+
yield return new object[]
361+
{
362+
new Func<RpcResolver, Task<ResolutionDetails<bool>>>(r => r.ResolveBooleanValueAsync(flagKey, false)),
363+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveBooleanAsync(Arg.Any<ResolveBooleanRequest>())
364+
.Returns(CreateRpcResponse(new ResolveBooleanResponse() { Value = true, Variant = "true", Reason = "TARGETING_MATCH", Metadata = metadata })))
365+
};
366+
yield return new object[]
367+
{
368+
new Func<RpcResolver, Task<ResolutionDetails<string>>>(r => r.ResolveStringValueAsync(flagKey, "def")),
369+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveStringAsync(Arg.Any<ResolveStringRequest>())
370+
.Returns(CreateRpcResponse(new ResolveStringResponse() { Value = "one", Variant = "default", Reason = "TARGETING_MATCH", Metadata = metadata })))
371+
};
372+
yield return new object[]
373+
{
374+
new Func<RpcResolver, Task<ResolutionDetails<int>>>(r => r.ResolveIntegerValueAsync(flagKey, 3)),
375+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveIntAsync(Arg.Any<ResolveIntRequest>())
376+
.Returns(CreateRpcResponse(new ResolveIntResponse() { Value = 1, Variant = "one", Reason = "TARGETING_MATCH", Metadata = metadata })))
377+
};
378+
yield return new object[]
379+
{
380+
new Func<RpcResolver, Task<ResolutionDetails<double>>>(r => r.ResolveDoubleValueAsync(flagKey, 3.5)),
381+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveFloatAsync(Arg.Any<ResolveFloatRequest>())
382+
.Returns(CreateRpcResponse(new ResolveFloatResponse() { Value = 1.61, Variant = "one", Reason = "TARGETING_MATCH", Metadata = metadata })))
383+
};
384+
yield return new object[]
385+
{
386+
new Func<RpcResolver, Task<ResolutionDetails<Value>>>(r => r.ResolveStructureValueAsync(flagKey, new Value(Structure.Builder().Set("value1", true).Build()))),
387+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveObjectAsync(Arg.Any<ResolveObjectRequest>())
388+
.Returns(CreateRpcResponse(new ResolveObjectResponse()
389+
{
390+
Value = new Google.Protobuf.WellKnownTypes.Struct(),
391+
Variant = "one",
392+
Reason = "TARGETING_MATCH",
393+
Metadata = metadata
394+
})))
395+
};
396+
}
397+
398+
private static AsyncUnaryCall<T> CreateRpcResponse<T>(T resp)
399+
where T : IMessage<T>, IBufferMessage
400+
{
401+
return new AsyncUnaryCall<T>(Task.FromResult(resp), Task.FromResult(Grpc.Core.Metadata.Empty), () => Status.DefaultSuccess, () => Grpc.Core.Metadata.Empty, () => { });
402+
}
403+
304404
private static Service.ServiceClient SetupGrpcStream(List<EventStreamResponse> responses)
305405
{
306406
var mockGrpcClient = Substitute.For<Service.ServiceClient>();

0 commit comments

Comments
 (0)