Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ permissions:
contents: read

jobs:
test:
unit-tests:
name: Unit Tests (Ruby ${{ matrix.ruby-version }})

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby-version: [ '3.2.0', '3.3.0', '3.4.1' ]
ruby-version: [ '3.4', '4.0' ]
env:
DEVCYCLE_SERVER_SDK_KEY: dvc_server_token_hash
steps:
Expand All @@ -27,3 +29,15 @@ jobs:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Run tests
run: bundle exec rake

unit-tests-required:
name: Unit Tests
if: always()
needs: unit-tests
runs-on: ubuntu-latest
steps:
- name: Verify all Ruby versions passed
if: needs.unit-tests.result != 'success'
run: exit 1
- name: Report success
run: echo "All Ruby version tests passed"
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ gem 'wasmtime'
gem 'concurrent-ruby'
gem 'google-protobuf'
gem 'ld-eventsource'
gem "openfeature-sdk", "~> 0.4.0"
gem "openfeature-sdk", "~> 0.6.5"

group :development, :test do
gem 'sorbet'
Expand Down
4 changes: 2 additions & 2 deletions devcycle-ruby-server-sdk.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Gem::Specification.new do |s|
s.summary = "DevCycle Bucketing API Ruby Gem"
s.description = "DevCycle Ruby Server SDK, for interacting with feature flags created with the DevCycle platform."
s.license = "MIT"
s.required_ruby_version = ">= 3.2"
s.required_ruby_version = ">= 3.4"

s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
s.add_runtime_dependency 'wasmtime', '39.0.1'
Expand All @@ -30,7 +30,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'oj', '~> 3.0'
s.add_runtime_dependency 'google-protobuf', '>= 3.22', '< 5.0'
s.add_runtime_dependency 'ld-eventsource', '~> 2.2.3'
s.add_runtime_dependency 'openfeature-sdk', '~> 0.4.1'
s.add_runtime_dependency 'openfeature-sdk', '~> 0.6.5'

s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'

Expand Down
70 changes: 55 additions & 15 deletions lib/devcycle-ruby-server-sdk/api/dev_cycle_provider.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'open_feature/sdk'

module DevCycle
class Provider
attr_reader :client
Expand All @@ -20,36 +22,27 @@ def shutdown

def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
# Retrieve a boolean value from provider source
@client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)
resolve(flag_key, default_value, evaluation_context)
end

def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
@client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)
resolve(flag_key, default_value, evaluation_context)
end

def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
@client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)
resolve(flag_key, default_value, evaluation_context)
end

def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
variable = @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)

Variable.new(
key: variable.key,
type: variable.type,
value: variable.value.to_i,
defaultValue: variable.defaultValue,
isDefaulted: variable.isDefaulted,
eval: variable.eval
)
resolve(flag_key, default_value, evaluation_context) { |value| value.to_i }
end

def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
@client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)
resolve(flag_key, default_value, evaluation_context)
end

def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
@client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value)
resolve(flag_key, default_value, evaluation_context)
end

def self.user_from_openfeature_context(context)
Expand Down Expand Up @@ -133,5 +126,52 @@ def self.user_from_openfeature_context(context)
args.merge!(privateCustomData: privateCustomData)
User.new(**args)
end

# Maps a DevCycle evaluation reason onto an OpenFeature reason.
def self.openfeature_reason(variable)
reason = eval_field(variable, :reason)
return reason if reason.is_a?(String) && !reason.empty?

if variable.isDefaulted
OpenFeature::SDK::Provider::Reason::DEFAULT
else
OpenFeature::SDK::Provider::Reason::TARGETING_MATCH
end
end

# Surfaces DevCycle's evaluation details as OpenFeature flag metadata.
def self.flag_metadata(variable)
metadata = {}
details = eval_field(variable, :details)
target_id = eval_field(variable, :target_id)
metadata[:details] = details unless details.nil?
metadata[:target_id] = target_id unless target_id.nil?
metadata
end

def self.eval_field(variable, key)
eval_details = variable.eval
return nil unless eval_details.is_a?(Hash)

eval_details[key] || eval_details[key.to_s]
end

private

# Evaluates a variable and wraps the result in the ResolutionDetails
# structure that the OpenFeature SDK expects back from a provider.
def resolve(flag_key, default_value, evaluation_context)
user = Provider.user_from_openfeature_context(evaluation_context)
variable = @client.variable(user, flag_key, default_value)

value = variable.value
value = yield(value) if block_given?

OpenFeature::SDK::Provider::ResolutionDetails.new(
value: value,
reason: Provider.openfeature_reason(variable),
flag_metadata: Provider.flag_metadata(variable)
)
end
end
end
Loading