├── 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 |
--------------------------------------------------------------------------------