Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Flipt provider #178

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ components:
- toddbaert
src/OpenFeature.Contrib.Providers.Statsig:
- jenshenneberg
src/OpenFeature.Contrib.Providers.Flipt:
- dmitryrogov

# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
Expand All @@ -41,6 +43,8 @@ components:
- toddbaert
test/src/OpenFeature.Contrib.Providers.Statsig.Test:
- jenshenneberg
test/src/OpenFeature.Contrib.Providers.Flipt.Test:
- dmitryrogov

ignored-authors:
- renovate-bot
14 changes: 14 additions & 0 deletions DotnetSdkContrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest", "test\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj", "{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt", "src\OpenFeature.Contrib.Providers.Flipt\OpenFeature.Contrib.Providers.Flipt.csproj", "{9649C012-F66B-46BB-A2C6-A3E814F4484E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig", "src\OpenFeature.Contrib.Providers.Statsig\OpenFeature.Contrib.Providers.Statsig.csproj", "{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}"
EndProject
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}"
Expand Down Expand Up @@ -107,6 +111,14 @@ Global
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.Build.0 = Release|Any CPU
{9649C012-F66B-46BB-A2C6-A3E814F4484E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9649C012-F66B-46BB-A2C6-A3E814F4484E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9649C012-F66B-46BB-A2C6-A3E814F4484E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9649C012-F66B-46BB-A2C6-A3E814F4484E}.Release|Any CPU.Build.0 = Release|Any CPU
{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Release|Any CPU.Build.0 = Release|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -135,6 +147,8 @@ Global
{4A2C6E0F-8A23-454F-8019-AE3DD91AA193} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{2ACD9150-A8F4-450E-B49A-C628895992BF} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{9649C012-F66B-46BB-A2C6-A3E814F4484E} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{D8CE4357-82B4-4176-9273-B1B76EF1D3C9} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
Expand Down
10 changes: 10 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@
"extra-files": [
"OpenFeature.Contrib.Providers.Statsig.csproj"
]
},
"src/OpenFeature.Contrib.Providers.Flipt": {
"package-name": "OpenFeature.Contrib.Providers.Flipt",
"release-type": "simple",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"versioning": "default",
"extra-files": [
"OpenFeature.Contrib.Providers.Flipt.csproj"
]
}
},
"changelog-sections": [
Expand Down
141 changes: 141 additions & 0 deletions src/OpenFeature.Contrib.Providers.Flipt/AttachmentParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using OpenFeature.Error;
using OpenFeature.Model;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;

namespace OpenFeature.Contrib.Providers.Flipt
{
internal static class AttachmentParser
{
/// <summary>
/// Converts the JSON string representation of a number to its double-precision
/// floating-point number equivalent.
/// </summary>
/// <param name="attachment">Attachment.</param>
/// <param name="value">Double-precision floating-point number result.</param>
/// <returns>true if attachment was converted successfully; otherwise false.</returns>
public static bool TryParseDouble(string attachment, out double value)
{
if (string.IsNullOrEmpty(attachment))
{
value = default;
return false;
}

return Utf8Parser.TryParse(Encoding.UTF8.GetBytes(attachment), out value, out int _); ;
}

/// <summary>
/// Converts the JSON string representation of a number to its 32-bit signed integer equivalent.
/// </summary>
/// <param name="attachment">Attachment.</param>
/// <param name="value">32-bit signed integer result.</param>
/// <returns>true if attachment was converted successfully; otherwise false.</returns>
public static bool TryParseInteger(string attachment, out int value)
{
if (string.IsNullOrEmpty(attachment))
{
value = default;
return false;
}

return Utf8Parser.TryParse(Encoding.UTF8.GetBytes(attachment), out value, out int _);
}

/// <summary>
/// Converts the JSON string.
/// </summary>
/// <param name="attachment">Attachment.</param>
/// <param name="value">String result.</param>
/// <returns>true if attachment was converted successfully; otherwise false.</returns>
public static bool TryParseString(string attachment, out string value)
{
if (string.IsNullOrEmpty(attachment))
{
value = default;
return false;
}

value = attachment.Trim('"');
return true;
}

/// <summary>
/// Attempts to parse a JSON attachment into a Value object.
/// It checks if the attachment is null or empty and tries to parse it using STJ.
/// If successful, it converts the parsed JSON element into a Value object.
/// </summary>
/// <param name="attachment">JSON string attachment.</param>
/// <param name="value">Value result.</param>
/// <returns>true if attachment was converted successfully; otherwise false.</returns>
public static bool TryParseJsonValue(string attachment, out Value value)
{
value = null;

if (string.IsNullOrEmpty(attachment))
{
return false;
}

try
{
value = ConvertJsonElementToValue(JsonDocument.Parse(attachment).RootElement);
return true;
}
catch
{
return false;
}
}

private static Value ConvertJsonElementToValue(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
{
if (element.TryGetDateTime(out var dateTimeValue))
{
return new Value(dateTimeValue);
}

return new Value(element.GetString());
}
case JsonValueKind.Number:
{
return new Value(element.GetDouble());
}
case JsonValueKind.True:
case JsonValueKind.False:
return new Value(element.GetBoolean());
case JsonValueKind.Object:
{
var structureValues = new Dictionary<string, Value>();
foreach (JsonProperty property in element.EnumerateObject())
{
structureValues.Add(property.Name, ConvertJsonElementToValue(property.Value));
}

return new Value(new Structure(structureValues));
}
case JsonValueKind.Array:
{
var arrayValues = new List<Value>();
foreach (JsonElement arrayElement in element.EnumerateArray())
{
arrayValues.Add(ConvertJsonElementToValue(arrayElement));
}

return new Value(arrayValues);
}
case JsonValueKind.Null:
case JsonValueKind.Undefined:
return new Value();
default:
throw new ParseErrorException($"Invalid variant value: {element.GetRawText()}");
}
}
}
}
109 changes: 109 additions & 0 deletions src/OpenFeature.Contrib.Providers.Flipt/FliptConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using Flipt.Evaluation;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;
using System.Diagnostics;

