diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..365ae4f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "providers/openfeature-flagd-provider/schemas"]
+ path = providers/openfeature-flagd-provider/schemas
+ url = https://github.com/open-feature/schemas
diff --git a/README.md b/README.md
index 69b9da9..67eb924 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,16 @@
-# ruby-sdk-contrib
-Community contributions for hooks and reference providers in Ruby
+# OpenFeature Ruby Contributions
+
+This repository is intended for OpenFeature contributions which are not included in the [OpenFeature SDK](https://github.com/open-feature/ruby-sdk).
+
+The project includes:
+
+- [Providers](./providers)
+- [Hooks](./hooks)
+
+## Releases
+
+This repo uses _Release Please_ to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request). When Release Please's running PR is merged, any changed artifacts are published.
+
+## License
+
+Apache 2.0 - See [LICENSE](./LICENSE) for more information.
diff --git a/providers/openfeature-flagd-provider/.gitignore b/providers/openfeature-flagd-provider/.gitignore
new file mode 100644
index 0000000..b04a8c8
--- /dev/null
+++ b/providers/openfeature-flagd-provider/.gitignore
@@ -0,0 +1,11 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/providers/openfeature-flagd-provider/.rspec b/providers/openfeature-flagd-provider/.rspec
new file mode 100644
index 0000000..44b132b
--- /dev/null
+++ b/providers/openfeature-flagd-provider/.rspec
@@ -0,0 +1,4 @@
+-I lib
+--format documentation
+--color
+--require spec_helper
diff --git a/providers/openfeature-flagd-provider/.rubocop.yml b/providers/openfeature-flagd-provider/.rubocop.yml
new file mode 100644
index 0000000..e2b1af5
--- /dev/null
+++ b/providers/openfeature-flagd-provider/.rubocop.yml
@@ -0,0 +1,50 @@
+AllCops:
+ TargetRubyVersion: 3.1
+ NewCops: enable
+
+Style/StringLiterals:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Style/StringLiteralsInInterpolation:
+ Enabled: true
+ EnforcedStyle: double_quotes
+
+Layout/LineLength:
+ Max: 120
+ Exclude:
+ - 'spec/**/*.rb'
+ - 'lib/openfeature/flagd/provider/schema/v1/**'
+
+Lint/EmptyBlock:
+ Exclude:
+ - 'lib/openfeature/flagd/provider/schema/v1/**'
+
+Metrics/BlockLength:
+ Exclude:
+ - 'spec/**/*_spec.rb'
+ - 'openfeature-flagd-provider.gemspec'
+ - 'lib/openfeature/flagd/provider/schema/v1/**'
+
+Gemspec/RequireMFA:
+ Enabled: false
+
+Style/FrozenStringLiteralComment:
+ Exclude:
+ - 'lib/openfeature/flagd/provider/schema/v1/**'
+
+DocumentDynamicEvalDefinition:
+ Exclude:
+ - lib/openfeature/flagd/provider/client.rb
+
+Metrics/AbcSize:
+ Exclude:
+ - lib/openfeature/flagd/provider/**/*.rb
+
+Metrics/MethodLength:
+ Exclude:
+ - lib/openfeature/flagd/provider/**/*.rb
+
+Metrics/CyclomaticComplexity:
+ Exclude:
+ - lib/openfeature/flagd/provider/**/*.rb
diff --git a/providers/openfeature-flagd-provider/.ruby-version b/providers/openfeature-flagd-provider/.ruby-version
new file mode 100644
index 0000000..ef538c2
--- /dev/null
+++ b/providers/openfeature-flagd-provider/.ruby-version
@@ -0,0 +1 @@
+3.1.2
diff --git a/providers/openfeature-flagd-provider/Gemfile b/providers/openfeature-flagd-provider/Gemfile
new file mode 100644
index 0000000..c607af6
--- /dev/null
+++ b/providers/openfeature-flagd-provider/Gemfile
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in openfeature-flagd-provider.gemspec
+gemspec
diff --git a/providers/openfeature-flagd-provider/Gemfile.lock b/providers/openfeature-flagd-provider/Gemfile.lock
new file mode 100644
index 0000000..2b0d3c7
--- /dev/null
+++ b/providers/openfeature-flagd-provider/Gemfile.lock
@@ -0,0 +1,64 @@
+PATH
+ remote: .
+ specs:
+ openfeature-flagd-provider (0.0.1)
+ grpc (~> 1.50)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ diff-lcs (1.5.0)
+ google-protobuf (3.21.12)
+ googleapis-common-protos-types (1.4.0)
+ google-protobuf (~> 3.14)
+ grpc (1.50.0)
+ google-protobuf (~> 3.21)
+ googleapis-common-protos-types (~> 1.0)
+ json (2.6.2)
+ parallel (1.22.1)
+ parser (3.1.3.0)
+ ast (~> 2.4.1)
+ rainbow (3.1.1)
+ rake (13.0.6)
+ regexp_parser (2.6.1)
+ rexml (3.2.5)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.0)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.0)
+ rubocop (1.37.1)
+ json (~> 2.3)
+ parallel (~> 1.10)
+ parser (>= 3.1.2.1)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.23.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 3.0)
+ rubocop-ast (1.23.0)
+ parser (>= 3.1.1.0)
+ ruby-progressbar (1.11.0)
+ unicode-display_width (2.3.0)
+
+PLATFORMS
+ arm64-darwin-21
+
+DEPENDENCIES
+ openfeature-flagd-provider!
+ rake (~> 13.0)
+ rspec (~> 3.12.0)
+ rubocop (~> 1.37.1)
+
+BUNDLED WITH
+ 2.3.25
diff --git a/providers/openfeature-flagd-provider/README.md b/providers/openfeature-flagd-provider/README.md
new file mode 100644
index 0000000..5f2cfb0
--- /dev/null
+++ b/providers/openfeature-flagd-provider/README.md
@@ -0,0 +1,47 @@
+# OpenFeature FlagD Provider for Ruby
+
+This is the Ruby [provider](https://docs.openfeature.dev/docs/specification/sections/providers) implementation of the [FlagD](https://github.com/open-feature/flagd)
+## Installation
+
+Add this line to your application's Gemfile:
+
+```ruby
+gem 'openfeature-flagd-provider'
+```
+
+And then execute:
+
+```sh
+bundle install
+```
+
+Or install it yourself as:
+
+```sh
+gem install openfeature-flagd-provider
+```
+
+## Usage
+
+The provider allows for configuration of host, port, socket_path, and tls connection.
+
+```ruby
+OpenFeature::FlagD::Provider.configure do |config|
+ config.host = "localhost"
+ config.port = 8013
+ config.tls = false
+end
+```
+
+If no configurations are provided, the provider will be initialized with the following environment variables:
+
+> FLAGD_HOST
+> FLAGD_PORT
+> FLAGD_TLS
+> FLAGD_SOCKET_PATH
+
+If no environment variables are set the [default configuration](./lib/openfeature/flagd/provider/configuration.rb) is set
+
+## Contributing
+
+https://github.com/open-feature/ruby-sdk-contrib
diff --git a/providers/openfeature-flagd-provider/Rakefile b/providers/openfeature-flagd-provider/Rakefile
new file mode 100644
index 0000000..b6ae734
--- /dev/null
+++ b/providers/openfeature-flagd-provider/Rakefile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: :spec
diff --git a/providers/openfeature-flagd-provider/bin/console b/providers/openfeature-flagd-provider/bin/console
new file mode 100755
index 0000000..bb15106
--- /dev/null
+++ b/providers/openfeature-flagd-provider/bin/console
@@ -0,0 +1,15 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "openfeature/flagd/provider"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/providers/openfeature-flagd-provider/bin/setup b/providers/openfeature-flagd-provider/bin/setup
new file mode 100755
index 0000000..dce67d8
--- /dev/null
+++ b/providers/openfeature-flagd-provider/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider.rb b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider.rb
new file mode 100644
index 0000000..dab1e74
--- /dev/null
+++ b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require_relative "provider/configuration"
+require_relative "provider/client"
+
+module OpenFeature
+ module FlagD
+ # Provider represents the entry point for interacting with the FlagD provider
+ # values. The implementation follows the details specified in https://docs.openfeature.dev/docs/specification/sections/providers
+ #
+ # Provider contains functionality to configure the GRPC connection via
+ #
+ # OpenFeature::FlagD::Provider.configure do |config|
+ # config.host = 'localhost'
+ # config.port = 8379
+ # config.tls = false
+ # end
+ # The Provider providers the following methods and attributes:
+ #
+ # * metadata - Returns the associated provider metadata with the name
+ #
+ # * resolve_boolean_value(flag_key:, default_value:, context: nil)
+ # manner; client.resolve_boolean(flag_key: 'boolean-flag', default_value: false)
+ #
+ # * resolve_integer_value(flag_key:, default_value:, context: nil)
+ # manner; client.resolve_integer_value(flag_key: 'integer-flag', default_value: 2)
+ #
+ # * resolve_float_value(flag_key:, default_value:, context: nil)
+ # manner; client.resolve_float_value(flag_key: 'float-flag', default_value: 2.0)
+ #
+ # * resolve_string_value(flag_key:, default_value:, context: nil)
+ # manner; client.resolve_string_value(flag_key: 'string-flag', default_value: 'some-defau;t-value')
+ #
+ # * resolve_object_value(flag_key:, default_value:, context: nil)
+ # manner; client.resolve_object_value(flag_key: 'object-flag', default_value: { default_value: 'value'})
+ module Provider
+ class << self
+ def method_missing(method_name, *args, **kwargs, &)
+ if client.respond_to?(method_name)
+ client.send(method_name, *args, **kwargs, &)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ client.respond_to?(method_name, include_private) || super
+ end
+
+ def configuration
+ @configuration ||= explicit_configuration
+ .merge(Configuration.environment_variables_config)
+ .merge(Configuration.default_config)
+ end
+
+ def configure(&block)
+ return unless block_given?
+
+ block.call(explicit_configuration)
+ end
+
+ private
+
+ def explicit_configuration
+ @explicit_configuration ||= Configuration.new
+ end
+
+ def client
+ @client ||= Client.new(configuration:)
+ end
+ end
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/client.rb b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/client.rb
new file mode 100644
index 0000000..9479fc1
--- /dev/null
+++ b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/client.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require_relative "schema/v1/schema_services_pb"
+require_relative "configuration"
+
+module OpenFeature
+ module FlagD
+ module Provider
+ # Client represents a wrapper for the GRPC stub that allows for resolution of boolean, string, number, and object
+ # values. The implementation follows the details specified in https://docs.openfeature.dev/docs/specification/sections/providers
+ #
+ #
+ # Within the Client instance, the following methods are available:
+ #
+ # * resolve_boolean_value(flag_key:, default_value:, context: nil) -
+ # Resolves the boolean value of the flag_key
+ # manner; client.resolve_boolean(flag_key: 'boolean-flag', default_value: false)
+ #
+ # * resolve_integer_value(flag_key:, default_value:, context: nil) - Resolves the
+ # manner; client.resolve_boolean = File.read('path/to/filename.png')
+
+ # * resolve_integer_value - Allows you to specify any header field in your email such
+ # as headers['X-No-Spam'] = 'True'. Note that declaring a header multiple times
+ # will add many fields of the same name. Read #headers doc for more information.
+ #
+ class Client
+ attr_reader :metadata
+
+ def initialize(configuration: nil)
+ @configuration = Configuration.default_config
+ .merge(Configuration.environment_variables_config)
+ .merge(configuration)
+ @metadata = Metadata.new(PROVIDER_NAME).freeze
+ @grpc_client = Grpc::Service::Stub.new(
+ "#{@configuration.host}:#{@configuration.port}",
+ :this_channel_is_insecure
+ ).freeze
+ end
+
+ PROVIDER_NAME = "flagd Provider"
+ TYPE_RESOLVER_MAPPER = {
+ boolean: Grpc::ResolveBooleanRequest,
+ integer: Grpc::ResolveIntRequest,
+ float: Grpc::ResolveFloatRequest,
+ string: Grpc::ResolveStringRequest,
+ object: Grpc::ResolveObjectRequest
+ }.freeze
+
+ TYPE_RESOLVER_MAPPER.each_pair do |type, resolver|
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def resolve_#{type}_value(flag_key:, default_value:, context: nil)
+ request = #{resolver}.new(flag_key: flag_key)
+ response = @grpc_client.resolve_#{type}(request)
+ ResolutionDetails.new(nil, nil, response.reason, response.value, response.variant).to_h.freeze
+ rescue GRPC::NotFound => e
+ error_response("FLAG_NOT_FOUND", e.message)
+ rescue GRPC::InvalidArgument => e
+ error_response("TYPE_MISMATCH", e.message)
+ rescue GRPC::Unavailable => e
+ error_response("FLAG_NOT_FOUND", e.message)
+ rescue GRPC::DataLoss => e
+ error_response("PARSE_ERROR", e.message)
+ rescue StandardError => e
+ error_response("GENERAL", e.message)
+ end
+ RUBY
+ end
+
+ private
+
+ Metadata = Struct.new("Metadata", :name)
+ ResolutionDetails = Struct.new("ResolutionDetails", :error_code, :error_message, :reason, :value, :variant)
+
+ def error_response(error_code, error_message)
+ ResolutionDetails.new(error_code, error_message, "ERROR", nil, nil).to_h.freeze
+ end
+ end
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/configuration.rb b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/configuration.rb
new file mode 100644
index 0000000..8a66822
--- /dev/null
+++ b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/configuration.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module OpenFeature
+ module FlagD
+ module Provider
+ # Represents the configuration object for the FlagD provider,
+ # This class is not meant to be interacted with directly but instead through the
+ # OpenFeature::FlagD::Provider.configure method
+ class Configuration
+ attr_accessor :host, :port, :tls
+
+ ENVIRONMENT_CONFIG_NAME = {
+ host: "FLAGD_HOST",
+ port: "FLAGD_PORT",
+ tls: "FLAGD_TLS"
+ }.freeze
+
+ def merge(other_configuration)
+ return self if other_configuration.nil?
+
+ @host = other_configuration.host if !other_configuration.host.nil? && @host.nil?
+ @port = other_configuration.port if !other_configuration.port.nil? && @port.nil?
+ @tls = other_configuration.tls if !other_configuration.tls.nil? && @tls.nil?
+ self
+ end
+
+ def self.environment_variables_config
+ configuration = Configuration.new
+ unless ENV[ENVIRONMENT_CONFIG_NAME[:host]].nil?
+ configuration.host = ENV.fetch(ENVIRONMENT_CONFIG_NAME[:host],
+ nil)
+ end
+ unless ENV[ENVIRONMENT_CONFIG_NAME[:port]].nil?
+ configuration.port = ENV.fetch(ENVIRONMENT_CONFIG_NAME[:port],
+ nil)
+ end
+ unless ENV[ENVIRONMENT_CONFIG_NAME[:tls]].nil?
+ configuration.tls = ENV.fetch(ENVIRONMENT_CONFIG_NAME[:tls],
+ nil) == "true"
+ end
+
+ configuration
+ end
+
+ def self.default_config
+ configuration = Configuration.new
+ configuration.host = "localhost"
+ configuration.port = 8013
+ configuration.tls = false
+ configuration
+ end
+ end
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_pb.rb b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_pb.rb
new file mode 100644
index 0000000..19fb9f7
--- /dev/null
+++ b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_pb.rb
@@ -0,0 +1,102 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: schema/v1/schema.proto
+
+require "google/protobuf"
+
+require "google/protobuf/struct_pb"
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("schema/v1/schema.proto", syntax: :proto3) do
+ add_message "schema.v1.ResolveAllRequest" do
+ optional :context, :message, 1, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveAllResponse" do
+ map :flags, :string, :message, 1, "schema.v1.AnyFlag"
+ end
+ add_message "schema.v1.AnyFlag" do
+ optional :reason, :string, 1, json_name: "reason"
+ optional :variant, :string, 2, json_name: "variant"
+ oneof :value do
+ optional :bool_value, :bool, 3, json_name: "boolValue"
+ optional :string_value, :string, 4, json_name: "stringValue"
+ optional :double_value, :double, 5, json_name: "doubleValue"
+ optional :object_value, :message, 6, "google.protobuf.Struct", json_name: "objectValue"
+ end
+ end
+ add_message "schema.v1.ResolveBooleanRequest" do
+ optional :flag_key, :string, 1, json_name: "flagKey"
+ optional :context, :message, 2, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveBooleanResponse" do
+ optional :value, :bool, 1, json_name: "value"
+ optional :reason, :string, 2, json_name: "reason"
+ optional :variant, :string, 3, json_name: "variant"
+ end
+ add_message "schema.v1.ResolveStringRequest" do
+ optional :flag_key, :string, 1, json_name: "flagKey"
+ optional :context, :message, 2, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveStringResponse" do
+ optional :value, :string, 1, json_name: "value"
+ optional :reason, :string, 2, json_name: "reason"
+ optional :variant, :string, 3, json_name: "variant"
+ end
+ add_message "schema.v1.ResolveFloatRequest" do
+ optional :flag_key, :string, 1, json_name: "flagKey"
+ optional :context, :message, 2, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveFloatResponse" do
+ optional :value, :double, 1, json_name: "value"
+ optional :reason, :string, 2, json_name: "reason"
+ optional :variant, :string, 3, json_name: "variant"
+ end
+ add_message "schema.v1.ResolveIntRequest" do
+ optional :flag_key, :string, 1, json_name: "flagKey"
+ optional :context, :message, 2, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveIntResponse" do
+ optional :value, :int64, 1, json_name: "value"
+ optional :reason, :string, 2, json_name: "reason"
+ optional :variant, :string, 3, json_name: "variant"
+ end
+ add_message "schema.v1.ResolveObjectRequest" do
+ optional :flag_key, :string, 1, json_name: "flagKey"
+ optional :context, :message, 2, "google.protobuf.Struct", json_name: "context"
+ end
+ add_message "schema.v1.ResolveObjectResponse" do
+ optional :value, :message, 1, "google.protobuf.Struct", json_name: "value"
+ optional :reason, :string, 2, json_name: "reason"
+ optional :variant, :string, 3, json_name: "variant"
+ end
+ add_message "schema.v1.EventStreamResponse" do
+ optional :type, :string, 1, json_name: "type"
+ optional :data, :message, 2, "google.protobuf.Struct", json_name: "data"
+ end
+ add_message "schema.v1.EventStreamRequest" do
+ end
+ end
+end
+
+module OpenFeature
+ module FlagD
+ module Provider
+ module Grpc
+ ResolveAllRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveAllRequest").msgclass
+ ResolveAllResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveAllResponse").msgclass
+ AnyFlag = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.AnyFlag").msgclass
+ ResolveBooleanRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveBooleanRequest").msgclass
+ ResolveBooleanResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveBooleanResponse").msgclass
+ ResolveStringRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveStringRequest").msgclass
+ ResolveStringResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveStringResponse").msgclass
+ ResolveFloatRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveFloatRequest").msgclass
+ ResolveFloatResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveFloatResponse").msgclass
+ ResolveIntRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveIntRequest").msgclass
+ ResolveIntResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveIntResponse").msgclass
+ ResolveObjectRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveObjectRequest").msgclass
+ ResolveObjectResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.ResolveObjectResponse").msgclass
+ EventStreamResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.EventStreamResponse").msgclass
+ EventStreamRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("schema.v1.EventStreamRequest").msgclass
+ end
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_services_pb.rb b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_services_pb.rb
new file mode 100644
index 0000000..9fd23ac
--- /dev/null
+++ b/providers/openfeature-flagd-provider/lib/openfeature/flagd/provider/schema/v1/schema_services_pb.rb
@@ -0,0 +1,41 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# Source: schema/v1/schema.proto for package 'OpenFeature.FlagD.Provider.Grpc'
+
+require "grpc"
+require_relative "schema_pb"
+
+module OpenFeature
+ module FlagD
+ module Provider
+ module Grpc
+ module Service
+ # Service defines the exposed rpcs of flagd
+ class Service
+ include ::GRPC::GenericService
+
+ self.marshal_class_method = :encode
+ self.unmarshal_class_method = :decode
+ self.service_name = "schema.v1.Service"
+
+ rpc :ResolveAll, ::OpenFeature::FlagD::Provider::Grpc::ResolveAllRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveAllResponse
+ rpc :ResolveBoolean, ::OpenFeature::FlagD::Provider::Grpc::ResolveBooleanRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveBooleanResponse
+ rpc :ResolveString, ::OpenFeature::FlagD::Provider::Grpc::ResolveStringRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveStringResponse
+ rpc :ResolveFloat, ::OpenFeature::FlagD::Provider::Grpc::ResolveFloatRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveFloatResponse
+ rpc :ResolveInt, ::OpenFeature::FlagD::Provider::Grpc::ResolveIntRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveIntResponse
+ rpc :ResolveObject, ::OpenFeature::FlagD::Provider::Grpc::ResolveObjectRequest,
+ ::OpenFeature::FlagD::Provider::Grpc::ResolveObjectResponse
+ rpc :EventStream, ::OpenFeature::FlagD::Provider::Grpc::EventStreamRequest,
+ stream(::OpenFeature::FlagD::Provider::Grpc::EventStreamResponse)
+ end
+
+ Stub = Service.rpc_stub_class
+ end
+ end
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec b/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec
new file mode 100644
index 0000000..70182ae
--- /dev/null
+++ b/providers/openfeature-flagd-provider/openfeature-flagd-provider.gemspec
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+Gem::Specification.new do |spec|
+ spec.name = "openfeature-flagd-provider"
+ spec.version = "0.0.1"
+ spec.authors = ["OpenFeature Authors"]
+ spec.email = ["cncf-openfeature-contributors@lists.cncf.io"]
+
+ spec.summary = "The FlagD provider for the OpenFeature Ruby SDK"
+ spec.description = "The FlagD provider for the OpenFeature Ruby SDK"
+ spec.homepage = "https://github.com/open-feature/ruby-sdk-contrib/providers/openfeature-flagd-provider"
+ spec.license = "Apache-2.0"
+ spec.required_ruby_version = ">= 3.1"
+
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/providers/openfeature-flagd-provider"
+ spec.metadata["changelog_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/blob/main/CHANGELOG.md"
+ spec.metadata["bug_tracker_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/issues"
+ spec.metadata["documentation_uri"] = "https://github.com/open-feature/ruby-sdk-contrib/README.md"
+
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_runtime_dependency "grpc", "~> 1.50"
+
+ spec.add_development_dependency "rake", "~> 13.0"
+ spec.add_development_dependency "rspec", "~> 3.12.0"
+ spec.add_development_dependency "rubocop", "~> 1.37.1"
+end
diff --git a/providers/openfeature-flagd-provider/schemas b/providers/openfeature-flagd-provider/schemas
new file mode 160000
index 0000000..890ef64
--- /dev/null
+++ b/providers/openfeature-flagd-provider/schemas
@@ -0,0 +1 @@
+Subproject commit 890ef64cbcf57b5175e32af5a216c9d227cff22d
diff --git a/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider/client_spec.rb b/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider/client_spec.rb
new file mode 100644
index 0000000..fa09bbf
--- /dev/null
+++ b/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider/client_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+# https://docs.openfeature.dev/docs/specification/sections/providers
+RSpec.describe OpenFeature::FlagD::Provider::Client do
+ subject(:client) { described_class.new }
+
+ context "https://docs.openfeature.dev/docs/specification/sections/providers#requirement-211" do
+ it do
+ expect(client).to respond_to(:metadata)
+ expect(client.metadata).to respond_to(:name)
+ expect(client.metadata.name).to eq("Flagd Provider")
+ end
+ end
+
+ context "https://docs.openfeature.dev/docs/specification/sections/providers#requirement-221" 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)
+ end
+ end
+
+ context "https://docs.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(
+ value: nil,
+ variant: nil,
+ reason: "ERROR",
+ error_code: "FLAG_NOT_FOUND"
+ )
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider_spec.rb b/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider_spec.rb
new file mode 100644
index 0000000..cdd99a3
--- /dev/null
+++ b/providers/openfeature-flagd-provider/spec/openfeature/flagd/provider_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+# https://docs.openfeature.dev/docs/specification/sections/providers
+
+RSpec.describe OpenFeature::FlagD::Provider do
+ context "#configure" do
+ context "when defining host, port and tls options of gRPC service it wishes to access with configure method" do
+ subject(:explicit_configuration) do
+ described_class.configure do |config|
+ config.host = explicit_host
+ config.port = explicit_port
+ config.tls = explicit_tls
+ end
+ end
+
+ let(:explicit_host) { "explicit_host" }
+ let(:explicit_port) { 8013 }
+ let(:explicit_tls) { false }
+
+ it "expects configuration to be values set from configure method" do
+ explicit_configuration
+ expect(described_class.configuration.host).to eq(explicit_host)
+ expect(described_class.configuration.port).to eq(explicit_port)
+ expect(described_class.configuration.tls).to eq(explicit_tls)
+ end
+
+ context "when defining environment variables" do
+ before do
+ ENV["FLAGD_HOST"] = "172.16.1.2"
+ ENV["FLAGD_PORT"] = "8014"
+ ENV["FLAGD_TLS"] = "true"
+ end
+
+ it "uses the explicit configuration" do
+ explicit_configuration
+ expect(described_class.configuration.host).to eq(explicit_host)
+ expect(described_class.configuration.port).to eq(explicit_port)
+ expect(described_class.configuration.tls).to eq(explicit_tls)
+ end
+
+ after do
+ ENV["FLAGD_HOST"] = nil
+ ENV["FLAGD_PORT"] = nil
+ ENV["FLAGD_TLS"] = nil
+ end
+ end
+ end
+
+ context "when defining environment variables" do
+ subject(:env_configuration) do
+ ENV["FLAGD_HOST"] = env_host
+ ENV["FLAGD_PORT"] = env_port
+ ENV["FLAGD_TLS"] = env_tls
+ described_class.configuration
+ end
+ let(:env_host) { "172.16.1.2" }
+ let(:env_port) { "8014" }
+ let(:env_tls) { "true" }
+
+ skip "uses environment variables when no explicit configuration" do
+ env_configuration
+ expect(env_configuration.host).to eq(env_host)
+ expect(env_configuration.port).to eq(env_port)
+ expect(env_configuration.tls).to eq(env_tls == "true")
+ end
+ end
+ end
+
+ # https://docs.openfeature.dev/docs/specification/sections/providers#requirement-211
+ context "#metadata" do
+ it "metadata name is defined" do
+ expect(described_class).to respond_to(:metadata)
+ expect(described_class.metadata).to respond_to(:name)
+ expect(described_class.metadata.name).to eq("Flagd Provider")
+ end
+ end
+end
diff --git a/providers/openfeature-flagd-provider/spec/spec_helper.rb b/providers/openfeature-flagd-provider/spec/spec_helper.rb
new file mode 100644
index 0000000..091bfdd
--- /dev/null
+++ b/providers/openfeature-flagd-provider/spec/spec_helper.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "openfeature/flagd/provider"
+
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+ # The settings below are suggested to provide a good initial experience
+ # with RSpec, but feel free to customize to your heart's content.
+ # # This allows you to limit a spec run to individual examples or groups
+ # # you care about by tagging them with `:focus` metadata. When nothing
+ # # is tagged with `:focus`, all examples get run. RSpec also provides
+ # # aliases for `it`, `describe`, and `context` that include `:focus`
+ # # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ # config.filter_run_when_matching :focus
+ #
+ # # Allows RSpec to persist some state between runs in order to support
+ # # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # # you configure your source control system to ignore this file.
+ # config.example_status_persistence_file_path = "spec/examples.txt"
+ #
+ # # Limits the available syntax to the non-monkey patched syntax that is
+ # # recommended. For more details, see:
+ # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode
+ # config.disable_monkey_patching!
+ #
+ # # This setting enables warnings. It's recommended, but in some cases may
+ # # be too noisy due to issues in dependencies.
+ # config.warnings = true
+ #
+ # # Many RSpec users commonly either run the entire suite or an individual
+ # # file, and it's useful to allow more verbose output when running an
+ # # individual spec file.
+ # if config.files_to_run.one?
+ # # Use the documentation formatter for detailed output,
+ # # unless a formatter has already been configured
+ # # (e.g. via a command-line flag).
+ # config.default_formatter = "doc"
+ # end
+ #
+ # # Print the 10 slowest examples and example groups at the
+ # # end of the spec run, to help surface which specs are running
+ # # particularly slow.
+ # config.profile_examples = 10
+ #
+ # # Run specs in random order to surface order dependencies. If you find an
+ # # order dependency and want to debug it, you can fix the order by providing
+ # # the seed, which is printed after each run.
+ # # --seed 1234
+ # config.order = :random
+ #
+ # # Seed global randomization in this process using the `--seed` CLI option.
+ # # Setting this allows you to use `--seed` to deterministically reproduce
+ # # test failures related to randomization by passing the same `--seed` value
+ # # as the one that triggered the failure.
+ # Kernel.srand config.seed
+end