├── examples ├── local │ ├── log │ │ └── .keep │ ├── storage │ │ └── .keep │ ├── tmp │ │ └── .keep │ ├── lib │ │ ├── assets │ │ │ └── .keep │ │ └── tasks │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── robots.txt │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── .ruby-version │ ├── app │ │ ├── models │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── application_record.rb │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_controller.rb │ │ │ └── demo_controller.rb │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── jobs │ │ │ └── application_job.rb │ │ └── views │ │ │ ├── layouts │ │ │ └── application.html.erb │ │ │ └── demo │ │ │ └── index.html.erb │ ├── bin │ │ ├── rake │ │ ├── rails │ │ ├── setup │ │ └── bundle │ ├── config │ │ ├── initializers │ │ │ ├── devcycle.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── permissions_policy.rb │ │ │ ├── assets.rb │ │ │ ├── inflections.rb │ │ │ └── content_security_policy.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── routes.rb │ │ ├── credentials.yml.enc │ │ ├── database.yml │ │ ├── storage.yml │ │ ├── application.rb │ │ ├── puma.rb │ │ └── environments │ │ │ ├── test.rb │ │ │ ├── development.rb │ │ │ └── production.rb │ ├── config.ru │ ├── Rakefile │ ├── Dockerfile │ ├── db │ │ └── seeds.rb │ ├── README.md │ ├── .gitignore │ └── Gemfile └── cloud │ ├── Gemfile │ ├── README.md │ └── app.rb ├── .openapi-generator ├── VERSION └── FILES ├── .rspec ├── sorbet ├── config ├── tapioca │ ├── require.rb │ └── config.yml └── rbi │ ├── gems │ ├── stringio@3.0.4.rbi │ ├── wasmtime@5.0.0.rbi │ ├── jaro_winkler@1.5.4.rbi │ ├── unicode-display_width@1.5.0.rbi │ ├── rspec@3.12.0.rbi │ ├── netrc@0.11.0.rbi │ └── parallel@1.22.1.rbi │ ├── todo.rbi │ └── annotations │ └── rainbow.rbi ├── lib ├── devcycle-ruby-server-sdk │ ├── version.rb │ ├── localbucketing │ │ ├── bucketing-lib.release.wasm │ │ ├── update_wasm.sh │ │ ├── event_types.rb │ │ ├── events_payload.rb │ │ ├── bucketed_user_config.rb │ │ ├── platform_data.rb │ │ ├── proto │ │ │ ├── helpers.rb │ │ │ ├── variableForUserParams.proto │ │ │ └── variableForUserParams_pb.rb │ │ ├── event_queue.rb │ │ ├── options.rb │ │ └── config_manager.rb │ ├── eval_reasons.rb │ ├── models │ │ ├── eval_hook_context.rb │ │ ├── eval_hook.rb │ │ ├── inline_response201.rb │ │ ├── user_data_and_events_body.rb │ │ ├── error_response.rb │ │ └── event.rb │ ├── api_error.rb │ ├── eval_hooks_runner.rb │ ├── api │ │ └── dev_cycle_provider.rb │ └── configuration.rb └── devcycle-ruby-server-sdk.rb ├── benchmark ├── Gemfile └── benchmark.rb ├── Rakefile ├── .github ├── CODEOWNERS └── workflows │ ├── check-semantic.yml │ ├── test-harness.yml │ ├── unit-tests.yaml │ └── release.yml ├── Gemfile ├── docs ├── ErrorResponse.md ├── Variable.md ├── Feature.md ├── Event.md └── User.md ├── bin └── tapioca ├── .gitignore ├── README.md ├── .openapi-generator-ignore ├── LICENSE ├── spec ├── configuration_spec.rb ├── spec_helper.rb ├── api │ └── devcycle_api_spec.rb └── eval_hooks_spec.rb ├── devcycle-ruby-server-sdk.gemspec ├── git_push.sh ├── test_integration.rb └── .rubocop.yml /examples/local/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 5.3.0 -------------------------------------------------------------------------------- /examples/local/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /examples/local/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /examples/local/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sorbet/config: -------------------------------------------------------------------------------- 1 | --dir 2 | . 3 | --ignore=vendor/ 4 | -------------------------------------------------------------------------------- /examples/local/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/local/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../stylesheets .css 2 | -------------------------------------------------------------------------------- /examples/local/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/version.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | VERSION = '3.8.0' 3 | end 4 | -------------------------------------------------------------------------------- /benchmark/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'devcycle-ruby-server-sdk', path: "../" 4 | gem 'webmock' 5 | -------------------------------------------------------------------------------- /examples/local/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /examples/local/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /examples/local/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /examples/local/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /examples/local/config/initializers/devcycle.rb: -------------------------------------------------------------------------------- 1 | DevCycleClient = DevCycle::Client.new(ENV['DEVCYCLE_SERVER_SDK_KEY'], DevCycle::Options.new, true) 2 | -------------------------------------------------------------------------------- /sorbet/tapioca/require.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | # Add your extra requires here (`bin/tapioca require` can be used to bootstrap this list) 5 | -------------------------------------------------------------------------------- /examples/local/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCycleHQ/ruby-server-sdk/HEAD/lib/devcycle-ruby-server-sdk/localbucketing/bucketing-lib.release.wasm -------------------------------------------------------------------------------- /examples/local/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.logger = Logger.new(STDOUT) 6 | Rails.application.initialize! 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | task default: :spec 8 | rescue LoadError 9 | # no rspec available 10 | end 11 | -------------------------------------------------------------------------------- /examples/local/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | require 'bundler/setup' 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /examples/cloud/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rubocop', group: 'development' 4 | gem 'sinatra' 5 | gem 'oj' 6 | gem 'sinatra-contrib' 7 | gem 'thin' 8 | gem 'devcycle-ruby-server-sdk', path: "../../" 9 | 10 | -------------------------------------------------------------------------------- /examples/local/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /examples/local/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/update_wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BUCKETING_LIB_VERSION="1.41.0" 3 | WAT_DOWNLOAD=0 4 | rm bucketing-lib.release.wasm 5 | wget "https://unpkg.com/@devcycle/bucketing-assembly-script@$BUCKETING_LIB_VERSION/build/bucketing-lib.release.wasm" 6 | -------------------------------------------------------------------------------- /examples/local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.1.2 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential 4 | 5 | WORKDIR /app 6 | 7 | COPY Gemfile . 8 | COPY . /app 9 | 10 | RUN bundle install 11 | 12 | EXPOSE 3000 13 | 14 | CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] -------------------------------------------------------------------------------- /sorbet/tapioca/config.yml: -------------------------------------------------------------------------------- 1 | gem: 2 | # Add your `gem` command parameters here: 3 | # 4 | # exclude: 5 | # - gem_name 6 | # doc: true 7 | # workers: 5 8 | dsl: 9 | # Add your `dsl` command parameters here: 10 | # 11 | # exclude: 12 | # - SomeGeneratorName 13 | # workers: 5 14 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/event_types.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | EventTypes = { 3 | variable_evaluated: 'variableEvaluated', 4 | agg_variable_evaluated: 'aggVariableEvaluated', 5 | variable_defaulted: 'variableDefaulted', 6 | agg_variable_defaulted: 'aggVariableDefaulted' 7 | }.freeze 8 | end 9 | -------------------------------------------------------------------------------- /examples/local/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/stringio@3.0.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `stringio` gem. 5 | # Please instead update this file by running `bin/tapioca gem stringio`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/wasmtime@5.0.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `wasmtime` gem. 5 | # Please instead update this file by running `bin/tapioca gem wasmtime`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS file 2 | # This file defines code ownership for the repository 3 | # All PRs require approval from the engineering team 4 | 5 | 6 | 7 | # Global rule - require engineering approval for all files 8 | * @devcyclehq/engineering 9 | 10 | # CODEOWNERS file itself requires foundation team approval 11 | .github/CODEOWNERS @devcyclehq/foundation 12 | -------------------------------------------------------------------------------- /examples/local/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | 4 | # Defines the root path route ("/") 5 | root to: "demo#index" 6 | get "/track", to: "demo#track" 7 | get "/flush_events", to: "demo#flush_events" 8 | get "/variable", to: "demo#variable" 9 | end 10 | -------------------------------------------------------------------------------- /examples/local/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LocalBucketingExample 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application" %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/local/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | -------------------------------------------------------------------------------- /.github/workflows/check-semantic.yml: -------------------------------------------------------------------------------- 1 | name: 'Semantic PR' 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | permissions: 9 | pull-requests: read 10 | jobs: 11 | main: 12 | name: Semantic PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | gem 'sorbet-runtime', '0.5.11481' 5 | gem 'oj' 6 | gem 'wasmtime' 7 | gem 'concurrent-ruby' 8 | gem 'google-protobuf' 9 | gem 'ld-eventsource' 10 | gem "openfeature-sdk", "~> 0.4.0" 11 | 12 | group :development, :test do 13 | gem 'sorbet' 14 | gem 'tapioca', require: false 15 | gem 'rake', '~> 13.0.1' 16 | gem 'pry-byebug' 17 | gem 'rubocop', '~> 1.57.1' 18 | gem 'webmock' 19 | end 20 | -------------------------------------------------------------------------------- /examples/local/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /examples/local/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /examples/local/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | 6JKikXIEv82ukDHV8KVEgkyMQ4YDQXnFDxRU+/GjV5TbNo6dLVh7dyApxd6+AQw4CewuAuBUT8BGW17EJ4PZDCtCNlbnmnmWsv/P/V8Hayl4LkykrdJY5QKcB70juRUwKxAvpBW2vrhWn1iWHwv2kiVSLb6AQSo4DWHIJYGR7ziXXpaMK3NmKgsQBUhx/axYkGHcv5VdUONVSOxMz63NaM56woDQtzt21Oi5S5aU35s5bjKINC1b8gujVrel94+ReIGBveYXCg2VN4F4/9iGiRHiYGWEY7/QqZAVIasCyefWWhWJz9v9cLJL7llbr2+n2JDi2pWeESmcnadiNib5/+dkho419OTWmfl7YMjYRq3UOQao7T+p4izfT+BEhyfeWWCiYGUxgJ19KkuENYj+UQTYea+NqUJKKnf7--5mFvzSj+jOl1XB/L--GYswxlYL3IVE/SlGo2ruEg== -------------------------------------------------------------------------------- /sorbet/rbi/todo.rbi: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT MANUALLY 2 | # This is an autogenerated file for unresolved constants. 3 | # Please instead update this file by running `bin/tapioca todo`. 4 | 5 | # typed: false 6 | 7 | module DevCycle::Configuration::Rails; end 8 | module LocalBucketing::Wasmtime::Engine; end 9 | module LocalBucketing::Wasmtime::Linker; end 10 | module LocalBucketing::Wasmtime::Module; end 11 | module LocalBucketing::Wasmtime::Store; end 12 | module LocalBucketing::Wasmtime::WasiCtxBuilder; end 13 | -------------------------------------------------------------------------------- /docs/ErrorResponse.md: -------------------------------------------------------------------------------- 1 | # DevCycle::ErrorResponse 2 | 3 | ## Properties 4 | 5 | | Name | Type | Description | Notes | 6 | | ---- | ---- | ----------- | ----- | 7 | | **message** | **String** | Error message | | 8 | | **data** | **Object** | Additional error information detailing the error reasoning | [optional] | 9 | 10 | ## Example 11 | 12 | ```ruby 13 | require 'devcycle-ruby-server-sdk' 14 | 15 | instance = DevCycle::ErrorResponse.new( 16 | message: null, 17 | data: null 18 | ) 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/eval_reasons.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | # Evaluation reasons for successful evaluations 3 | module EVAL_REASONS 4 | DEFAULT = 'DEFAULT' 5 | ERROR = 'ERROR' 6 | end 7 | 8 | # Default reason details 9 | module DEFAULT_REASON_DETAILS 10 | MISSING_CONFIG = 'Missing Config' 11 | USER_NOT_TARGETED = 'User Not Targeted' 12 | TYPE_MISMATCH = 'Variable Type Mismatch' 13 | MISSING_VARIABLE = 'Missing Variable' 14 | ERROR = 'Error' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/cloud/README.md: -------------------------------------------------------------------------------- 1 | # DevCycle Ruby Cloud SDK Example App 2 | 3 | This is a test application demonstrating the use of the DevCycle Ruby SDK. It uses Sinatra as 4 | a web framework to define several routes which can be called to trigger SDK functionality. 5 | 6 | ## Installation 7 | Install the dependencies using bundler: 8 | `bundle install` 9 | 10 | ## Run 11 | Run the application using bundler: 12 | `bundle exec ruby app.rb ` 13 | 14 | A valid DevCycle SDK token must be provided. 15 | 16 | The server will be running on port 3000 17 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/events_payload.rb: -------------------------------------------------------------------------------- 1 | require 'sorbet-runtime' 2 | 3 | module DevCycle 4 | class EventsPayload 5 | attr_reader :records 6 | attr_reader :payloadId 7 | attr_reader :eventCount 8 | 9 | def initialize(records, payloadId, eventCount) 10 | @records = records 11 | @payloadId = payloadId 12 | @eventCount = eventCount 13 | end 14 | end 15 | 16 | class EventsRecord 17 | def initialize(user, events) 18 | @user = user 19 | @events = events 20 | end 21 | 22 | end 23 | end -------------------------------------------------------------------------------- /examples/local/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /examples/local/app/views/demo/index.html.erb: -------------------------------------------------------------------------------- 1 |

Ruby Local Bucketing Example

2 | 3 |

Variable:

4 |
bool-var: <% if @bool_var == true %> True <% else %> False <% end %>
5 |
string-var: <%= @string_var %>
6 |
integer-var: <%= @number_var %>
7 |
json-var:
<%= JSON.pretty_generate(@json_var) %>
8 |
non-existent-var: <%= @non_existent_var %>
9 | 10 |

All Variables:

11 |
<%= JSON.pretty_generate(@all_variables) %>
12 |

All Features:

