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: integrate flagd provider with OpenFeature SDK #18

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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 if !evaluation_context
alxckn marked this conversation as resolved.
Show resolved Hide resolved

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
Loading