From 96a17aa271fc3cf0c63d76b485a284dc89cfc727 Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Tue, 29 Jul 2025 14:47:46 -0400 Subject: [PATCH 1/3] feat: support cloud eval reasons --- Gemfile | 1 + lib/devcycle-ruby-server-sdk/api/client.rb | 10 ++++- lib/devcycle-ruby-server-sdk/eval_reasons.rb | 3 ++ .../models/variable.rb | 3 +- spec/api/devcycle_api_spec.rb | 43 +++++++++++++++++++ spec/spec_helper.rb | 4 ++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index b356cad..5bb001b 100644 --- a/Gemfile +++ b/Gemfile @@ -15,4 +15,5 @@ group :development, :test do gem 'rake', '~> 13.0.1' gem 'pry-byebug' gem 'rubocop', '~> 1.57.1' + gem 'webmock' end diff --git a/lib/devcycle-ruby-server-sdk/api/client.rb b/lib/devcycle-ruby-server-sdk/api/client.rb index 5261730..1b48396 100644 --- a/lib/devcycle-ruby-server-sdk/api/client.rb +++ b/lib/devcycle-ruby-server-sdk/api/client.rb @@ -325,14 +325,20 @@ def variable_with_http_info(key, user, default, opts = {}) if @api_client.config.debugging @api_client.config.logger.debug "API called: DevCycle::Client#variable\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" end + if default && data.type && data.type.to_s != default.class.name + eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::TYPE_MISMATCH } + return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) + end return data rescue ApiError => error + eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::MISSING_VARIABLE } if error.code != 404 @api_client.config.logger.error("Failed to retrieve variable value: #{error.message}") + eval[:details] = DevCycle::DEFAULT_REASON_DETAILS::ERROR end - return Variable.new(key: key, value: default, isDefaulted: true) - end + return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) + end end # Get all variables by key for user data diff --git a/lib/devcycle-ruby-server-sdk/eval_reasons.rb b/lib/devcycle-ruby-server-sdk/eval_reasons.rb index 01f58de..030e891 100644 --- a/lib/devcycle-ruby-server-sdk/eval_reasons.rb +++ b/lib/devcycle-ruby-server-sdk/eval_reasons.rb @@ -9,5 +9,8 @@ module EVAL_REASONS module DEFAULT_REASON_DETAILS MISSING_CONFIG = 'Missing Config' USER_NOT_TARGETED = 'User Not Targeted' + TYPE_MISMATCH = 'Variable Type Mismatch' + MISSING_VARIABLE = 'Missing Variable' + ERROR = 'Error' end end diff --git a/lib/devcycle-ruby-server-sdk/models/variable.rb b/lib/devcycle-ruby-server-sdk/models/variable.rb index 675c3c8..359780b 100644 --- a/lib/devcycle-ruby-server-sdk/models/variable.rb +++ b/lib/devcycle-ruby-server-sdk/models/variable.rb @@ -78,7 +78,8 @@ def self.openapi_types :'type' => :'String', :'value' => :'Object', :'defaultValue' => :'Object', - :'isDefaulted' => :'Boolean' + :'isDefaulted' => :'Boolean', + :'eval' => :'Object' } end diff --git a/spec/api/devcycle_api_spec.rb b/spec/api/devcycle_api_spec.rb index 437c210..1a8c45b 100644 --- a/spec/api/devcycle_api_spec.rb +++ b/spec/api/devcycle_api_spec.rb @@ -12,6 +12,9 @@ require 'spec_helper' require 'json' +require 'webmock' + +include WebMock::API # Unit tests for DevCycle::Client # Automatically generated by openapi-generator (https://openapi-generator.tech) @@ -68,6 +71,8 @@ it 'should work' do result = @api_instance.variable(@user, "ruby-example-tests-default", false) expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Missing Variable" result = @api_instance.variable_value(@user, "ruby-example-tests-default", true) expect(result).to eq true @@ -88,6 +93,11 @@ result = @api_instance.variable_value(@user, "test", true) expect(result).to eq true + + result = @api_instance.variable(@user, "test", "false") + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Variable Type Mismatch" end end @@ -104,4 +114,37 @@ end end + describe 'get_variable_by_key test' do + before do + WebMock.disable_net_connect!(allow_localhost: true) + end + + after do + WebMock.allow_net_connect! + end + + it 'should work with mocked response' do + stub_request(:post, "https://bucketing-api.devcycle.com/v1/variables/mocked_variable"). + to_return(status: 200, body: "{\"isDefaulted\": false, \"value\": true, \"eval\": {\"reason\": \"SPLIT\", \"details\": \"Random Distribution | All Users\", \"target_id\": \"621642332ea68943c8833c4d\"}}", headers: {}) + + result = @api_instance.variable(@user, "mocked_variable", false) + expect(result.isDefaulted).to eq false + expect(result.value).to eq true + # Use Hash syntax since eval is deserialized as a Hash + expect(result.eval[:reason]).to eq "SPLIT" + expect(result.eval[:details]).to eq "Random Distribution | All Users" + expect(result.eval[:target_id]).to eq "621642332ea68943c8833c4d" + end + + it 'should return error details' do + stub_request(:post, "https://bucketing-api.devcycle.com/v1/variables/test"). + to_return(status: 500, body: "{\"isDefaulted\": true, \"value\": false, \"eval\": {\"reason\": \"DEFAULT\", \"details\": \"Error\"}}", headers: {}) + + result = @api_instance.variable(@user, "test", false) + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Error" + end + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c57463b..60942ed 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,6 +12,10 @@ # load the gem require 'devcycle-ruby-server-sdk' +require 'webmock/rspec' + +# Configure WebMock to allow real HTTP requests by default, but enable it for specific tests +WebMock.allow_net_connect! # The following was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. From 6866b51801c0419e649b9ba755463892adcb563f Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Tue, 29 Jul 2025 15:22:59 -0400 Subject: [PATCH 2/3] Update lib/devcycle-ruby-server-sdk/api/client.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/devcycle-ruby-server-sdk/api/client.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/devcycle-ruby-server-sdk/api/client.rb b/lib/devcycle-ruby-server-sdk/api/client.rb index 1b48396..5664705 100644 --- a/lib/devcycle-ruby-server-sdk/api/client.rb +++ b/lib/devcycle-ruby-server-sdk/api/client.rb @@ -331,10 +331,11 @@ def variable_with_http_info(key, user, default, opts = {}) end return data rescue ApiError => error - eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::MISSING_VARIABLE } - if error.code != 404 + if error.code == 404 + eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::MISSING_VARIABLE } + else @api_client.config.logger.error("Failed to retrieve variable value: #{error.message}") - eval[:details] = DevCycle::DEFAULT_REASON_DETAILS::ERROR + eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::ERROR } end return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) From 15eb7fce38312a9602bb9bd45908af9edff4bbac Mon Sep 17 00:00:00 2001 From: Jason Salaber Date: Tue, 29 Jul 2025 15:23:05 -0400 Subject: [PATCH 3/3] Update lib/devcycle-ruby-server-sdk/api/client.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/devcycle-ruby-server-sdk/api/client.rb | 20 +++++++---- spec/api/devcycle_api_spec.rb | 42 +++++++++++++++++++++- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/devcycle-ruby-server-sdk/api/client.rb b/lib/devcycle-ruby-server-sdk/api/client.rb index 5664705..73bf626 100644 --- a/lib/devcycle-ruby-server-sdk/api/client.rb +++ b/lib/devcycle-ruby-server-sdk/api/client.rb @@ -325,20 +325,26 @@ def variable_with_http_info(key, user, default, opts = {}) if @api_client.config.debugging @api_client.config.logger.debug "API called: DevCycle::Client#variable\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" end - if default && data.type && data.type.to_s != default.class.name - eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::TYPE_MISMATCH } - return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) + if default && data.type + api_type_to_ruby_class = { + 'Boolean' => [TrueClass, FalseClass], + 'Number' => [Integer, Float], + 'String' => [String], + 'JSON' => [Hash] + } + ruby_classes = api_type_to_ruby_class[data.type.to_s] + unless ruby_classes && ruby_classes.any? { |klass| default.is_a?(klass) } + eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::TYPE_MISMATCH } + return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) + end end return data rescue ApiError => error if error.code == 404 - eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::MISSING_VARIABLE } - else @api_client.config.logger.error("Failed to retrieve variable value: #{error.message}") - eval = { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::ERROR } end - return Variable.new(key: key, value: default, isDefaulted: true, eval: eval) + return Variable.new(key: key, value: default, isDefaulted: true, eval: { reason: DevCycle::EVAL_REASONS::DEFAULT, details: DevCycle::DEFAULT_REASON_DETAILS::ERROR }) end end diff --git a/spec/api/devcycle_api_spec.rb b/spec/api/devcycle_api_spec.rb index 1a8c45b..f18f139 100644 --- a/spec/api/devcycle_api_spec.rb +++ b/spec/api/devcycle_api_spec.rb @@ -72,7 +72,7 @@ result = @api_instance.variable(@user, "ruby-example-tests-default", false) expect(result.isDefaulted).to eq true expect(result.eval[:reason]).to eq "DEFAULT" - expect(result.eval[:details]).to eq "Missing Variable" + expect(result.eval[:details]).to eq "Error" result = @api_instance.variable_value(@user, "ruby-example-tests-default", true) expect(result).to eq true @@ -94,10 +94,50 @@ result = @api_instance.variable_value(@user, "test", true) expect(result).to eq true + result = @api_instance.variable(@user, "test-number-variable", 0) + expect(result.isDefaulted).to eq false + expect(result.value).to eq 123 + + result = @api_instance.variable(@user, "test-number-variable", 0) + expect(result.isDefaulted).to eq false + expect(result.value).to eq 123 + + result = @api_instance.variable(@user, "test-float-variable", 0.0) + expect(result.isDefaulted).to eq false + expect(result.value).to eq 4.56 + + result = @api_instance.variable(@user, "test-string-variable", "") + expect(result.isDefaulted).to eq false + expect(result.value).to eq "on" + + result = @api_instance.variable(@user, "test-json-variable", {}) + expect(result.isDefaulted).to eq false + expect(result.value).to eq({:message => "a"}) + result = @api_instance.variable(@user, "test", "false") expect(result.isDefaulted).to eq true expect(result.eval[:reason]).to eq "DEFAULT" expect(result.eval[:details]).to eq "Variable Type Mismatch" + + result = @api_instance.variable(@user, "test-number-variable", "123") + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Variable Type Mismatch" + + result = @api_instance.variable(@user, "test-number-variable", true) + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Variable Type Mismatch" + + result = @api_instance.variable(@user, "test-string-variable", true) + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Variable Type Mismatch" + + result = @api_instance.variable(@user, "test-json-variable", "on") + expect(result.isDefaulted).to eq true + expect(result.eval[:reason]).to eq "DEFAULT" + expect(result.eval[:details]).to eq "Variable Type Mismatch" end end