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