Skip to content

Commit cc24731

Browse files
fix(flagd): Flag metadata not being mapped when using RpcResolver (#575)
Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
1 parent 8ed7f04 commit cc24731

2 files changed

Lines changed: 151 additions & 11 deletions

File tree

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

Lines changed: 49 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,39 @@ private FeatureProviderException GetOFException(RpcException e)
451456
}
452457
}
453458

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

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

Lines changed: 102 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,107 @@ 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+
{ "key11", Google.Protobuf.WellKnownTypes.Value.ForNumber(int.MaxValue) }
331+
}
332+
};
333+
334+
setup(mockGrpcClient, setupMetadata);
335+
336+
var config = new FlagdConfig();
337+
var resolver = new RpcResolver(mockGrpcClient, config, null);
338+
339+
// Act
340+
var value = await act(resolver);
341+
342+
// Assert
343+
var metadata = value.FlagMetadata;
344+
Assert.NotNull(metadata);
345+
Assert.Equal("value1", metadata.GetString("key1"));
346+
Assert.Equal(string.Empty, metadata.GetString("key2"));
347+
Assert.True(metadata.GetBool("key3"));
348+
Assert.False(metadata.GetBool("key4"));
349+
Assert.Equal(1, metadata.GetInt("key5"));
350+
Assert.Equal(3.14, metadata.GetDouble("key6"));
351+
Assert.Equal(-0.531921, metadata.GetDouble("key7"));
352+
Assert.Null(metadata.GetString("key8"));
353+
Assert.Null(metadata.GetString("key9"));
354+
Assert.Null(metadata.GetString("key10"));
355+
Assert.Equal(int.MaxValue, metadata.GetInt("key11"));
356+
}
357+
358+
public static IEnumerable<object[]> ResolveValueFlagdMetadata()
359+
{
360+
const string flagKey = "test-key";
361+
362+
yield return new object[]
363+
{
364+
new Func<RpcResolver, Task<ResolutionDetails<bool>>>(r => r.ResolveBooleanValueAsync(flagKey, false)),
365+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveBooleanAsync(Arg.Any<ResolveBooleanRequest>())
366+
.Returns(CreateRpcResponse(new ResolveBooleanResponse() { Value = true, Variant = "true", Reason = "TARGETING_MATCH", Metadata = metadata })))
367+
};
368+
yield return new object[]
369+
{
370+
new Func<RpcResolver, Task<ResolutionDetails<string>>>(r => r.ResolveStringValueAsync(flagKey, "def")),
371+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveStringAsync(Arg.Any<ResolveStringRequest>())
372+
.Returns(CreateRpcResponse(new ResolveStringResponse() { Value = "one", Variant = "default", Reason = "TARGETING_MATCH", Metadata = metadata })))
373+
};
374+
yield return new object[]
375+
{
376+
new Func<RpcResolver, Task<ResolutionDetails<int>>>(r => r.ResolveIntegerValueAsync(flagKey, 3)),
377+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveIntAsync(Arg.Any<ResolveIntRequest>())
378+
.Returns(CreateRpcResponse(new ResolveIntResponse() { Value = 1, Variant = "one", Reason = "TARGETING_MATCH", Metadata = metadata })))
379+
};
380+
yield return new object[]
381+
{
382+
new Func<RpcResolver, Task<ResolutionDetails<double>>>(r => r.ResolveDoubleValueAsync(flagKey, 3.5)),
383+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveFloatAsync(Arg.Any<ResolveFloatRequest>())
384+
.Returns(CreateRpcResponse(new ResolveFloatResponse() { Value = 1.61, Variant = "one", Reason = "TARGETING_MATCH", Metadata = metadata })))
385+
};
386+
yield return new object[]
387+
{
388+
new Func<RpcResolver, Task<ResolutionDetails<Value>>>(r => r.ResolveStructureValueAsync(flagKey, new Value(Structure.Builder().Set("value1", true).Build()))),
389+
new Action<Service.ServiceClient, Google.Protobuf.WellKnownTypes.Struct>((client, metadata) => client.ResolveObjectAsync(Arg.Any<ResolveObjectRequest>())
390+
.Returns(CreateRpcResponse(new ResolveObjectResponse()
391+
{
392+
Value = new Google.Protobuf.WellKnownTypes.Struct(),
393+
Variant = "one",
394+
Reason = "TARGETING_MATCH",
395+
Metadata = metadata
396+
})))
397+
};
398+
}
399+
400+
private static AsyncUnaryCall<T> CreateRpcResponse<T>(T resp)
401+
where T : IMessage<T>, IBufferMessage
402+
{
403+
return new AsyncUnaryCall<T>(Task.FromResult(resp), Task.FromResult(Grpc.Core.Metadata.Empty), () => Status.DefaultSuccess, () => Grpc.Core.Metadata.Empty, () => { });
404+
}
405+
304406
private static Service.ServiceClient SetupGrpcStream(List<EventStreamResponse> responses)
305407
{
306408
var mockGrpcClient = Substitute.For<Service.ServiceClient>();

0 commit comments

Comments
 (0)