Skip to content

Commit 4d59bc3

Browse files
authored
feat: Introduce flipt provider for dotnet (#293)
Signed-off-by: Andrei de la Cruz <[email protected]>
1 parent cbe61a9 commit 4d59bc3

21 files changed

+3523
-3
lines changed

.github/component_owners.yml

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ components:
2222
src/OpenFeature.Contrib.Providers.Statsig:
2323
- jenshenneberg
2424
- lattenborough
25+
src/OpenFeature.Contrib.Providers.Flipt:
26+
- jeandreidc
27+
- markphelps
2528

2629
# test/
2730
test/OpenFeature.Contrib.Hooks.Otel.Test:
@@ -45,6 +48,9 @@ components:
4548
test/src/OpenFeature.Contrib.Providers.Statsig.Test:
4649
- jenshenneberg
4750
- lattenborough
51+
test/src/OpenFeature.Contrib.Providers.Flipt.Test:
52+
- jeandreidc
53+
- markphelps
4854

4955
ignored-authors:
5056
- renovate-bot

.release-please-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0",
66
"src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1",
77
"src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0",
8-
"src/OpenFeature.Contrib.Providers.Statsig": "0.1.0"
8+
"src/OpenFeature.Contrib.Providers.Statsig": "0.1.0",
9+
"src/OpenFeature.Contrib.Providers.Flipt": "0.0.1"
910
}

DotnetSdkContrib.sln