namespace OpenFeature.Contrib.Providers.Flipt
{
internal static class FliptConverter
{
/// <summary>
/// Converts an EvaluationReason value into a corresponding string representation based on predefined reasons.
/// </summary>
/// <param name="reason">Predefined OpenFeature reason.</param>
/// <returns></returns>
public static string ConvertReason(EvaluationReason reason)
{
switch (reason)
{
case EvaluationReason.UnknownEvaluationReason:
return Reason.Unknown;
case EvaluationReason.FlagDisabledEvaluationReason:
return Reason.Disabled;
case EvaluationReason.MatchEvaluationReason:
return Reason.TargetingMatch;
case EvaluationReason.DefaultEvaluationReason:
return Reason.Default;
default:
return Reason.Default;
}
}

/// <summary>
/// Creates Flipt evaluation request.
/// </summary>
/// <param name="flagKey">Flag key.</param>
/// <param name="context">Evaluation context.</param>
/// <param name="config">Provider configuration.</param>
/// <returns>Flipt evaluation request.</returns>
/// <exception cref="InvalidContextException">Unable to convert context value.</exception>
public static EvaluationRequest CreateRequest(string flagKey, EvaluationContext context, FliptProviderConfiguration config)
{
var request = new EvaluationRequest
{
NamespaceKey = config.Namespace,
FlagKey = flagKey
};

if (Activity.Current != null)
{
request.RequestId = Activity.Current.Id;
}

if (context == null || context.Count == 0)
{
return request;
}

foreach (var item in context)
{
var key = item.Key;
var value = item.Value;

if (value.IsNull || value.IsList || value.IsStructure)
{
// Skip null, lists and complex objects
continue;
}

if (key == config.TargetingKey && value.IsString)
{
// Skip targeting key and add its value as EntityId to request
request.EntityId = value.AsString;
continue;
}

if (key == config.RequestIdKey && value.IsString)
{
// Skip request id key and add its value as RequestId to request
request.RequestId = value.AsString;
continue;
}

if (value.IsString)
{
request.Context.Add(key, value.AsString);
}
else if (value.IsBoolean)
{
request.Context.Add(key, value.AsBoolean.ToString());
}
else if (value.IsNumber)
{
request.Context.Add(key, value.AsDouble.ToString());
}
else if (value.IsDateTime)
{
request.Context.Add(key, $"{value.AsDateTime.Value:o}");
}
else
{
throw new InvalidContextException($"Unable to convert context value with key: {key}.");
}
}

return request;
}
}
}
Loading
Loading