Skip to content

Commit

Permalink
feat: integrate flagd provider with OpenFeature SDK (#18)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Chakroun <[email protected]>
  • Loading branch information
alxckn authored Apr 15, 2024
1 parent 3686eb5 commit 80d6d02
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 63 deletions.
2 changes: 2 additions & 0 deletions providers/openfeature-flagd-provider/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ source "https://rubygems.org"

# Specify your gem's dependencies in openfeature-flagd-provider.gemspec
gemspec

gem "openfeature-sdk"
2 changes: 2 additions & 0 deletions providers/openfeature-flagd-provider/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ GEM
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
json (2.7.2)
openfeature-sdk (0.3.0)
parallel (1.24.0)
parser (3.3.0.5)
ast (~> 2.4.1)
Expand Down Expand Up @@ -63,6 +64,7 @@ PLATFORMS

DEPENDENCIES
openfeature-flagd-provider!
openfeature-sdk
rake (~> 13.0)
rspec (~> 3.12.0)
rubocop (~> 1.37.1)
Expand Down
51 changes: 51 additions & 0 deletions providers/openfeature-flagd-provider/docker/flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,57 @@
"real-object": { "real": "value" }
},
"defaultVariant": "real-object"
},
"boolean-flag-targeting": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off",
"targeting": {
"if": [
{
"==": [
{
"var": "be_true"
},
true
]
},
"on"
]
}
},
"color-palette-experiment": {
"state": "ENABLED",
"defaultVariant": "grey",
"variants": {
"red": "#b91c1c",
"blue": "#0284c7",
"green": "#16a34a",
"grey": "#4b5563"
},
"targeting": {
"fractional": [
[
"red",
25
],
[
"blue",
25
],
[
"green",
25
],
[
"grey",
25
]
]
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ module FlagD
# values. The implementation follows the details specified in https://openfeature.dev/docs/specification/sections/providers
#
# Provider contains functionality to configure the GRPC connection via
#
# OpenFeature::FlagD::Provider.configure do |config|
# flagd_client = OpenFeature::FlagD::Provider.get_client
# flagd_client.configure do |config|
# config.host = 'localhost'
# config.port = 8379
# config.tls = false
Expand All @@ -37,6 +37,12 @@ module FlagD
# manner; <tt>client.resolve_object_value(flag_key: 'object-flag', default_value: { default_value: 'value'})</tt>
module Provider
class << self
def build_client
ConfiguredClient.new
end
end

class ConfiguredClient
def method_missing(method_name, *args, **kwargs, &)
if client.respond_to?(method_name)
client.send(method_name, *args, **kwargs, &)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# frozen_string_literal: true

require "grpc"
require 'google/protobuf/well_known_types'

require_relative "schema/v1/schema_services_pb"
require_relative "configuration"

module OpenFeature
module FlagD
module Provider
Expand All @@ -15,20 +17,20 @@ module Provider
#
# * <tt>metadata</tt> - Returns the associated provider metadata with the name
#
# * <tt>resolve_boolean_value(flag_key:, default_value:, context: nil)</tt>
# manner; <tt>client.resolve_boolean(flag_key: 'boolean-flag', default_value: false)</tt>
# * <tt>fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)</tt>
# manner; <tt>client.fetch_boolean(flag_key: 'boolean-flag', default_value: false)</tt>
#
# * <tt>resolve_integer_value(flag_key:, default_value:, context: nil)</tt>
# manner; <tt>client.resolve_integer_value(flag_key: 'integer-flag', default_value: 2)</tt>
# * <tt>fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)</tt>
# manner; <tt>client.fetch_integer_value(flag_key: 'integer-flag', default_value: 2)</tt>
#
# * <tt>resolve_float_value(flag_key:, default_value:, context: nil)</tt>
# manner; <tt>client.resolve_float_value(flag_key: 'float-flag', default_value: 2.0)</tt>
# * <tt>fetch_float_value(flag_key:, default_value:, evaluation_context: nil)</tt>
# manner; <tt>client.fetch_float_value(flag_key: 'float-flag', default_value: 2.0)</tt>
#
# * <tt>resolve_string_value(flag_key:, default_value:, context: nil)</tt>
# manner; <tt>client.resolve_string_value(flag_key: 'string-flag', default_value: 'some-default-value')</tt>
# * <tt>fetch_string_value(flag_key:, default_value:, evaluation_context: nil)</tt>
# manner; <tt>client.fetch_string_value(flag_key: 'string-flag', default_value: 'some-default-value')</tt>
#
# * <tt>resolve_object_value(flag_key:, default_value:, context: nil)</tt>
# manner; <tt>client.resolve_object_value(flag_key: 'flag', default_value: { default_value: 'value'})</tt>
# * <tt>fetch_object_value(flag_key:, default_value:, evaluation_context: nil)</tt>
# manner; <tt>client.fetch_object_value(flag_key: 'flag', default_value: { default_value: 'value'})</tt>
class Client
PROVIDER_NAME = "flagd Provider"

Expand All @@ -39,29 +41,37 @@ def initialize(configuration: nil)
@grpc_client = grpc_client(configuration)
end


def resolve_boolean_value(flag_key:, default_value:, context: nil)
request = Grpc::ResolveBooleanRequest.new(flag_key: flag_key)
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
request = Grpc::ResolveBooleanRequest.new(flag_key: flag_key, context: prepare_evaluation_context(evaluation_context))
process_request { @grpc_client.resolve_boolean(request) }
end

def resolve_integer_value(flag_key:, default_value:, context: nil)
request = Grpc::ResolveIntRequest.new(flag_key: flag_key)
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
case default_value
when Integer
fetch_integer_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
when Float
fetch_float_value(flag_key: flag_key, default_value: default_value, evaluation_context: evaluation_context)
end
end

def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
request = Grpc::ResolveIntRequest.new(flag_key: flag_key, context: prepare_evaluation_context(evaluation_context))
process_request { @grpc_client.resolve_int(request) }
end

def resolve_float_value(flag_key:, default_value:, context: nil)
request = Grpc::ResolveFloatRequest.new(flag_key: flag_key)
process_request { @grpc_client.resolve_float(request) }
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
request = Grpc::ResolveFloatRequest.new(flag_key: flag_key, context: prepare_evaluation_context(evaluation_context))
process_request { @grpc_client.resolve_float(request) }
end

def resolve_string_value(flag_key:, default_value:, context: nil)
request = Grpc::ResolveStringRequest.new(flag_key: flag_key)
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
request = Grpc::ResolveStringRequest.new(flag_key: flag_key, context: prepare_evaluation_context(evaluation_context))
process_request { @grpc_client.resolve_string(request) }
end

def resolve_object_value(flag_key:, default_value:, context: nil)
request = Grpc::ResolveObjectRequest.new(flag_key: flag_key)
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
request = Grpc::ResolveObjectRequest.new(flag_key: flag_key, context: prepare_evaluation_context(evaluation_context))
process_request { @grpc_client.resolve_object(request) }
end

Expand All @@ -72,7 +82,7 @@ def resolve_object_value(flag_key:, default_value:, context: nil)

def process_request(&block)
response = block.call
ResolutionDetails.new(nil, nil, response.reason, response.value, response.variant).to_h
ResolutionDetails.new(nil, nil, response.reason, response.value, response.variant)
rescue GRPC::NotFound => e
error_response("FLAG_NOT_FOUND", e.message)
rescue GRPC::InvalidArgument => e
Expand All @@ -85,8 +95,16 @@ def process_request(&block)
error_response("GENERAL", e.message)
end

def prepare_evaluation_context(evaluation_context)
return nil unless evaluation_context

fields = evaluation_context.fields
fields["targetingKey"] = fields.delete(:targeting_key)
Google::Protobuf::Struct.from_hash(fields)
end

def error_response(error_code, error_message)
ResolutionDetails.new(error_code, error_message, "ERROR", nil, nil).to_h
ResolutionDetails.new(error_code, error_message, "ERROR", nil, nil)
end

def grpc_client(configuration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@

context "https://openfeature.dev/docs/specification/sections/providers#requirement-221|222" do
it do
expect(client).to respond_to(:resolve_boolean_value).with_keywords(:flag_key, :default_value, :context)
expect(client).to respond_to(:resolve_integer_value).with_keywords(:flag_key, :default_value, :context)
expect(client).to respond_to(:resolve_float_value).with_keywords(:flag_key, :default_value, :context)
expect(client).to respond_to(:resolve_string_value).with_keywords(:flag_key, :default_value, :context)
expect(client).to respond_to(:resolve_object_value).with_keywords(:flag_key, :default_value, :context)
expect(client).to respond_to(:fetch_boolean_value).with_keywords(:flag_key, :default_value, :evaluation_context)
expect(client).to respond_to(:fetch_integer_value).with_keywords(:flag_key, :default_value, :evaluation_context)
expect(client).to respond_to(:fetch_float_value).with_keywords(:flag_key, :default_value, :evaluation_context)
expect(client).to respond_to(:fetch_string_value).with_keywords(:flag_key, :default_value, :evaluation_context)
expect(client).to respond_to(:fetch_object_value).with_keywords(:flag_key, :default_value, :evaluation_context)
end
end

context "https://openfeature.dev/docs/specification/sections/providers#requirement-223|224|225|226" do
it do
expect(client.resolve_boolean_value(flag_key: "boolean-flag", default_value: false)).to include(
expect(client.fetch_boolean_value(flag_key: "boolean-flag", default_value: false).to_h).to include(
error_code: nil,
error_message: nil,
reason: "STATIC",
Expand All @@ -37,7 +37,7 @@
end

it do
expect(client.resolve_integer_value(flag_key: "integer-flag", default_value: 1)).to include(
expect(client.fetch_integer_value(flag_key: "integer-flag", default_value: 1).to_h).to include(
error_code: nil,
error_message: nil,
reason: "STATIC",
Expand All @@ -47,7 +47,7 @@
end

it do
expect(client.resolve_float_value(flag_key: "float-flag", default_value: 1.1)).to include(
expect(client.fetch_float_value(flag_key: "float-flag", default_value: 1.1).to_h).to include(
error_code: nil,
error_message: nil,
reason: "STATIC",
Expand All @@ -57,7 +57,7 @@
end

it do
expect(client.resolve_string_value(flag_key: "string-flag", default_value: "lololo")).to include(
expect(client.fetch_string_value(flag_key: "string-flag", default_value: "lololo").to_h).to include(
error_code: nil,
error_message: nil,
reason: "STATIC",
Expand All @@ -67,8 +67,8 @@
end

it do
resolution_details = client.resolve_object_value(flag_key: "object-flag", default_value: { "a" => "b" })
expect(resolution_details).to include(
resolution_details = client.fetch_object_value(flag_key: "object-flag", default_value: { "a" => "b" })
expect(resolution_details.to_h).to include(
error_code: nil,
error_message: nil,
reason: "STATIC",
Expand All @@ -80,7 +80,7 @@

context "https://openfeature.dev/docs/specification/sections/providers#requirement-227" do
it do
expect(client.resolve_boolean_value(flag_key: "some-non-existant-flag", default_value: false)).to include(
expect(client.fetch_boolean_value(flag_key: "some-non-existant-flag", default_value: false).to_h).to include(
value: nil,
variant: nil,
reason: "ERROR",
Expand Down
Loading

0 comments on commit 80d6d02

Please sign in to comment.