13 |
<%= JSON.pretty_generate(@all_features) %>
14 | -------------------------------------------------------------------------------- /.github/workflows/test-harness.yml: -------------------------------------------------------------------------------- 1 | name: Run Test Harness 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | harness-tests: 9 | name: Harness Tests 10 | permissions: 11 | contents: read 12 | runs-on: 13 | labels: ubuntu-latest-4-core 14 | steps: 15 | - uses: DevCycleHQ/test-harness@main 16 | with: 17 | sdks-to-test: ruby 18 | sdk-github-sha: ${{github.event.pull_request.head.sha}} 19 | sdk-capabilities: '{ "Ruby": ["clientCustomData", "v2Config", "allVariables", "allFeatures", "evalReason", "eventsEvalReason", "variablesFeatureId"]}' 20 | -------------------------------------------------------------------------------- /docs/Variable.md: -------------------------------------------------------------------------------- 1 | # DevCycle::Variable 2 | 3 | ## Properties 4 | 5 | | Name | Type | Description | Notes | 6 | | ---- | ---- | ----------- | ----- | 7 | | **_id** | **String** | unique database id | | 8 | | **key** | **String** | Unique key by Project, can be used in the SDK / API to reference by 'key' rather than _id. | | 9 | | **type** | **String** | Variable type | | 10 | | **value** | **Object** | Variable value can be a string, number, boolean, or JSON | | 11 | 12 | ## Example 13 | 14 | ```ruby 15 | require 'devcycle-server-sdk' 16 | 17 | instance = DevCycle::Variable.new( 18 | _id: null, 19 | key: null, 20 | type: null, 21 | value: null 22 | ) 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/jaro_winkler@1.5.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `jaro_winkler` gem. 5 | # Please instead update this file by running `bin/tapioca gem jaro_winkler`. 6 | 7 | # source://jaro_winkler//lib/jaro_winkler/version.rb#3 8 | module JaroWinkler 9 | class << self 10 | def distance(*_arg0); end 11 | def jaro_distance(*_arg0); end 12 | end 13 | end 14 | 15 | class JaroWinkler::Error < ::RuntimeError; end 16 | class JaroWinkler::InvalidWeightError < ::JaroWinkler::Error; end 17 | 18 | # source://jaro_winkler//lib/jaro_winkler/version.rb#4 19 | JaroWinkler::VERSION = T.let(T.unsafe(nil), String) 20 | -------------------------------------------------------------------------------- /examples/local/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /examples/local/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /docs/Feature.md: -------------------------------------------------------------------------------- 1 | # DevCycle::Feature 2 | 3 | ## Properties 4 | 5 | | Name | Type | Description | Notes | 6 | | ---- | ---- | ----------- | ----- | 7 | | **_id** | **String** | unique database id | | 8 | | **key** | **String** | Unique key by Project, can be used in the SDK / API to reference by 'key' rather than _id. | | 9 | | **type** | **String** | Feature type | | 10 | | **_variation** | **String** | Bucketed feature variation | | 11 | | **eval_reason** | **String** | Evaluation reasoning | [optional] | 12 | 13 | ## Example 14 | 15 | ```ruby 16 | require 'devcycle-server-sdk' 17 | 18 | instance = DevCycle::Feature.new( 19 | _id: null, 20 | key: null, 21 | type: null, 22 | _variation: null, 23 | eval_reason: null 24 | ) 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Local Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby-version: [ '3.2.0', '3.3.0', '3.4.1' ] 19 | env: 20 | DEVCYCLE_SERVER_SDK_KEY: dvc_server_token_hash 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1.264.0 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 28 | - name: Run tests 29 | run: bundle exec rake 30 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/eval_hook_context.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | class HookContext 3 | # The key of the variable being evaluated 4 | attr_accessor :key 5 | 6 | # The user for whom the variable is being evaluated 7 | attr_accessor :user 8 | 9 | # The default value for the variable 10 | attr_accessor :default_value 11 | 12 | # Initializes the object 13 | # @param [String] key The key of the variable being evaluated 14 | # @param [DevCycle::User] user The user for whom the variable is being evaluated 15 | # @param [Object] default_value The default value for the variable 16 | def initialize(key:, user:, default_value:) 17 | @key = key 18 | @user = user 19 | @default_value = default_value 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/bucketed_user_config.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | class BucketedUserConfig 3 | attr_accessor :project 4 | attr_accessor :environment 5 | attr_accessor :features 6 | attr_accessor :feature_variation_map 7 | attr_accessor :variable_variation_map 8 | attr_accessor :variables 9 | attr_accessor :known_variable_keys 10 | 11 | def initialize(project, environment, features, feature_var_map, variable_var_map, variables, known_variable_keys) 12 | @project = project 13 | @environment = environment 14 | @features = features 15 | @feature_variation_map = feature_var_map 16 | @variable_variation_map = variable_var_map 17 | @variables = variables 18 | @known_variable_keys = known_variable_keys 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/local/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /docs/Event.md: -------------------------------------------------------------------------------- 1 | # DevCycle::Event 2 | 3 | ## Properties 4 | 5 | | Name | Type | Description | Notes | 6 | | ---- | ---- | ----------- | ----- | 7 | | **type** | **String** | Custom event type | | 8 | | **target** | **String** | Custom event target / subject of event. Contextual to event type | [optional] | 9 | | **date** | **Float** | Unix epoch time the event occurred according to client | [optional] | 10 | | **value** | **Float** | Value for numerical events. Contextual to event type | [optional] | 11 | | **meta_data** | **Object** | Extra JSON metadata for event. Contextual to event type | [optional] | 12 | 13 | ## Example 14 | 15 | ```ruby 16 | require 'devcycle-server-sdk' 17 | 18 | instance = DevCycle::Event.new( 19 | type: null, 20 | target: null, 21 | date: null, 22 | value: null, 23 | meta_data: null 24 | ) 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /bin/tapioca: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'tapioca' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("tapioca", "tapioca") 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by: https://openapi-generator.tech 2 | # 3 | 4 | *.gem 5 | *.rbc 6 | /.config 7 | /coverage/ 8 | /InstalledFiles 9 | /pkg/ 10 | /spec/reports/ 11 | /spec/examples.txt 12 | /test/tmp/ 13 | /test/version_tmp/ 14 | /tmp/ 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | 21 | ## Documentation cache and generated files: 22 | /.yardoc/ 23 | /_yardoc/ 24 | /doc/ 25 | /rdoc/ 26 | 27 | ## Environment normalization: 28 | /.bundle/ 29 | /vendor/bundle 30 | /lib/bundler/man/ 31 | 32 | /examples/sinatra/.bundle/ 33 | /examples/sinatra/vendor/bundle/ 34 | /examples/sinatra/config.h/ 35 | 36 | # for a library or gem, you might want to ignore these files since the code is 37 | # intended to run in multiple environments; otherwise, check them in: 38 | # Gemfile.lock 39 | # .ruby-version 40 | # .ruby-gemset 41 | 42 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 43 | .rvmrc 44 | .vscode 45 | .idea/* 46 | *.iml 47 | **/.DS_Store 48 | 49 | Gemfile.lock -------------------------------------------------------------------------------- /benchmark/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'webmock' 2 | require 'devcycle-ruby-server-sdk' 3 | require 'benchmark' 4 | 5 | include WebMock::API 6 | 7 | WebMock.enable! 8 | WebMock.disable_net_connect! 9 | 10 | stub_request(:get, 'https://config-cdn.devcycle.com/config/v2/server/dvc_server_token_hash.json'). 11 | to_return(headers: { 'Etag': 'test' }, body: File.new('../examples/local/local-bucketing-example/test_data/large_config.json'), status: 200) 12 | 13 | stub_request(:post, 'https://events.devcycle.com/v1/events/batch'). 14 | to_return(status: 201, body: '{}') 15 | 16 | dvc_client = DevCycle::Client.new('dvc_server_token_hash', DevCycle::Options.new, true) 17 | user = DevCycle::User.new({ user_id: 'test' }) 18 | 19 | n = 500 20 | Benchmark.bm do |benchmark| 21 | benchmark.report('Single Variable Evaluation') do 22 | dvc_client.variable(user, 'v-key-25', false) 23 | end 24 | 25 | benchmark.report("#{n} Variable Evaluations") do 26 | n.times do 27 | dvc_client.variable(user, 'v-key-25', false) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevCycle Ruby Server SDK 2 | 3 | Welcome to the the DevCycle Ruby SDK, initially generated via the [DevCycle Bucketing API](https://docs.devcycle.com/bucketing-api/#tag/devcycle). 4 | 5 | ## Installation 6 | 7 | Install the gem 8 | 9 | `gem install devcycle-ruby-server-sdk` 10 | 11 | 12 | ## Getting Started 13 | 14 | Please follow the [installation](#installation) procedure and then run the following code: 15 | 16 | ```ruby 17 | # Load the gem 18 | require 'devcycle-ruby-server-sdk' 19 | 20 | # Setup authorization 21 | devcycle_client = DevCycle::Client.new(ENV['DEVCYCLE_SERVER_SDK_KEY'], DevCycle::Options.new, true) 22 | user = DevCycle::User.new({ user_id: 'user_id_example' }) 23 | 24 | begin 25 | # Get all features for user data 26 | result = devcycle_client.all_features(user) 27 | p result 28 | rescue DevCycle::ApiError => e 29 | puts "Exception when calling DevCycle::Client->all_features: #{e}" 30 | end 31 | 32 | ``` 33 | 34 | ## Usage 35 | 36 | To find usage documentation, visit our [docs](https://docs.devcycle.com/docs/sdk/server-side-sdks/ruby#usage). -------------------------------------------------------------------------------- /.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .openapi-generator-ignore 3 | .rspec 4 | .rubocop.yml 5 | .travis.yml 6 | Gemfile 7 | README.md 8 | Rakefile 9 | devcycle-server-sdk.gemspec 10 | docs/Client.md 11 | docs/User.md 12 | docs/ErrorResponse.md 13 | docs/Event.md 14 | docs/Feature.md 15 | docs/InlineResponse201.md 16 | docs/User.md 17 | docs/UserDataAndEventsBody.md 18 | docs/Variable.md 19 | git_push.sh 20 | lib/devcycle-server-sdk.rb 21 | lib/devcycle-server-sdk/api/devcycle_api.rb 22 | lib/devcycle-server-sdk/api_client.rb 23 | lib/devcycle-server-sdk/api_error.rb 24 | lib/devcycle-server-sdk/configuration.rb 25 | lib/devcycle-server-sdk/models/error_response.rb 26 | lib/devcycle-server-sdk/models/event.rb 27 | lib/devcycle-server-sdk/models/feature.rb 28 | lib/devcycle-server-sdk/models/inline_response201.rb 29 | lib/devcycle-server-sdk/models/user.rb 30 | lib/devcycle-server-sdk/models/user_data_and_events_body.rb 31 | lib/devcycle-server-sdk/models/variable.rb 32 | lib/devcycle-server-sdk/version.rb 33 | spec/api/devcycle_api_spec.rb 34 | spec/api_client_spec.rb 35 | spec/configuration_spec.rb 36 | spec/spec_helper.rb 37 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/eval_hook.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | class EvalHook 3 | # Callback to be executed before evaluation 4 | attr_accessor :before 5 | 6 | # Callback to be executed after evaluation 7 | attr_accessor :after 8 | 9 | # Callback to be executed finally (always runs) 10 | attr_accessor :on_finally 11 | 12 | # Callback to be executed on error 13 | attr_accessor :error 14 | 15 | # Initializes the object with optional callback functions 16 | # @param [Hash] callbacks Callback functions in the form of hash 17 | # @option callbacks [Proc, nil] :before Callback to execute before evaluation 18 | # @option callbacks [Proc, nil] :after Callback to execute after evaluation 19 | # @option callbacks [Proc, nil] :on_finally Callback to execute finally (always runs) 20 | # @option callbacks [Proc, nil] :error Callback to execute on error 21 | def initialize(callbacks = {}) 22 | @before = callbacks[:before] 23 | @after = callbacks[:after] 24 | @on_finally = callbacks[:on_finally] 25 | @error = callbacks[:error] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Taplytics Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/local/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /examples/local/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/platform_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'socket' 4 | require 'oj' 5 | 6 | module DevCycle 7 | class PlatformData 8 | attr_accessor :deviceModel, :platformVersion, :sdkVersion, :sdkType, :platform, :hostname 9 | 10 | def initialize(sdk_type = nil, sdk_version = nil, platform_version = nil, device_model = nil, platform = nil, hostname = nil) 11 | @sdkType = sdk_type 12 | @sdkVersion = sdk_version 13 | @platformVersion = platform_version 14 | @deviceModel = device_model 15 | @platform = platform 16 | @hostname = hostname 17 | end 18 | 19 | def default 20 | @sdkType = 'server' 21 | @sdkVersion = VERSION 22 | @platformVersion = RUBY_VERSION 23 | @deviceModel = nil 24 | @platform = 'Ruby' 25 | @hostname = Socket.gethostname 26 | self 27 | end 28 | 29 | def to_hash 30 | { 31 | sdkType: @sdkType, 32 | sdkVersion: @sdkVersion, 33 | platformVersion: @platformVersion, 34 | deviceModel: @deviceModel, 35 | platform: @platform, 36 | hostname: @hostname 37 | } 38 | end 39 | 40 | def to_json 41 | Oj.dump(to_hash, mode: :json) 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /examples/local/README.md: -------------------------------------------------------------------------------- 1 | # Ruby Local Bucketing Example 2 | 3 | ## Setup 4 | 5 | You will need Ruby and Ruby on Rails to run this app 6 | 7 | ### Installing Ruby 8 | 9 | You can install a specific version of Ruby via Homebrew or use a ruby version manager like [rbenv](https://github.com/rbenv/rbenv) 10 | 11 | ### Installing Rails 12 | 13 | Once you have Ruby installed you'll be able to run `gem install rails` to install rails 14 | 15 | ## Running the app 16 | 17 | In the root directory run `bundle install` to install required dependencies. 18 | 19 | Run `DEVCYCLE_SERVER_SDK_KEY={sdk_key} bundle exec rails server` to start the rails server. The server should be running on `localhost:3000` 20 | 21 | ## Running With a Mocked Config 22 | 23 | The Rails app can also be run with a mocked config by setting the `MOCK_CONFIG` environment variable to `true`. The test config can be can be found in `test_data/large_config.json`. 24 | 25 | ## Benchmarking 26 | 27 | Start the app with a mocked config `MOCK_CONFIG=true bundle exec rails server`. 28 | 29 | You can now benchmark the variable evaluation time by making a request to the `/variable` endpoint using an HTTP load generator such as [hey](https://github.com/rakyll/hey): 30 | 31 | ```bash 32 | hey -n 500 -c 100 http://localhost:3000/variable 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/local/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /spec/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'spec_helper' 14 | 15 | describe DevCycle::Configuration do 16 | let(:config) { DevCycle::Configuration.default } 17 | 18 | before(:each) do 19 | # uncomment below to setup host and base_path 20 | # require 'URI' 21 | # uri = URI.parse("https://bucketing-api.devcycle.com") 22 | # DevCycle.configure do |c| 23 | # c.host = uri.host 24 | # c.base_path = uri.path 25 | # end 26 | end 27 | 28 | describe '#base_url' do 29 | it 'should have the default value' do 30 | # uncomment below to test default value of the base path 31 | # expect(config.base_url).to eq("https://bucketing-api.devcycle.com") 32 | end 33 | 34 | it 'should remove trailing slashes' do 35 | [nil, '', '/', '//'].each do |base_path| 36 | config.base_path = base_path 37 | # uncomment below to test trailing slashes 38 | # expect(config.base_url).to eq("https://bucketing-api.devcycle.com") 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /examples/local/app/controllers/demo_controller.rb: -------------------------------------------------------------------------------- 1 | class DemoController < ApplicationController 2 | def index 3 | user = DevCycle::User.new({ user_id: 'test', country: 'JP' }) 4 | @bool_var = DevCycleClient.variable_value(user, 'bool-var', false) 5 | @string_var = DevCycleClient.variable_value(user, 'string-var', 'default') 6 | @number_var = DevCycleClient.variable_value(user, 'number-var', 0) 7 | @json_var = DevCycleClient.variable_value(user, 'json-var-ruby-too', {}) 8 | 9 | @non_existent_var = DevCycleClient.variable_value(user, 'non-existent-variable', "I don't exist") 10 | 11 | @all_variables = DevCycleClient.all_variables(user) 12 | 13 | @all_features = DevCycleClient.all_features(user) 14 | end 15 | 16 | def track 17 | user = DevCycle::User.new({ 18 | user_id: 'test_' + rand(5).to_s, 19 | name: 'Mr. Test', 20 | email: 'mr_test@gmail.com', 21 | country: 'JP' 22 | }) 23 | event = DevCycle::Event.new({ :'type' => :'randomEval', :'target' => :'custom target' }) 24 | DevCycleClient.track(user, event) 25 | render json: "track called on DevCycle client" 26 | end 27 | 28 | def flush_events 29 | DevCycleClient.flush_events 30 | render json: "flush_events called on DevCycle client" 31 | end 32 | 33 | def variable 34 | user = DevCycle::User.new({ user_id: 'test' }) 35 | variable = DevCycleClient.variable(user, 'v-key-25', false) 36 | render json: variable 37 | end 38 | end -------------------------------------------------------------------------------- /examples/cloud/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require "sinatra/reloader" if development? 3 | require 'devcycle-ruby-server-sdk' 4 | require 'json' 5 | 6 | set :port, 3000 7 | 8 | sdk_key = ARGV[0] 9 | 10 | if !sdk_key 11 | fail Exception, 'Must provide server SDK token' 12 | end 13 | 14 | DevCycle.configure do |config| 15 | # Configure API key authorization: bearerAuth 16 | config.api_key['bearerAuth'] = sdk_key 17 | config.enable_edge_db = false 18 | end 19 | 20 | options = DevCycle::Options.new(enable_cloud_bucketing: true) 21 | api_instance = DevCycle::Client.new(sdk_key, options) 22 | user = DevCycle::User.new({ user_id: 'my-user' }) 23 | 24 | 25 | get '/' do 26 | variable = api_instance.variable(user, "bool-var", false) 27 | puts "bool-var variable value is: #{variable.value}" 28 | puts "\n" 29 | 30 | variable_value = api_instance.variable_value(user, "string-var", "string-var-default") 31 | puts "string-var variable value is: #{variable_value}" 32 | 33 | all_variables = api_instance.all_variables(user) 34 | puts "all_variables result is:\n#{JSON.pretty_generate(all_variables.to_hash)}" 35 | puts "\n" 36 | 37 | all_features = api_instance.all_features(user) 38 | puts "all_features result is:\n#{JSON.pretty_generate(all_features.to_hash)}" 39 | end 40 | 41 | get '/track_event' do 42 | user = DevCycle::User.new({ user_id: 'my-user' }) 43 | event_data = DevCycle::Event.new({ 44 | type: "my-event", 45 | target: "some_event_target", 46 | value: 12, 47 | metaData: { 48 | myKey: "my-value" 49 | } 50 | }) 51 | 52 | result = api_instance.track(user, event_data) 53 | end 54 | -------------------------------------------------------------------------------- /examples/local/.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /db/*.sqlite3 5 | /db/*.sqlite3-journal 6 | /db/*.sqlite3-[0-9]* 7 | /public/system 8 | /coverage/ 9 | /spec/tmp 10 | *.orig 11 | rerun.txt 12 | pickle-email-*.html 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # TODO Comment out this rule if you are OK with secrets being uploaded to the repo 21 | config/initializers/secret_token.rb 22 | config/master.key 23 | 24 | # Only include if you have production secrets in this file, which is no longer a Rails default 25 | # config/secrets.yml 26 | 27 | # dotenv, dotenv-rails 28 | # TODO Comment out these rules if environment variables can be committed 29 | .env 30 | .env*.local 31 | 32 | ## Environment normalization: 33 | /.bundle 34 | /vendor/bundle 35 | 36 | # these should all be checked in to normalize the environment: 37 | # Gemfile.lock, .ruby-version, .ruby-gemset 38 | 39 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 40 | .rvmrc 41 | 42 | # if using bower-rails ignore default bower_components path bower.json files 43 | /vendor/assets/bower_components 44 | *.bowerrc 45 | bower.json 46 | 47 | # Ignore pow environment settings 48 | .powenv 49 | 50 | # Ignore Byebug command history file. 51 | .byebug_history 52 | 53 | # Ignore node_modules 54 | node_modules/ 55 | 56 | # Ignore precompiled javascript packs 57 | /public/packs 58 | /public/packs-test 59 | /public/assets 60 | 61 | # Ignore yarn files 62 | /yarn-error.log 63 | yarn-debug.log* 64 | .yarn-integrity 65 | 66 | # Ignore uploaded files in development 67 | /storage/* 68 | !/storage/.keep 69 | /public/uploads -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/api_error.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | module DevCycle 14 | class ApiError < StandardError 15 | attr_reader :code, :response_headers, :response_body 16 | 17 | # Usage examples: 18 | # ApiError.new 19 | # ApiError.new("message") 20 | # ApiError.new(:code => 500, :response_headers => {}, :response_body => "") 21 | # ApiError.new(:code => 404, :message => "Not Found") 22 | def initialize(arg = nil) 23 | if arg.is_a? Hash 24 | if arg.key?(:message) || arg.key?('message') 25 | super(arg[:message] || arg['message']) 26 | else 27 | super arg 28 | end 29 | 30 | arg.each do |k, v| 31 | instance_variable_set "@#{k}", v 32 | end 33 | else 34 | super arg 35 | end 36 | end 37 | 38 | # Override to_s to display a friendly error message 39 | def to_s 40 | message 41 | end 42 | 43 | def message 44 | if @message.nil? 45 | msg = "Error message: the server returns an error" 46 | else 47 | msg = @message 48 | end 49 | 50 | msg += "\nHTTP status code: #{code}" if code 51 | msg += "\nResponse headers: #{response_headers}" if response_headers 52 | msg += "\nResponse body: #{response_body}" if response_body 53 | 54 | msg 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/proto/helpers.rb: -------------------------------------------------------------------------------- 1 | def create_nullable_string(val) 2 | if val.nil? || val.empty? 3 | Proto::NullableString.new(value: "", isNull: true) 4 | else 5 | Proto::NullableString.new(value: val, isNull: false) 6 | end 7 | end 8 | 9 | def create_nullable_double(val) 10 | if val.nil? 11 | Proto::NullableDouble.new(value: 0, isNull: true) 12 | else 13 | Proto::NullableDouble.new(value: val, isNull: false) 14 | end 15 | end 16 | 17 | def create_nullable_custom_data(data) 18 | data_map = {} 19 | if data.nil? || data.length == 0 20 | return Proto::NullableCustomData.new(value: data_map, isNull: true) 21 | end 22 | 23 | data.each do |key, value| 24 | if value.nil? 25 | data_map[key] = Proto::CustomDataValue.new(type: Proto::CustomDataType::Null) 26 | end 27 | 28 | if value.is_a? String 29 | data_map[key] = Proto::CustomDataValue.new(type: Proto::CustomDataType::Str, stringValue: value) 30 | elsif value.is_a?(Float) || value.is_a?(Integer) 31 | data_map[key] = Proto::CustomDataValue.new(type: Proto::CustomDataType::Num, doubleValue: value) 32 | elsif value.is_a?(TrueClass) || value.is_a?(FalseClass) 33 | data_map[key] = Proto::CustomDataValue.new(type: Proto::CustomDataType::Bool, boolValue: value) 34 | end 35 | end 36 | 37 | Proto::NullableCustomData.new(value: data_map, isNull: false) 38 | end 39 | 40 | def get_variable_value(variable_pb) 41 | case variable_pb.type 42 | when :Boolean 43 | variable_pb.boolValue 44 | when :Number 45 | variable_pb.doubleValue 46 | when :String 47 | variable_pb.stringValue 48 | when :JSON 49 | JSON.parse variable_pb.stringValue 50 | end 51 | end -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package proto; 3 | 4 | enum VariableType_PB { 5 | Boolean = 0; 6 | Number = 1; 7 | String = 2; 8 | JSON = 3; 9 | } 10 | 11 | message NullableString { 12 | string value = 1; 13 | bool isNull = 2; 14 | } 15 | 16 | message NullableDouble { 17 | double value = 1; 18 | bool isNull = 2; 19 | } 20 | 21 | enum CustomDataType { 22 | Bool = 0; 23 | Num = 1; 24 | Str = 2; 25 | Null = 3; 26 | } 27 | 28 | message CustomDataValue { 29 | CustomDataType type = 1; 30 | bool boolValue = 2; 31 | double doubleValue = 3; 32 | string stringValue = 4; 33 | } 34 | 35 | message NullableCustomData { 36 | map value = 1; 37 | bool isNull = 2; 38 | } 39 | 40 | message VariableForUserParams_PB { 41 | string sdkKey = 1; 42 | string variableKey = 2; 43 | VariableType_PB variableType = 3; 44 | DVCUser_PB user = 4; 45 | bool shouldTrackEvent = 5; 46 | } 47 | 48 | message DVCUser_PB { 49 | string user_id = 1; 50 | NullableString email = 2; 51 | NullableString name = 3; 52 | NullableString language = 4; 53 | NullableString country = 5; 54 | NullableDouble appBuild = 6; 55 | NullableString appVersion = 7; 56 | NullableString deviceModel = 8; 57 | NullableCustomData customData = 9; 58 | NullableCustomData privateCustomData = 10; 59 | } 60 | 61 | message SDKVariable_PB { 62 | string _id = 1; 63 | VariableType_PB type = 2; 64 | string key = 3; 65 | bool boolValue = 4; 66 | double doubleValue = 5; 67 | string stringValue = 6; 68 | NullableString evalReason = 7; 69 | NullableString _feature = 8; 70 | EvalReason_PB eval = 9; 71 | } 72 | 73 | message EvalReason_PB { 74 | string reason = 1; 75 | string details = 2; 76 | string target_id = 3; 77 | } -------------------------------------------------------------------------------- /devcycle-ruby-server-sdk.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | =begin 4 | #DevCycle Bucketing API 5 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 6 | The version of the OpenAPI document: 1.0.0 7 | Generated by: https://openapi-generator.tech 8 | OpenAPI Generator version: 5.3.0 9 | =end 10 | 11 | $:.push File.expand_path("../lib", __FILE__) 12 | require "devcycle-ruby-server-sdk/version" 13 | 14 | Gem::Specification.new do |s| 15 | s.name = "devcycle-ruby-server-sdk" 16 | s.version = DevCycle::VERSION 17 | s.platform = Gem::Platform::RUBY 18 | s.authors = ["DevCycleHQ"] 19 | s.email = ["support@devcycle.com"] 20 | s.homepage = "https://devcycle.com" 21 | s.summary = "DevCycle Bucketing API Ruby Gem" 22 | s.description = "DevCycle Ruby Server SDK, for interacting with feature flags created with the DevCycle platform." 23 | s.license = "MIT" 24 | s.required_ruby_version = ">= 3.2" 25 | 26 | s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1' 27 | s.add_runtime_dependency 'wasmtime', '39.0.1' 28 | s.add_runtime_dependency 'concurrent-ruby', '~> 1.2.0' 29 | s.add_runtime_dependency 'sorbet-runtime', '>= 0.5.11481' 30 | s.add_runtime_dependency 'oj', '~> 3.0' 31 | s.add_runtime_dependency 'google-protobuf', '~> 3.22' 32 | s.add_runtime_dependency 'ld-eventsource', '~> 2.2.3' 33 | s.add_runtime_dependency 'openfeature-sdk', '~> 0.4.1' 34 | 35 | s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0' 36 | 37 | s.files = Dir['README.md', 'LICENSE', 38 | 'lib/**/*', 39 | 'devcycle-ruby-server-sdk.gemspec', 40 | 'Gemfile'] 41 | s.test_files = `find spec/*`.split("\n") 42 | s.executables = [] 43 | s.require_paths = ["lib"] 44 | end 45 | -------------------------------------------------------------------------------- /examples/local/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | require "action_text/engine" 13 | require "action_view/railtie" 14 | # require "action_cable/engine" 15 | require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | require 'webmock' 22 | include WebMock::API 23 | 24 | if ENV['MOCK_CONFIG'] == 'true' 25 | ENV['DEVCYCLE_SERVER_SDK_KEY'] = 'dvc_server_token_hash' 26 | WebMock.enable! 27 | WebMock.disable_net_connect! 28 | 29 | config_path = File.expand_path('../test_data/large_config.json', __dir__) 30 | stub_request(:get, "https://config-cdn.devcycle.com/config/v2/server/#{ENV['DEVCYCLE_SERVER_SDK_KEY']}.json"). 31 | to_return(headers: { 'Etag': 'test' }, body: File.new(config_path).read, status: 200) 32 | 33 | stub_request(:post, 'https://events.devcycle.com/v1/events/batch'). 34 | to_return(status: 201, body: '{}') 35 | end 36 | 37 | module LocalBucketingExample 38 | class Application < Rails::Application 39 | # Initialize configuration defaults for originally generated Rails version. 40 | config.load_defaults 7.0 41 | 42 | # Configuration for the application, engines, and railties goes here. 43 | # 44 | # These settings can be overridden in specific environments using the files 45 | # in config/environments, which are processed later. 46 | # 47 | # config.time_zone = "Central Time (US & Canada)" 48 | # config.eager_load_paths << Rails.root.join("extras") 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /examples/local/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/unicode-display_width@1.5.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `unicode-display_width` gem. 5 | # Please instead update this file by running `bin/tapioca gem unicode-display_width`. 6 | 7 | # source://unicode-display_width//lib/unicode/display_width/no_string_ext.rb#1 8 | module Unicode; end 9 | 10 | # source://unicode-display_width//lib/unicode/display_width/no_string_ext.rb#2 11 | module Unicode::DisplayWidth 12 | class << self 13 | # source://unicode-display_width//lib/unicode/display_width.rb#26 14 | def emoji_extra_width_of(string, ambiguous = T.unsafe(nil), overwrite = T.unsafe(nil), _ = T.unsafe(nil)); end 15 | 16 | # source://unicode-display_width//lib/unicode/display_width.rb#8 17 | def of(string, ambiguous = T.unsafe(nil), overwrite = T.unsafe(nil), options = T.unsafe(nil)); end 18 | end 19 | end 20 | 21 | # source://unicode-display_width//lib/unicode/display_width/constants.rb#5 22 | Unicode::DisplayWidth::DATA_DIRECTORY = T.let(T.unsafe(nil), String) 23 | 24 | # source://unicode-display_width//lib/unicode/display_width.rb#6 25 | Unicode::DisplayWidth::DEPTHS = T.let(T.unsafe(nil), Array) 26 | 27 | # source://unicode-display_width//lib/unicode/display_width/index.rb#9 28 | Unicode::DisplayWidth::INDEX = T.let(T.unsafe(nil), Array) 29 | 30 | # source://unicode-display_width//lib/unicode/display_width/constants.rb#6 31 | Unicode::DisplayWidth::INDEX_FILENAME = T.let(T.unsafe(nil), String) 32 | 33 | # source://unicode-display_width//lib/unicode/display_width/no_string_ext.rb#3 34 | Unicode::DisplayWidth::NO_STRING_EXT = T.let(T.unsafe(nil), TrueClass) 35 | 36 | # source://unicode-display_width//lib/unicode/display_width/constants.rb#4 37 | Unicode::DisplayWidth::UNICODE_VERSION = T.let(T.unsafe(nil), String) 38 | 39 | # source://unicode-display_width//lib/unicode/display_width/constants.rb#3 40 | Unicode::DisplayWidth::VERSION = T.let(T.unsafe(nil), String) 41 | -------------------------------------------------------------------------------- /examples/local/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/local/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /examples/local/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | 34 | cluster_mode = ENV.fetch("CLUSTER_MODE") { 'false' } 35 | if cluster_mode == 'true' 36 | workers ENV.fetch("WEB_CONCURRENCY") { 2 } 37 | end 38 | 39 | # Use the `preload_app!` method when specifying a `workers` number. 40 | # This directive tells Puma to first boot the application and load code 41 | # before forking the application. This takes advantage of Copy On Write 42 | # process behavior so workers use less memory. 43 | # 44 | # preload_app! 45 | 46 | # Allow puma to be restarted by `bin/rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /test_integration.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Simple integration test for eval hooks functionality 4 | puts "Testing eval hooks integration..." 5 | 6 | # Load the eval hooks classes 7 | require_relative 'lib/devcycle-ruby-server-sdk/eval_hooks_runner' 8 | require_relative 'lib/devcycle-ruby-server-sdk/models/eval_hook' 9 | require_relative 'lib/devcycle-ruby-server-sdk/models/eval_hook_context' 10 | 11 | # Test the eval hooks classes work together 12 | puts "\n1. Testing EvalHooksRunner with EvalHook..." 13 | 14 | runner = DevCycle::EvalHooksRunner.new 15 | hook = DevCycle::EvalHook.new( 16 | before: ->(context) { 17 | puts " Before hook called with key: #{context.key}" 18 | context 19 | }, 20 | after: ->(context) { 21 | puts " After hook called with key: #{context.key}" 22 | }, 23 | error: ->(context, error) { 24 | puts " Error hook called with error: #{error.message}" 25 | }, 26 | on_finally: ->(context) { 27 | puts " Finally hook called with key: #{context.key}" 28 | } 29 | ) 30 | 31 | runner.add_hook(hook) 32 | 33 | context = DevCycle::HookContext.new( 34 | key: 'test-key', 35 | user: 'test-user', 36 | default_value: 'test-default' 37 | ) 38 | 39 | puts "Running hooks..." 40 | runner.run_before_hooks(context) 41 | runner.run_after_hooks(context) 42 | runner.run_finally_hooks(context) 43 | 44 | puts "\n2. Testing error handling..." 45 | test_error = StandardError.new('Test error') 46 | runner.run_error_hooks(context, test_error) 47 | 48 | puts "\n3. Testing multiple hooks..." 49 | runner.clear_hooks 50 | 51 | hook1 = DevCycle::EvalHook.new( 52 | before: ->(context) { 53 | puts " Hook1 before" 54 | context 55 | }, 56 | after: ->(context) { 57 | puts " Hook1 after" 58 | } 59 | ) 60 | 61 | hook2 = DevCycle::EvalHook.new( 62 | before: ->(context) { 63 | puts " Hook2 before" 64 | context 65 | }, 66 | after: ->(context) { 67 | puts " Hook2 after" 68 | } 69 | ) 70 | 71 | runner.add_hook(hook1) 72 | runner.add_hook(hook2) 73 | 74 | puts "Running multiple hooks..." 75 | runner.run_before_hooks(context) 76 | runner.run_after_hooks(context) 77 | 78 | puts "\nIntegration test completed successfully!" -------------------------------------------------------------------------------- /examples/local/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby ">= 3.1.2" 5 | 6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 7 | gem "rails", "~> 7.0.4", ">= 7.0.4.2" 8 | 9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 10 | gem "sprockets-rails" 11 | 12 | # Use sqlite3 as the database for Active Record 13 | gem "sqlite3", "~> 1.4" 14 | 15 | # Use the Puma web server [https://github.com/puma/puma] 16 | gem "puma", "~> 5.0" 17 | 18 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 19 | gem "jbuilder" 20 | 21 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 22 | # gem "kredis" 23 | 24 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 25 | # gem "bcrypt", "~> 3.1.7" 26 | 27 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 28 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 29 | 30 | # Reduces boot times through caching; required in config/boot.rb 31 | gem "bootsnap", require: false 32 | 33 | # Use Sass to process CSS 34 | # gem "sassc-rails" 35 | 36 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 37 | # gem "image_processing", "~> 1.2" 38 | 39 | gem "devcycle-ruby-server-sdk" 40 | 41 | group :development, :test do 42 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 43 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 44 | end 45 | 46 | group :development do 47 | # Use console on exceptions pages [https://github.com/rails/web-console] 48 | gem "web-console" 49 | gem "byebug" 50 | 51 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 52 | # gem "rack-mini-profiler" 53 | 54 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 55 | # gem "spring" 56 | end 57 | 58 | group :test do 59 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 60 | gem "capybara" 61 | gem "selenium-webdriver" 62 | gem "webdrivers" 63 | gem "webmock" 64 | end 65 | -------------------------------------------------------------------------------- /docs/User.md: -------------------------------------------------------------------------------- 1 | # DevCycle::User 2 | 3 | ## Properties 4 | 5 | | Name | Type | Description | Notes | 6 | | ---- | ---- | ----------- | ----- | 7 | | **user_id** | **String** | Unique id to identify the user | | 8 | | **email** | **String** | User's email used to identify the user on the dashboard / target audiences | [optional] | 9 | | **name** | **String** | User's name used to identify the user on the dashboard / target audiences | [optional] | 10 | | **language** | **String** | User's language in ISO 639-1 format | [optional] | 11 | | **country** | **String** | User's country in ISO 3166 alpha-2 format | [optional] | 12 | | **app_version** | **String** | App Version of the running application | [optional] | 13 | | **app_build** | **String** | App Build number of the running application | [optional] | 14 | | **custom_data** | **Object** | User's custom data to target the user with, data will be logged to DevCycle for use in dashboard. | [optional] | 15 | | **private_custom_data** | **Object** | User's custom data to target the user with, data will not be logged to DevCycle only used for feature bucketing. | [optional] | 16 | | **created_date** | **Float** | Date the user was created, Unix epoch timestamp format | [optional] | 17 | | **last_seen_date** | **Float** | Date the user was created, Unix epoch timestamp format | [optional] | 18 | | **platform** | **String** | Platform the Client SDK is running on | [optional] | 19 | | **platform_version** | **String** | Version of the platform the Client SDK is running on | [optional] | 20 | | **device_model** | **String** | User's device model | [optional] | 21 | | **sdk_type** | **String** | DevCycle SDK type | [optional] | 22 | | **sdk_version** | **String** | DevCycle SDK Version | [optional] | 23 | 24 | ## Example 25 | 26 | ```ruby 27 | require 'devcycle-ruby-server-sdk' 28 | 29 | instance = DevCycle::User.new( 30 | user_id: null, 31 | email: null, 32 | name: null, 33 | language: null, 34 | country: null, 35 | app_version: null, 36 | app_build: null, 37 | custom_data: null, 38 | private_custom_data: null, 39 | created_date: null, 40 | last_seen_date: null, 41 | platform: null, 42 | platform_version: null, 43 | device_model: null, 44 | sdk_type: null, 45 | sdk_version: null 46 | ) 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /examples/local/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Store uploaded files on the local file system in a temporary directory. 37 | config.active_storage.service = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | 42 | # Raise exceptions for disallowed deprecations. 43 | config.active_support.disallowed_deprecation = :raise 44 | 45 | # Tell Active Support which deprecation messages to disallow. 46 | config.active_support.disallowed_deprecation_warnings = [] 47 | 48 | # Raises error for missing translations. 49 | # config.i18n.raise_on_missing_translations = true 50 | 51 | # Annotate rendered view with file names. 52 | # config.action_view.annotate_rendered_view_with_filenames = true 53 | end 54 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | # Common files 14 | require 'devcycle-ruby-server-sdk/api_client' 15 | require 'devcycle-ruby-server-sdk/api_error' 16 | require 'devcycle-ruby-server-sdk/version' 17 | require 'devcycle-ruby-server-sdk/configuration' 18 | 19 | # Models 20 | require 'devcycle-ruby-server-sdk/models/error_response' 21 | require 'devcycle-ruby-server-sdk/models/event' 22 | require 'devcycle-ruby-server-sdk/models/feature' 23 | require 'devcycle-ruby-server-sdk/models/inline_response201' 24 | require 'devcycle-ruby-server-sdk/models/user' 25 | require 'devcycle-ruby-server-sdk/models/user_data_and_events_body' 26 | require 'devcycle-ruby-server-sdk/models/variable' 27 | 28 | # Eval Hooks 29 | require 'devcycle-ruby-server-sdk/eval_hooks_runner' 30 | require 'devcycle-ruby-server-sdk/models/eval_hook' 31 | require 'devcycle-ruby-server-sdk/models/eval_hook_context' 32 | require 'devcycle-ruby-server-sdk/eval_reasons' 33 | 34 | # APIs 35 | require 'devcycle-ruby-server-sdk/api/client' 36 | require 'devcycle-ruby-server-sdk/api/dev_cycle_provider' 37 | 38 | require 'devcycle-ruby-server-sdk/localbucketing/options' 39 | require 'devcycle-ruby-server-sdk/localbucketing/local_bucketing' 40 | require 'devcycle-ruby-server-sdk/localbucketing/platform_data' 41 | require 'devcycle-ruby-server-sdk/localbucketing/bucketed_user_config' 42 | require 'devcycle-ruby-server-sdk/localbucketing/event_queue' 43 | require 'devcycle-ruby-server-sdk/localbucketing/event_types' 44 | require 'devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams_pb' 45 | require 'devcycle-ruby-server-sdk/localbucketing/proto/helpers' 46 | 47 | module DevCycle 48 | class << self 49 | # Customize default settings for the SDK using block. 50 | # DevCycle.configure do |config| 51 | # config.username = "xxx" 52 | # config.password = "xxx" 53 | # end 54 | # If no block given, return the default Configuration object. 55 | def configure 56 | if block_given? 57 | yield(Configuration.default) 58 | else 59 | Configuration.default 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /examples/local/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Store uploaded files on the local file system (see config/storage.yml for options). 37 | config.active_storage.service = :local 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise exceptions for disallowed deprecations. 43 | config.active_support.disallowed_deprecation = :raise 44 | 45 | # Tell Active Support which deprecation messages to disallow. 46 | config.active_support.disallowed_deprecation_warnings = [] 47 | 48 | # Raise an error on page load if there are pending migrations. 49 | config.active_record.migration_error = :page_load 50 | 51 | # Highlight code that triggered database queries in logs. 52 | config.active_record.verbose_query_logs = true 53 | 54 | # Suppress logger output for asset requests. 55 | config.assets.quiet = true 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | # config.action_view.annotate_rendered_view_with_filenames = true 62 | 63 | # Uncomment if you wish to allow Action Cable access from any origin. 64 | # config.action_cable.disable_request_forgery_protection = true 65 | end 66 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/rspec@3.12.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `rspec` gem. 5 | # Please instead update this file by running `bin/tapioca gem rspec`. 6 | 7 | # source://rspec//lib/rspec/version.rb#1 8 | module RSpec 9 | class << self 10 | # source://rspec-core/3.12.0/lib/rspec/core.rb#70 11 | def clear_examples; end 12 | 13 | # source://rspec-core/3.12.0/lib/rspec/core.rb#85 14 | def configuration; end 15 | 16 | # source://rspec-core/3.12.0/lib/rspec/core.rb#49 17 | def configuration=(_arg0); end 18 | 19 | # source://rspec-core/3.12.0/lib/rspec/core.rb#97 20 | def configure; end 21 | 22 | # source://rspec-core/3.12.0/lib/rspec/core.rb#194 23 | def const_missing(name); end 24 | 25 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 26 | def context(*args, &example_group_block); end 27 | 28 | # source://rspec-core/3.12.0/lib/rspec/core.rb#122 29 | def current_example; end 30 | 31 | # source://rspec-core/3.12.0/lib/rspec/core.rb#128 32 | def current_example=(example); end 33 | 34 | # source://rspec-core/3.12.0/lib/rspec/core.rb#154 35 | def current_scope; end 36 | 37 | # source://rspec-core/3.12.0/lib/rspec/core.rb#134 38 | def current_scope=(scope); end 39 | 40 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 41 | def describe(*args, &example_group_block); end 42 | 43 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 44 | def example_group(*args, &example_group_block); end 45 | 46 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 47 | def fcontext(*args, &example_group_block); end 48 | 49 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 50 | def fdescribe(*args, &example_group_block); end 51 | 52 | # source://rspec-core/3.12.0/lib/rspec/core.rb#58 53 | def reset; end 54 | 55 | # source://rspec-core/3.12.0/lib/rspec/core/shared_example_group.rb#110 56 | def shared_context(name, *args, &block); end 57 | 58 | # source://rspec-core/3.12.0/lib/rspec/core/shared_example_group.rb#110 59 | def shared_examples(name, *args, &block); end 60 | 61 | # source://rspec-core/3.12.0/lib/rspec/core/shared_example_group.rb#110 62 | def shared_examples_for(name, *args, &block); end 63 | 64 | # source://rspec-core/3.12.0/lib/rspec/core.rb#160 65 | def world; end 66 | 67 | # source://rspec-core/3.12.0/lib/rspec/core.rb#49 68 | def world=(_arg0); end 69 | 70 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 71 | def xcontext(*args, &example_group_block); end 72 | 73 | # source://rspec-core/3.12.0/lib/rspec/core/dsl.rb#42 74 | def xdescribe(*args, &example_group_block); end 75 | end 76 | end 77 | 78 | # source://rspec-core/3.12.0/lib/rspec/core.rb#187 79 | RSpec::MODULES_TO_AUTOLOAD = T.let(T.unsafe(nil), Hash) 80 | 81 | # source://rspec-core/3.12.0/lib/rspec/core/shared_context.rb#54 82 | RSpec::SharedContext = RSpec::Core::SharedContext 83 | 84 | # source://rspec//lib/rspec/version.rb#2 85 | module RSpec::Version; end 86 | 87 | # source://rspec//lib/rspec/version.rb#3 88 | RSpec::Version::STRING = T.let(T.unsafe(nil), String) 89 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | prerelease: 7 | description: "Prerelease" 8 | required: true 9 | default: false 10 | type: boolean 11 | draft: 12 | description: "Draft" 13 | required: true 14 | default: false 15 | type: boolean 16 | version-increment-type: 17 | description: 'Which part of the version to increment:' 18 | required: true 19 | type: choice 20 | options: 21 | - major 22 | - minor 23 | - patch 24 | default: 'patch' 25 | 26 | permissions: 27 | contents: write 28 | attestations: write 29 | 30 | jobs: 31 | release: 32 | name: Version Bump and Release 33 | runs-on: ubuntu-latest 34 | permissions: 35 | contents: write 36 | id-token: write 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | token: ${{ secrets.AUTOMATION_USER_TOKEN }} 41 | fetch-depth: 0 42 | 43 | - uses: DevCycleHQ/release-action/prepare-release@v2.3.0 44 | id: prepare-release 45 | with: 46 | github-token: ${{ secrets.AUTOMATION_USER_TOKEN }} 47 | prerelease: ${{ github.event.inputs.prerelease }} 48 | draft: ${{ github.event.inputs.draft }} 49 | version-increment-type: ${{ github.event.inputs.version-increment-type }} 50 | 51 | 52 | - name: Update Version in code 53 | run: | 54 | sed -i "s/VERSION = '[0-9]\+\.[0-9]\+\.[0-9]\+'/VERSION = '${{steps.prepare-release.outputs.next-release-tag}}'/g" ./lib/devcycle-ruby-server-sdk/version.rb 55 | 56 | - name: Commit version change 57 | run: | 58 | git config --global user.email "foundation-admin@devcycle.com" 59 | git config --global user.name "DevCycle Automation" 60 | git add ./lib/devcycle-ruby-server-sdk/version.rb 61 | git commit -m "Release ${{steps.prepare-release.outputs.next-release-tag}}" 62 | 63 | - name: Push version change 64 | run: | 65 | git pull 66 | git push -u origin main 67 | if: inputs.draft != true 68 | 69 | - name: Set up Ruby 70 | uses: ruby/setup-ruby@v1 71 | with: 72 | ruby-version: ruby 73 | bundler-cache: true 74 | 75 | - name: Publish to RubyGems 76 | uses: rubygems/release-gem@v1 77 | if: inputs.prerelease != true && inputs.draft != true 78 | 79 | - uses: DevCycleHQ/release-action/create-release@v2.3.0 80 | id: create-release 81 | with: 82 | github-token: ${{ secrets.AUTOMATION_USER_TOKEN }} 83 | tag: ${{ steps.prepare-release.outputs.next-release-tag }} 84 | target: main 85 | prerelease: ${{ github.event.inputs.prerelease }} 86 | draft: ${{ github.event.inputs.draft }} 87 | changelog: ${{ steps.prepare-release.outputs.changelog }} 88 | 89 | - name: Display link to release 90 | run: | 91 | echo "::notice title=Release ID::${{ steps.create-release.outputs.release-id }}" 92 | echo "::notice title=Release URL::${{ steps.create-release.outputs.release-url }}" 93 | -------------------------------------------------------------------------------- /examples/local/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /examples/local/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Compress CSS using a preprocessor. 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 34 | # config.asset_host = "http://assets.example.com" 35 | 36 | # Specifies the header that your server uses for sending files. 37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 39 | 40 | # Store uploaded files on the local file system (see config/storage.yml for options). 41 | config.active_storage.service = :local 42 | 43 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 44 | # config.force_ssl = true 45 | 46 | # Include generic and useful information about system operation, but avoid logging too much 47 | # information to avoid inadvertent exposure of personally identifiable information (PII). 48 | config.log_level = :info 49 | 50 | # Prepend all log lines with the following tags. 51 | config.log_tags = [ :request_id ] 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Use a real queuing backend for Active Job (and separate queues per environment). 57 | # config.active_job.queue_adapter = :resque 58 | # config.active_job.queue_name_prefix = "local_bucketing_example_production" 59 | 60 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 61 | # the I18n.default_locale when a translation cannot be found). 62 | config.i18n.fallbacks = true 63 | 64 | # Don't log any deprecations. 65 | config.active_support.report_deprecations = false 66 | 67 | # Use default logging formatter so that PID and timestamp are not suppressed. 68 | config.log_formatter = ::Logger::Formatter.new 69 | 70 | # Use a different logger for distributed setups. 71 | # require "syslog/logger" 72 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 73 | 74 | if ENV["RAILS_LOG_TO_STDOUT"].present? 75 | logger = ActiveSupport::Logger.new(STDOUT) 76 | logger.formatter = config.log_formatter 77 | config.logger = ActiveSupport::TaggedLogging.new(logger) 78 | end 79 | 80 | # Do not dump schema after migrations. 81 | config.active_record.dump_schema_after_migration = false 82 | end 83 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/event_queue.rb: -------------------------------------------------------------------------------- 1 | require 'typhoeus' 2 | require 'sorbet-runtime' 3 | require 'concurrent-ruby' 4 | require 'securerandom' 5 | 6 | 7 | module DevCycle 8 | class EventQueue 9 | extend T::Sig 10 | 11 | sig { params(sdkKey: String, options: EventQueueOptions, local_bucketing: LocalBucketing).void } 12 | def initialize(sdkKey, options, local_bucketing) 13 | @sdkKey = sdkKey 14 | @client_uuid = SecureRandom.uuid 15 | @events_api_uri = options.events_api_uri 16 | @logger = options.logger 17 | @event_flush_interval_ms = options.event_flush_interval_ms 18 | @flush_event_queue_size = options.flush_event_queue_size 19 | @max_event_queue_size = options.max_event_queue_size 20 | @flush_timer_task = Concurrent::TimerTask.new( 21 | execution_interval: @event_flush_interval_ms.fdiv(1000) 22 | ) { 23 | flush_events 24 | } 25 | @flush_timer_task.execute 26 | @flush_mutex = Mutex.new 27 | @local_bucketing = local_bucketing 28 | @local_bucketing.init_event_queue(@client_uuid, options) 29 | 30 | end 31 | 32 | def close 33 | @flush_timer_task.shutdown 34 | flush_events 35 | end 36 | 37 | sig { returns(NilClass) } 38 | def flush_events 39 | @flush_mutex.synchronize do 40 | payloads = @local_bucketing.flush_event_queue 41 | if payloads.length == 0 42 | return 43 | end 44 | eventCount = payloads.reduce(0) { |sum, payload| sum + payload.eventCount } 45 | @logger.debug("DevCycle: Flushing #{eventCount} event(s) for #{payloads.length} user(s)") 46 | 47 | payloads.each do |payload| 48 | begin 49 | response = Typhoeus.post( 50 | @events_api_uri + '/v1/events/batch', 51 | headers: { 'Authorization': @sdkKey, 'Content-Type': 'application/json' }, 52 | body: { 'batch': payload.records }.to_json 53 | ) 54 | if response.code != 201 55 | @logger.error("Error publishing events, status: #{response.code}, body: #{response.return_message}") 56 | @local_bucketing.on_payload_failure(payload.payloadId, response.code >= 500) 57 | else 58 | @logger.debug("DevCycle: Flushed #{eventCount} event(s), for #{payload.records.length} user(s)") 59 | @local_bucketing.on_payload_success(payload.payloadId) 60 | end 61 | rescue => e 62 | @logger.error("DevCycle: Error Flushing Events response message: #{e.message}") 63 | @local_bucketing.on_payload_failure(payload.payloadId, false) 64 | end 65 | end 66 | end 67 | nil 68 | end 69 | 70 | # Todo: implement PopulatedUser 71 | sig { params(user: User, event: Event).returns(NilClass) } 72 | def queue_event(user, event) 73 | if max_event_queue_size_reached? 74 | @logger.warn("Max event queue size reached, dropping event: #{event}") 75 | return 76 | end 77 | 78 | @local_bucketing.queue_event(user, event) 79 | nil 80 | end 81 | 82 | sig { params(event: Event, bucketed_config: T.nilable(BucketedUserConfig)).returns(NilClass) } 83 | def queue_aggregate_event(event, bucketed_config) 84 | if max_event_queue_size_reached? 85 | @logger.warn("Max event queue size reached, dropping event: #{event}") 86 | return 87 | end 88 | 89 | @local_bucketing.queue_aggregate_event(event, bucketed_config) 90 | nil 91 | end 92 | 93 | sig { returns(T::Boolean) } 94 | def max_event_queue_size_reached? 95 | queue_size = @local_bucketing.check_event_queue_size() 96 | if queue_size >= @flush_event_queue_size 97 | flush_events 98 | if queue_size >= @max_event_queue_size 99 | return true 100 | end 101 | end 102 | false 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This file is based on https://github.com/rails/rails/blob/master/.rubocop.yml (MIT license) 2 | # Automatically generated by OpenAPI Generator (https://openapi-generator.tech) 3 | AllCops: 4 | TargetRubyVersion: 2.4 5 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop 6 | # to ignore them, so only the ones explicitly set in this file are enabled. 7 | DisabledByDefault: true 8 | Exclude: 9 | - '**/templates/**/*' 10 | - '**/vendor/**/*' 11 | - 'actionpack/lib/action_dispatch/journey/parser.rb' 12 | 13 | # Prefer &&/|| over and/or. 14 | Style/AndOr: 15 | Enabled: true 16 | 17 | # Align `when` with `case`. 18 | Layout/CaseIndentation: 19 | Enabled: true 20 | 21 | # Align comments with method definitions. 22 | Layout/CommentIndentation: 23 | Enabled: true 24 | 25 | Layout/ElseAlignment: 26 | Enabled: true 27 | 28 | Layout/EmptyLineAfterMagicComment: 29 | Enabled: true 30 | 31 | # In a regular class definition, no empty lines around the body. 32 | Layout/EmptyLinesAroundClassBody: 33 | Enabled: true 34 | 35 | # In a regular method definition, no empty lines around the body. 36 | Layout/EmptyLinesAroundMethodBody: 37 | Enabled: true 38 | 39 | # In a regular module definition, no empty lines around the body. 40 | Layout/EmptyLinesAroundModuleBody: 41 | Enabled: true 42 | 43 | Layout/FirstArgumentIndentation: 44 | Enabled: true 45 | 46 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 47 | Style/HashSyntax: 48 | Enabled: false 49 | 50 | # Method definitions after `private` or `protected` isolated calls need one 51 | # extra level of indentation. 52 | Layout/IndentationConsistency: 53 | Enabled: true 54 | EnforcedStyle: normal 55 | 56 | # Two spaces, no tabs (for indentation). 57 | Layout/IndentationWidth: 58 | Enabled: true 59 | 60 | Layout/LeadingCommentSpace: 61 | Enabled: true 62 | 63 | Layout/SpaceAfterColon: 64 | Enabled: true 65 | 66 | Layout/SpaceAfterComma: 67 | Enabled: true 68 | 69 | Layout/SpaceAroundEqualsInParameterDefault: 70 | Enabled: true 71 | 72 | Layout/SpaceAroundKeyword: 73 | Enabled: true 74 | 75 | Layout/SpaceAroundOperators: 76 | Enabled: true 77 | 78 | Layout/SpaceBeforeComma: 79 | Enabled: true 80 | 81 | Layout/SpaceBeforeFirstArg: 82 | Enabled: true 83 | 84 | Style/DefWithParentheses: 85 | Enabled: true 86 | 87 | # Defining a method with parameters needs parentheses. 88 | Style/MethodDefParentheses: 89 | Enabled: true 90 | 91 | Style/FrozenStringLiteralComment: 92 | Enabled: false 93 | EnforcedStyle: always 94 | 95 | # Use `foo {}` not `foo{}`. 96 | Layout/SpaceBeforeBlockBraces: 97 | Enabled: true 98 | 99 | # Use `foo { bar }` not `foo {bar}`. 100 | Layout/SpaceInsideBlockBraces: 101 | Enabled: true 102 | 103 | # Use `{ a: 1 }` not `{a:1}`. 104 | Layout/SpaceInsideHashLiteralBraces: 105 | Enabled: true 106 | 107 | Layout/SpaceInsideParens: 108 | Enabled: true 109 | 110 | # Check quotes usage according to lint rule below. 111 | #Style/StringLiterals: 112 | # Enabled: true 113 | # EnforcedStyle: single_quotes 114 | 115 | # Detect hard tabs, no hard tabs. 116 | Layout/IndentationStyle: 117 | Enabled: true 118 | 119 | # Blank lines should not have any spaces. 120 | Layout/TrailingEmptyLines: 121 | Enabled: true 122 | 123 | # No trailing whitespace. 124 | Layout/TrailingWhitespace: 125 | Enabled: false 126 | 127 | # Use quotes for string literals when they are enough. 128 | Style/RedundantPercentQ: 129 | Enabled: true 130 | 131 | # Align `end` with the matching keyword or starting expression except for 132 | # assignments, where it should be aligned with the LHS. 133 | Layout/EndAlignment: 134 | Enabled: true 135 | EnforcedStyleAlignWith: variable 136 | AutoCorrect: true 137 | 138 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 139 | Lint/RequireParentheses: 140 | Enabled: true 141 | 142 | Style/RedundantReturn: 143 | Enabled: true 144 | AllowMultipleReturnValues: true 145 | 146 | Style/Semicolon: 147 | Enabled: true 148 | AllowAsExpressionSeparator: true 149 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/netrc@0.11.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `netrc` gem. 5 | # Please instead update this file by running `bin/tapioca gem netrc`. 6 | 7 | # source://netrc//lib/netrc.rb#3 8 | class Netrc 9 | # @return [Netrc] a new instance of Netrc 10 | # 11 | # source://netrc//lib/netrc.rb#166 12 | def initialize(path, data); end 13 | 14 | # source://netrc//lib/netrc.rb#180 15 | def [](k); end 16 | 17 | # source://netrc//lib/netrc.rb#188 18 | def []=(k, info); end 19 | 20 | # source://netrc//lib/netrc.rb#200 21 | def delete(key); end 22 | 23 | # source://netrc//lib/netrc.rb#211 24 | def each(&block); end 25 | 26 | # source://netrc//lib/netrc.rb#196 27 | def length; end 28 | 29 | # source://netrc//lib/netrc.rb#215 30 | def new_item(m, l, p); end 31 | 32 | # Returns the value of attribute new_item_prefix. 33 | # 34 | # source://netrc//lib/netrc.rb#178 35 | def new_item_prefix; end 36 | 37 | # Sets the attribute new_item_prefix 38 | # 39 | # @param value the value to set the attribute new_item_prefix to. 40 | # 41 | # source://netrc//lib/netrc.rb#178 42 | def new_item_prefix=(_arg0); end 43 | 44 | # source://netrc//lib/netrc.rb#219 45 | def save; end 46 | 47 | # source://netrc//lib/netrc.rb#233 48 | def unparse; end 49 | 50 | class << self 51 | # source://netrc//lib/netrc.rb#42 52 | def check_permissions(path); end 53 | 54 | # source://netrc//lib/netrc.rb#33 55 | def config; end 56 | 57 | # @yield [self.config] 58 | # 59 | # source://netrc//lib/netrc.rb#37 60 | def configure; end 61 | 62 | # source://netrc//lib/netrc.rb#10 63 | def default_path; end 64 | 65 | # source://netrc//lib/netrc.rb#14 66 | def home_path; end 67 | 68 | # source://netrc//lib/netrc.rb#85 69 | def lex(lines); end 70 | 71 | # source://netrc//lib/netrc.rb#29 72 | def netrc_filename; end 73 | 74 | # Returns two values, a header and a list of items. 75 | # Each item is a tuple, containing some or all of: 76 | # - machine keyword (including trailing whitespace+comments) 77 | # - machine name 78 | # - login keyword (including surrounding whitespace+comments) 79 | # - login 80 | # - password keyword (including surrounding whitespace+comments) 81 | # - password 82 | # - trailing chars 83 | # This lets us change individual fields, then write out the file 84 | # with all its original formatting. 85 | # 86 | # source://netrc//lib/netrc.rb#129 87 | def parse(ts); end 88 | 89 | # Reads path and parses it as a .netrc file. If path doesn't 90 | # exist, returns an empty object. Decrypt paths ending in .gpg. 91 | # 92 | # source://netrc//lib/netrc.rb#51 93 | def read(path = T.unsafe(nil)); end 94 | 95 | # @return [Boolean] 96 | # 97 | # source://netrc//lib/netrc.rb#112 98 | def skip?(s); end 99 | end 100 | end 101 | 102 | # source://netrc//lib/netrc.rb#8 103 | Netrc::CYGWIN = T.let(T.unsafe(nil), T.untyped) 104 | 105 | # source://netrc//lib/netrc.rb#244 106 | class Netrc::Entry < ::Struct 107 | # Returns the value of attribute login 108 | # 109 | # @return [Object] the current value of login 110 | def login; end 111 | 112 | # Sets the attribute login 113 | # 114 | # @param value [Object] the value to set the attribute login to. 115 | # @return [Object] the newly set value 116 | def login=(_); end 117 | 118 | # Returns the value of attribute password 119 | # 120 | # @return [Object] the current value of password 121 | def password; end 122 | 123 | # Sets the attribute password 124 | # 125 | # @param value [Object] the value to set the attribute password to. 126 | # @return [Object] the newly set value 127 | def password=(_); end 128 | 129 | def to_ary; end 130 | 131 | class << self 132 | def [](*_arg0); end 133 | def inspect; end 134 | def keyword_init?; end 135 | def members; end 136 | def new(*_arg0); end 137 | end 138 | end 139 | 140 | # source://netrc//lib/netrc.rb#250 141 | class Netrc::Error < ::StandardError; end 142 | 143 | # source://netrc//lib/netrc.rb#68 144 | class Netrc::TokenArray < ::Array 145 | # source://netrc//lib/netrc.rb#76 146 | def readto; end 147 | 148 | # source://netrc//lib/netrc.rb#69 149 | def take; end 150 | end 151 | 152 | # source://netrc//lib/netrc.rb#4 153 | Netrc::VERSION = T.let(T.unsafe(nil), String) 154 | 155 | # see http://stackoverflow.com/questions/4871309/what-is-the-correct-way-to-detect-if-ruby-is-running-on-windows 156 | # 157 | # source://netrc//lib/netrc.rb#7 158 | Netrc::WINDOWS = T.let(T.unsafe(nil), T.untyped) 159 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/eval_hooks_runner.rb: -------------------------------------------------------------------------------- 1 | require 'devcycle-ruby-server-sdk/models/eval_hook' 2 | require 'devcycle-ruby-server-sdk/models/eval_hook_context' 3 | 4 | module DevCycle 5 | # Custom error raised when a before hook fails 6 | class BeforeHookError < StandardError 7 | attr_reader :original_error, :hook_context 8 | 9 | def initialize(message = nil, original_error = nil, hook_context = nil) 10 | super(message || "Before hook execution failed") 11 | @original_error = original_error 12 | @hook_context = hook_context 13 | end 14 | 15 | def to_s 16 | msg = super 17 | msg += "\nOriginal error: #{@original_error.message}" if @original_error 18 | msg 19 | end 20 | end 21 | 22 | # Custom error raised when an after hook fails 23 | class AfterHookError < StandardError 24 | attr_reader :original_error, :hook_context 25 | 26 | def initialize(message = nil, original_error = nil, hook_context = nil) 27 | super(message || "After hook execution failed") 28 | @original_error = original_error 29 | @hook_context = hook_context 30 | end 31 | 32 | def to_s 33 | msg = super 34 | msg += "\nOriginal error: #{@original_error.message}" if @original_error 35 | msg 36 | end 37 | end 38 | 39 | class EvalHooksRunner 40 | # @return [Array] Array of eval hooks to run 41 | attr_reader :eval_hooks 42 | 43 | # Initializes the EvalHooksRunner with an optional array of eval hooks 44 | # @param [Array, nil] eval_hooks Array of eval hooks to run 45 | def initialize(eval_hooks = []) 46 | @eval_hooks = eval_hooks || [] 47 | end 48 | 49 | # Runs all before hooks with the given context 50 | # @param [HookContext] context The context to pass to the hooks 51 | # @return [HookContext] The potentially modified context 52 | # @raise [BeforeHookError] when a before hook fails 53 | def run_before_hooks(context) 54 | current_context = context 55 | 56 | @eval_hooks.each do |hook| 57 | next unless hook.before 58 | 59 | begin 60 | result = hook.before.call(current_context) 61 | # If the hook returns a new context, use it for subsequent hooks 62 | current_context = result if result.is_a?(DevCycle::HookContext) 63 | rescue => e 64 | # Raise BeforeHookError to allow client to handle and skip after hooks 65 | raise BeforeHookError.new(e.message, e, current_context) 66 | end 67 | end 68 | 69 | current_context 70 | end 71 | 72 | # Runs all after hooks with the given context 73 | # @param [HookContext] context The context to pass to the hooks 74 | # @return [void] 75 | # @raise [AfterHookError] when an after hook fails 76 | def run_after_hooks(context) 77 | @eval_hooks.each do |hook| 78 | next unless hook.after 79 | 80 | begin 81 | hook.after.call(context) 82 | rescue => e 83 | # Log error but continue with next hook 84 | raise AfterHookError.new(e.message, e, context) 85 | end 86 | end 87 | end 88 | 89 | # Runs all finally hooks with the given context 90 | # @param [HookContext] context The context to pass to the hooks 91 | # @return [void] 92 | def run_finally_hooks(context) 93 | @eval_hooks.each do |hook| 94 | next unless hook.on_finally 95 | 96 | begin 97 | hook.on_finally.call(context) 98 | rescue => e 99 | # Log error but don't re-raise to prevent blocking evaluation 100 | warn "Error in finally hook: #{e.message}" 101 | end 102 | end 103 | end 104 | 105 | # Runs all error hooks with the given context and error 106 | # @param [HookContext] context The context to pass to the hooks 107 | # @param [Exception] error The error that occurred 108 | # @return [void] 109 | def run_error_hooks(context, error) 110 | @eval_hooks.each do |hook| 111 | next unless hook.error 112 | 113 | begin 114 | hook.error.call(context, error) 115 | rescue => e 116 | # Log error but don't re-raise to prevent blocking evaluation 117 | warn "Error in error hook: #{e.message}" 118 | end 119 | end 120 | end 121 | 122 | # Adds an eval hook to the runner 123 | # @param [EvalHook] eval_hook The eval hook to add 124 | # @return [void] 125 | def add_hook(eval_hook) 126 | @eval_hooks << eval_hook 127 | end 128 | 129 | # Clears all eval hooks from the runner 130 | # @return [void] 131 | def clear_hooks 132 | @eval_hooks.clear 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/proto/variableForUserParams_pb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: variableForUserParams.proto 4 | 5 | require 'google/protobuf' 6 | 7 | 8 | descriptor_data = "\n\x1bvariableForUserParams.proto\x12\x05proto\"/\n\x0eNullableString\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"/\n\x0eNullableDouble\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"s\n\x0f\x43ustomDataValue\x12#\n\x04type\x18\x01 \x01(\x0e\x32\x15.proto.CustomDataType\x12\x11\n\tboolValue\x18\x02 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x03 \x01(\x01\x12\x13\n\x0bstringValue\x18\x04 \x01(\t\"\x9f\x01\n\x12NullableCustomData\x12\x33\n\x05value\x18\x01 \x03(\x0b\x32$.proto.NullableCustomData.ValueEntry\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\x1a\x44\n\nValueEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.proto.CustomDataValue:\x02\x38\x01\"\xa8\x01\n\x18VariableForUserParams_PB\x12\x0e\n\x06sdkKey\x18\x01 \x01(\t\x12\x13\n\x0bvariableKey\x18\x02 \x01(\t\x12,\n\x0cvariableType\x18\x03 \x01(\x0e\x32\x16.proto.VariableType_PB\x12\x1f\n\x04user\x18\x04 \x01(\x0b\x32\x11.proto.DVCUser_PB\x12\x18\n\x10shouldTrackEvent\x18\x05 \x01(\x08\"\x9e\x03\n\nDVCUser_PB\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12$\n\x05\x65mail\x18\x02 \x01(\x0b\x32\x15.proto.NullableString\x12#\n\x04name\x18\x03 \x01(\x0b\x32\x15.proto.NullableString\x12\'\n\x08language\x18\x04 \x01(\x0b\x32\x15.proto.NullableString\x12&\n\x07\x63ountry\x18\x05 \x01(\x0b\x32\x15.proto.NullableString\x12\'\n\x08\x61ppBuild\x18\x06 \x01(\x0b\x32\x15.proto.NullableDouble\x12)\n\nappVersion\x18\x07 \x01(\x0b\x32\x15.proto.NullableString\x12*\n\x0b\x64\x65viceModel\x18\x08 \x01(\x0b\x32\x15.proto.NullableString\x12-\n\ncustomData\x18\t \x01(\x0b\x32\x19.proto.NullableCustomData\x12\x34\n\x11privateCustomData\x18\n \x01(\x0b\x32\x19.proto.NullableCustomData\"\x85\x02\n\x0eSDKVariable_PB\x12\x0b\n\x03_id\x18\x01 \x01(\t\x12$\n\x04type\x18\x02 \x01(\x0e\x32\x16.proto.VariableType_PB\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x11\n\tboolValue\x18\x04 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x05 \x01(\x01\x12\x13\n\x0bstringValue\x18\x06 \x01(\t\x12)\n\nevalReason\x18\x07 \x01(\x0b\x32\x15.proto.NullableString\x12\'\n\x08_feature\x18\x08 \x01(\x0b\x32\x15.proto.NullableString\x12\"\n\x04\x65val\x18\t \x01(\x0b\x32\x14.proto.EvalReason_PB\"C\n\rEvalReason_PB\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x0f\n\x07\x64\x65tails\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t*@\n\x0fVariableType_PB\x12\x0b\n\x07\x42oolean\x10\x00\x12\n\n\x06Number\x10\x01\x12\n\n\x06String\x10\x02\x12\x08\n\x04JSON\x10\x03*6\n\x0e\x43ustomDataType\x12\x08\n\x04\x42ool\x10\x00\x12\x07\n\x03Num\x10\x01\x12\x07\n\x03Str\x10\x02\x12\x08\n\x04Null\x10\x03\x62\x06proto3" 9 | 10 | pool = Google::Protobuf::DescriptorPool.generated_pool 11 | 12 | begin 13 | pool.add_serialized_file(descriptor_data) 14 | rescue TypeError => e 15 | # Compatibility code: will be removed in the next major version. 16 | require 'google/protobuf/descriptor_pb' 17 | parsed = Google::Protobuf::FileDescriptorProto.decode(descriptor_data) 18 | parsed.clear_dependency 19 | serialized = parsed.class.encode(parsed) 20 | file = pool.add_serialized_file(serialized) 21 | warn "Warning: Protobuf detected an import path issue while loading generated file #{__FILE__}" 22 | imports = [ 23 | ] 24 | imports.each do |type_name, expected_filename| 25 | import_file = pool.lookup(type_name).file_descriptor 26 | if import_file.name != expected_filename 27 | warn "- #{file.name} imports #{expected_filename}, but that import was loaded as #{import_file.name}" 28 | end 29 | end 30 | warn "Each proto file must use a consistent fully-qualified name." 31 | warn "This will become an error in the next major version." 32 | end 33 | 34 | module Proto 35 | NullableString = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.NullableString").msgclass 36 | NullableDouble = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.NullableDouble").msgclass 37 | CustomDataValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.CustomDataValue").msgclass 38 | NullableCustomData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.NullableCustomData").msgclass 39 | VariableForUserParams_PB = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.VariableForUserParams_PB").msgclass 40 | DVCUser_PB = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.DVCUser_PB").msgclass 41 | SDKVariable_PB = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.SDKVariable_PB").msgclass 42 | EvalReason_PB = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.EvalReason_PB").msgclass 43 | VariableType_PB = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.VariableType_PB").enummodule 44 | CustomDataType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("proto.CustomDataType").enummodule 45 | end 46 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/api/dev_cycle_provider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DevCycle 4 | class Provider 5 | attr_reader :client 6 | def initialize(client) 7 | unless client.is_a?(DevCycle::Client) 8 | fail ArgumentError('Client must be an instance of DevCycleClient') 9 | end 10 | @client = client 11 | end 12 | 13 | def init 14 | # We handle all initialization on the DVC Client itself 15 | end 16 | 17 | def shutdown 18 | @client.close 19 | end 20 | 21 | def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil) 22 | # Retrieve a boolean value from provider source 23 | @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 24 | end 25 | 26 | def fetch_string_value(flag_key:, default_value:, evaluation_context: nil) 27 | @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 28 | end 29 | 30 | def fetch_number_value(flag_key:, default_value:, evaluation_context: nil) 31 | @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 32 | end 33 | 34 | def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil) 35 | variable = @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 36 | 37 | Variable.new( 38 | key: variable.key, 39 | type: variable.type, 40 | value: variable.value.to_i, 41 | defaultValue: variable.defaultValue, 42 | isDefaulted: variable.isDefaulted, 43 | eval: variable.eval 44 | ) 45 | end 46 | 47 | def fetch_float_value(flag_key:, default_value:, evaluation_context: nil) 48 | @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 49 | end 50 | 51 | def fetch_object_value(flag_key:, default_value:, evaluation_context: nil) 52 | @client.variable(Provider.user_from_openfeature_context(evaluation_context), flag_key, default_value) 53 | end 54 | 55 | def self.user_from_openfeature_context(context) 56 | unless context.is_a?(OpenFeature::SDK::EvaluationContext) 57 | raise ArgumentError, "Invalid context type, expected OpenFeature::SDK::EvaluationContext but got #{context.class}" 58 | end 59 | args = {} 60 | user_id = nil 61 | user_id_field = nil 62 | 63 | # Priority order: targeting_key -> user_id -> userId 64 | if context.field('targeting_key') 65 | user_id = context.field('targeting_key') 66 | user_id_field = 'targeting_key' 67 | elsif context.field('user_id') 68 | user_id = context.field('user_id') 69 | user_id_field = 'user_id' 70 | elsif context.field('userId') 71 | user_id = context.field('userId') 72 | user_id_field = 'userId' 73 | end 74 | 75 | # Validate user_id is present and is a string 76 | if user_id.nil? 77 | raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId" 78 | end 79 | 80 | unless user_id.is_a?(String) 81 | raise ArgumentError, "User ID field '#{user_id_field}' must be a string, got #{user_id.class}" 82 | end 83 | 84 | # Check after type validation to avoid NoMethodError on non-strings 85 | if user_id.empty? 86 | raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId" 87 | end 88 | 89 | args.merge!(user_id: user_id) 90 | 91 | customData = {} 92 | privateCustomData = {} 93 | context.fields.each do |field, value| 94 | # Skip all user ID fields from custom data 95 | if field === 'targeting_key' || field === 'user_id' || field === 'userId' 96 | next 97 | end 98 | if !(field === 'privateCustomData' || field === 'customData') && value.is_a?(Hash) 99 | next 100 | end 101 | case field 102 | when 'email' 103 | args.merge!(email: value) 104 | when 'name' 105 | args.merge!(name: value) 106 | when 'language' 107 | args.merge!(language: value) 108 | when 'country' 109 | args.merge!(country: value) 110 | when 'appVersion' 111 | if value.is_a?(String) 112 | args.merge!(appVersion: value) 113 | end 114 | next 115 | when 'appBuild' 116 | if value.is_a?(Numeric) 117 | args.merge!(appBuild: value) 118 | end 119 | when 'customData' 120 | if value.is_a?(Hash) 121 | customData.merge!(value) 122 | end 123 | next 124 | when 'privateCustomData' 125 | if value.is_a?(Hash) 126 | privateCustomData.merge!(value) 127 | end 128 | else 129 | customData.merge!(field => value) 130 | end 131 | end 132 | args.merge!(customData: customData) 133 | args.merge!(privateCustomData: privateCustomData) 134 | User.new(**args) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | # load the gem 14 | require 'devcycle-ruby-server-sdk' 15 | require 'webmock/rspec' 16 | 17 | # Configure WebMock to allow real HTTP requests by default, but enable it for specific tests 18 | WebMock.allow_net_connect! 19 | 20 | # The following was generated by the `rspec --init` command. Conventionally, all 21 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 22 | # The generated `.rspec` file contains `--require spec_helper` which will cause 23 | # this file to always be loaded, without a need to explicitly require it in any 24 | # files. 25 | # 26 | # Given that it is always loaded, you are encouraged to keep this file as 27 | # light-weight as possible. Requiring heavyweight dependencies from this file 28 | # will add to the boot time of your test suite on EVERY test run, even for an 29 | # individual file that may not need all of that loaded. Instead, consider making 30 | # a separate helper file that requires the additional dependencies and performs 31 | # the additional setup, and require it from the spec files that actually need 32 | # it. 33 | # 34 | # The `.rspec` file also contains a few flags that are not defaults but that 35 | # users commonly want. 36 | # 37 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 38 | RSpec.configure do |config| 39 | # rspec-expectations config goes here. You can use an alternate 40 | # assertion/expectation library such as wrong or the stdlib/minitest 41 | # assertions if you prefer. 42 | config.expect_with :rspec do |expectations| 43 | # This option will default to `true` in RSpec 4. It makes the `description` 44 | # and `failure_message` of custom matchers include text for helper methods 45 | # defined using `chain`, e.g.: 46 | # be_bigger_than(2).and_smaller_than(4).description 47 | # # => "be bigger than 2 and smaller than 4" 48 | # ...rather than: 49 | # # => "be bigger than 2" 50 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 51 | end 52 | 53 | # rspec-mocks config goes here. You can use an alternate test double 54 | # library (such as bogus or mocha) by changing the `mock_with` option here. 55 | config.mock_with :rspec do |mocks| 56 | # Prevents you from mocking or stubbing a method that does not exist on 57 | # a real object. This is generally recommended, and will default to 58 | # `true` in RSpec 4. 59 | mocks.verify_partial_doubles = true 60 | end 61 | 62 | # The settings below are suggested to provide a good initial experience 63 | # with RSpec, but feel free to customize to your heart's content. 64 | =begin 65 | # These two settings work together to allow you to limit a spec run 66 | # to individual examples or groups you care about by tagging them with 67 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 68 | # get run. 69 | config.filter_run :focus 70 | config.run_all_when_everything_filtered = true 71 | 72 | # Allows RSpec to persist some state between runs in order to support 73 | # the `--only-failures` and `--next-failure` CLI options. We recommend 74 | # you configure your source control system to ignore this file. 75 | config.example_status_persistence_file_path = "spec/examples.txt" 76 | 77 | # Limits the available syntax to the non-monkey patched syntax that is 78 | # recommended. For more details, see: 79 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 80 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 81 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 82 | config.disable_monkey_patching! 83 | 84 | # This setting enables warnings. It's recommended, but in some cases may 85 | # be too noisy due to issues in dependencies. 86 | config.warnings = true 87 | 88 | # Many RSpec users commonly either run the entire suite or an individual 89 | # file, and it's useful to allow more verbose output when running an 90 | # individual spec file. 91 | if config.files_to_run.one? 92 | # Use the documentation formatter for detailed output, 93 | # unless a formatter has already been configured 94 | # (e.g. via a command-line flag). 95 | config.default_formatter = 'doc' 96 | end 97 | 98 | # Print the 10 slowest examples and example groups at the 99 | # end of the spec run, to help surface which specs are running 100 | # particularly slow. 101 | config.profile_examples = 10 102 | 103 | # Run specs in random order to surface order dependencies. If you find an 104 | # order dependency and want to debug it, you can fix the order by providing 105 | # the seed, which is printed after each run. 106 | # --seed 1234 107 | config.order = :random 108 | 109 | # Seed global randomization in this process using the `--seed` CLI option. 110 | # Setting this allows you to use `--seed` to deterministically reproduce 111 | # test failures related to randomization by passing the same `--seed` value 112 | # as the one that triggered the failure. 113 | Kernel.srand config.seed 114 | =end 115 | end 116 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/options.rb: -------------------------------------------------------------------------------- 1 | module DevCycle 2 | class Options 3 | attr_reader :config_polling_interval_ms 4 | attr_reader :enable_edge_db 5 | attr_reader :enable_cloud_bucketing 6 | attr_reader :enable_beta_realtime_updates 7 | attr_reader :disable_realtime_updates 8 | attr_reader :config_cdn_uri 9 | attr_reader :events_api_uri 10 | attr_reader :bucketing_api_uri 11 | attr_reader :logger 12 | 13 | def initialize( 14 | enable_cloud_bucketing: false, 15 | event_flush_interval_ms: 10_000, 16 | disable_custom_event_logging: false, 17 | disable_automatic_event_logging: false, 18 | config_polling_interval_ms: 10_000, 19 | enable_beta_realtime_updates: false, 20 | disable_realtime_updates: false, 21 | request_timeout_ms: 5_000, 22 | max_event_queue_size: 2_000, 23 | flush_event_queue_size: 1_000, 24 | event_request_chunk_size: 100, 25 | logger: nil, 26 | config_cdn_uri: 'https://config-cdn.devcycle.com', 27 | events_api_uri: 'https://events.devcycle.com', 28 | enable_edge_db: false 29 | ) 30 | @logger = logger || defined?(Rails) ? Rails.logger : Logger.new(STDOUT) 31 | @enable_cloud_bucketing = enable_cloud_bucketing 32 | 33 | @enable_edge_db = enable_edge_db 34 | 35 | if !@enable_cloud_bucketing && @enable_edge_db 36 | raise ArgumentError.new('Cannot enable edgedb without enabling cloud bucketing.') 37 | end 38 | 39 | if config_polling_interval_ms < 1000 40 | raise ArgumentError.new('config_polling_interval cannot be less than 1000ms') 41 | end 42 | @config_polling_interval_ms = config_polling_interval_ms 43 | 44 | if request_timeout_ms <= 5000 45 | request_timeout_ms = 5000 46 | end 47 | @request_timeout_ms = request_timeout_ms 48 | 49 | 50 | if event_flush_interval_ms < 500 || event_flush_interval_ms > (60 * 1000) 51 | raise ArgumentError.new('event_flush_interval_ms must be between 500ms and 1 minute') 52 | end 53 | @event_flush_interval_ms = event_flush_interval_ms 54 | 55 | if flush_event_queue_size >= max_event_queue_size 56 | raise ArgumentError.new( 57 | "flush_event_queue_size: #{flush_event_queue_size} must be " + 58 | "smaller than max_event_queue_size: #{@max_event_queue_size}" 59 | ) 60 | elsif flush_event_queue_size < event_request_chunk_size || max_event_queue_size < event_request_chunk_size 61 | throw ArgumentError.new( 62 | "flush_event_queue_size: #{flush_event_queue_size} and " + 63 | "max_event_queue_size: #{max_event_queue_size} " + 64 | "must be larger than event_request_chunk_size: #{event_request_chunk_size}" 65 | ) 66 | elsif flush_event_queue_size > 20000 || max_event_queue_size > 20000 67 | raise ArgumentError.new( 68 | "flush_event_queue_size: #{flush_event_queue_size} or " + 69 | "max_event_queue_size: #{max_event_queue_size} must be smaller than 20,000" 70 | ) 71 | end 72 | @flush_event_queue_size = flush_event_queue_size 73 | @max_event_queue_size = max_event_queue_size 74 | @event_request_chunk_size = event_request_chunk_size 75 | 76 | @disable_custom_event_logging = disable_custom_event_logging 77 | @disable_automatic_event_logging = disable_automatic_event_logging 78 | 79 | if enable_beta_realtime_updates 80 | warn '[DEPRECATION] `enable_beta_realtime_updates` is deprecated and will be removed in a future release.' 81 | end 82 | @enable_beta_realtime_updates = enable_beta_realtime_updates 83 | @disable_realtime_updates = disable_realtime_updates 84 | @config_cdn_uri = config_cdn_uri 85 | @events_api_uri = events_api_uri 86 | @bucketing_api_uri = "https://bucketing-api.devcyle.com" 87 | end 88 | 89 | def event_queue_options 90 | EventQueueOptions.new( 91 | @event_flush_interval_ms, 92 | @disable_automatic_event_logging, 93 | @disable_custom_event_logging, 94 | @max_event_queue_size, 95 | @flush_event_queue_size, 96 | @events_api_uri, 97 | @event_request_chunk_size, 98 | @logger 99 | ) 100 | end 101 | end 102 | 103 | class EventQueueOptions 104 | attr_reader :event_flush_interval_ms 105 | attr_reader :disable_automatic_event_logging 106 | attr_reader :disable_custom_event_logging 107 | attr_reader :max_event_queue_size 108 | attr_reader :flush_event_queue_size 109 | attr_reader :events_api_uri 110 | attr_reader :event_request_chunk_size 111 | attr_reader :logger 112 | 113 | def initialize ( 114 | event_flush_interval_ms, 115 | disable_automatic_event_logging, 116 | disable_custom_event_logging, 117 | max_event_queue_size, 118 | flush_event_queue_size, 119 | events_api_uri, 120 | event_request_chunk_size, 121 | logger 122 | ) 123 | @event_flush_interval_ms = event_flush_interval_ms 124 | @disable_automatic_event_logging = disable_automatic_event_logging 125 | @disable_custom_event_logging = disable_custom_event_logging 126 | @max_event_queue_size = max_event_queue_size 127 | @flush_event_queue_size = flush_event_queue_size 128 | @events_api_uri = events_api_uri 129 | @event_request_chunk_size = event_request_chunk_size 130 | @logger = logger 131 | end 132 | end 133 | 134 | # @deprecated Use `DevCycle::Options` instead. 135 | DVCOptions = Options 136 | end 137 | -------------------------------------------------------------------------------- /spec/api/devcycle_api_spec.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'spec_helper' 14 | require 'json' 15 | require 'webmock' 16 | 17 | include WebMock::API 18 | 19 | # Unit tests for DevCycle::Client 20 | # Automatically generated by openapi-generator (https://openapi-generator.tech) 21 | # Please update as you see appropriate 22 | describe 'DevCycle::Client' do 23 | before(:all) do 24 | sdk_key = ENV["DEVCYCLE_SERVER_SDK_KEY"] 25 | if sdk_key.nil? 26 | puts("SDK KEY NOT SET - SKIPPING INIT") 27 | @skip_tests = true 28 | else 29 | # run before each test 30 | options = DevCycle::Options.new(enable_cloud_bucketing: true) 31 | @api_instance = DevCycle::Client.new(sdk_key, options) 32 | 33 | @user = DevCycle::User.new({ 34 | user_id: 'test-user', 35 | appVersion: '1.2.3' 36 | }) 37 | @skip_tests = false 38 | end 39 | end 40 | 41 | after do 42 | # run after each test 43 | end 44 | 45 | describe 'test an instance of DevcycleApi' do 46 | it 'should create an instance of DevcycleApi' do 47 | expect(@api_instance).to be_instance_of(DevCycle::Client) 48 | end 49 | end 50 | 51 | # unit tests for get_features 52 | # Get all features by key for user data 53 | # @param user 54 | # @param [Hash] opts the optional parameters 55 | # @return [Hash] 56 | describe 'get_features test' do 57 | it 'should work' do # but it don't 58 | #result = @api_instance.all_features(@user) 59 | 60 | #expect(result.length).to eq 1 61 | end 62 | end 63 | 64 | # unit tests for get_variable_by_key 65 | # Get variable by key for user data 66 | # @param key Variable key 67 | # @param user 68 | # @param [Hash] opts the optional parameters 69 | # @return [Variable] 70 | describe 'get_variable_by_key ruby-example-tests' do 71 | it 'should work' do 72 | result = @api_instance.variable(@user, "ruby-example-tests-default", false) 73 | expect(result.isDefaulted).to eq true 74 | expect(result.eval[:reason]).to eq "DEFAULT" 75 | expect(result.eval[:details]).to eq "Error" 76 | 77 | result = @api_instance.variable_value(@user, "ruby-example-tests-default", true) 78 | expect(result).to eq true 79 | end 80 | end 81 | 82 | # unit tests for get_variable_by_key 83 | # Get variable by key for user data 84 | # @param key Variable key 85 | # @param user 86 | # @param [Hash] opts the optional parameters 87 | # @return [Variable] 88 | describe 'get_variable_by_key test' do 89 | it 'should work' do 90 | result = @api_instance.variable(@user, "test", false) 91 | expect(result.isDefaulted).to eq false 92 | expect(result.value).to eq true 93 | 94 | result = @api_instance.variable_value(@user, "test", true) 95 | expect(result).to eq true 96 | 97 | result = @api_instance.variable(@user, "test-number-variable", 0) 98 | expect(result.isDefaulted).to eq false 99 | expect(result.value).to eq 123 100 | 101 | result = @api_instance.variable(@user, "test-number-variable", 0) 102 | expect(result.isDefaulted).to eq false 103 | expect(result.value).to eq 123 104 | 105 | result = @api_instance.variable(@user, "test-float-variable", 0.0) 106 | expect(result.isDefaulted).to eq false 107 | expect(result.value).to eq 4.56 108 | 109 | result = @api_instance.variable(@user, "test-string-variable", "") 110 | expect(result.isDefaulted).to eq false 111 | expect(result.value).to eq "on" 112 | 113 | result = @api_instance.variable(@user, "test-json-variable", {}) 114 | expect(result.isDefaulted).to eq false 115 | expect(result.value).to eq({:message => "a"}) 116 | 117 | result = @api_instance.variable(@user, "test", "false") 118 | expect(result.isDefaulted).to eq true 119 | expect(result.eval[:reason]).to eq "DEFAULT" 120 | expect(result.eval[:details]).to eq "Variable Type Mismatch" 121 | 122 | result = @api_instance.variable(@user, "test-number-variable", "123") 123 | expect(result.isDefaulted).to eq true 124 | expect(result.eval[:reason]).to eq "DEFAULT" 125 | expect(result.eval[:details]).to eq "Variable Type Mismatch" 126 | 127 | result = @api_instance.variable(@user, "test-number-variable", true) 128 | expect(result.isDefaulted).to eq true 129 | expect(result.eval[:reason]).to eq "DEFAULT" 130 | expect(result.eval[:details]).to eq "Variable Type Mismatch" 131 | 132 | result = @api_instance.variable(@user, "test-string-variable", true) 133 | expect(result.isDefaulted).to eq true 134 | expect(result.eval[:reason]).to eq "DEFAULT" 135 | expect(result.eval[:details]).to eq "Variable Type Mismatch" 136 | 137 | result = @api_instance.variable(@user, "test-json-variable", "on") 138 | expect(result.isDefaulted).to eq true 139 | expect(result.eval[:reason]).to eq "DEFAULT" 140 | expect(result.eval[:details]).to eq "Variable Type Mismatch" 141 | end 142 | end 143 | 144 | # unit tests for get_variables 145 | # Get all variables by key for user data 146 | # @param user 147 | # @param [Hash] opts the optional parameters 148 | # @return [Hash] 149 | describe 'get_variables test' do 150 | it 'should work' do 151 | result = @api_instance.all_variables(@user) 152 | 153 | expect(result.length >= 1).to eq true 154 | end 155 | end 156 | 157 | describe 'get_variable_by_key test' do 158 | before do 159 | WebMock.disable_net_connect!(allow_localhost: true) 160 | end 161 | 162 | after do 163 | WebMock.allow_net_connect! 164 | end 165 | 166 | it 'should work with mocked response' do 167 | stub_request(:post, "https://bucketing-api.devcycle.com/v1/variables/mocked_variable"). 168 | to_return(status: 200, body: "{\"isDefaulted\": false, \"value\": true, \"eval\": {\"reason\": \"SPLIT\", \"details\": \"Random Distribution | All Users\", \"target_id\": \"621642332ea68943c8833c4d\"}}", headers: {}) 169 | 170 | result = @api_instance.variable(@user, "mocked_variable", false) 171 | expect(result.isDefaulted).to eq false 172 | expect(result.value).to eq true 173 | # Use Hash syntax since eval is deserialized as a Hash 174 | expect(result.eval[:reason]).to eq "SPLIT" 175 | expect(result.eval[:details]).to eq "Random Distribution | All Users" 176 | expect(result.eval[:target_id]).to eq "621642332ea68943c8833c4d" 177 | end 178 | 179 | it 'should return error details' do 180 | stub_request(:post, "https://bucketing-api.devcycle.com/v1/variables/test"). 181 | to_return(status: 500, body: "{\"isDefaulted\": true, \"value\": false, \"eval\": {\"reason\": \"DEFAULT\", \"details\": \"Error\"}}", headers: {}) 182 | 183 | result = @api_instance.variable(@user, "test", false) 184 | expect(result.isDefaulted).to eq true 185 | expect(result.eval[:reason]).to eq "DEFAULT" 186 | expect(result.eval[:details]).to eq "Error" 187 | end 188 | end 189 | 190 | end 191 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/inline_response201.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'date' 14 | require 'time' 15 | 16 | module DevCycle 17 | class InlineResponse201 18 | attr_accessor :message 19 | 20 | # Attribute mapping from ruby-style variable name to JSON key. 21 | def self.attribute_map 22 | { 23 | :'message' => :'message' 24 | } 25 | end 26 | 27 | # Returns all the JSON keys this model knows about 28 | def self.acceptable_attributes 29 | attribute_map.values 30 | end 31 | 32 | # Attribute type mapping. 33 | def self.openapi_types 34 | { 35 | :'message' => :'String' 36 | } 37 | end 38 | 39 | # List of attributes with nullable: true 40 | def self.openapi_nullable 41 | Set.new([ 42 | ]) 43 | end 44 | 45 | # Initializes the object 46 | # @param [Hash] attributes Model attributes in the form of hash 47 | def initialize(attributes = {}) 48 | if (!attributes.is_a?(Hash)) 49 | fail ArgumentError, "The input argument (attributes) must be a hash in `DevCycle::InlineResponse201` initialize method" 50 | end 51 | 52 | # check to see if the attribute exists and convert string to symbol for hash key 53 | attributes = attributes.each_with_object({}) { |(k, v), h| 54 | if (!self.class.attribute_map.key?(k.to_sym)) 55 | fail ArgumentError, "`#{k}` is not a valid attribute in `DevCycle::InlineResponse201`. Please check the name to make sure it's valid. List of attributes: " + self.class.attribute_map.keys.inspect 56 | end 57 | h[k.to_sym] = v 58 | } 59 | 60 | if attributes.key?(:'message') 61 | self.message = attributes[:'message'] 62 | end 63 | end 64 | 65 | # Show invalid properties with the reasons. Usually used together with valid? 66 | # @return Array for valid properties with the reasons 67 | def list_invalid_properties 68 | invalid_properties = Array.new 69 | invalid_properties 70 | end 71 | 72 | # Check to see if the all the properties in the model are valid 73 | # @return true if the model is valid 74 | def valid? 75 | true 76 | end 77 | 78 | # Checks equality by comparing each attribute. 79 | # @param [Object] Object to be compared 80 | def ==(o) 81 | return true if self.equal?(o) 82 | self.class == o.class && 83 | message == o.message 84 | end 85 | 86 | # @see the `==` method 87 | # @param [Object] Object to be compared 88 | def eql?(o) 89 | self == o 90 | end 91 | 92 | # Calculates hash code according to all attributes. 93 | # @return [Integer] Hash code 94 | def hash 95 | [message].hash 96 | end 97 | 98 | # Builds the object from hash 99 | # @param [Hash] attributes Model attributes in the form of hash 100 | # @return [Object] Returns the model itself 101 | def self.build_from_hash(attributes) 102 | new.build_from_hash(attributes) 103 | end 104 | 105 | # Builds the object from hash 106 | # @param [Hash] attributes Model attributes in the form of hash 107 | # @return [Object] Returns the model itself 108 | def build_from_hash(attributes) 109 | return nil unless attributes.is_a?(Hash) 110 | self.class.openapi_types.each_pair do |key, type| 111 | if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) 112 | self.send("#{key}=", nil) 113 | elsif type =~ /\AArray<(.*)>/i 114 | # check to ensure the input is an array given that the attribute 115 | # is documented as an array but the input is not 116 | if attributes[self.class.attribute_map[key]].is_a?(Array) 117 | self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) 118 | end 119 | elsif !attributes[self.class.attribute_map[key]].nil? 120 | self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) 121 | end 122 | end 123 | 124 | self 125 | end 126 | 127 | # Deserializes the data based on type 128 | # @param string type Data type 129 | # @param string value Value to be deserialized 130 | # @return [Object] Deserialized data 131 | def _deserialize(type, value) 132 | case type.to_sym 133 | when :Time 134 | Time.parse(value) 135 | when :Date 136 | Date.parse(value) 137 | when :String 138 | value.to_s 139 | when :Integer 140 | value.to_i 141 | when :Float 142 | value.to_f 143 | when :Boolean 144 | if value.to_s =~ /\A(true|t|yes|y|1)\z/i 145 | true 146 | else 147 | false 148 | end 149 | when :Object 150 | # generic object (usually a Hash), return directly 151 | value 152 | when /\AArray<(?.+)>\z/ 153 | inner_type = Regexp.last_match[:inner_type] 154 | value.map { |v| _deserialize(inner_type, v) } 155 | when /\AHash<(?.+?), (?.+)>\z/ 156 | k_type = Regexp.last_match[:k_type] 157 | v_type = Regexp.last_match[:v_type] 158 | {}.tap do |hash| 159 | value.each do |k, v| 160 | hash[_deserialize(k_type, k)] = _deserialize(v_type, v) 161 | end 162 | end 163 | else # model 164 | # models (e.g. Pet) or oneOf 165 | klass = DevCycle.const_get(type) 166 | klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) 167 | end 168 | end 169 | 170 | # Returns the string representation of the object 171 | # @return [String] String presentation of the object 172 | def to_s 173 | to_hash.to_s 174 | end 175 | 176 | # to_body is an alias to to_hash (backward compatibility) 177 | # @return [Hash] Returns the object in the form of hash 178 | def to_body 179 | to_hash 180 | end 181 | 182 | # Returns the object in the form of hash 183 | # @return [Hash] Returns the object in the form of hash 184 | def to_hash 185 | hash = {} 186 | self.class.attribute_map.each_pair do |attr, param| 187 | value = self.send(attr) 188 | if value.nil? 189 | is_nullable = self.class.openapi_nullable.include?(attr) 190 | next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) 191 | end 192 | 193 | hash[param] = _to_hash(value) 194 | end 195 | hash 196 | end 197 | 198 | # Outputs non-array value in the form of hash 199 | # For object, use to_hash. Otherwise, just return the value 200 | # @param [Object] value Any valid value 201 | # @return [Hash] Returns the value in the form of hash 202 | def _to_hash(value) 203 | if value.is_a?(Array) 204 | value.compact.map { |v| _to_hash(v) } 205 | elsif value.is_a?(Hash) 206 | {}.tap do |hash| 207 | value.each { |k, v| hash[k] = _to_hash(v) } 208 | end 209 | elsif value.respond_to? :to_hash 210 | value.to_hash 211 | else 212 | value 213 | end 214 | end 215 | 216 | end 217 | 218 | end 219 | -------------------------------------------------------------------------------- /sorbet/rbi/annotations/rainbow.rbi: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This file was pulled from a central RBI files repository. 5 | # Please run `bin/tapioca annotations` to update it. 6 | 7 | module Rainbow 8 | # @shim: https://github.com/sickill/rainbow/blob/master/lib/rainbow.rb#L10-L12 9 | sig { returns(T::Boolean) } 10 | attr_accessor :enabled 11 | 12 | class Color 13 | sig { returns(Symbol) } 14 | attr_reader :ground 15 | 16 | sig { params(ground: Symbol, values: T.any([Integer], [Integer, Integer, Integer])).returns(Color) } 17 | def self.build(ground, values); end 18 | 19 | sig { params(hex: String).returns([Integer, Integer, Integer]) } 20 | def self.parse_hex_color(hex); end 21 | 22 | class Indexed < Rainbow::Color 23 | sig { returns(Integer) } 24 | attr_reader :num 25 | 26 | sig { params(ground: Symbol, num: Integer).void } 27 | def initialize(ground, num); end 28 | 29 | sig { returns(T::Array[Integer]) } 30 | def codes; end 31 | end 32 | 33 | class Named < Rainbow::Color::Indexed 34 | NAMES = T.let(nil, T::Hash[Symbol, Integer]) 35 | 36 | sig { params(ground: Symbol, name: Symbol).void } 37 | def initialize(ground, name); end 38 | 39 | sig { returns(T::Array[Symbol]) } 40 | def self.color_names; end 41 | 42 | sig { returns(String) } 43 | def self.valid_names; end 44 | end 45 | 46 | class RGB < Rainbow::Color::Indexed 47 | sig { returns(Integer) } 48 | attr_reader :r, :g, :b 49 | 50 | sig { params(ground: Symbol, values: Integer).void } 51 | def initialize(ground, *values); end 52 | 53 | sig { returns(T::Array[Integer]) } 54 | def codes; end 55 | 56 | sig { params(value: Numeric).returns(Integer) } 57 | def self.to_ansi_domain(value); end 58 | end 59 | 60 | class X11Named < Rainbow::Color::RGB 61 | include Rainbow::X11ColorNames 62 | 63 | sig { returns(T::Array[Symbol]) } 64 | def self.color_names; end 65 | 66 | sig { returns(String) } 67 | def self.valid_names; end 68 | 69 | sig { params(ground: Symbol, name: Symbol).void } 70 | def initialize(ground, name); end 71 | end 72 | end 73 | 74 | sig { returns(Wrapper) } 75 | def self.global; end 76 | 77 | sig { returns(T::Boolean) } 78 | def self.enabled; end 79 | 80 | sig { params(value: T::Boolean).returns(T::Boolean) } 81 | def self.enabled=(value); end 82 | 83 | sig { params(string: String).returns(String) } 84 | def self.uncolor(string); end 85 | 86 | class NullPresenter < String 87 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(NullPresenter) } 88 | def color(*values); end 89 | 90 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(NullPresenter) } 91 | def foreground(*values); end 92 | 93 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(NullPresenter) } 94 | def fg(*values); end 95 | 96 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(NullPresenter) } 97 | def background(*values); end 98 | 99 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(NullPresenter) } 100 | def bg(*values); end 101 | 102 | sig { returns(NullPresenter) } 103 | def reset; end 104 | 105 | sig { returns(NullPresenter) } 106 | def bright; end 107 | 108 | sig { returns(NullPresenter) } 109 | def faint; end 110 | 111 | sig { returns(NullPresenter) } 112 | def italic; end 113 | 114 | sig { returns(NullPresenter) } 115 | def underline; end 116 | 117 | sig { returns(NullPresenter) } 118 | def blink; end 119 | 120 | sig { returns(NullPresenter) } 121 | def inverse; end 122 | 123 | sig { returns(NullPresenter) } 124 | def hide; end 125 | 126 | sig { returns(NullPresenter) } 127 | def cross_out; end 128 | 129 | sig { returns(NullPresenter) } 130 | def black; end 131 | 132 | sig { returns(NullPresenter) } 133 | def red; end 134 | 135 | sig { returns(NullPresenter) } 136 | def green; end 137 | 138 | sig { returns(NullPresenter) } 139 | def yellow; end 140 | 141 | sig { returns(NullPresenter) } 142 | def blue; end 143 | 144 | sig { returns(NullPresenter) } 145 | def magenta; end 146 | 147 | sig { returns(NullPresenter) } 148 | def cyan; end 149 | 150 | sig { returns(NullPresenter) } 151 | def white; end 152 | 153 | sig { returns(NullPresenter) } 154 | def bold; end 155 | 156 | sig { returns(NullPresenter) } 157 | def dark; end 158 | 159 | sig { returns(NullPresenter) } 160 | def strike; end 161 | end 162 | 163 | class Presenter < String 164 | TERM_EFFECTS = T.let(nil, T::Hash[Symbol, Integer]) 165 | 166 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(Presenter) } 167 | def color(*values); end 168 | 169 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(Presenter) } 170 | def foreground(*values); end 171 | 172 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(Presenter) } 173 | def fg(*values); end 174 | 175 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(Presenter) } 176 | def background(*values); end 177 | 178 | sig { params(values: T.any([Integer], [Integer, Integer, Integer])).returns(Presenter) } 179 | def bg(*values); end 180 | 181 | sig { returns(Presenter) } 182 | def reset; end 183 | 184 | sig { returns(Presenter) } 185 | def bright; end 186 | 187 | sig { returns(Presenter) } 188 | def faint; end 189 | 190 | sig { returns(Presenter) } 191 | def italic; end 192 | 193 | sig { returns(Presenter) } 194 | def underline; end 195 | 196 | sig { returns(Presenter) } 197 | def blink; end 198 | 199 | sig { returns(Presenter) } 200 | def inverse; end 201 | 202 | sig { returns(Presenter) } 203 | def hide; end 204 | 205 | sig { returns(Presenter) } 206 | def cross_out; end 207 | 208 | sig { returns(Presenter) } 209 | def black; end 210 | 211 | sig { returns(Presenter) } 212 | def red; end 213 | 214 | sig { returns(Presenter) } 215 | def green; end 216 | 217 | sig { returns(Presenter) } 218 | def yellow; end 219 | 220 | sig { returns(Presenter) } 221 | def blue; end 222 | 223 | sig { returns(Presenter) } 224 | def magenta; end 225 | 226 | sig { returns(Presenter) } 227 | def cyan; end 228 | 229 | sig { returns(Presenter) } 230 | def white; end 231 | 232 | sig { returns(Presenter) } 233 | def bold; end 234 | 235 | sig { returns(Presenter) } 236 | def dark; end 237 | 238 | sig { returns(Presenter) } 239 | def strike; end 240 | end 241 | 242 | class StringUtils 243 | sig { params(string: String, codes: T::Array[Integer]).returns(String) } 244 | def self.wrap_with_sgr(string, codes); end 245 | 246 | sig { params(string: String).returns(String) } 247 | def self.uncolor(string); end 248 | end 249 | 250 | VERSION = T.let(nil, String) 251 | 252 | class Wrapper 253 | sig { returns(T::Boolean) } 254 | attr_accessor :enabled 255 | 256 | sig { params(enabled: T::Boolean).void } 257 | def initialize(enabled = true); end 258 | 259 | sig { params(string: String).returns(T.any(Rainbow::Presenter, Rainbow::NullPresenter)) } 260 | def wrap(string); end 261 | end 262 | 263 | module X11ColorNames 264 | NAMES = T.let(nil, T::Hash[Symbol, [Integer, Integer, Integer]]) 265 | end 266 | end 267 | 268 | sig { params(string: String).returns(Rainbow::Presenter) } 269 | def Rainbow(string); end 270 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/user_data_and_events_body.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'date' 14 | require 'time' 15 | 16 | module DevCycle 17 | class UserDataAndEventsBody 18 | attr_accessor :events 19 | 20 | attr_accessor :user 21 | 22 | # Attribute mapping from ruby-style variable name to JSON key. 23 | def self.attribute_map 24 | { 25 | :'events' => :'events', 26 | :'user' => :'user' 27 | } 28 | end 29 | 30 | # Returns all the JSON keys this model knows about 31 | def self.acceptable_attributes 32 | attribute_map.values 33 | end 34 | 35 | # Attribute type mapping. 36 | def self.openapi_types 37 | { 38 | :'events' => :'Array', 39 | :'user' => :'User' 40 | } 41 | end 42 | 43 | # List of attributes with nullable: true 44 | def self.openapi_nullable 45 | Set.new([ 46 | ]) 47 | end 48 | 49 | # Initializes the object 50 | # @param [Hash] attributes Model attributes in the form of hash 51 | def initialize(attributes = {}) 52 | if (!attributes.is_a?(Hash)) 53 | fail ArgumentError, "The input argument (attributes) must be a hash in `DevCycle::UserDataAndEventsBody` initialize method" 54 | end 55 | 56 | # check to see if the attribute exists and convert string to symbol for hash key 57 | attributes = attributes.each_with_object({}) { |(k, v), h| 58 | if (!self.class.attribute_map.key?(k.to_sym)) 59 | fail ArgumentError, "`#{k}` is not a valid attribute in `DevCycle::UserDataAndEventsBody`. Please check the name to make sure it's valid. List of attributes: " + self.class.attribute_map.keys.inspect 60 | end 61 | h[k.to_sym] = v 62 | } 63 | 64 | if attributes.key?(:'events') 65 | if (value = attributes[:'events']).is_a?(Array) 66 | self.events = value 67 | end 68 | end 69 | 70 | if attributes.key?(:'user') 71 | self.user = attributes[:'user'] 72 | end 73 | end 74 | 75 | # Show invalid properties with the reasons. Usually used together with valid? 76 | # @return Array for valid properties with the reasons 77 | def list_invalid_properties 78 | invalid_properties = Array.new 79 | invalid_properties 80 | end 81 | 82 | # Check to see if the all the properties in the model are valid 83 | # @return true if the model is valid 84 | def valid? 85 | true 86 | end 87 | 88 | # Checks equality by comparing each attribute. 89 | # @param [Object] Object to be compared 90 | def ==(o) 91 | return true if self.equal?(o) 92 | self.class == o.class && 93 | events == o.events && 94 | user == o.user 95 | end 96 | 97 | # @see the `==` method 98 | # @param [Object] Object to be compared 99 | def eql?(o) 100 | self == o 101 | end 102 | 103 | # Calculates hash code according to all attributes. 104 | # @return [Integer] Hash code 105 | def hash 106 | [events, user].hash 107 | end 108 | 109 | # Builds the object from hash 110 | # @param [Hash] attributes Model attributes in the form of hash 111 | # @return [Object] Returns the model itself 112 | def self.build_from_hash(attributes) 113 | new.build_from_hash(attributes) 114 | end 115 | 116 | # Builds the object from hash 117 | # @param [Hash] attributes Model attributes in the form of hash 118 | # @return [Object] Returns the model itself 119 | def build_from_hash(attributes) 120 | return nil unless attributes.is_a?(Hash) 121 | self.class.openapi_types.each_pair do |key, type| 122 | if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) 123 | self.send("#{key}=", nil) 124 | elsif type =~ /\AArray<(.*)>/i 125 | # check to ensure the input is an array given that the attribute 126 | # is documented as an array but the input is not 127 | if attributes[self.class.attribute_map[key]].is_a?(Array) 128 | self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) 129 | end 130 | elsif !attributes[self.class.attribute_map[key]].nil? 131 | self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) 132 | end 133 | end 134 | 135 | self 136 | end 137 | 138 | # Deserializes the data based on type 139 | # @param string type Data type 140 | # @param string value Value to be deserialized 141 | # @return [Object] Deserialized data 142 | def _deserialize(type, value) 143 | case type.to_sym 144 | when :Time 145 | Time.parse(value) 146 | when :Date 147 | Date.parse(value) 148 | when :String 149 | value.to_s 150 | when :Integer 151 | value.to_i 152 | when :Float 153 | value.to_f 154 | when :Boolean 155 | if value.to_s =~ /\A(true|t|yes|y|1)\z/i 156 | true 157 | else 158 | false 159 | end 160 | when :Object 161 | # generic object (usually a Hash), return directly 162 | value 163 | when /\AArray<(?.+)>\z/ 164 | inner_type = Regexp.last_match[:inner_type] 165 | value.map { |v| _deserialize(inner_type, v) } 166 | when /\AHash<(?.+?), (?.+)>\z/ 167 | k_type = Regexp.last_match[:k_type] 168 | v_type = Regexp.last_match[:v_type] 169 | {}.tap do |hash| 170 | value.each do |k, v| 171 | hash[_deserialize(k_type, k)] = _deserialize(v_type, v) 172 | end 173 | end 174 | else # model 175 | # models (e.g. Pet) or oneOf 176 | klass = DevCycle.const_get(type) 177 | klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) 178 | end 179 | end 180 | 181 | # Returns the string representation of the object 182 | # @return [String] String presentation of the object 183 | def to_s 184 | to_hash.to_s 185 | end 186 | 187 | # to_body is an alias to to_hash (backward compatibility) 188 | # @return [Hash] Returns the object in the form of hash 189 | def to_body 190 | to_hash 191 | end 192 | 193 | # Returns the object in the form of hash 194 | # @return [Hash] Returns the object in the form of hash 195 | def to_hash 196 | hash = {} 197 | self.class.attribute_map.each_pair do |attr, param| 198 | value = self.send(attr) 199 | if value.nil? 200 | is_nullable = self.class.openapi_nullable.include?(attr) 201 | next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) 202 | end 203 | 204 | hash[param] = _to_hash(value) 205 | end 206 | hash 207 | end 208 | 209 | # Outputs non-array value in the form of hash 210 | # For object, use to_hash. Otherwise, just return the value 211 | # @param [Object] value Any valid value 212 | # @return [Hash] Returns the value in the form of hash 213 | def _to_hash(value) 214 | if value.is_a?(Array) 215 | value.compact.map { |v| _to_hash(v) } 216 | elsif value.is_a?(Hash) 217 | {}.tap do |hash| 218 | value.each { |k, v| hash[k] = _to_hash(v) } 219 | end 220 | elsif value.respond_to? :to_hash 221 | value.to_hash 222 | else 223 | value 224 | end 225 | end 226 | 227 | end 228 | 229 | end 230 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/error_response.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'date' 14 | require 'time' 15 | 16 | module DevCycle 17 | class ErrorResponse 18 | # Error message 19 | attr_accessor :message 20 | 21 | # Additional error information detailing the error reasoning 22 | attr_accessor :data 23 | 24 | # Attribute mapping from ruby-style variable name to JSON key. 25 | def self.attribute_map 26 | { 27 | :'message' => :'message', 28 | :'data' => :'data' 29 | } 30 | end 31 | 32 | # Returns all the JSON keys this model knows about 33 | def self.acceptable_attributes 34 | attribute_map.values 35 | end 36 | 37 | # Attribute type mapping. 38 | def self.openapi_types 39 | { 40 | :'message' => :'String', 41 | :'data' => :'Object' 42 | } 43 | end 44 | 45 | # List of attributes with nullable: true 46 | def self.openapi_nullable 47 | Set.new([ 48 | ]) 49 | end 50 | 51 | # Initializes the object 52 | # @param [Hash] attributes Model attributes in the form of hash 53 | def initialize(attributes = {}) 54 | if (!attributes.is_a?(Hash)) 55 | fail ArgumentError, "The input argument (attributes) must be a hash in `DevCycle::ErrorResponse` initialize method" 56 | end 57 | 58 | # check to see if the attribute exists and convert string to symbol for hash key 59 | attributes = attributes.each_with_object({}) { |(k, v), h| 60 | if (!self.class.attribute_map.key?(k.to_sym)) 61 | fail ArgumentError, "`#{k}` is not a valid attribute in `DevCycle::ErrorResponse`. Please check the name to make sure it's valid. List of attributes: " + self.class.attribute_map.keys.inspect 62 | end 63 | h[k.to_sym] = v 64 | } 65 | 66 | if attributes.key?(:'message') 67 | self.message = attributes[:'message'] 68 | end 69 | 70 | if attributes.key?(:'data') 71 | self.data = attributes[:'data'] 72 | end 73 | end 74 | 75 | # Show invalid properties with the reasons. Usually used together with valid? 76 | # @return Array for valid properties with the reasons 77 | def list_invalid_properties 78 | invalid_properties = Array.new 79 | if @message.nil? 80 | invalid_properties.push('invalid value for "message", message cannot be nil.') 81 | end 82 | 83 | invalid_properties 84 | end 85 | 86 | # Check to see if the all the properties in the model are valid 87 | # @return true if the model is valid 88 | def valid? 89 | return false if @message.nil? 90 | true 91 | end 92 | 93 | # Checks equality by comparing each attribute. 94 | # @param [Object] Object to be compared 95 | def ==(o) 96 | return true if self.equal?(o) 97 | self.class == o.class && 98 | message == o.message && 99 | data == o.data 100 | end 101 | 102 | # @see the `==` method 103 | # @param [Object] Object to be compared 104 | def eql?(o) 105 | self == o 106 | end 107 | 108 | # Calculates hash code according to all attributes. 109 | # @return [Integer] Hash code 110 | def hash 111 | [message, data].hash 112 | end 113 | 114 | # Builds the object from hash 115 | # @param [Hash] attributes Model attributes in the form of hash 116 | # @return [Object] Returns the model itself 117 | def self.build_from_hash(attributes) 118 | new.build_from_hash(attributes) 119 | end 120 | 121 | # Builds the object from hash 122 | # @param [Hash] attributes Model attributes in the form of hash 123 | # @return [Object] Returns the model itself 124 | def build_from_hash(attributes) 125 | return nil unless attributes.is_a?(Hash) 126 | self.class.openapi_types.each_pair do |key, type| 127 | if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) 128 | self.send("#{key}=", nil) 129 | elsif type =~ /\AArray<(.*)>/i 130 | # check to ensure the input is an array given that the attribute 131 | # is documented as an array but the input is not 132 | if attributes[self.class.attribute_map[key]].is_a?(Array) 133 | self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) 134 | end 135 | elsif !attributes[self.class.attribute_map[key]].nil? 136 | self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) 137 | end 138 | end 139 | 140 | self 141 | end 142 | 143 | # Deserializes the data based on type 144 | # @param string type Data type 145 | # @param string value Value to be deserialized 146 | # @return [Object] Deserialized data 147 | def _deserialize(type, value) 148 | case type.to_sym 149 | when :Time 150 | Time.parse(value) 151 | when :Date 152 | Date.parse(value) 153 | when :String 154 | value.to_s 155 | when :Integer 156 | value.to_i 157 | when :Float 158 | value.to_f 159 | when :Boolean 160 | if value.to_s =~ /\A(true|t|yes|y|1)\z/i 161 | true 162 | else 163 | false 164 | end 165 | when :Object 166 | # generic object (usually a Hash), return directly 167 | value 168 | when /\AArray<(?.+)>\z/ 169 | inner_type = Regexp.last_match[:inner_type] 170 | value.map { |v| _deserialize(inner_type, v) } 171 | when /\AHash<(?.+?), (?.+)>\z/ 172 | k_type = Regexp.last_match[:k_type] 173 | v_type = Regexp.last_match[:v_type] 174 | {}.tap do |hash| 175 | value.each do |k, v| 176 | hash[_deserialize(k_type, k)] = _deserialize(v_type, v) 177 | end 178 | end 179 | else # model 180 | # models (e.g. Pet) or oneOf 181 | klass = DevCycle.const_get(type) 182 | klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) 183 | end 184 | end 185 | 186 | # Returns the string representation of the object 187 | # @return [String] String presentation of the object 188 | def to_s 189 | to_hash.to_s 190 | end 191 | 192 | # to_body is an alias to to_hash (backward compatibility) 193 | # @return [Hash] Returns the object in the form of hash 194 | def to_body 195 | to_hash 196 | end 197 | 198 | # Returns the object in the form of hash 199 | # @return [Hash] Returns the object in the form of hash 200 | def to_hash 201 | hash = {} 202 | self.class.attribute_map.each_pair do |attr, param| 203 | value = self.send(attr) 204 | if value.nil? 205 | is_nullable = self.class.openapi_nullable.include?(attr) 206 | next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) 207 | end 208 | 209 | hash[param] = _to_hash(value) 210 | end 211 | hash 212 | end 213 | 214 | # Outputs non-array value in the form of hash 215 | # For object, use to_hash. Otherwise, just return the value 216 | # @param [Object] value Any valid value 217 | # @return [Hash] Returns the value in the form of hash 218 | def _to_hash(value) 219 | if value.is_a?(Array) 220 | value.compact.map { |v| _to_hash(v) } 221 | elsif value.is_a?(Hash) 222 | {}.tap do |hash| 223 | value.each { |k, v| hash[k] = _to_hash(v) } 224 | end 225 | elsif value.respond_to? :to_hash 226 | value.to_hash 227 | else 228 | value 229 | end 230 | end 231 | 232 | end 233 | 234 | end 235 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/localbucketing/config_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'sorbet-runtime' 4 | require 'concurrent-ruby' 5 | require 'typhoeus' 6 | require 'json' 7 | require 'time' 8 | require 'ld-eventsource' 9 | 10 | module DevCycle 11 | class ConfigManager 12 | extend T::Sig 13 | sig { params( 14 | sdkKey: String, 15 | local_bucketing: LocalBucketing, 16 | wait_for_init: T::Boolean 17 | ).void } 18 | def initialize(sdkKey, local_bucketing, wait_for_init) 19 | @first_load = true 20 | @config_version = "v2" 21 | @local_bucketing = local_bucketing 22 | @sdkKey = sdkKey 23 | @sse_url = "" 24 | @sse_polling = false 25 | @config_e_tag = "" 26 | @config_last_modified = "" 27 | @logger = local_bucketing.options.logger 28 | @enable_sse = !local_bucketing.options.disable_realtime_updates 29 | @polling_enabled = true 30 | @sse_active = false 31 | @max_config_retries = 2 32 | @config_poller = Concurrent::TimerTask.new({ 33 | execution_interval: @local_bucketing.options.config_polling_interval_ms.fdiv(1000) 34 | }) do |_| 35 | fetch_config 36 | end 37 | 38 | t = Thread.new { initialize_config } 39 | t.join if wait_for_init 40 | end 41 | 42 | def initialize_config 43 | begin 44 | fetch_config 45 | start_polling(false) 46 | rescue => e 47 | @logger.error("DevCycle: Error Initializing Config: #{e.message}") 48 | ensure 49 | @local_bucketing.initialized = true 50 | end 51 | end 52 | 53 | def fetch_config(min_last_modified: -1) 54 | return unless @polling_enabled || (@sse_active && @enable_sse) 55 | 56 | req = Typhoeus::Request.new( 57 | get_config_url, 58 | headers: { 59 | Accept: "application/json", 60 | }) 61 | 62 | begin 63 | # Blind parse the lastmodified string to check if it's a valid date. 64 | # This short circuits the rest of the checks if it's not set 65 | if @config_last_modified != "" 66 | if min_last_modified != -1 67 | stored_date = Date.parse(@config_last_modified) 68 | parsed_sse_ts = Time.at(min_last_modified) 69 | if parsed_sse_ts.utc > stored_date.utc 70 | req.options[:headers]["If-Modified-Since"] = parsed_sse_ts.utc.httpdate 71 | else 72 | req.options[:headers]["If-Modified-Since"] = @config_last_modified 73 | end 74 | else 75 | req.options[:headers]["If-Modified-Since"] = @config_last_modified 76 | end 77 | end 78 | rescue 79 | end 80 | 81 | if @config_e_tag != "" 82 | req.options[:headers]['If-None-Match'] = @config_e_tag 83 | end 84 | 85 | @max_config_retries.times do 86 | @logger.debug("Requesting new config from #{get_config_url}, current etag: #{@config_e_tag}, last modified: #{@config_last_modified}") 87 | resp = req.run 88 | @logger.debug("Config request complete, status: #{resp.code}") 89 | case resp.code 90 | when 304 91 | @logger.debug("Config not modified, using cache, etag: #{@config_e_tag}, last modified: #{@config_last_modified}") 92 | break 93 | when 200 94 | @logger.debug("New config received, etag: #{resp.headers['Etag']} LastModified:#{resp.headers['Last-Modified']}") 95 | begin 96 | if @config_last_modified == "" 97 | set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified']) 98 | return 99 | end 100 | 101 | lm_timestamp = Time.rfc2822(resp.headers['Last-Modified']) 102 | current_lm = Time.rfc2822(@config_last_modified) 103 | if lm_timestamp == "" && @config_last_modified == "" || (current_lm.utc < lm_timestamp.utc) 104 | set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified']) 105 | else 106 | @logger.warn("Config last-modified was an older date than currently stored config.") 107 | end 108 | rescue 109 | @logger.warn("Failed to parse last modified header, setting config anyway.") 110 | set_config(resp.body, resp.headers['Etag'], resp.headers['Last-Modified']) 111 | end 112 | break 113 | when 403 114 | stop_polling 115 | stop_sse 116 | @logger.error("Failed to download DevCycle config; Invalid SDK Key.") 117 | break 118 | when 404 119 | stop_polling 120 | stop_sse 121 | @logger.error("Failed to download DevCycle config; Config not found.") 122 | break 123 | when 500...599 124 | @logger.error("Failed to download DevCycle config. Status: #{resp.code}") 125 | else 126 | @logger.error("Unexpected response from DevCycle CDN") 127 | @logger.error("Response code: #{resp.code}") 128 | @logger.error("Response body: #{resp.body}") 129 | break 130 | end 131 | end 132 | nil 133 | end 134 | 135 | def set_config(config, etag, lastmodified) 136 | if !JSON.parse(config).is_a?(Hash) 137 | raise("Invalid JSON body parsed from Config Response") 138 | end 139 | parsed_config = JSON.parse(config) 140 | 141 | if parsed_config['sse'] != nil 142 | raw_url = "#{parsed_config['sse']['hostname']}#{parsed_config['sse']['path']}" 143 | if @sse_url != raw_url && raw_url != "" 144 | stop_sse 145 | @sse_url = raw_url 146 | init_sse(@sse_url) 147 | end 148 | end 149 | @local_bucketing.store_config(config) 150 | @config_e_tag = etag 151 | @config_last_modified = lastmodified 152 | @local_bucketing.has_config = true 153 | @logger.debug("New config stored, etag: #{@config_e_tag}, last modified: #{@config_last_modified}") 154 | end 155 | 156 | def get_config_url 157 | configBasePath = @local_bucketing.options.config_cdn_uri 158 | "#{configBasePath}/config/#{@config_version}/server/#{@sdkKey}.json" 159 | end 160 | 161 | def start_polling(sse) 162 | if sse 163 | @config_poller.shutdown if @config_poller.running? 164 | @config_poller = Concurrent::TimerTask.new({ execution_interval: 60 * 10 }) do |_| 165 | fetch_config 166 | end 167 | @sse_polling = sse 168 | end 169 | @polling_enabled = true 170 | @config_poller.execute if @polling_enabled && (!@sse_active || sse) 171 | end 172 | 173 | def stop_polling() 174 | @polling_enabled = false 175 | @config_poller.shutdown if @config_poller.running? 176 | end 177 | 178 | def stop_sse 179 | return unless @enable_sse 180 | @sse_active = false 181 | @sse_polling = false 182 | @sse_client.close if @sse_client 183 | start_polling(@sse_polling) 184 | end 185 | 186 | def close 187 | @config_poller.shutdown if @config_poller.running? 188 | nil 189 | end 190 | 191 | def init_sse(path) 192 | return unless @enable_sse 193 | @logger.debug("Initializing SSE with url: #{path}") 194 | @sse_active = true 195 | @sse_client = SSE::Client.new(path) do |client| 196 | client.on_event do |event| 197 | 198 | parsed_json = JSON.parse(event.data) 199 | handle_sse(parsed_json) 200 | end 201 | client.on_error do |error| 202 | @logger.debug("SSE Error: #{error.message}") 203 | end 204 | end 205 | end 206 | 207 | def handle_sse(event_data) 208 | unless @sse_polling 209 | stop_polling 210 | start_polling(true) 211 | end 212 | if event_data["data"] == nil 213 | return 214 | end 215 | @logger.debug("SSE: Message received: #{event_data["data"]}") 216 | parsed_event_data = JSON.parse(event_data["data"]) 217 | 218 | last_modified = parsed_event_data["lastModified"] 219 | event_type = parsed_event_data["type"] 220 | 221 | if event_type == "refetchConfig" || event_type == nil 222 | @logger.debug("SSE: Re-fetching new config with TS: #{last_modified}") 223 | fetch_config(min_last_modified: last_modified / 1000) 224 | end 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/models/event.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | require 'date' 14 | require 'time' 15 | require 'oj' 16 | 17 | module DevCycle 18 | class Event 19 | # Custom event type 20 | attr_accessor :type 21 | 22 | # Custom event target / subject of event. Contextual to event type 23 | attr_accessor :target 24 | 25 | # Unix epoch time the event occurred according to client 26 | attr_accessor :date 27 | 28 | # Value for numerical events. Contextual to event type 29 | attr_accessor :value 30 | 31 | # Extra JSON metadata for event. Contextual to event type 32 | attr_accessor :metaData 33 | 34 | # Attribute mapping from ruby-style variable name to JSON key. 35 | def self.attribute_map 36 | { 37 | :'type' => :'type', 38 | :'target' => :'target', 39 | :'date' => :'date', 40 | :'value' => :'value', 41 | :'metaData' => :'metaData' 42 | } 43 | end 44 | 45 | # Returns all the JSON keys this model knows about 46 | def self.acceptable_attributes 47 | attribute_map.values 48 | end 49 | 50 | # Attribute type mapping. 51 | def self.openapi_types 52 | { 53 | :'type' => :'String', 54 | :'target' => :'String', 55 | :'date' => :'Float', 56 | :'value' => :'Float', 57 | :'metaData' => :'Object' 58 | } 59 | end 60 | 61 | # List of attributes with nullable: true 62 | def self.openapi_nullable 63 | Set.new([ 64 | ]) 65 | end 66 | 67 | # Initializes the object 68 | # @param [Hash] attributes Model attributes in the form of hash 69 | def initialize(attributes = {}) 70 | if (!attributes.is_a?(Hash)) 71 | fail ArgumentError, "The input argument (attributes) must be a hash in `DevCycle::Event` initialize method" 72 | end 73 | 74 | # check to see if the attribute exists and convert string to symbol for hash key 75 | attributes = attributes.each_with_object({}) { |(k, v), h| 76 | if (!self.class.attribute_map.key?(k.to_sym)) 77 | fail ArgumentError, "`#{k}` is not a valid attribute in `DevCycle::Event`. Please check the name to make sure it's valid. List of attributes: " + self.class.attribute_map.keys.inspect 78 | end 79 | h[k.to_sym] = v 80 | } 81 | 82 | if attributes.key?(:'type') 83 | self.type = attributes[:'type'] 84 | end 85 | 86 | if attributes.key?(:'target') 87 | self.target = attributes[:'target'] 88 | end 89 | 90 | if attributes.key?(:'date') 91 | self.date = attributes[:'date'] 92 | end 93 | 94 | if attributes.key?(:'value') 95 | self.value = attributes[:'value'] 96 | end 97 | 98 | if attributes.key?(:'metaData') 99 | self.metaData = attributes[:'metaData'] 100 | end 101 | end 102 | 103 | # Show invalid properties with the reasons. Usually used together with valid? 104 | # @return Array for valid properties with the reasons 105 | def list_invalid_properties 106 | invalid_properties = Array.new 107 | if @type.nil? 108 | invalid_properties.push('invalid value for "type", type cannot be nil.') 109 | end 110 | 111 | invalid_properties 112 | end 113 | 114 | # Check to see if the all the properties in the model are valid 115 | # @return true if the model is valid 116 | def valid? 117 | return false if @type.nil? 118 | true 119 | end 120 | 121 | # Checks equality by comparing each attribute. 122 | # @param [Object] Object to be compared 123 | def ==(o) 124 | return true if self.equal?(o) 125 | self.class == o.class && 126 | type == o.type && 127 | target == o.target && 128 | date == o.date && 129 | value == o.value && 130 | meta_data == o.meta_data 131 | end 132 | 133 | # @see the `==` method 134 | # @param [Object] Object to be compared 135 | def eql?(o) 136 | self == o 137 | end 138 | 139 | # Calculates hash code according to all attributes. 140 | # @return [Integer] Hash code 141 | def hash 142 | [type, target, date, value, metaData].hash 143 | end 144 | 145 | # Builds the object from hash 146 | # @param [Hash] attributes Model attributes in the form of hash 147 | # @return [Object] Returns the model itself 148 | def self.build_from_hash(attributes) 149 | new.build_from_hash(attributes) 150 | end 151 | 152 | # Builds the object from hash 153 | # @param [Hash] attributes Model attributes in the form of hash 154 | # @return [Object] Returns the model itself 155 | def build_from_hash(attributes) 156 | return nil unless attributes.is_a?(Hash) 157 | self.class.openapi_types.each_pair do |key, type| 158 | if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) 159 | self.send("#{key}=", nil) 160 | elsif type =~ /\AArray<(.*)>/i 161 | # check to ensure the input is an array given that the attribute 162 | # is documented as an array but the input is not 163 | if attributes[self.class.attribute_map[key]].is_a?(Array) 164 | self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) 165 | end 166 | elsif !attributes[self.class.attribute_map[key]].nil? 167 | self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) 168 | end 169 | end 170 | 171 | self 172 | end 173 | 174 | # Deserializes the data based on type 175 | # @param string type Data type 176 | # @param string value Value to be deserialized 177 | # @return [Object] Deserialized data 178 | def _deserialize(type, value) 179 | case type.to_sym 180 | when :Time 181 | Time.parse(value) 182 | when :Date 183 | Date.parse(value) 184 | when :String 185 | value.to_s 186 | when :Integer 187 | value.to_i 188 | when :Float 189 | value.to_f 190 | when :Boolean 191 | if value.to_s =~ /\A(true|t|yes|y|1)\z/i 192 | true 193 | else 194 | false 195 | end 196 | when :Object 197 | # generic object (usually a Hash), return directly 198 | value 199 | when /\AArray<(?.+)>\z/ 200 | inner_type = Regexp.last_match[:inner_type] 201 | value.map { |v| _deserialize(inner_type, v) } 202 | when /\AHash<(?.+?), (?.+)>\z/ 203 | k_type = Regexp.last_match[:k_type] 204 | v_type = Regexp.last_match[:v_type] 205 | {}.tap do |hash| 206 | value.each do |k, v| 207 | hash[_deserialize(k_type, k)] = _deserialize(v_type, v) 208 | end 209 | end 210 | else # model 211 | # models (e.g. Pet) or oneOf 212 | klass = DevCycle.const_get(type) 213 | klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) 214 | end 215 | end 216 | 217 | # Returns the string representation of the object 218 | # @return [String] String presentation of the object 219 | def to_s 220 | to_hash.to_s 221 | end 222 | 223 | # to_body is an alias to to_hash (backward compatibility) 224 | # @return [Hash] Returns the object in the form of hash 225 | def to_body 226 | to_hash 227 | end 228 | 229 | # Returns the object in the form of hash 230 | # @return [Hash] Returns the object in the form of hash 231 | def to_hash 232 | hash = {} 233 | self.class.attribute_map.each_pair do |attr, param| 234 | value = self.send(attr) 235 | if value.nil? 236 | is_nullable = self.class.openapi_nullable.include?(attr) 237 | next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) 238 | end 239 | 240 | hash[param] = _to_hash(value) 241 | end 242 | hash 243 | end 244 | 245 | # Outputs non-array value in the form of hash 246 | # For object, use to_hash. Otherwise, just return the value 247 | # @param [Object] value Any valid value 248 | # @return [Hash] Returns the value in the form of hash 249 | def _to_hash(value) 250 | if value.is_a?(Array) 251 | value.compact.map { |v| _to_hash(v) } 252 | elsif value.is_a?(Hash) 253 | {}.tap do |hash| 254 | value.each { |k, v| hash[k] = _to_hash(v) } 255 | end 256 | elsif value.respond_to? :to_hash 257 | value.to_hash 258 | else 259 | value 260 | end 261 | end 262 | 263 | def to_json 264 | Oj.dump(to_hash, mode: :json) 265 | end 266 | end 267 | 268 | end 269 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/parallel@1.22.1.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `parallel` gem. 5 | # Please instead update this file by running `bin/tapioca gem parallel`. 6 | 7 | # source://parallel//lib/parallel/version.rb#2 8 | module Parallel 9 | extend ::Parallel::ProcessorCount 10 | 11 | class << self 12 | # @return [Boolean] 13 | # 14 | # source://parallel//lib/parallel.rb#246 15 | def all?(*args, &block); end 16 | 17 | # @return [Boolean] 18 | # 19 | # source://parallel//lib/parallel.rb#241 20 | def any?(*args, &block); end 21 | 22 | # source://parallel//lib/parallel.rb#237 23 | def each(array, options = T.unsafe(nil), &block); end 24 | 25 | # source://parallel//lib/parallel.rb#251 26 | def each_with_index(array, options = T.unsafe(nil), &block); end 27 | 28 | # source://parallel//lib/parallel.rb#306 29 | def flat_map(*args, &block); end 30 | 31 | # source://parallel//lib/parallel.rb#231 32 | def in_processes(options = T.unsafe(nil), &block); end 33 | 34 | # source://parallel//lib/parallel.rb#215 35 | def in_threads(options = T.unsafe(nil)); end 36 | 37 | # source://parallel//lib/parallel.rb#255 38 | def map(source, options = T.unsafe(nil), &block); end 39 | 40 | # source://parallel//lib/parallel.rb#302 41 | def map_with_index(array, options = T.unsafe(nil), &block); end 42 | 43 | # source://parallel//lib/parallel.rb#310 44 | def worker_number; end 45 | 46 | # TODO: this does not work when doing threads in forks, so should remove and yield the number instead if needed 47 | # 48 | # source://parallel//lib/parallel.rb#315 49 | def worker_number=(worker_num); end 50 | 51 | private 52 | 53 | # source://parallel//lib/parallel.rb#321 54 | def add_progress_bar!(job_factory, options); end 55 | 56 | # source://parallel//lib/parallel.rb#584 57 | def call_with_index(item, index, options, &block); end 58 | 59 | # source://parallel//lib/parallel.rb#516 60 | def create_workers(job_factory, options, &block); end 61 | 62 | # options is either a Integer or a Hash with :count 63 | # 64 | # source://parallel//lib/parallel.rb#574 65 | def extract_count_from_options(options); end 66 | 67 | # source://parallel//lib/parallel.rb#602 68 | def instrument_finish(item, index, result, options); end 69 | 70 | # source://parallel//lib/parallel.rb#607 71 | def instrument_start(item, index, options); end 72 | 73 | # source://parallel//lib/parallel.rb#550 74 | def process_incoming_jobs(read, write, job_factory, options, &block); end 75 | 76 | # source://parallel//lib/parallel.rb#504 77 | def replace_worker(job_factory, workers, index, options, blk); end 78 | 79 | # source://parallel//lib/parallel.rb#595 80 | def with_instrumentation(item, index, options); end 81 | 82 | # source://parallel//lib/parallel.rb#346 83 | def work_direct(job_factory, options, &block); end 84 | 85 | # source://parallel//lib/parallel.rb#456 86 | def work_in_processes(job_factory, options, &blk); end 87 | 88 | # source://parallel//lib/parallel.rb#390 89 | def work_in_ractors(job_factory, options); end 90 | 91 | # source://parallel//lib/parallel.rb#365 92 | def work_in_threads(job_factory, options, &block); end 93 | 94 | # source://parallel//lib/parallel.rb#524 95 | def worker(job_factory, options, &block); end 96 | end 97 | end 98 | 99 | # source://parallel//lib/parallel.rb#14 100 | class Parallel::Break < ::StandardError 101 | # @return [Break] a new instance of Break 102 | # 103 | # source://parallel//lib/parallel.rb#17 104 | def initialize(value = T.unsafe(nil)); end 105 | 106 | # Returns the value of attribute value. 107 | # 108 | # source://parallel//lib/parallel.rb#15 109 | def value; end 110 | end 111 | 112 | # source://parallel//lib/parallel.rb#11 113 | class Parallel::DeadWorker < ::StandardError; end 114 | 115 | # source://parallel//lib/parallel.rb#35 116 | class Parallel::ExceptionWrapper 117 | # @return [ExceptionWrapper] a new instance of ExceptionWrapper 118 | # 119 | # source://parallel//lib/parallel.rb#38 120 | def initialize(exception); end 121 | 122 | # Returns the value of attribute exception. 123 | # 124 | # source://parallel//lib/parallel.rb#36 125 | def exception; end 126 | end 127 | 128 | # source://parallel//lib/parallel.rb#101 129 | class Parallel::JobFactory 130 | # @return [JobFactory] a new instance of JobFactory 131 | # 132 | # source://parallel//lib/parallel.rb#102 133 | def initialize(source, mutex); end 134 | 135 | # source://parallel//lib/parallel.rb#110 136 | def next; end 137 | 138 | # generate item that is sent to workers 139 | # just index is faster + less likely to blow up with unserializable errors 140 | # 141 | # source://parallel//lib/parallel.rb#139 142 | def pack(item, index); end 143 | 144 | # source://parallel//lib/parallel.rb#129 145 | def size; end 146 | 147 | # unpack item that is sent to workers 148 | # 149 | # source://parallel//lib/parallel.rb#144 150 | def unpack(data); end 151 | 152 | private 153 | 154 | # @return [Boolean] 155 | # 156 | # source://parallel//lib/parallel.rb#150 157 | def producer?; end 158 | 159 | # source://parallel//lib/parallel.rb#154 160 | def queue_wrapper(array); end 161 | end 162 | 163 | # source://parallel//lib/parallel.rb#23 164 | class Parallel::Kill < ::Parallel::Break; end 165 | 166 | # TODO: inline this method into parallel.rb and kill physical_processor_count in next major release 167 | # 168 | # source://parallel//lib/parallel/processor_count.rb#4 169 | module Parallel::ProcessorCount 170 | # Number of physical processor cores on the current system. 171 | # 172 | # source://parallel//lib/parallel/processor_count.rb#12 173 | def physical_processor_count; end 174 | 175 | # Number of processors seen by the OS, used for process scheduling 176 | # 177 | # source://parallel//lib/parallel/processor_count.rb#6 178 | def processor_count; end 179 | end 180 | 181 | # source://parallel//lib/parallel.rb#9 182 | Parallel::Stop = T.let(T.unsafe(nil), Object) 183 | 184 | # source://parallel//lib/parallel.rb#26 185 | class Parallel::UndumpableException < ::StandardError 186 | # @return [UndumpableException] a new instance of UndumpableException 187 | # 188 | # source://parallel//lib/parallel.rb#29 189 | def initialize(original); end 190 | 191 | # Returns the value of attribute backtrace. 192 | # 193 | # source://parallel//lib/parallel.rb#27 194 | def backtrace; end 195 | end 196 | 197 | # source://parallel//lib/parallel.rb#159 198 | class Parallel::UserInterruptHandler 199 | class << self 200 | # source://parallel//lib/parallel.rb#184 201 | def kill(thing); end 202 | 203 | # kill all these pids or threads if user presses Ctrl+c 204 | # 205 | # source://parallel//lib/parallel.rb#164 206 | def kill_on_ctrl_c(pids, options); end 207 | 208 | private 209 | 210 | # source://parallel//lib/parallel.rb#208 211 | def restore_interrupt(old, signal); end 212 | 213 | # source://parallel//lib/parallel.rb#193 214 | def trap_interrupt(signal); end 215 | end 216 | end 217 | 218 | # source://parallel//lib/parallel.rb#160 219 | Parallel::UserInterruptHandler::INTERRUPT_SIGNAL = T.let(T.unsafe(nil), Symbol) 220 | 221 | # source://parallel//lib/parallel/version.rb#3 222 | Parallel::VERSION = T.let(T.unsafe(nil), String) 223 | 224 | # source://parallel//lib/parallel/version.rb#3 225 | Parallel::Version = T.let(T.unsafe(nil), String) 226 | 227 | # source://parallel//lib/parallel.rb#54 228 | class Parallel::Worker 229 | # @return [Worker] a new instance of Worker 230 | # 231 | # source://parallel//lib/parallel.rb#58 232 | def initialize(read, write, pid); end 233 | 234 | # might be passed to started_processes and simultaneously closed by another thread 235 | # when running in isolation mode, so we have to check if it is closed before closing 236 | # 237 | # source://parallel//lib/parallel.rb#71 238 | def close_pipes; end 239 | 240 | # Returns the value of attribute pid. 241 | # 242 | # source://parallel//lib/parallel.rb#55 243 | def pid; end 244 | 245 | # Returns the value of attribute read. 246 | # 247 | # source://parallel//lib/parallel.rb#55 248 | def read; end 249 | 250 | # source://parallel//lib/parallel.rb#64 251 | def stop; end 252 | 253 | # Returns the value of attribute thread. 254 | # 255 | # source://parallel//lib/parallel.rb#56 256 | def thread; end 257 | 258 | # Sets the attribute thread 259 | # 260 | # @param value the value to set the attribute thread to. 261 | # 262 | # source://parallel//lib/parallel.rb#56 263 | def thread=(_arg0); end 264 | 265 | # source://parallel//lib/parallel.rb#76 266 | def work(data); end 267 | 268 | # Returns the value of attribute write. 269 | # 270 | # source://parallel//lib/parallel.rb#55 271 | def write; end 272 | 273 | private 274 | 275 | # source://parallel//lib/parallel.rb#94 276 | def wait; end 277 | end 278 | -------------------------------------------------------------------------------- /spec/eval_hooks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DevCycle::Client do 4 | let(:test_user) { DevCycle::User.new(user_id: 'test-user', email: 'test@example.com') } 5 | let(:test_key) { 'test-variable' } 6 | let(:test_default) { 'default-value' } 7 | let(:options) { DevCycle::Options.new } 8 | 9 | # Use unique SDK keys for each test to avoid WASM initialization conflicts 10 | let(:valid_sdk_key) { "server-test-key-#{SecureRandom.hex(4)}" } 11 | let(:client) { DevCycle::Client.new(valid_sdk_key, options) } 12 | 13 | after(:each) do 14 | client.close if client.respond_to?(:close) 15 | end 16 | 17 | describe 'eval hooks functionality' do 18 | context 'hook management' do 19 | it 'initializes with an empty eval hooks runner' do 20 | expect(client.instance_variable_get(:@eval_hooks_runner)).to be_a(DevCycle::EvalHooksRunner) 21 | expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to be_empty 22 | end 23 | 24 | it 'can add eval hooks' do 25 | hook = DevCycle::EvalHook.new 26 | client.add_eval_hook(hook) 27 | expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to include(hook) 28 | end 29 | 30 | it 'can clear eval hooks' do 31 | hook = DevCycle::EvalHook.new 32 | client.add_eval_hook(hook) 33 | expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).not_to be_empty 34 | 35 | client.clear_eval_hooks 36 | expect(client.instance_variable_get(:@eval_hooks_runner).eval_hooks).to be_empty 37 | end 38 | end 39 | 40 | context 'variable evaluation with hooks' do 41 | it 'runs before hooks before variable evaluation' do 42 | before_hook_called = false 43 | hook = DevCycle::EvalHook.new( 44 | before: ->(context) { 45 | before_hook_called = true 46 | expect(context.key).to eq(test_key) 47 | expect(context.user).to eq(test_user) 48 | expect(context.default_value).to eq(test_default) 49 | context 50 | } 51 | ) 52 | client.add_eval_hook(hook) 53 | 54 | result = client.variable(test_user, test_key, test_default) 55 | expect(before_hook_called).to be true 56 | expect(result.isDefaulted).to be true 57 | expect(result.value).to eq(test_default) 58 | end 59 | 60 | it 'runs after hooks after successful variable evaluation' do 61 | after_hook_called = false 62 | hook = DevCycle::EvalHook.new( 63 | after: ->(context) { 64 | after_hook_called = true 65 | expect(context.key).to eq(test_key) 66 | expect(context.user).to eq(test_user) 67 | expect(context.default_value).to eq(test_default) 68 | } 69 | ) 70 | client.add_eval_hook(hook) 71 | 72 | result = client.variable(test_user, test_key, test_default) 73 | expect(after_hook_called).to be true 74 | expect(result.isDefaulted).to be true 75 | expect(result.value).to eq(test_default) 76 | end 77 | 78 | it 'runs error hooks when variable evaluation fails' do 79 | error_hook_called = false 80 | hook = DevCycle::EvalHook.new( 81 | error: ->(context, error) { 82 | error_hook_called = true 83 | expect(context.key).to eq(test_key) 84 | expect(context.user).to eq(test_user) 85 | expect(context.default_value).to eq(test_default) 86 | } 87 | ) 88 | client.add_eval_hook(hook) 89 | 90 | # Force an error by making determine_variable_type raise an error 91 | allow(client).to receive(:determine_variable_type).and_raise(StandardError, 'Variable type error') 92 | 93 | client.variable(test_user, test_key, test_default) 94 | expect(error_hook_called).to be true 95 | end 96 | 97 | it 'runs finally hooks regardless of success or failure' do 98 | finally_hook_called = false 99 | hook = DevCycle::EvalHook.new( 100 | on_finally: ->(context) { 101 | finally_hook_called = true 102 | expect(context.key).to eq(test_key) 103 | expect(context.user).to eq(test_user) 104 | expect(context.default_value).to eq(test_default) 105 | } 106 | ) 107 | client.add_eval_hook(hook) 108 | 109 | result = client.variable(test_user, test_key, test_default) 110 | expect(finally_hook_called).to be true 111 | expect(result.isDefaulted).to be true 112 | expect(result.value).to eq(test_default) 113 | end 114 | 115 | it 'skips after hooks when before hook raises an error' do 116 | before_hook_called = false 117 | after_hook_called = false 118 | error_hook_called = false 119 | finally_hook_called = false 120 | 121 | hook = DevCycle::EvalHook.new( 122 | before: ->(context) { 123 | before_hook_called = true 124 | raise StandardError, 'Before hook error' 125 | }, 126 | after: ->(context) { 127 | after_hook_called = true 128 | }, 129 | error: ->(context, error) { 130 | error_hook_called = true 131 | expect(error).to be_a(StandardError) 132 | expect(error.message).to include('Before hook error') 133 | }, 134 | on_finally: ->(context) { 135 | finally_hook_called = true 136 | } 137 | ) 138 | client.add_eval_hook(hook) 139 | 140 | client.variable(test_user, test_key, test_default) 141 | expect(before_hook_called).to be true 142 | expect(after_hook_called).to be false 143 | expect(error_hook_called).to be true 144 | expect(finally_hook_called).to be true 145 | end 146 | 147 | it 'runs multiple hooks in order' do 148 | execution_order = [] 149 | 150 | hook1 = DevCycle::EvalHook.new( 151 | before: ->(context) { 152 | execution_order << 'hook1_before' 153 | context 154 | }, 155 | after: ->(context) { 156 | execution_order << 'hook1_after' 157 | }, 158 | on_finally: ->(context) { 159 | execution_order << 'hook1_finally' 160 | } 161 | ) 162 | 163 | hook2 = DevCycle::EvalHook.new( 164 | before: ->(context) { 165 | execution_order << 'hook2_before' 166 | context 167 | }, 168 | after: ->(context) { 169 | execution_order << 'hook2_after' 170 | }, 171 | on_finally: ->(context) { 172 | execution_order << 'hook2_finally' 173 | } 174 | ) 175 | 176 | client.add_eval_hook(hook1) 177 | client.add_eval_hook(hook2) 178 | 179 | result = client.variable(test_user, test_key, test_default) 180 | 181 | expect(execution_order).to eq([ 182 | 'hook1_before', 'hook2_before', 183 | 'hook1_after', 'hook2_after', 184 | 'hook1_finally', 'hook2_finally' 185 | ]) 186 | expect(result.isDefaulted).to be true 187 | expect(result.value).to eq(test_default) 188 | end 189 | 190 | it 'allows before hooks to modify context' do 191 | modified_context = nil 192 | hook = DevCycle::EvalHook.new( 193 | before: ->(context) { 194 | # Modify the context 195 | context.key = 'modified-key' 196 | context.user = DevCycle::User.new(user_id: 'modified-user', email: 'modified@example.com') 197 | context 198 | }, 199 | after: ->(context) { 200 | modified_context = context 201 | } 202 | ) 203 | client.add_eval_hook(hook) 204 | 205 | result = client.variable(test_user, test_key, test_default) 206 | 207 | expect(modified_context.key).to eq('modified-key') 208 | expect(modified_context.user).to eq(DevCycle::User.new(user_id: 'modified-user', email: 'modified@example.com')) 209 | expect(result.isDefaulted).to be true 210 | expect(result.value).to eq(test_default) 211 | end 212 | 213 | it 'works with different variable types' do 214 | # Test with boolean default 215 | boolean_hook_called = false 216 | boolean_hook = DevCycle::EvalHook.new( 217 | after: ->(context) { 218 | boolean_hook_called = true 219 | } 220 | ) 221 | client.add_eval_hook(boolean_hook) 222 | 223 | boolean_result = client.variable(test_user, 'boolean-test', true) 224 | expect(boolean_hook_called).to be true 225 | expect(boolean_result.isDefaulted).to be true 226 | expect(boolean_result.value).to eq(true) 227 | 228 | # Test with number default 229 | number_hook_called = false 230 | number_hook = DevCycle::EvalHook.new( 231 | after: ->(context) { 232 | number_hook_called = true 233 | } 234 | ) 235 | client.add_eval_hook(number_hook) 236 | 237 | number_result = client.variable(test_user, 'number-test', 42) 238 | expect(number_hook_called).to be true 239 | expect(number_result.isDefaulted).to be true 240 | expect(number_result.value).to eq(42) 241 | end 242 | 243 | end 244 | end 245 | end -------------------------------------------------------------------------------- /lib/devcycle-ruby-server-sdk/configuration.rb: -------------------------------------------------------------------------------- 1 | =begin 2 | #DevCycle Bucketing API 3 | 4 | #Documents the DevCycle Bucketing API which provides and API interface to User Bucketing and for generated SDKs. 5 | 6 | The version of the OpenAPI document: 1.0.0 7 | 8 | Generated by: https://openapi-generator.tech 9 | OpenAPI Generator version: 5.3.0 10 | 11 | =end 12 | 13 | module DevCycle 14 | class Configuration 15 | # Defines url scheme 16 | attr_accessor :scheme 17 | 18 | # Defines url host 19 | attr_accessor :host 20 | 21 | # Defines url base path 22 | attr_accessor :base_path 23 | 24 | # Define server configuration index 25 | attr_accessor :server_index 26 | 27 | # Define server operation configuration index 28 | attr_accessor :server_operation_index 29 | 30 | # Default server variables 31 | attr_accessor :server_variables 32 | 33 | # Default server operation variables 34 | attr_accessor :server_operation_variables 35 | 36 | # Defines API keys used with API Key authentications. 37 | # 38 | # @return [Hash] key: parameter name, value: parameter value (API key) 39 | # 40 | # @example parameter name is "api_key", API key is "xxx" (e.g. "api_key=xxx" in query string) 41 | # config.api_key['api_key'] = 'xxx' 42 | attr_accessor :api_key 43 | 44 | # Defines API key prefixes used with API Key authentications. 45 | # 46 | # @return [Hash] key: parameter name, value: API key prefix 47 | # 48 | # @example parameter name is "Authorization", API key prefix is "Token" (e.g. "Authorization: Token xxx" in headers) 49 | # config.api_key_prefix['api_key'] = 'Token' 50 | attr_accessor :api_key_prefix 51 | 52 | # Defines the username used with HTTP basic authentication. 53 | # 54 | # @return [String] 55 | attr_accessor :username 56 | 57 | # Defines the password used with HTTP basic authentication. 58 | # 59 | # @return [String] 60 | attr_accessor :password 61 | 62 | # Defines the access token (Bearer) used with OAuth2. 63 | attr_accessor :access_token 64 | 65 | # Set this to enable/disable debugging. When enabled (set to true), HTTP request/response 66 | # details will be logged with `logger.debug` (see the `logger` attribute). 67 | # Default to false. 68 | # 69 | # @return [true, false] 70 | attr_accessor :debugging 71 | 72 | # Defines the logger used for debugging. 73 | # Default to `Rails.logger` (when in Rails) or logging to STDOUT. 74 | # 75 | # @return [#debug] 76 | attr_accessor :logger 77 | 78 | # Defines the temporary folder to store downloaded files 79 | # (for API endpoints that have file response). 80 | # Default to use `Tempfile`. 81 | # 82 | # @return [String] 83 | attr_accessor :temp_folder_path 84 | 85 | # The time limit for HTTP request in seconds. 86 | # Default to 0 (never times out). 87 | attr_accessor :timeout 88 | 89 | # Set this to false to skip client side validation in the operation. 90 | # Default to true. 91 | # @return [true, false] 92 | attr_accessor :client_side_validation 93 | 94 | ### TLS/SSL setting 95 | # Set this to false to skip verifying SSL certificate when calling API from https server. 96 | # Default to true. 97 | # 98 | # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. 99 | # 100 | # @return [true, false] 101 | attr_accessor :verify_ssl 102 | 103 | ### TLS/SSL setting 104 | # Set this to false to skip verifying SSL host name 105 | # Default to true. 106 | # 107 | # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. 108 | # 109 | # @return [true, false] 110 | attr_accessor :verify_ssl_host 111 | 112 | ### TLS/SSL setting 113 | # Set this to customize the certificate file to verify the peer. 114 | # 115 | # @return [String] the path to the certificate file 116 | # 117 | # @see The `cainfo` option of Typhoeus, `--cert` option of libcurl. Related source code: 118 | # https://github.com/typhoeus/typhoeus/blob/master/lib/typhoeus/easy_factory.rb#L145 119 | attr_accessor :ssl_ca_cert 120 | 121 | ### TLS/SSL setting 122 | # Client certificate file (for client certificate) 123 | attr_accessor :cert_file 124 | 125 | ### TLS/SSL setting 126 | # Client private key file (for client certificate) 127 | attr_accessor :key_file 128 | 129 | # Set this to customize parameters encoding of array parameter with multi collectionFormat. 130 | # Default to nil. 131 | # 132 | # @see The params_encoding option of Ethon. Related source code: 133 | # https://github.com/typhoeus/ethon/blob/master/lib/ethon/easy/queryable.rb#L96 134 | attr_accessor :params_encoding 135 | 136 | attr_accessor :inject_format 137 | 138 | attr_accessor :force_ending_format 139 | 140 | # Define if EdgeDB is Enabled (Boolean) 141 | # Default to false 142 | attr_accessor :enable_edge_db 143 | 144 | def initialize 145 | @scheme = 'https' 146 | @host = 'bucketing-api.devcycle.com' 147 | @base_path = '' 148 | @server_index = 0 149 | @server_operation_index = {} 150 | @server_variables = {} 151 | @server_operation_variables = {} 152 | @api_key = {} 153 | @api_key_prefix = {} 154 | @client_side_validation = true 155 | @verify_ssl = true 156 | @verify_ssl_host = true 157 | @params_encoding = nil 158 | @cert_file = nil 159 | @key_file = nil 160 | @timeout = 0 161 | @debugging = false 162 | @inject_format = false 163 | @force_ending_format = false 164 | @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT) 165 | @enable_edge_db = false 166 | 167 | yield(self) if block_given? 168 | end 169 | 170 | # The default Configuration object. 171 | def self.default 172 | @default ||= Configuration.new 173 | end 174 | 175 | def configure 176 | yield(self) if block_given? 177 | end 178 | 179 | def scheme=(scheme) 180 | # remove :// from scheme 181 | @scheme = scheme.sub(/:\/\//, '') 182 | end 183 | 184 | def host=(host) 185 | # remove http(s):// and anything after a slash 186 | @host = host.sub(/https?:\/\//, '').split('/').first 187 | end 188 | 189 | def base_path=(base_path) 190 | # Add leading and trailing slashes to base_path 191 | @base_path = "/#{base_path}".gsub(/\/+/, '/') 192 | @base_path = '' if @base_path == '/' 193 | end 194 | 195 | # Returns base URL for specified operation based on server settings 196 | def base_url(operation = nil) 197 | index = server_operation_index.fetch(operation, server_index) 198 | return "#{scheme}://#{[host, base_path].join('/').gsub(/\/+/, '/')}".sub(/\/+\z/, '') if index == nil 199 | 200 | server_url(index, server_operation_variables.fetch(operation, server_variables), operation_server_settings[operation]) 201 | end 202 | 203 | # Gets API key (with prefix if set). 204 | # @param [String] param_name the parameter name of API key auth 205 | def api_key_with_prefix(param_name, param_alias = nil) 206 | key = @api_key[param_name] 207 | key = @api_key.fetch(param_alias, key) unless param_alias.nil? 208 | if @api_key_prefix[param_name] 209 | "#{@api_key_prefix[param_name]} #{key}" 210 | else 211 | key 212 | end 213 | end 214 | 215 | # Gets Basic Auth token string 216 | def basic_auth_token 217 | 'Basic ' + ["#{username}:#{password}"].pack('m').delete("\r\n") 218 | end 219 | 220 | # Returns Auth Settings hash for api client. 221 | def auth_settings 222 | { 223 | 'bearerAuth' => 224 | { 225 | type: 'api_key', 226 | in: 'header', 227 | key: 'Authorization', 228 | value: api_key_with_prefix('bearerAuth') 229 | }, 230 | } 231 | end 232 | 233 | # Returns an array of Server setting 234 | def server_settings 235 | [ 236 | { 237 | url: "https://bucketing-api.devcycle.com", 238 | description: "No description provided", 239 | } 240 | ] 241 | end 242 | 243 | def operation_server_settings 244 | { 245 | } 246 | end 247 | 248 | # Returns URL based on server settings 249 | # 250 | # @param index array index of the server settings 251 | # @param variables hash of variable and the corresponding value 252 | def server_url(index, variables = {}, servers = nil) 253 | servers = server_settings if servers == nil 254 | 255 | # check array index out of bound 256 | if (index < 0 || index >= servers.size) 257 | fail ArgumentError, "Invalid index #{index} when selecting the server. Must be less than #{servers.size}" 258 | end 259 | 260 | server = servers[index] 261 | url = server[:url] 262 | 263 | return url unless server.key? :variables 264 | 265 | # go through variable and assign a value 266 | server[:variables].each do |name, variable| 267 | if variables.key?(name) 268 | if (!server[:variables][name].key?(:enum_values) || server[:variables][name][:enum_values].include?(variables[name])) 269 | url.gsub! "{" + name.to_s + "}", variables[name] 270 | else 271 | fail ArgumentError, "The variable `#{name}` in the server URL has invalid value #{variables[name]}. Must be #{server[:variables][name][:enum_values]}." 272 | end 273 | else 274 | # use default value 275 | url.gsub! "{" + name.to_s + "}", server[:variables][name][:default_value] 276 | end 277 | end 278 | 279 | url 280 | end 281 | 282 | def enable_edge_db=(enable_edge_db = false) 283 | if (enable_edge_db) 284 | @enable_edge_db = true 285 | end 286 | end 287 | 288 | end 289 | end 290 | --------------------------------------------------------------------------------