+14
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide
4141
EndProject
4242
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig.Test", "test\OpenFeature.Contrib.Providers.Statsig.Test\OpenFeature.Contrib.Providers.Statsig.Test.csproj", "{F3080350-B0AB-4D59-B416-50CC38C99087}"
4343
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt", "src\OpenFeature.Contrib.Providers.Flipt\OpenFeature.Contrib.Providers.Flipt.csproj", "{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}"
45+
EndProject
46+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{B446D481-B5A3-4509-8933-C4CF6DA9B147}"
47+
EndProject
4448
Global
4549
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4650
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +119,14 @@ Global
115119
{F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.Build.0 = Debug|Any CPU
116120
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.ActiveCfg = Release|Any CPU
117121
{F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.Build.0 = Release|Any CPU
122+
{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
123+
{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
124+
{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
125+
{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.Build.0 = Release|Any CPU
126+
{B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
127+
{B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.Build.0 = Debug|Any CPU
128+
{B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.ActiveCfg = Release|Any CPU
129+
{B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.Build.0 = Release|Any CPU
118130
EndGlobalSection
119131
GlobalSection(SolutionProperties) = preSolution
120132
HideSolutionNode = FALSE
@@ -137,5 +149,7 @@ Global
137149
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
138150
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
139151
{F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
152+
{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
153+
{B446D481-B5A3-4509-8933-C4CF6DA9B147} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
140154
EndGlobalSection
141155
EndGlobal

nuget.config

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22

33
<configuration>
44

55
<packageSources>
66
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
7-
<add key="github-open-feature" value="https://nuget.pkg.github.com/open-feature/index.json" />
7+
<add key="github-open-feature" value="https://nuget.pkg.github.com/open-feature/index.json"/>
88
</packageSources>
99

1010
<packageSourceMapping>

release-please-config.json

+10
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272
"extra-files": [
7373
"OpenFeature.Contrib.Providers.Statsig.csproj"
7474
]
75+
},
76+
"src/OpenFeature.Contrib.Providers.Flipt": {
77+
"package-name": "OpenFeature.Contrib.Providers.Flipt",
78+
"release-type": "simple",
79+
"bump-minor-pre-major": true,
80+
"bump-patch-for-minor-pre-major": true,
81+
"versioning": "default",
82+
"extra-files": [
83+
"OpenFeature.Contrib.Providers.Flipt.csproj"
84+
]
7585
}
7686
},
7787
"changelog-sections": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using Flipt.Rest;
5+
6+
namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
7+
8+
/// <summary>
9+
/// Wrapper for Flipt server sdk client for .net
10+
/// </summary>
11+
public class FliptClientWrapper : IFliptClientWrapper
12+
{
13+
private readonly FliptRestClient _fliptRestClient;
14+
15+
/// <summary>
16+
/// </summary>
17+
/// <param name="fliptUrl">Url of flipt instance</param>
18+
/// <param name="clientToken">Authentication access token</param>
19+
/// <param name="timeoutInSeconds">Timeout when calling flipt endpoints in seconds</param>
20+
public FliptClientWrapper(string fliptUrl,
21+
string clientToken = "",
22+
int timeoutInSeconds = 30)
23+
{
24+
_fliptRestClient = BuildClient(fliptUrl, clientToken, timeoutInSeconds);
25+
}
26+
27+
/// <inheritdoc />
28+
public async Task<VariantEvaluationResponse> EvaluateVariantAsync(EvaluationRequest evaluationRequest)
29+
{
30+
return await _fliptRestClient.EvaluateV1VariantAsync(evaluationRequest);
31+
}
32+
33+
/// <inheritdoc />
34+
public async Task<BooleanEvaluationResponse> EvaluateBooleanAsync(EvaluationRequest evaluationRequest)
35+
{
36+
return await _fliptRestClient.EvaluateV1BooleanAsync(evaluationRequest);
37+
}
38+
39+
private static FliptRestClient BuildClient(string fliptUrl, string clientToken, int timeoutInSeconds = 30)
40+
{
41+
var httpClient = new HttpClient
42+
{
43+
BaseAddress = new Uri(fliptUrl),
44+
Timeout = TimeSpan.FromSeconds(timeoutInSeconds),
45+
DefaultRequestHeaders = { { "Authorization", $"Bearer {clientToken}" } }
46+
};
47+
return new FliptRestClient(httpClient);
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Threading.Tasks;
2+
using Flipt.Rest;
3+
4+
namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper;
5+
6+
/// <summary>
7+
/// </summary>
8+
public interface IFliptClientWrapper
9+
{
10+
/// <summary>
11+
/// Wrapper to Flipt.io/EvaluateVariantAsync method
12+
/// </summary>
13+
/// <param name="evaluationRequest"></param>
14+
/// <returns></returns>
15+
Task<VariantEvaluationResponse> EvaluateVariantAsync(EvaluationRequest evaluationRequest);
16+
17+
/// <summary>
18+
/// Wrapper to Flipt.io/EvaluateBooleanAsync method
19+
/// </summary>
20+
/// <param name="evaluationRequest"></param>
21+
/// <returns></returns>
22+
Task<BooleanEvaluationResponse> EvaluateBooleanAsync(EvaluationRequest evaluationRequest);
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Text.Json;
2+
3+
namespace OpenFeature.Contrib.Providers.Flipt.Converters;
4+
5+
/// <summary>
6+
/// Extensions for default JsonConverter behavior
7+
/// </summary>
8+
public static class JsonConverterExtensions
9+
{
10+
/// <summary>
11+
/// JsonConverter serializer settings for Flipt to OpenFeature model deserialization
12+
/// </summary>
13+
public static readonly JsonSerializerOptions DefaultSerializerSettings = new()
14+
{
15+
WriteIndented = true,
16+
AllowTrailingCommas = true,
17+
Converters =
18+
{
19+
new OpenFeatureStructureConverter(),
20+
new OpenFeatureValueConverter()
21+
}
22+
};
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using OpenFeature.Model;
6+
7+
namespace OpenFeature.Contrib.Providers.Flipt.Converters;
8+
9+
/// <summary>
10+
/// JsonConverter for OpenFeature Structure type
11+
/// </summary>
12+
public class OpenFeatureStructureConverter : JsonConverter<Structure>
13+
{
14+
/// <inheritdoc />
15+
public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options)
16+
{
17+
var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(),
18+
JsonConverterExtensions.DefaultSerializerSettings));
19+
jsonDoc.WriteTo(writer);
20+
}
21+
22+
/// <inheritdoc />
23+
public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
24+
{
25+
using var jsonDocument = JsonDocument.ParseValue(ref reader);
26+
var jsonText = jsonDocument.RootElement.GetRawText();
27+
return new Structure(JsonSerializer.Deserialize<Dictionary<string, Value>>(jsonText,
28+
JsonConverterExtensions.DefaultSerializerSettings));
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using OpenFeature.Model;
6+
7+
namespace OpenFeature.Contrib.Providers.Flipt.Converters;
8+
9+
/// <summary>
10+
/// OpenFeature Value type converter
11+
/// </summary>
12+
public class OpenFeatureValueConverter : JsonConverter<Value>
13+
{
14+
/// <inheritdoc />
15+
public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
16+
{
17+
var value = new Value();
18+
switch (reader.TokenType)
19+
{
20+
case JsonTokenType.String:
21+
return reader.TryGetDateTime(out var dateTimeValue)
22+
? new Value(dateTimeValue)
23+
: new Value(reader.GetString() ?? string.Empty);
24+
case JsonTokenType.True:
25+
case JsonTokenType.False:
26+
return new Value(reader.GetBoolean());
27+
case JsonTokenType.Number:
28+
if (reader.TryGetInt32(out var intValue)) return new Value(intValue);
29+
if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue);
30+
break;
31+
case JsonTokenType.StartArray:
32+
return new Value(GenerateValueArray(ref reader, typeToConvert, options));
33+
case JsonTokenType.StartObject:
34+
return new Value(GetStructure(ref reader, typeToConvert, options));
35+
}
36+
37+
return value;
38+
}
39+
40+
private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
41+
{
42+
var startDepth = reader.CurrentDepth;
43+
var structureDictionary = new Dictionary<string, Value>();
44+
while (reader.Read())
45+
{
46+
if (reader.TokenType == JsonTokenType.PropertyName)
47+
{
48+
var key = reader.GetString();
49+
reader.Read();
50+
var val = Read(ref reader, typeToConvert, options);
51+
structureDictionary[key ?? string.Empty] = val;
52+
}
53+
54+
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break;
55+
}
56+
57+
return new Structure(structureDictionary);
58+
}
59+
60+
61+
private IList<Value> GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert,
62+
JsonSerializerOptions options)
63+
{
64+
var valuesArray = new List<Value>();
65+
var startDepth = reader.CurrentDepth;
66+
67+
while (reader.Read())
68+
switch (reader.TokenType)
69+
{
70+
case JsonTokenType.EndArray when reader.CurrentDepth == startDepth:
71+
return valuesArray;
72+
default:
73+
valuesArray.Add(Read(ref reader, typeToConvert, options));
74+
break;
75+
}
76+
77+
return valuesArray;
78+
}
79+
80+
/// <inheritdoc />
81+
public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options)
82+
{
83+
if (value.IsList)
84+
{
85+
writer.WriteStartArray();
86+
foreach (var val in value.AsList!)
87+
{
88+
var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject,
89+
JsonConverterExtensions.DefaultSerializerSettings));
90+
jsonDoc.WriteTo(writer);
91+
}
92+
93+
writer.WriteEndArray();
94+
}
95+
else
96+
{
97+
var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject,
98+
JsonConverterExtensions.DefaultSerializerSettings));
99+
jsonDoc.WriteTo(writer);
100+
}
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text.Json;
4+
using OpenFeature.Contrib.Providers.Flipt.Converters;
5+
using OpenFeature.Model;
6+
7+
namespace OpenFeature.Contrib.Providers.Flipt;
8+
9+
/// <summary>
10+
/// Extension helper methods
11+
/// </summary>
12+
public static class FliptExtensions
13+
{
14+
/// <summary>
15+
/// Transforms openFeature EvaluationContext to a mutable Dictionary that flipt sdk accepts
16+
/// </summary>
17+
/// <param name="evaluationContext">OpenFeature EvaluationContext</param>
18+
/// <returns></returns>
19+
public static Dictionary<string, string> ToStringDictionary(this EvaluationContext evaluationContext)
20+
{
21+
return evaluationContext?.AsDictionary()
22+
.ToDictionary(k => k.Key,
23+
v => JsonSerializer.Serialize(v.Value.AsObject,
24+
JsonConverterExtensions.DefaultSerializerSettings)) ??
25+
[];
26+
}
27+
}

0 commit comments

Comments
 (0)