├── .github ├── CODEOWNERS └── workflows │ ├── ruby-ci.yml │ ├── snyk.yml │ └── stale.yml ├── .gitignore ├── .rubocop.yml ├── .sonarcloud.properties ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPLOY.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── configcat.gemspec ├── lib ├── configcat.rb └── configcat │ ├── config.rb │ ├── configcache.rb │ ├── configcatclient.rb │ ├── configcatlogger.rb │ ├── configcatoptions.rb │ ├── configentry.rb │ ├── configfetcher.rb │ ├── configservice.rb │ ├── datagovernance.rb │ ├── evaluationcontext.rb │ ├── evaluationdetails.rb │ ├── evaluationlogbuilder.rb │ ├── interfaces.rb │ ├── localdictionarydatasource.rb │ ├── localfiledatasource.rb │ ├── overridedatasource.rb │ ├── pollingmode.rb │ ├── refreshresult.rb │ ├── rolloutevaluator.rb │ ├── user.rb │ ├── utils.rb │ └── version.rb ├── media └── readme02-3.png ├── samples ├── README.md ├── consolesample.rb └── consolesample2.rb └── spec ├── config_spec.rb ├── configcat ├── autopollingcachepolicy_spec.rb ├── configcache_spec.rb ├── configcatclient_spec.rb ├── configfetcher_spec.rb ├── hooks_spec.rb ├── lazyloadingcachepolicy_spec.rb ├── manualpollingcachepolicy_spec.rb ├── mocks.rb └── user_spec.rb ├── configcat_spec.rb ├── data ├── comparison_attribute_conversion.json ├── comparison_attribute_trimming.json ├── comparison_value_trimming.json ├── evaluation │ ├── 1_targeting_rule.json │ ├── 1_targeting_rule │ │ ├── 1_rule_matching_targeted_attribute.txt │ │ ├── 1_rule_no_targeted_attribute.txt │ │ ├── 1_rule_no_user.txt │ │ └── 1_rule_not_matching_targeted_attribute.txt │ ├── 2_targeting_rules.json │ ├── 2_targeting_rules │ │ ├── 2_rules_matching_targeted_attribute.txt │ │ ├── 2_rules_no_targeted_attribute.txt │ │ ├── 2_rules_no_user.txt │ │ └── 2_rules_not_matching_targeted_attribute.txt │ ├── and_rules.json │ ├── and_rules │ │ ├── and_rules_no_user.txt │ │ └── and_rules_user.txt │ ├── comparators.json │ ├── comparators │ │ └── allinone.txt │ ├── epoch_date_validation.json │ ├── epoch_date_validation │ │ └── date_error.txt │ ├── list_truncation.json │ ├── list_truncation │ │ ├── list_truncation.txt │ │ └── test_list_truncation.json │ ├── number_validation.json │ ├── number_validation │ │ └── number_error.txt │ ├── options_after_targeting_rule.json │ ├── options_after_targeting_rule │ │ ├── options_after_targeting_rule_matching_targeted_attribute.txt │ │ ├── options_after_targeting_rule_no_targeted_attribute.txt │ │ ├── options_after_targeting_rule_no_user.txt │ │ └── options_after_targeting_rule_not_matching_targeted_attribute.txt │ ├── options_based_on_custom_attr.json │ ├── options_based_on_custom_attr │ │ ├── matching_options_custom_attribute.txt │ │ ├── no_options_custom_attribute.txt │ │ └── options_custom_attribute_no_user.txt │ ├── options_based_on_user_id.json │ ├── options_based_on_user_id │ │ ├── options_user_attribute_no_user.txt │ │ └── options_user_attribute_user.txt │ ├── options_within_targeting_rule.json │ ├── options_within_targeting_rule │ │ ├── options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt │ │ ├── options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt │ │ ├── options_within_targeting_rule_no_targeted_attribute.txt │ │ ├── options_within_targeting_rule_no_user.txt │ │ └── options_within_targeting_rule_not_matching_targeted_attribute.txt │ ├── prerequisite_flag.json │ ├── prerequisite_flag │ │ ├── prerequisite_flag.txt │ │ ├── prerequisite_flag_multilevel.txt │ │ ├── prerequisite_flag_no_user_needed_by_both.txt │ │ ├── prerequisite_flag_no_user_needed_by_dep.txt │ │ └── prerequisite_flag_no_user_needed_by_prereq.txt │ ├── segment.json │ ├── segment │ │ ├── segment_matching.txt │ │ ├── segment_no_matching.txt │ │ ├── segment_no_targeted_attribute.txt │ │ ├── segment_no_user.txt │ │ └── segment_no_user_multi_conditions.txt │ ├── semver_validation.json │ ├── semver_validation │ │ ├── semver_error.txt │ │ └── semver_relations_error.txt │ ├── simple_value.json │ └── simple_value │ │ ├── double_setting.txt │ │ ├── int_setting.txt │ │ ├── off_flag.txt │ │ ├── on_flag.txt │ │ └── text_setting.txt ├── test-simple.json ├── test.json ├── test_circulardependency_v6.json ├── test_override_flagdependency_v6.json ├── test_override_segments_v6.json ├── testmatrix.csv ├── testmatrix_and_or.csv ├── testmatrix_comparators_v6.csv ├── testmatrix_number.csv ├── testmatrix_prerequisite_flag.csv ├── testmatrix_segments.csv ├── testmatrix_segments_old.csv ├── testmatrix_semantic.csv ├── testmatrix_semantic_2.csv ├── testmatrix_sensitive.csv ├── testmatrix_unicode.csv └── testmatrix_variationId.csv ├── datagovernance_spec.rb ├── evaluationlog_spec.rb ├── integration_spec.rb ├── override_spec.rb ├── rollout_spec.rb ├── spec_helper.rb └── specialcharacter_spec.rb /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @configcat/developers 2 | -------------------------------------------------------------------------------- /.github/workflows/ruby-ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ master ] 8 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | ruby-versions: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3' ] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-versions }} 27 | bundler-cache: true 28 | 29 | - name: Run tests 30 | run: bundle exec rake 31 | 32 | lint: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@v1 38 | with: 39 | ruby-version: 2.6 40 | bundler-cache: true 41 | - name: Run Rubocop 42 | run: bundle exec rubocop 43 | 44 | coverage: 45 | needs: test 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Set up Ruby 50 | uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: 2.6 53 | bundler-cache: true 54 | - name: Run tests with coverage 55 | run: bundle exec rake 56 | - name: Upload coverage to Codecov 57 | uses: codecov/codecov-action@v3 58 | 59 | publish: 60 | needs: coverage 61 | runs-on: ubuntu-latest 62 | if: startsWith(github.ref, 'refs/tags') 63 | steps: 64 | - uses: actions/checkout@v3 65 | - name: Set up Ruby 66 | uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: 2.6 69 | - name: Publish to RubyGems 70 | run: | 71 | mkdir -p $HOME/.gem 72 | touch $HOME/.gem/credentials 73 | chmod 0600 $HOME/.gem/credentials 74 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 75 | gem build *.gemspec 76 | gem push *.gem 77 | env: 78 | GEM_HOST_API_KEY: "${{ secrets.GEM_API_KEY }}" 79 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Snyk 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 1' 6 | pull_request: 7 | branches: [ master ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | snyk: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: '3.0' 20 | bundler-cache: true 21 | 22 | - name: Run tests 23 | run: bundle exec rake 24 | 25 | - name: Run Snyk to check for vulnerabilities 26 | uses: snyk/actions/ruby@master 27 | env: 28 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 29 | with: 30 | command: monitor -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * *' 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | stale: 11 | uses: configcat/.github/.github/workflows/stale.yml@master 12 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | vendor/* 3 | .bundle 4 | bin 5 | coverage 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /.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 | # Two spaces, no tabs (for indentation). 51 | Layout/IndentationWidth: 52 | Enabled: true 53 | 54 | Layout/LeadingCommentSpace: 55 | Enabled: true 56 | 57 | Layout/SpaceAfterColon: 58 | Enabled: true 59 | 60 | Layout/SpaceAfterComma: 61 | Enabled: true 62 | 63 | Layout/SpaceAroundEqualsInParameterDefault: 64 | Enabled: true 65 | 66 | Layout/SpaceAroundKeyword: 67 | Enabled: true 68 | 69 | Layout/SpaceAroundOperators: 70 | Enabled: true 71 | 72 | Layout/SpaceBeforeComma: 73 | Enabled: true 74 | 75 | Layout/SpaceBeforeFirstArg: 76 | Enabled: true 77 | 78 | Style/DefWithParentheses: 79 | Enabled: true 80 | 81 | # Defining a method with parameters needs parentheses. 82 | Style/MethodDefParentheses: 83 | Enabled: true 84 | 85 | Style/FrozenStringLiteralComment: 86 | Enabled: false 87 | EnforcedStyle: always 88 | 89 | # Use `foo {}` not `foo{}`. 90 | Layout/SpaceBeforeBlockBraces: 91 | Enabled: true 92 | 93 | # Use `foo { bar }` not `foo {bar}`. 94 | Layout/SpaceInsideBlockBraces: 95 | Enabled: true 96 | 97 | # Use `{ a: 1 }` not `{a:1}`. 98 | Layout/SpaceInsideHashLiteralBraces: 99 | Enabled: true 100 | 101 | Layout/SpaceInsideParens: 102 | Enabled: true 103 | 104 | # Check quotes usage according to lint rule below. 105 | #Style/StringLiterals: 106 | # Enabled: true 107 | # EnforcedStyle: single_quotes 108 | 109 | # Detect hard tabs, no hard tabs. 110 | Layout/IndentationStyle: 111 | Enabled: true 112 | 113 | # Blank lines should not have any spaces. 114 | Layout/TrailingEmptyLines: 115 | Enabled: true 116 | 117 | # No trailing whitespace. 118 | Layout/TrailingWhitespace: 119 | Enabled: false 120 | 121 | # Use quotes for string literals when they are enough. 122 | Style/RedundantPercentQ: 123 | Enabled: true 124 | 125 | # Align `end` with the matching keyword or starting expression except for 126 | # assignments, where it should be aligned with the LHS. 127 | Layout/EndAlignment: 128 | Enabled: true 129 | EnforcedStyleAlignWith: variable 130 | AutoCorrect: true 131 | 132 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 133 | Lint/RequireParentheses: 134 | Enabled: true 135 | 136 | Style/RedundantReturn: 137 | Enabled: false 138 | AllowMultipleReturnValues: true 139 | 140 | Style/Semicolon: 141 | Enabled: true 142 | AllowAsExpressionSeparator: true 143 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.sources=lib 2 | sonar.tests=spec 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Please check the [Github Releases](https://github.com/configcat/ruby-sdk/releases) page for the changelog of the ConfigCat Ruby SDK. 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ConfigCat SDK for Ruby 2 | 3 | ConfigCat SDK is an open source project. Feedback and contribution are welcome. Contributions are made to this repo via Issues and Pull Requests. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The ConfigCat SDK team monitors the [issue tracker](https://github.com/configcat/ruby-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The team will respond to all newly filed issues. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. 12 | - Before submitting pull requests, ensure that all temporary or unintended code is removed. 13 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 14 | - Add unit or integration tests for fixed or changed functionality. 15 | 16 | When you submit a pull request or otherwise seek to include your change in the repository, you waive all your intellectual property rights, including your copyright and patent claims for the submission. For more details please read the [contribution agreement](https://github.com/configcat/legal/blob/main/contribution-agreement.md). 17 | 18 | In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 19 | 20 | 1. Fork the repository to your own Github account 21 | 2. Clone the project to your machine 22 | 3. Create a branch locally with a succinct but descriptive name 23 | 4. Commit changes to the branch 24 | 5. Following any formatting and testing guidelines specific to this repo 25 | 6. Push changes to your fork 26 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 27 | 28 | ## Build instructions 29 | 30 | This ConfigCat SDK is built with [Bundler](https://bundler.io). To install Bundler, run `gem install bundler`. 31 | 32 | To install dependencies: 33 | 34 | ```bash 35 | bundle install 36 | ``` 37 | 38 | ## Running tests 39 | 40 | ```bash 41 | bundle exec rspec spec 42 | ``` 43 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Steps to deploy 2 | ## Preparation 3 | 1. Run tests 4 | ```bash 5 | bundle install --binstubs 6 | bin/rspec --format doc 7 | ``` 8 | 2. Increase the version in the `lib/configcat/version.rb` file. 9 | 3. Commit & Push 10 | ## Publish 11 | Use the **same version** for the git tag as in the `version.rb`. 12 | - Via git tag 13 | 1. Create a new version tag. 14 | ```bash 15 | git tag v[MAJOR].[MINOR].[PATCH] 16 | ``` 17 | > Example: `git tag v2.5.5` 18 | 2. Push the tag. 19 | ```bash 20 | git push origin --tags 21 | ``` 22 | - Via Github release 23 | 24 | Create a new [Github release](https://github.com/configcat/ruby-sdk/releases) with a new version tag and release notes. 25 | 26 | ## RubyGems 27 | Make sure the new version is available on [RubyGems](https://rubygems.org/gems/configcat). 28 | 29 | ## Update samples 30 | Update and test sample apps with the new SDK version. 31 | ```bash 32 | gem update configcat 33 | ``` 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ConfigCat 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat SDK for Ruby 2 | https://configcat.com 3 | ConfigCat SDK for Ruby provides easy integration for your application to ConfigCat. 4 | 5 | ConfigCat is a feature flag and configuration management service that lets you separate releases from deployments. You can turn your features ON/OFF using ConfigCat Dashboard even after they are deployed. ConfigCat lets you target specific groups of users based on region, email or any other custom user attribute. 6 | 7 | ConfigCat is a hosted feature flag service. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs. 8 | 9 | [![Ruby CI](https://github.com/configcat/ruby-sdk/actions/workflows/ruby-ci.yml/badge.svg?branch=master)](https://github.com/configcat/ruby-sdk/actions/workflows/ruby-ci.yml) 10 | [![Coverage Status](https://img.shields.io/codecov/c/github/ConfigCat/ruby-sdk.svg)](https://codecov.io/gh/ConfigCat/ruby-sdk) 11 | [![Gem version](https://badge.fury.io/rb/configcat.svg)](https://rubygems.org/gems/configcat) 12 | ![License](https://img.shields.io/github/license/configcat/ruby-sdk.svg) 13 | 14 | ## Getting started 15 | ### 1. Install the package with `RubyGems` 16 | ```bash 17 | gem install configcat 18 | ``` 19 | 20 | ### 2. Import `configcat` to your application 21 | ```ruby 22 | require 'configcat' 23 | ``` 24 | 25 | ### 3. Go to the ConfigCat Dashboard to get your *SDK Key*: 26 | ![SDK-KEY](https://raw.githubusercontent.com/ConfigCat/ruby-sdk/master/media/readme02-3.png "SDK-KEY") 27 | 28 | ### 4. Create a *ConfigCat* client instance: 29 | ```ruby 30 | configcat_client = ConfigCat.get("#YOUR-SDK-KEY#") 31 | ``` 32 | > We strongly recommend using the *ConfigCat Client* as a Singleton object in your application. The `ConfigCat.get` static factory method constructs singleton client instances for your SDK keys. 33 | 34 | ### 5. Get your setting value 35 | ```ruby 36 | isMyAwesomeFeatureEnabled = configcat_client.get_value("isMyAwesomeFeatureEnabled", false) 37 | if isMyAwesomeFeatureEnabled 38 | do_the_new_thing 39 | else 40 | do_the_old_thing 41 | end 42 | ``` 43 | 44 | ### 6. Stop *ConfigCat* client on application exit 45 | ```ruby 46 | configcat_client.close 47 | ``` 48 | 49 | ## Getting user specific setting values with Targeting 50 | Using this feature, you will be able to get different setting values for different users in your application by passing a `User Object` to the `get_value()` function. 51 | 52 | Read more about [Targeting here](https://configcat.com/docs/advanced/targeting/). 53 | ```ruby 54 | user = ConfigCat::User.new("#USER-IDENTIFIER#") 55 | 56 | isMyAwesomeFeatureEnabled = configcat_client.get_value("isMyAwesomeFeatureEnabled", false, user) 57 | if isMyAwesomeFeatureEnabled 58 | do_the_new_thing 59 | else 60 | do_the_old_thing 61 | end 62 | ``` 63 | 64 | ## Sample/Demo apps 65 | * [Sample Console Apps](https://github.com/configcat/ruby-sdk/tree/master/samples) 66 | 67 | ## Polling Modes 68 | The ConfigCat SDK supports 3 different polling mechanisms to acquire the setting values from ConfigCat. After latest setting values are downloaded, they are stored in the internal cache then all requests are served from there. Read more about Polling Modes and how to use them at [ConfigCat Docs](https://configcat.com/docs/sdk-reference/ruby/). 69 | 70 | ## Need help? 71 | https://configcat.com/support 72 | 73 | ## Contributing 74 | Contributions are welcome. For more info please read the [Contribution Guideline](CONTRIBUTING.md). 75 | 76 | ## About ConfigCat 77 | - [Official ConfigCat SDKs for other platforms](https://github.com/configcat) 78 | - [Documentation](https://configcat.com/docs) 79 | - [Blog](https://configcat.com/blog) 80 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | desc "Run specs" 4 | RSpec::Core::RakeTask.new do |t| 5 | end 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /configcat.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'configcat/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'configcat' 7 | spec.version = ConfigCat::VERSION 8 | spec.authors = ['ConfigCat'] 9 | spec.email = ["developer@configcat.com"] 10 | spec.licenses = ["MIT"] 11 | 12 | spec.summary = "ConfigCat SDK for Ruby." 13 | spec.description = "Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages. Alternative to LaunchDarkly. Host yourself, or use the hosted management app at https://configcat.com." 14 | 15 | spec.homepage = "https://configcat.com" 16 | 17 | spec.files = Dir['lib/*'] + Dir['lib/**/*.rb'] 18 | spec.require_paths = ["lib"] 19 | spec.required_ruby_version = ">= 2.2" 20 | 21 | spec.add_dependency "concurrent-ruby", "~> 1.1" 22 | spec.add_dependency "semantic", "~> 1.6" 23 | 24 | spec.add_development_dependency "rspec", "~> 3.0" 25 | spec.add_development_dependency "rake", "~> 12.3" 26 | spec.add_development_dependency "codecov", "~> 0.5" 27 | if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5') 28 | spec.add_development_dependency "webmock", "~> 3.18" 29 | else 30 | spec.add_development_dependency "webmock", "~> 3.25" 31 | end 32 | spec.add_development_dependency "rubocop" 33 | end 34 | -------------------------------------------------------------------------------- /lib/configcat.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/interfaces' 2 | require 'configcat/localdictionarydatasource' 3 | require 'configcat/localfiledatasource' 4 | require 'configcat/configcatclient' 5 | require 'configcat/user' 6 | require 'logger' 7 | 8 | module ConfigCat 9 | @logger = Logger.new(STDOUT, level: Logger::WARN) 10 | class << self 11 | attr_accessor :logger 12 | end 13 | 14 | # Creates a new or gets an already existing `ConfigCatClient` for the given `sdk_key`. 15 | # 16 | # :param sdk_key [String] ConfigCat SDK Key to access your configuration. 17 | # :param options [ConfigCatOptions] Configuration `ConfigCatOptions` for `ConfigCatClient`. 18 | # :return [ConfigCatClient] the `ConfigCatClient` instance. 19 | def ConfigCat.get(sdk_key, options = nil) 20 | return ConfigCatClient.get(sdk_key, options) 21 | end 22 | 23 | # Closes all ConfigCatClient instances. 24 | def ConfigCat.close_all 25 | ConfigCatClient.close_all 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/configcat/config.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | CONFIG_FILE_NAME = 'config_v6' 3 | SERIALIZATION_FORMAT_VERSION = 'v2' 4 | 5 | # Config 6 | PREFERENCES = 'p' 7 | SEGMENTS = 's' 8 | FEATURE_FLAGS = 'f' 9 | 10 | # Preferences 11 | BASE_URL = 'u' 12 | REDIRECT = 'r' 13 | SALT = 's' 14 | 15 | # Segment 16 | SEGMENT_NAME = 'n' # The first 4 characters of the Segment's name 17 | SEGMENT_CONDITIONS = 'r' # The list of segment rule conditions (has a logical AND relation between the items). 18 | 19 | # Segment Condition (User Condition) 20 | COMPARISON_ATTRIBUTE = 'a' # The attribute of the user object that should be used to evaluate this rule 21 | COMPARATOR = 'c' 22 | 23 | # Feature flag (Evaluation Formula) 24 | SETTING_TYPE = 't' # 0 = bool, 1 = string, 2 = int, 3 = double 25 | PERCENTAGE_RULE_ATTRIBUTE = 'a' # Percentage rule evaluation hashes this attribute of the User object to calculate the buckets 26 | TARGETING_RULES = 'r' # Targeting Rules (Logically connected by OR) 27 | PERCENTAGE_OPTIONS = 'p' # Percentage Options without conditions 28 | VALUE = 'v' 29 | VARIATION_ID = 'i' 30 | INLINE_SALT = 'inline_salt' 31 | 32 | # Targeting Rule (Evaluation Rule) 33 | CONDITIONS = 'c' 34 | SERVED_VALUE = 's' # Value and Variation ID 35 | TARGETING_RULE_PERCENTAGE_OPTIONS = 'p' 36 | 37 | # Condition 38 | USER_CONDITION = 'u' 39 | SEGMENT_CONDITION = 's' # Segment targeting rule 40 | PREREQUISITE_FLAG_CONDITION = 'p' # Prerequisite flag targeting rule 41 | 42 | # Segment Condition 43 | SEGMENT_INDEX = 's' 44 | SEGMENT_COMPARATOR = 'c' 45 | INLINE_SEGMENT = 'inline_segment' 46 | 47 | # Prerequisite Flag Condition 48 | PREREQUISITE_FLAG_KEY = 'f' 49 | PREREQUISITE_COMPARATOR = 'c' 50 | 51 | # Percentage Option 52 | PERCENTAGE = 'p' 53 | 54 | # Value 55 | BOOL_VALUE = 'b' 56 | STRING_VALUE = 's' 57 | INT_VALUE = 'i' 58 | DOUBLE_VALUE = 'd' 59 | STRING_LIST_VALUE = 'l' 60 | UNSUPPORTED_VALUE = 'unsupported_value' 61 | 62 | module Config 63 | def self.is_type_mismatch(value, ruby_type) 64 | is_float_int_mismatch = \ 65 | (value.is_a?(Float) && ruby_type == Integer) || \ 66 | (value.is_a?(Integer) && ruby_type == Float) 67 | 68 | is_bool_mismatch = value.is_a?(TrueClass) && ruby_type == FalseClass || \ 69 | value.is_a?(FalseClass) && ruby_type == TrueClass 70 | 71 | if value.class != ruby_type && !is_float_int_mismatch && !is_bool_mismatch 72 | return true 73 | end 74 | 75 | return false 76 | end 77 | 78 | def self.get_value(dictionary, setting_type) 79 | value_descriptor = dictionary[VALUE] 80 | if value_descriptor.nil? 81 | raise 'Value is missing' 82 | end 83 | 84 | expected_value_type, expected_ruby_type = SettingType.get_type_info(setting_type) 85 | if expected_value_type.nil? 86 | raise 'Unsupported setting type' 87 | end 88 | 89 | value = value_descriptor[expected_value_type] 90 | if value.nil? || is_type_mismatch(value, expected_ruby_type) 91 | raise "Setting value is not of the expected type #{expected_ruby_type}" 92 | end 93 | 94 | return value 95 | end 96 | 97 | def self.get_value_type(dictionary) 98 | value = dictionary[VALUE] 99 | if !value.nil? 100 | if !value[BOOL_VALUE].nil? 101 | return TrueClass 102 | end 103 | if !value[STRING_VALUE].nil? 104 | return String 105 | end 106 | if !value[INT_VALUE].nil? 107 | return Integer 108 | end 109 | if !value[DOUBLE_VALUE].nil? 110 | return Float 111 | end 112 | end 113 | 114 | return nil 115 | end 116 | 117 | def self.fixup_config_salt_and_segments(config) 118 | """ 119 | Adds the inline salt and segment to the config. 120 | When using flag overrides, the original salt and segment indexes may become invalid. Therefore, we copy the 121 | object references to the locations where they are referenced and use these references instead of the indexes. 122 | """ 123 | salt = config.fetch(PREFERENCES, {}).fetch(SALT, '') 124 | segments = config[SEGMENTS] || [] 125 | settings = config[FEATURE_FLAGS] || {} 126 | settings.each do |_, setting| 127 | next unless setting.is_a?(Hash) 128 | 129 | # add salt 130 | setting[INLINE_SALT] = salt 131 | 132 | # add segment to the segment conditions 133 | targeting_rules = setting[TARGETING_RULES] || [] 134 | targeting_rules.each do |targeting_rule| 135 | conditions = targeting_rule[CONDITIONS] || [] 136 | conditions.each do |condition| 137 | segment_condition = condition[SEGMENT_CONDITION] 138 | if segment_condition 139 | segment_index = segment_condition[SEGMENT_INDEX] 140 | segment = segments[segment_index] 141 | segment_condition[INLINE_SEGMENT] = segment 142 | end 143 | end 144 | end 145 | end 146 | end 147 | end 148 | 149 | class SettingType 150 | BOOL = 0 151 | STRING = 1 152 | INT = 2 153 | DOUBLE = 3 154 | 155 | @@setting_type_mapping = { 156 | SettingType::BOOL => [BOOL_VALUE, TrueClass], 157 | SettingType::STRING => [STRING_VALUE, String], 158 | SettingType::INT => [INT_VALUE, Integer], 159 | SettingType::DOUBLE => [DOUBLE_VALUE, Float] 160 | } 161 | 162 | def self.get_type_info(setting_type) 163 | return @@setting_type_mapping[setting_type] || [nil, nil] 164 | end 165 | 166 | def self.from_type(object_type) 167 | if object_type == TrueClass || object_type == FalseClass 168 | return BOOL 169 | elsif object_type == String 170 | return STRING 171 | elsif object_type == Integer 172 | return INT 173 | elsif object_type == Float 174 | return DOUBLE 175 | end 176 | 177 | return nil 178 | end 179 | 180 | def self.to_type(setting_type) 181 | return get_type_info(setting_type)[1] 182 | end 183 | 184 | def self.to_value_type(setting_type) 185 | return get_type_info(setting_type)[0] 186 | end 187 | end 188 | 189 | module PrerequisiteComparator 190 | EQUALS = 0 191 | NOT_EQUALS = 1 192 | end 193 | 194 | module SegmentComparator 195 | IS_IN = 0 196 | IS_NOT_IN = 1 197 | end 198 | 199 | module Comparator 200 | IS_ONE_OF = 0 201 | IS_NOT_ONE_OF = 1 202 | CONTAINS_ANY_OF = 2 203 | NOT_CONTAINS_ANY_OF = 3 204 | IS_ONE_OF_SEMVER = 4 205 | IS_NOT_ONE_OF_SEMVER = 5 206 | LESS_THAN_SEMVER = 6 207 | LESS_THAN_OR_EQUAL_SEMVER = 7 208 | GREATER_THAN_SEMVER = 8 209 | GREATER_THAN_OR_EQUAL_SEMVER = 9 210 | EQUALS_NUMBER = 10 211 | NOT_EQUALS_NUMBER = 11 212 | LESS_THAN_NUMBER = 12 213 | LESS_THAN_OR_EQUAL_NUMBER = 13 214 | GREATER_THAN_NUMBER = 14 215 | GREATER_THAN_OR_EQUAL_NUMBER = 15 216 | IS_ONE_OF_HASHED = 16 217 | IS_NOT_ONE_OF_HASHED = 17 218 | BEFORE_DATETIME = 18 219 | AFTER_DATETIME = 19 220 | EQUALS_HASHED = 20 221 | NOT_EQUALS_HASHED = 21 222 | STARTS_WITH_ANY_OF_HASHED = 22 223 | NOT_STARTS_WITH_ANY_OF_HASHED = 23 224 | ENDS_WITH_ANY_OF_HASHED = 24 225 | NOT_ENDS_WITH_ANY_OF_HASHED = 25 226 | ARRAY_CONTAINS_ANY_OF_HASHED = 26 227 | ARRAY_NOT_CONTAINS_ANY_OF_HASHED = 27 228 | EQUALS = 28 229 | NOT_EQUALS = 29 230 | STARTS_WITH_ANY_OF = 30 231 | NOT_STARTS_WITH_ANY_OF = 31 232 | ENDS_WITH_ANY_OF = 32 233 | NOT_ENDS_WITH_ANY_OF = 33 234 | ARRAY_CONTAINS_ANY_OF = 34 235 | ARRAY_NOT_CONTAINS_ANY_OF = 35 236 | end 237 | 238 | COMPARATOR_TEXTS = [ 239 | 'IS ONE OF', # IS_ONE_OF 240 | 'IS NOT ONE OF', # IS_NOT_ONE_OF 241 | 'CONTAINS ANY OF', # CONTAINS_ANY_OF 242 | 'NOT CONTAINS ANY OF', # NOT_CONTAINS_ANY_OF 243 | 'IS ONE OF', # IS_ONE_OF_SEMVER 244 | 'IS NOT ONE OF', # IS_NOT_ONE_OF_SEMVER 245 | '<', # LESS_THAN_SEMVER 246 | '<=', # LESS_THAN_OR_EQUAL_SEMVER 247 | '>', # GREATER_THAN_SEMVER 248 | '>=', # GREATER_THAN_OR_EQUAL_SEMVER 249 | '=', # EQUALS_NUMBER 250 | '!=', # NOT_EQUALS_NUMBER 251 | '<', # LESS_THAN_NUMBER 252 | '<=', # LESS_THAN_OR_EQUAL_NUMBER 253 | '>', # GREATER_THAN_NUMBER 254 | '>=', # GREATER_THAN_OR_EQUAL_NUMBER 255 | 'IS ONE OF', # IS_ONE_OF_HASHED 256 | 'IS NOT ONE OF', # IS_NOT_ONE_OF_HASHED 257 | 'BEFORE', # BEFORE_DATETIME 258 | 'AFTER', # AFTER_DATETIME 259 | 'EQUALS', # EQUALS_HASHED 260 | 'NOT EQUALS', # NOT_EQUALS_HASHED 261 | 'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF_HASHED 262 | 'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF_HASHED 263 | 'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF_HASHED 264 | 'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF_HASHED 265 | 'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF_HASHED 266 | 'ARRAY NOT CONTAINS ANY OF', # ARRAY_NOT_CONTAINS_ANY_OF_HASHED 267 | 'EQUALS', # EQUALS 268 | 'NOT EQUALS', # NOT_EQUALS 269 | 'STARTS WITH ANY OF', # STARTS_WITH_ANY_OF 270 | 'NOT STARTS WITH ANY OF', # NOT_STARTS_WITH_ANY_OF 271 | 'ENDS WITH ANY OF', # ENDS_WITH_ANY_OF 272 | 'NOT ENDS WITH ANY OF', # NOT_ENDS_WITH_ANY_OF 273 | 'ARRAY CONTAINS ANY OF', # ARRAY_CONTAINS_ANY_OF 274 | 'ARRAY NOT CONTAINS ANY OF' # ARRAY_NOT_CONTAINS_ANY_OF 275 | ] 276 | 277 | COMPARISON_VALUES = [ 278 | STRING_LIST_VALUE, # IS_ONE_OF 279 | STRING_LIST_VALUE, # IS_NOT_ONE_OF 280 | STRING_LIST_VALUE, # CONTAINS_ANY_OF 281 | STRING_LIST_VALUE, # NOT_CONTAINS_ANY_OF 282 | STRING_LIST_VALUE, # IS_ONE_OF_SEMVER 283 | STRING_LIST_VALUE, # IS_NOT_ONE_OF_SEMVER 284 | STRING_VALUE, # LESS_THAN_SEMVER 285 | STRING_VALUE, # LESS_THAN_OR_EQUAL_SEMVER 286 | STRING_VALUE, # GREATER_THAN_SEMVER 287 | STRING_VALUE, # GREATER_THAN_OR_EQUAL_SEMVER 288 | DOUBLE_VALUE, # EQUALS_NUMBER 289 | DOUBLE_VALUE, # NOT_EQUALS_NUMBER 290 | DOUBLE_VALUE, # LESS_THAN_NUMBER 291 | DOUBLE_VALUE, # LESS_THAN_OR_EQUAL_NUMBER 292 | DOUBLE_VALUE, # GREATER_THAN_NUMBER 293 | DOUBLE_VALUE, # GREATER_THAN_OR_EQUAL_NUMBER 294 | STRING_LIST_VALUE, # IS_ONE_OF_HASHED 295 | STRING_LIST_VALUE, # IS_NOT_ONE_OF_HASHED 296 | DOUBLE_VALUE, # BEFORE_DATETIME 297 | DOUBLE_VALUE, # AFTER_DATETIME 298 | STRING_VALUE, # EQUALS_HASHED 299 | STRING_VALUE, # NOT_EQUALS_HASHED 300 | STRING_LIST_VALUE, # STARTS_WITH_ANY_OF_HASHED 301 | STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF_HASHED 302 | STRING_LIST_VALUE, # ENDS_WITH_ANY_OF_HASHED 303 | STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF_HASHED 304 | STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF_HASHED 305 | STRING_LIST_VALUE, # ARRAY_NOT_CONTAINS_ANY_OF_HASHED 306 | STRING_VALUE, # EQUALS 307 | STRING_VALUE, # NOT_EQUALS 308 | STRING_LIST_VALUE, # STARTS_WITH_ANY_OF 309 | STRING_LIST_VALUE, # NOT_STARTS_WITH_ANY_OF 310 | STRING_LIST_VALUE, # ENDS_WITH_ANY_OF 311 | STRING_LIST_VALUE, # NOT_ENDS_WITH_ANY_OF 312 | STRING_LIST_VALUE, # ARRAY_CONTAINS_ANY_OF 313 | STRING_LIST_VALUE # ARRAY_NOT_CONTAINS_ANY_OF 314 | ] 315 | SEGMENT_COMPARATOR_TEXTS = ['IS IN SEGMENT', 'IS NOT IN SEGMENT'] 316 | PREREQUISITE_COMPARATOR_TEXTS = ['EQUALS', 'DOES NOT EQUAL'] 317 | end 318 | -------------------------------------------------------------------------------- /lib/configcat/configcache.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/interfaces' 2 | 3 | module ConfigCat 4 | class NullConfigCache < ConfigCache 5 | def initialize 6 | @value = {} 7 | end 8 | 9 | def get(key) 10 | return nil 11 | end 12 | 13 | def set(key, value) 14 | # do nothing 15 | end 16 | end 17 | 18 | class InMemoryConfigCache < ConfigCache 19 | attr_reader :value 20 | def initialize 21 | @value = {} 22 | end 23 | 24 | def get(key) 25 | return @value.fetch(key, nil) 26 | end 27 | 28 | def set(key, value) 29 | @value[key] = value 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/configcat/configcatlogger.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class ConfigCatLogger 3 | def initialize(hooks) 4 | @hooks = hooks 5 | end 6 | 7 | def enabled_for?(log_level) 8 | ConfigCat.logger.level <= log_level 9 | end 10 | 11 | def debug(message) 12 | ConfigCat.logger.debug("[0] " + message) 13 | end 14 | 15 | def info(event_id, message) 16 | ConfigCat.logger.info("[" + event_id.to_s + "] " + message) 17 | end 18 | 19 | def warn(event_id, message) 20 | ConfigCat.logger.warn("[" + event_id.to_s + "] " + message) 21 | end 22 | 23 | def error(event_id, message) 24 | @hooks.invoke_on_error(message) 25 | ConfigCat.logger.error("[" + event_id.to_s + "] " + message) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/configcat/configcatoptions.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/datagovernance' 2 | require 'configcat/pollingmode' 3 | 4 | module ConfigCat 5 | class Hooks 6 | # 7 | # Events fired by [ConfigCatClient]. 8 | # 9 | 10 | def initialize(on_client_ready: nil, on_config_changed: nil, on_flag_evaluated: nil, on_error: nil) 11 | @_on_client_ready_callbacks = on_client_ready ? [on_client_ready] : [] 12 | @_on_config_changed_callbacks = on_config_changed ? [on_config_changed] : [] 13 | @_on_flag_evaluated_callbacks = on_flag_evaluated ? [on_flag_evaluated] : [] 14 | @_on_error_callbacks = on_error ? [on_error] : [] 15 | end 16 | 17 | def add_on_client_ready(callback) 18 | @_on_client_ready_callbacks.push(callback) 19 | end 20 | 21 | def add_on_config_changed(callback) 22 | @_on_config_changed_callbacks.push(callback) 23 | end 24 | 25 | def add_on_flag_evaluated(callback) 26 | @_on_flag_evaluated_callbacks.push(callback) 27 | end 28 | 29 | def add_on_error(callback) 30 | @_on_error_callbacks.push(callback) 31 | end 32 | 33 | def invoke_on_client_ready 34 | @_on_client_ready_callbacks.each { |callback| 35 | begin 36 | callback.() 37 | rescue Exception => e 38 | error = "Exception occurred during invoke_on_client_ready callback: #{e}" 39 | invoke_on_error(error) 40 | ConfigCat.logger.error(error) 41 | end 42 | } 43 | end 44 | 45 | def invoke_on_config_changed(config) 46 | @_on_config_changed_callbacks.each { |callback| 47 | begin 48 | callback.(config) 49 | rescue Exception => e 50 | error = "Exception occurred during invoke_on_config_changed callback: #{e}" 51 | invoke_on_error(error) 52 | ConfigCat.logger.error(error) 53 | end 54 | } 55 | end 56 | 57 | def invoke_on_flag_evaluated(evaluation_details) 58 | @_on_flag_evaluated_callbacks.each { |callback| 59 | begin 60 | callback.(evaluation_details) 61 | rescue Exception => e 62 | error = "Exception occurred during invoke_on_flag_evaluated callback: #{e}" 63 | invoke_on_error(error) 64 | ConfigCat.logger.error(error) 65 | end 66 | } 67 | end 68 | 69 | def invoke_on_error(error) 70 | @_on_error_callbacks.each { |callback| 71 | begin 72 | callback.(error) 73 | rescue Exception => e 74 | ConfigCat.logger.error("Exception occurred during invoke_on_error callback: #{e}") 75 | end 76 | } 77 | end 78 | 79 | def clear 80 | @_on_client_ready_callbacks.clear 81 | @_on_config_changed_callbacks.clear 82 | @_on_flag_evaluated_callbacks.clear 83 | @_on_error_callbacks.clear 84 | end 85 | end 86 | 87 | class ConfigCatOptions 88 | # Configuration options for ConfigCatClient. 89 | attr_reader :base_url, :polling_mode, :config_cache, :proxy_address, :proxy_port, :proxy_user, :proxy_pass, 90 | :open_timeout_seconds, :read_timeout_seconds, :flag_overrides, :data_governance, :default_user, 91 | :hooks, :offline 92 | 93 | def initialize(base_url: nil, 94 | polling_mode: PollingMode.auto_poll(), 95 | config_cache: nil, 96 | proxy_address: nil, 97 | proxy_port: nil, 98 | proxy_user: nil, 99 | proxy_pass: nil, 100 | open_timeout_seconds: 10, 101 | read_timeout_seconds: 30, 102 | flag_overrides: nil, 103 | data_governance: DataGovernance::GLOBAL, 104 | default_user: nil, 105 | hooks: nil, 106 | offline: false) 107 | # The base ConfigCat CDN url. 108 | @base_url = base_url 109 | 110 | # The polling mode. 111 | @polling_mode = polling_mode 112 | 113 | # The cache implementation used to cache the downloaded config files. 114 | @config_cache = config_cache 115 | 116 | # Proxy address 117 | @proxy_address = proxy_address 118 | 119 | # Proxy port 120 | @proxy_port = proxy_port 121 | 122 | # username for proxy authentication 123 | @proxy_user = proxy_user 124 | 125 | # password for proxy authentication 126 | @proxy_pass = proxy_pass 127 | 128 | # The number of seconds to wait for the server to make the initial connection 129 | # (i.e. completing the TCP connection handshake). 130 | @open_timeout_seconds = open_timeout_seconds 131 | 132 | # The number of seconds to wait for the server to respond before giving up. 133 | @read_timeout_seconds = read_timeout_seconds 134 | 135 | # Feature flag and setting overrides. 136 | @flag_overrides = flag_overrides 137 | 138 | # Default: `DataGovernance.Global`. Set this parameter to be in sync with the 139 | # Data Governance preference on the [Dashboard](https://app.configcat.com/organization/data-governance). 140 | # (Only Organization Admins have access) 141 | @data_governance = data_governance 142 | 143 | # The default user to be used for evaluating feature flags and getting settings. 144 | @default_user = default_user 145 | 146 | # The Hooks instance to subscribe to events. 147 | @hooks = hooks 148 | 149 | # Indicates whether the client should work in offline mode. 150 | @offline = offline 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/configcat/configentry.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/utils' 2 | 3 | module ConfigCat 4 | class ConfigEntry 5 | attr_accessor :config, :etag, :config_json_string, :fetch_time 6 | 7 | def initialize(config = {}, etag = '', config_json_string = '{}', fetch_time = Utils::DISTANT_PAST) 8 | @config = config 9 | @etag = etag 10 | @config_json_string = config_json_string 11 | @fetch_time = fetch_time 12 | end 13 | 14 | def empty? 15 | self == ConfigEntry::EMPTY 16 | end 17 | 18 | def serialize 19 | "#{(fetch_time * 1000).floor}\n#{etag}\n#{config_json_string}" 20 | end 21 | 22 | def self.create_from_string(string) 23 | return ConfigEntry.empty if string.nil? || string.empty? 24 | 25 | fetch_time_index = string.index("\n") 26 | etag_index = string.index("\n", fetch_time_index + 1) 27 | if fetch_time_index.nil? || etag_index.nil? 28 | raise 'Number of values is fewer than expected.' 29 | end 30 | 31 | begin 32 | fetch_time = Float(string[0...fetch_time_index]) 33 | rescue ArgumentError 34 | raise "Invalid fetch time: #{string[0...fetch_time_index]}" 35 | end 36 | 37 | etag = string[fetch_time_index + 1...etag_index] 38 | if etag.nil? || etag.empty? 39 | raise 'Empty eTag value' 40 | end 41 | begin 42 | config_json = string[etag_index + 1..-1] 43 | config = JSON.parse(config_json) 44 | Config.fixup_config_salt_and_segments(config) 45 | rescue => e 46 | raise "Invalid config JSON: #{config_json}. #{e.message}" 47 | end 48 | 49 | ConfigEntry.new(config, etag, config_json, fetch_time / 1000.0) 50 | end 51 | 52 | EMPTY = ConfigEntry.new(etag: 'empty') 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/configcat/configfetcher.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/interfaces' 2 | require 'configcat/version' 3 | require 'configcat/datagovernance' 4 | require 'configcat/config' 5 | require 'configcat/configentry' 6 | require 'net/http' 7 | require 'uri' 8 | require 'json' 9 | 10 | module ConfigCat 11 | BASE_URL_GLOBAL = "https://cdn-global.configcat.com" 12 | BASE_URL_EU_ONLY = "https://cdn-eu.configcat.com" 13 | BASE_PATH = "configuration-files/" 14 | BASE_EXTENSION = "/" + CONFIG_FILE_NAME + ".json" 15 | 16 | class RedirectMode 17 | NO_REDIRECT = 0 18 | SHOULD_REDIRECT = 1 19 | FORCE_REDIRECT = 2 20 | end 21 | 22 | class Status 23 | FETCHED = 0 24 | NOT_MODIFIED = 1 25 | FAILURE = 2 26 | end 27 | 28 | class FetchResponse 29 | attr_reader :entry, :error, :is_transient_error 30 | 31 | def initialize(status, entry, error = nil, is_transient_error = false) 32 | @status = status 33 | @entry = entry 34 | @error = error 35 | @is_transient_error = is_transient_error 36 | end 37 | 38 | # Gets whether a new configuration value was fetched or not. 39 | # :return [Boolean] true if a new configuration value was fetched, otherwise false. 40 | def is_fetched 41 | @status == Status::FETCHED 42 | end 43 | 44 | # Gets whether the fetch resulted a '304 Not Modified' or not. 45 | # :return [Boolean] true if the fetch resulted a '304 Not Modified' code, otherwise false. 46 | def is_not_modified 47 | @status == Status::NOT_MODIFIED 48 | end 49 | 50 | # Gets whether the fetch failed or not. 51 | # :return [Boolean] true if the fetch failed, otherwise false. 52 | def is_failed 53 | @status == Status::FAILURE 54 | end 55 | 56 | def self.success(entry) 57 | FetchResponse.new(Status::FETCHED, entry) 58 | end 59 | 60 | def self.not_modified 61 | FetchResponse.new(Status::NOT_MODIFIED, ConfigEntry::EMPTY) 62 | end 63 | 64 | def self.failure(error, is_transient_error) 65 | FetchResponse.new(Status::FAILURE, ConfigEntry::EMPTY, error, is_transient_error) 66 | end 67 | end 68 | 69 | class ConfigFetcher 70 | def initialize(sdk_key, log, mode, base_url: nil, proxy_address: nil, proxy_port: nil, proxy_user: nil, proxy_pass: nil, 71 | open_timeout: 10, read_timeout: 30, 72 | data_governance: DataGovernance::GLOBAL) 73 | @_sdk_key = sdk_key 74 | @log = log 75 | @_proxy_address = proxy_address 76 | @_proxy_port = proxy_port 77 | @_proxy_user = proxy_user 78 | @_proxy_pass = proxy_pass 79 | @_open_timeout = open_timeout 80 | @_read_timeout = read_timeout 81 | @_headers = { "User-Agent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "X-ConfigCat-UserAgent" => ((("ConfigCat-Ruby/") + mode) + ("-")) + VERSION, "Content-Type" => "application/json" } 82 | if !base_url.equal?(nil) 83 | @_base_url_overridden = true 84 | @_base_url = base_url.chomp("/") 85 | else 86 | @_base_url_overridden = false 87 | if data_governance == DataGovernance::EU_ONLY 88 | @_base_url = BASE_URL_EU_ONLY 89 | else 90 | @_base_url = BASE_URL_GLOBAL 91 | end 92 | end 93 | end 94 | 95 | def get_open_timeout 96 | return @_open_timeout 97 | end 98 | 99 | def get_read_timeout 100 | return @_read_timeout 101 | end 102 | 103 | # Returns the FetchResponse object contains configuration entry 104 | def get_configuration(etag = "", retries = 0) 105 | fetch_response = _fetch(etag) 106 | 107 | # If there wasn't a config change, we return the response. 108 | if !fetch_response.is_fetched() 109 | return fetch_response 110 | end 111 | 112 | preferences = fetch_response.entry.config.fetch(PREFERENCES, nil) 113 | if preferences === nil 114 | return fetch_response 115 | end 116 | 117 | base_url = preferences.fetch(BASE_URL, nil) 118 | 119 | # If the base_url is the same as the last called one, just return the response. 120 | if base_url.equal?(nil) || @_base_url == base_url 121 | return fetch_response 122 | end 123 | 124 | redirect = preferences.fetch(REDIRECT, nil) 125 | # If the base_url is overridden, and the redirect parameter is not 2 (force), 126 | # the SDK should not redirect the calls, and it just has to return the response. 127 | if @_base_url_overridden && redirect != RedirectMode::FORCE_REDIRECT 128 | return fetch_response 129 | end 130 | 131 | # The next call should use the base_url provided in the config json 132 | @_base_url = base_url 133 | 134 | # If the redirect property == 0 (redirect not needed), return the response 135 | if redirect == RedirectMode::NO_REDIRECT 136 | # Return the response 137 | return fetch_response 138 | end 139 | 140 | # Try to download again with the new url 141 | 142 | if redirect == RedirectMode::SHOULD_REDIRECT 143 | @log.warn(3002, "The `dataGovernance` parameter specified at the client initialization is not in sync with the preferences on the ConfigCat Dashboard. " \ 144 | "Read more: https://configcat.com/docs/advanced/data-governance/") 145 | end 146 | 147 | # To prevent loops we check if we retried at least 3 times with the new base_url 148 | if retries >= 2 149 | @log.error(1104, "Redirection loop encountered while trying to fetch config JSON. Please contact us at https://configcat.com/support/") 150 | return fetch_response 151 | end 152 | 153 | # Retry the config download with the new base_url 154 | return get_configuration(etag, retries + 1) 155 | end 156 | 157 | def close 158 | if @_http 159 | @_http = nil 160 | end 161 | end 162 | 163 | private 164 | 165 | def _fetch(etag) 166 | begin 167 | @log.debug("Fetching configuration from ConfigCat") 168 | uri = URI.parse((((@_base_url + ("/")) + BASE_PATH) + @_sdk_key) + BASE_EXTENSION) 169 | headers = @_headers 170 | headers["If-None-Match"] = etag.empty? ? nil : etag 171 | _create_http() 172 | request = Net::HTTP::Get.new(uri.request_uri, headers) 173 | response = @_http.request(request) 174 | case response 175 | when Net::HTTPSuccess 176 | @log.debug("ConfigCat configuration json fetch response code:#{response.code} Cached:#{response['ETag']}") 177 | response_etag = response["ETag"] 178 | if response_etag.nil? 179 | response_etag = "" 180 | end 181 | config = JSON.parse(response.body) 182 | Config.fixup_config_salt_and_segments(config) 183 | return FetchResponse.success(ConfigEntry.new(config, response_etag, response.body, Utils.get_utc_now_seconds_since_epoch)) 184 | when Net::HTTPNotModified 185 | return FetchResponse.not_modified 186 | when Net::HTTPNotFound, Net::HTTPForbidden 187 | error = "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. Received unexpected response: #{response}" 188 | @log.error(1100, error) 189 | return FetchResponse.failure(error, false) 190 | else 191 | raise Net::HTTPError.new("", response) 192 | end 193 | rescue Net::HTTPError => e 194 | error = "Unexpected HTTP response was received while trying to fetch config JSON: #{e}" 195 | @log.error(1101, error) 196 | return FetchResponse.failure(error, true) 197 | rescue Timeout::Error => e 198 | error = "Request timed out while trying to fetch config JSON. Timeout values: [connect: #{get_open_timeout()}s, read: #{get_read_timeout()}s]" 199 | @log.error(1102, error) 200 | return FetchResponse.failure(error, true) 201 | rescue Exception => e 202 | error = "Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network " \ 203 | "issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) " \ 204 | "over HTTP. #{e}" 205 | @log.error(1103, error) 206 | return FetchResponse.failure(error, true) 207 | end 208 | end 209 | 210 | def _create_http 211 | uri = URI.parse(@_base_url) 212 | use_ssl = true if uri.scheme == 'https' 213 | if @_http.equal?(nil) || @_http.address != uri.host || @_http.port != uri.port || @_http.use_ssl? != use_ssl 214 | close() 215 | @_http = Net::HTTP.new(uri.host, uri.port, @_proxy_address, @_proxy_port, @_proxy_user, @_proxy_pass) 216 | @_http.use_ssl = use_ssl 217 | @_http.open_timeout = @_open_timeout 218 | @_http.read_timeout = @_read_timeout 219 | end 220 | end 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /lib/configcat/configservice.rb: -------------------------------------------------------------------------------- 1 | require 'concurrent' 2 | require 'configcat/configentry' 3 | require 'configcat/pollingmode' 4 | require 'configcat/refreshresult' 5 | 6 | 7 | module ConfigCat 8 | class ConfigService 9 | def initialize(sdk_key, polling_mode, hooks, config_fetcher, log, config_cache, is_offline) 10 | @cached_entry = ConfigEntry::EMPTY 11 | @cached_entry_string = '' 12 | @polling_mode = polling_mode 13 | @log = log 14 | @config_cache = config_cache 15 | @hooks = hooks 16 | @cache_key = ConfigService.get_cache_key(sdk_key) 17 | @config_fetcher = config_fetcher 18 | @is_offline = is_offline 19 | @response_future = nil 20 | @initialized = Concurrent::Event.new 21 | @lock = Mutex.new 22 | @ongoing_fetch = false 23 | @fetch_finished = Concurrent::Event.new 24 | @start_time = Utils.get_utc_now_seconds_since_epoch 25 | 26 | if @polling_mode.is_a?(AutoPollingMode) && !@is_offline 27 | start_poll 28 | else 29 | set_initialized 30 | end 31 | end 32 | 33 | def get_config 34 | threshold = Utils::DISTANT_PAST 35 | prefer_cached = @initialized.set? 36 | if @polling_mode.is_a?(LazyLoadingMode) 37 | threshold = Utils.get_utc_now_seconds_since_epoch - @polling_mode.cache_refresh_interval_seconds 38 | prefer_cached = false 39 | elsif @polling_mode.is_a?(AutoPollingMode) && !@initialized.set? 40 | elapsed_time = Utils.get_utc_now_seconds_since_epoch - @start_time # Elapsed time in seconds 41 | threshold = Utils.get_utc_now_seconds_since_epoch - @polling_mode.poll_interval_seconds 42 | if elapsed_time < @polling_mode.max_init_wait_time_seconds 43 | @initialized.wait(@polling_mode.max_init_wait_time_seconds - elapsed_time) 44 | 45 | # Max wait time expired without result, notify subscribers with the cached config. 46 | if !@initialized.set? 47 | set_initialized 48 | return !@cached_entry.empty? ? 49 | [@cached_entry.config, @cached_entry.fetch_time] : 50 | [nil, Utils::DISTANT_PAST] 51 | end 52 | end 53 | end 54 | 55 | # If we are initialized, we prefer the cached results 56 | entry, _ = fetch_if_older(threshold, prefer_cached: prefer_cached) 57 | return !entry.empty? ? 58 | [entry.config, entry.fetch_time] : 59 | [nil, Utils::DISTANT_PAST] 60 | end 61 | 62 | # :return [RefreshResult] 63 | def refresh 64 | if offline? 65 | offline_warning = "Client is in offline mode, it cannot initiate HTTP calls." 66 | @log.warn(3200, offline_warning) 67 | return RefreshResult.new(success = false, error = offline_warning) 68 | end 69 | 70 | _, error = fetch_if_older(Utils::DISTANT_FUTURE) 71 | return RefreshResult.new(success = error.nil?, error = error) 72 | end 73 | 74 | def set_online 75 | @lock.synchronize do 76 | if !@is_offline 77 | return 78 | end 79 | 80 | @is_offline = false 81 | if @polling_mode.is_a?(AutoPollingMode) 82 | start_poll 83 | end 84 | @log.info(5200, "Switched to ONLINE mode.") 85 | end 86 | end 87 | 88 | def set_offline 89 | @lock.synchronize do 90 | if @is_offline 91 | return 92 | end 93 | 94 | @is_offline = true 95 | if @polling_mode.is_a?(AutoPollingMode) 96 | @stopped.set 97 | @thread.join 98 | end 99 | 100 | @log.info(5200, "Switched to OFFLINE mode.") 101 | end 102 | end 103 | 104 | def offline? 105 | return @is_offline 106 | end 107 | 108 | def close 109 | if @polling_mode.is_a?(AutoPollingMode) 110 | @stopped.set 111 | end 112 | end 113 | 114 | private 115 | 116 | def self.get_cache_key(sdk_key) 117 | Digest::SHA1.hexdigest("#{sdk_key}_#{CONFIG_FILE_NAME}.json_#{SERIALIZATION_FORMAT_VERSION}") 118 | end 119 | 120 | # :return [ConfigEntry, String] Returns the ConfigEntry object and error message in case of any error. 121 | def fetch_if_older(threshold, prefer_cached: false) 122 | # Sync up with the cache and use it when it's not expired. 123 | @lock.synchronize do 124 | # Sync up with the cache and use it when it's not expired. 125 | from_cache = read_cache 126 | if !from_cache.empty? && from_cache.etag != @cached_entry.etag 127 | @cached_entry = from_cache 128 | @hooks.invoke_on_config_changed(from_cache.config[FEATURE_FLAGS]) 129 | end 130 | 131 | # Cache isn't expired 132 | if @cached_entry.fetch_time > threshold 133 | set_initialized 134 | return @cached_entry, nil 135 | end 136 | 137 | # If we are in offline mode or the caller prefers cached values, do not initiate fetch. 138 | if @is_offline || prefer_cached 139 | return @cached_entry, nil 140 | end 141 | end 142 | 143 | # No fetch is running, initiate a new one. 144 | # Ensure only one fetch request is running at a time. 145 | # If there's an ongoing fetch running, we will wait for the ongoing fetch. 146 | if @ongoing_fetch 147 | @fetch_finished.wait 148 | else 149 | @ongoing_fetch = true 150 | @fetch_finished.reset 151 | response = @config_fetcher.get_configuration(@cached_entry.etag) 152 | 153 | @lock.synchronize do 154 | if response.is_fetched 155 | @cached_entry = response.entry 156 | write_cache(response.entry) 157 | @hooks.invoke_on_config_changed(response.entry.config[FEATURE_FLAGS]) 158 | elsif (response.is_not_modified || !response.is_transient_error) && !@cached_entry.empty? 159 | @cached_entry.fetch_time = Utils.get_utc_now_seconds_since_epoch 160 | write_cache(@cached_entry) 161 | end 162 | 163 | set_initialized 164 | end 165 | 166 | @ongoing_fetch = false 167 | @fetch_finished.set 168 | end 169 | 170 | return @cached_entry, nil 171 | end 172 | 173 | def start_poll 174 | @started = Concurrent::Event.new 175 | @thread = Thread.new { run() } 176 | @started.wait() 177 | end 178 | 179 | def run 180 | @stopped = Concurrent::Event.new 181 | @started.set 182 | loop do 183 | fetch_if_older(Utils.get_utc_now_seconds_since_epoch - @polling_mode.poll_interval_seconds) 184 | @stopped.wait(@polling_mode.poll_interval_seconds) 185 | break if @stopped.set? 186 | end 187 | end 188 | 189 | def set_initialized 190 | if !@initialized.set? 191 | @initialized.set 192 | @hooks.invoke_on_client_ready 193 | end 194 | end 195 | 196 | def read_cache 197 | begin 198 | json_string = @config_cache.get(@cache_key) 199 | if !json_string || json_string == @cached_entry_string 200 | return ConfigEntry::EMPTY 201 | end 202 | 203 | @cached_entry_string = json_string 204 | return ConfigEntry.create_from_string(json_string) 205 | rescue Exception => e 206 | @log.error(2200, "Error occurred while reading the cache. #{e}") 207 | return ConfigEntry::EMPTY 208 | end 209 | end 210 | 211 | def write_cache(config_entry) 212 | begin 213 | @config_cache.set(@cache_key, config_entry.serialize) 214 | rescue Exception => e 215 | @log.error(2201, "Error occurred while writing the cache. #{e}") 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/configcat/datagovernance.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class DataGovernance 3 | # Control the location of the config.json files containing your feature flags 4 | # and settings within the ConfigCat CDN. 5 | # Global: Select this if your feature flags are published to all global CDN nodes. 6 | # EuOnly: Select this if your feature flags are published to CDN nodes only in the EU. 7 | GLOBAL = 0 8 | EU_ONLY = 1 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/configcat/evaluationcontext.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class EvaluationContext 3 | attr_accessor :key, :setting_type, :user, :visited_keys, :is_missing_user_object_logged, :is_missing_user_object_attribute_logged 4 | 5 | def initialize(key, setting_type, user, visited_keys = nil, is_missing_user_object_logged = false, is_missing_user_object_attribute_logged = false) 6 | @key = key 7 | @setting_type = setting_type 8 | @user = user 9 | @visited_keys = visited_keys || [] 10 | @is_missing_user_object_logged = is_missing_user_object_logged 11 | @is_missing_user_object_attribute_logged = is_missing_user_object_attribute_logged 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/configcat/evaluationdetails.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class EvaluationDetails 3 | attr_reader :key, :value, :variation_id, :fetch_time, :user, :is_default_value, :error, 4 | :matched_targeting_rule, :matched_percentage_option 5 | 6 | def initialize(key:, value:, variation_id: nil, fetch_time: nil, user: nil, is_default_value: false, error: nil, 7 | matched_targeting_rule: nil, matched_percentage_option: nil) 8 | # Key of the feature flag or setting. 9 | @key = key 10 | 11 | # Evaluated value of the feature flag or setting. 12 | @value = value 13 | 14 | # Variation ID of the feature flag or setting (if available). 15 | @variation_id = variation_id 16 | 17 | # Time of last successful config download. 18 | @fetch_time = fetch_time 19 | 20 | # The User Object used for the evaluation (if available). 21 | @user = user 22 | 23 | # Indicates whether the default value passed to the setting evaluation methods like ConfigCatClient.get_value, 24 | # ConfigCatClient.get_value_details, etc. is used as the result of the evaluation. 25 | @is_default_value = is_default_value 26 | 27 | # Error message in case evaluation failed. 28 | @error = error 29 | 30 | # The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value. 31 | @matched_targeting_rule = matched_targeting_rule 32 | 33 | # The percentage option (if any) that was used to select the evaluated value. 34 | @matched_percentage_option = matched_percentage_option 35 | end 36 | 37 | def self.from_error(key, value, error:, variation_id: nil) 38 | EvaluationDetails.new(key: key, value: value, variation_id: variation_id, is_default_value: true, error: error) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/configcat/evaluationlogbuilder.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class EvaluationLogBuilder 3 | def initialize 4 | @indent_level = 0 5 | @text = '' 6 | end 7 | 8 | def self.trunc_comparison_value_if_needed(comparator, comparison_value) 9 | if [ 10 | Comparator::IS_ONE_OF_HASHED, 11 | Comparator::IS_NOT_ONE_OF_HASHED, 12 | Comparator::EQUALS_HASHED, 13 | Comparator::NOT_EQUALS_HASHED, 14 | Comparator::STARTS_WITH_ANY_OF_HASHED, 15 | Comparator::NOT_STARTS_WITH_ANY_OF_HASHED, 16 | Comparator::ENDS_WITH_ANY_OF_HASHED, 17 | Comparator::NOT_ENDS_WITH_ANY_OF_HASHED, 18 | Comparator::ARRAY_CONTAINS_ANY_OF_HASHED, 19 | Comparator::ARRAY_NOT_CONTAINS_ANY_OF_HASHED 20 | ].include?(comparator) 21 | if comparison_value.is_a?(Array) 22 | length = comparison_value.length 23 | if length > 1 24 | return "[<#{length} hashed values>]" 25 | end 26 | return "[<#{length} hashed value>]" 27 | end 28 | 29 | return "''" 30 | end 31 | 32 | if comparison_value.is_a?(Array) 33 | length_limit = 10 34 | length = comparison_value.length 35 | if length > length_limit 36 | remaining = length - length_limit 37 | more_text = remaining == 1 ? "<1 more value>" : "<#{remaining} more values>" 38 | 39 | formatted_strings = comparison_value.first(length_limit).map { |str| "'#{str}'" }.join(", ") 40 | return "[#{formatted_strings}, ... #{more_text}]" 41 | end 42 | 43 | # replace '"' with "'" in the string representation of the array 44 | formatted_strings = comparison_value.map { |str| "'#{str}'" }.join(", ") 45 | return "[#{formatted_strings}]" 46 | end 47 | 48 | if [Comparator::BEFORE_DATETIME, Comparator::AFTER_DATETIME].include?(comparator) 49 | time = Utils.get_date_time(comparison_value) 50 | return "'#{comparison_value}' (#{time.strftime('%Y-%m-%dT%H:%M:%S.%L')}Z UTC)" 51 | end 52 | 53 | "'#{comparison_value.to_s}'" 54 | end 55 | 56 | def increase_indent 57 | @indent_level += 1 58 | self 59 | end 60 | 61 | def decrease_indent 62 | @indent_level = [@indent_level - 1, 0].max 63 | self 64 | end 65 | 66 | def append(text) 67 | @text += text 68 | self 69 | end 70 | 71 | def new_line(text = nil) 72 | @text += "\n" + ' ' * @indent_level 73 | @text += text if text 74 | self 75 | end 76 | 77 | def to_s 78 | @text 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/configcat/interfaces.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | # Config cache interface 3 | class ConfigCache 4 | # :returns the config json object from the cache 5 | def get(key) 6 | end 7 | 8 | # Sets the config json cache. 9 | def set(key, value) 10 | end 11 | end 12 | 13 | # Generic ConfigCatClientException 14 | class ConfigCatClientException < Exception 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/configcat/localdictionarydatasource.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/overridedatasource' 2 | require 'configcat/config' 3 | 4 | 5 | module ConfigCat 6 | class LocalDictionaryFlagOverrides < FlagOverrides 7 | def initialize(source, override_behaviour) 8 | @source = source 9 | @override_behaviour = override_behaviour 10 | end 11 | 12 | def create_data_source(log) 13 | return LocalDictionaryDataSource.new(@source, @override_behaviour) 14 | end 15 | end 16 | 17 | class LocalDictionaryDataSource < OverrideDataSource 18 | def initialize(source, override_behaviour) 19 | super(override_behaviour) 20 | @_config = {} 21 | source.each do |key, value| 22 | value_type = case value 23 | when TrueClass, FalseClass 24 | BOOL_VALUE 25 | when String 26 | STRING_VALUE 27 | when Integer 28 | INT_VALUE 29 | when Float 30 | DOUBLE_VALUE 31 | else 32 | UNSUPPORTED_VALUE 33 | end 34 | 35 | @_config[FEATURE_FLAGS] ||= {} 36 | @_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } } 37 | setting_type = SettingType.from_type(value.class) 38 | @_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil? 39 | end 40 | end 41 | 42 | def get_overrides 43 | return @_config 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/configcat/localfiledatasource.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/overridedatasource' 2 | require 'configcat/config' 3 | 4 | 5 | module ConfigCat 6 | class LocalFileFlagOverrides < FlagOverrides 7 | def initialize(file_path, override_behaviour) 8 | @file_path = file_path 9 | @override_behaviour = override_behaviour 10 | end 11 | 12 | def create_data_source(log) 13 | return LocalFileDataSource.new(@file_path, @override_behaviour, log) 14 | end 15 | end 16 | 17 | class LocalFileDataSource < OverrideDataSource 18 | def initialize(file_path, override_behaviour, log) 19 | super(override_behaviour) 20 | @log = log 21 | if !File.exist?(file_path) 22 | @log.error(1300, "Cannot find the local config file '#{file_path}'. This is a path that your application provided to the ConfigCat SDK by passing it to the `LocalFileFlagOverrides.new()` method. Read more: https://configcat.com/docs/sdk-reference/ruby/#json-file") 23 | end 24 | @_file_path = file_path 25 | @_config = nil 26 | @_cached_file_stamp = 0 27 | end 28 | 29 | def get_overrides 30 | reload_file_content() 31 | return @_config 32 | end 33 | 34 | private 35 | 36 | def reload_file_content 37 | begin 38 | stamp = File.mtime(@_file_path) 39 | if stamp != @_cached_file_stamp 40 | @_cached_file_stamp = stamp 41 | file = File.read(@_file_path) 42 | data = JSON.parse(file) 43 | if data.key?("flags") 44 | @_config = { FEATURE_FLAGS => {} } 45 | source = data["flags"] 46 | source.each do |key, value| 47 | value_type = case value 48 | when true, false 49 | BOOL_VALUE 50 | when String 51 | STRING_VALUE 52 | when Integer 53 | INT_VALUE 54 | when Float 55 | DOUBLE_VALUE 56 | else 57 | UNSUPPORTED_VALUE 58 | end 59 | 60 | @_config[FEATURE_FLAGS][key] = { VALUE => { value_type => value } } 61 | setting_type = SettingType.from_type(value.class) 62 | @_config[FEATURE_FLAGS][key][SETTING_TYPE] = setting_type.to_i unless setting_type.nil? 63 | end 64 | else 65 | Config.fixup_config_salt_and_segments(data) 66 | @_config = data 67 | end 68 | end 69 | rescue JSON::ParserError => e 70 | @log.error(2302, "Failed to decode JSON from the local config file '#{@_file_path}'. #{e}") 71 | rescue Exception => e 72 | @log.error(1302, "Failed to read the local config file '#{@_file_path}'. #{e}") 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/configcat/overridedatasource.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class OverrideBehaviour 3 | # When evaluating values, the SDK will not use feature flags & settings from the ConfigCat CDN, but it will use 4 | # all feature flags & settings that are loaded from local-override sources. 5 | LOCAL_ONLY = 0 6 | 7 | # When evaluating values, the SDK will use all feature flags & settings that are downloaded from the ConfigCat CDN, 8 | # plus all feature flags & settings that are loaded from local-override sources. If a feature flag or a setting is 9 | # defined both in the fetched and the local-override source then the local-override version will take precedence. 10 | LOCAL_OVER_REMOTE = 1 11 | 12 | # When evaluating values, the SDK will use all feature flags & settings that are downloaded from the ConfigCat CDN, 13 | # plus all feature flags & settings that are loaded from local-override sources. If a feature flag or a setting is 14 | # defined both in the fetched and the local-override source then the fetched version will take precedence. 15 | REMOTE_OVER_LOCAL = 2 16 | end 17 | 18 | class FlagOverrides 19 | # :returns [OverrideDataSource] the created OverrideDataSource 20 | def create_data_source(log) 21 | end 22 | end 23 | 24 | class OverrideDataSource 25 | def initialize(override_behaviour) 26 | @_override_behaviour = override_behaviour 27 | end 28 | 29 | def get_behaviour 30 | return @_override_behaviour 31 | end 32 | 33 | def get_overrides 34 | # :returns the override dictionary 35 | return {} 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/configcat/pollingmode.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class PollingMode 3 | # Creates a configured auto polling configuration. 4 | # 5 | # :param poll_interval_seconds: sets at least how often this policy should fetch the latest configuration and refresh the cache. 6 | # :param max_init_wait_time_seconds: sets the maximum waiting time between initialization and the first config acquisition in seconds. 7 | # :return [AutoPollingMode] 8 | def self.auto_poll(poll_interval_seconds: 60, max_init_wait_time_seconds: 5) 9 | poll_interval_seconds = 1 if poll_interval_seconds < 1 10 | max_init_wait_time_seconds = 0 if max_init_wait_time_seconds < 0 11 | 12 | AutoPollingMode.new(poll_interval_seconds, max_init_wait_time_seconds) 13 | end 14 | 15 | # Creates a configured lazy loading polling configuration. 16 | # 17 | # :param cache_refresh_interval_seconds: sets how long the cache will store its value before fetching the latest from the network again. 18 | # :return [LazyLoadingMode] 19 | def self.lazy_load(cache_refresh_interval_seconds: 60) 20 | cache_refresh_interval_seconds = 1 if cache_refresh_interval_seconds < 1 21 | 22 | LazyLoadingMode.new(cache_refresh_interval_seconds) 23 | end 24 | 25 | # Creates a configured manual polling configuration. 26 | # :return [ManualPollingMode] 27 | def self.manual_poll 28 | ManualPollingMode.new 29 | end 30 | end 31 | 32 | class AutoPollingMode < PollingMode 33 | attr_reader :poll_interval_seconds, :max_init_wait_time_seconds 34 | 35 | def initialize(poll_interval_seconds, max_init_wait_time_seconds) 36 | @poll_interval_seconds = poll_interval_seconds 37 | @max_init_wait_time_seconds = max_init_wait_time_seconds 38 | end 39 | 40 | def identifier 41 | return "a" 42 | end 43 | end 44 | 45 | class LazyLoadingMode < PollingMode 46 | attr_reader :cache_refresh_interval_seconds 47 | 48 | def initialize(cache_refresh_interval_seconds) 49 | @cache_refresh_interval_seconds = cache_refresh_interval_seconds 50 | end 51 | 52 | def identifier 53 | return "l" 54 | end 55 | end 56 | 57 | class ManualPollingMode < PollingMode 58 | def identifier 59 | return "m" 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/configcat/refreshresult.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | RefreshResult = Struct.new(:success, :error) 3 | end 4 | -------------------------------------------------------------------------------- /lib/configcat/user.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | # User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. 3 | class User 4 | PREDEFINED = ["Identifier", "Email", "Country"] 5 | 6 | attr_reader :identifier 7 | 8 | # Initialize a User object. 9 | # Args: 10 | # identifier: The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) 11 | # email: Email address of the user. 12 | # country: Country of the user. 13 | # custom: Custom attributes of the user for advanced targeting rule definitions (e.g. role, subscription type, etc.) 14 | # All comparators support string values as User Object attribute (in some cases they need to be provided in a 15 | # specific format though, see below), but some of them also support other types of values. It depends on the 16 | # comparator how the values will be handled. The following rules apply: 17 | # Text-based comparators (EQUALS, IS_ONE_OF, etc.) 18 | # * accept string values, 19 | # * all other values are automatically converted to string 20 | # (a warning will be logged but evaluation will continue as normal). 21 | # SemVer-based comparators (IS_ONE_OF_SEMVER, LESS_THAN_SEMVER, GREATER_THAN_SEMVER, etc.) 22 | # * accept string values containing a properly formatted, valid semver value, 23 | # * all other values are considered invalid 24 | # (a warning will be logged and the currently evaluated targeting rule will be skipped). 25 | # Number-based comparators (EQUALS_NUMBER, LESS_THAN_NUMBER, GREATER_THAN_OR_EQUAL_NUMBER, etc.) 26 | # * accept float values and all other numeric values which can safely be converted to float, 27 | # * accept string values containing a properly formatted, valid float value, 28 | # * all other values are considered invalid 29 | # (a warning will be logged and the currently evaluated targeting rule will be skipped). 30 | # Date time-based comparators (BEFORE_DATETIME / AFTER_DATETIME) 31 | # * accept datetime values, which are automatically converted to a second-based Unix timestamp 32 | # (datetime values with naive timezone are considered to be in UTC), 33 | # * accept float values representing a second-based Unix timestamp 34 | # and all other numeric values which can safely be converted to float, 35 | # * accept string values containing a properly formatted, valid float value, 36 | # * all other values are considered invalid 37 | # (a warning will be logged and the currently evaluated targeting rule will be skipped). 38 | # String array-based comparators (ARRAY_CONTAINS_ANY_OF / ARRAY_NOT_CONTAINS_ANY_OF) 39 | # * accept arrays of strings, 40 | # * accept string values containing a valid JSON string which can be deserialized to an array of strings, 41 | # * all other values are considered invalid 42 | # (a warning will be logged and the currently evaluated targeting rule will be skipped). 43 | def initialize(identifier, email: nil, country: nil, custom: nil) 44 | @identifier = (!identifier.equal?(nil)) ? identifier : "" 45 | @data = { "Identifier" => identifier, "Email" => email, "Country" => country } 46 | @custom = custom 47 | end 48 | 49 | def get_identifier 50 | return @identifier 51 | end 52 | 53 | def get_attribute(attribute) 54 | attribute = attribute.to_s 55 | return @data[attribute] if PREDEFINED.include?(attribute) 56 | return @custom[attribute] if @custom 57 | return nil 58 | end 59 | 60 | def to_s 61 | dump = { 62 | 'Identifier': @identifier, 63 | 'Email': @data['Email'], 64 | 'Country': @data['Country'] 65 | } 66 | dump.merge!(@custom) if @custom 67 | filtered_dump = dump.reject { |_, v| v.nil? } 68 | formatted_dump = filtered_dump.transform_values do |value| 69 | value.is_a?(DateTime) ? value.strftime('%Y-%m-%dT%H:%M:%S.%L%z') : value 70 | end 71 | return JSON.generate(formatted_dump, ascii_only: false, separators: %w[, :]) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/configcat/utils.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | class Utils 3 | DISTANT_FUTURE = Float::INFINITY 4 | DISTANT_PAST = 0 5 | 6 | def self.get_date_time(seconds_since_epoch) 7 | Time.at(seconds_since_epoch).utc 8 | end 9 | 10 | def self.get_utc_now_seconds_since_epoch 11 | Time.now.utc.to_f 12 | end 13 | 14 | def self.get_seconds_since_epoch(date_time) 15 | date_time.to_time.to_f 16 | end 17 | 18 | def self.is_string_list(value) 19 | # Check if the value is an Array 20 | return false unless value.is_a?(Array) 21 | 22 | # Check if all elements in the Array are Strings 23 | value.each do |item| 24 | return false unless item.is_a?(String) 25 | end 26 | 27 | return true 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/configcat/version.rb: -------------------------------------------------------------------------------- 1 | module ConfigCat 2 | VERSION = "8.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /media/readme02-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/ruby-sdk/1c7c154bbe25ab77be158fa40ea64e6c6c45c240/media/readme02-3.png -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat Ruby Sample Console App 2 | 3 | To run this sample you need [Ruby](https://www.ruby-lang.org) installed. 4 | 5 | 1. Install the ConfigCat SDK with `RubyGems` 6 | ```bash 7 | gem install configcat 8 | ``` 9 | 2. Run sample app 10 | ```bash 11 | ruby consolesample.rb 12 | ``` 13 | or 14 | ```bash 15 | ruby consolesample2.rb 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /samples/consolesample.rb: -------------------------------------------------------------------------------- 1 | require 'configcat' 2 | 3 | # Info level logging helps to inspect the feature flag evaluation process. 4 | # Use the default warning level to avoid too detailed logging in your application. 5 | ConfigCat.logger.level = Logger::INFO 6 | 7 | # Initialize the ConfigCatClient with an SDK Key. 8 | client = ConfigCat.get("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") 9 | 10 | # In the project there is a 'keySampleText' setting with the following rules: 11 | # 1. If the User's country is Hungary, the value should be 'Dog' 12 | # 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion' 13 | # 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules. 14 | # 4. There is also a default value configured: 'Cat' 15 | 16 | # 1. As the passed User's country is Hungary this will print 'Dog' 17 | my_setting_value = client.get_value("keySampleText", "default value", ConfigCat::User.new("key", country: "Hungary")) 18 | puts("'keySampleText' value from ConfigCat: " + my_setting_value.to_s) 19 | 20 | # 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will print 'Lion' 21 | my_setting_value = client.get_value("keySampleText", "default value", ConfigCat::User.new("key", custom: { "SubscriptionType" => "unlimited" })) 22 | puts("'keySampleText' value from ConfigCat: " + my_setting_value.to_s) 23 | 24 | # 3/a. As the passed User doesn't fill in any rules, this will serve 'Falcon' or 'Horse'. 25 | my_setting_value = client.get_value("keySampleText", "default value", ConfigCat::User.new("key")) 26 | puts("'keySampleText' value from ConfigCat: " + my_setting_value.to_s) 27 | 28 | # 3/b. As this is the same user from 3/a., this will print the same value as the previous one ('Falcon' or 'Horse') 29 | my_setting_value = client.get_value("keySampleText", "default value", ConfigCat::User.new("key")) 30 | puts("'keySampleText' value from ConfigCat: " + my_setting_value.to_s) 31 | 32 | # 4. As we don't pass an User object to this call, this will print the setting's default value - 'Cat' 33 | my_setting_value = client.get_value("keySampleText", "default value") 34 | puts("'keySampleText' value from ConfigCat: " + my_setting_value.to_s) 35 | 36 | # 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('default value') 37 | my_setting_not_exists = client.get_value("myKeyNotExists", "default value") 38 | puts("'myKeyNotExists' value from ConfigCat: " + my_setting_not_exists.to_s) 39 | 40 | client.close 41 | -------------------------------------------------------------------------------- /samples/consolesample2.rb: -------------------------------------------------------------------------------- 1 | require 'configcat' 2 | 3 | # Info level logging helps to inspect the feature flag evaluation process. 4 | # Use the default warning level to avoid too detailed logging in your application. 5 | ConfigCat.logger.level = Logger::INFO 6 | 7 | # Initializing the ConfigCatClient with an SDK Key. 8 | client = ConfigCat.get("PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ") 9 | 10 | # Creating a user object to identify your user (optional). 11 | user_object = ConfigCat::User.new("Some UserID", email: "configcat@example.com", custom: { 12 | 'version': '1.0.0' 13 | }) 14 | 15 | value = client.get_value("isPOCFeatureEnabled", "default value", user_object) 16 | puts("'isPOCFeatureEnabled' value from ConfigCat: " + value.to_s) 17 | 18 | value = client.get_value("isAwesomeFeatureEnabled", "default value") 19 | puts("'isAwesomeFeatureEnabled' value from ConfigCat: " + value.to_s) 20 | 21 | client.close 22 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/localdictionarydatasource' 3 | require 'configcat/localfiledatasource' 4 | require 'tempfile' 5 | require 'json' 6 | require_relative 'configcat/mocks' 7 | 8 | 9 | RSpec.describe 'Config test', type: :feature do 10 | it "test_value_setting_type_is_missing" do 11 | value_dictionary = { 12 | 't' => 6, # unsupported setting type 13 | 'v' => { 14 | 'b' => true 15 | } 16 | } 17 | setting_type = value_dictionary[SETTING_TYPE] 18 | expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Unsupported setting type") 19 | end 20 | 21 | it "test_value_setting_type_is_valid_but_return_value_is_missing" do 22 | value_dictionary = { 23 | 't' => 0, # boolean 24 | 'v' => { 25 | 's' => true # the wrong property is set ("b" should be set) 26 | } 27 | } 28 | setting_type = value_dictionary[SETTING_TYPE] 29 | expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Setting value is not of the expected type TrueClass") 30 | end 31 | 32 | it "test_value_setting_type_is_valid_and_the_return_value_is_present_but_it_is_invalid" do 33 | value_dictionary = { 34 | 't' => 0, # boolean 35 | 'v' => { 36 | 'b' => 'true' # the value is a string instead of a boolean 37 | } 38 | } 39 | setting_type = value_dictionary[SETTING_TYPE] 40 | expect { Config.get_value(value_dictionary, setting_type) }.to raise_error("Setting value is not of the expected type TrueClass") 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/configcat/configcache_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/configcache' 3 | require_relative 'mocks' 4 | 5 | RSpec.describe ConfigCat::InMemoryConfigCache do 6 | it "test_cache" do 7 | config_store = InMemoryConfigCache.new() 8 | 9 | value = config_store.get("key") 10 | expect(value).to be nil 11 | 12 | config_store.set("key", TEST_JSON) 13 | value = config_store.get("key") 14 | expect(value).to eq TEST_JSON 15 | 16 | value2 = config_store.get("key2") 17 | expect(value2).to be nil 18 | end 19 | 20 | it "test_cache_key" do 21 | expect(ConfigService.send(:get_cache_key, 'configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012')).to eq('f83ba5d45bceb4bb704410f51b704fb6dfa19942') 22 | expect(ConfigService.send(:get_cache_key, 'configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012')).to eq('da7bfd8662209c8ed3f9db96daed4f8d91ba5876') 23 | end 24 | 25 | it "test_cache_payload" do 26 | now_seconds = 1686756435.8449 27 | etag = 'test-etag' 28 | entry = ConfigEntry.new(JSON.parse(TEST_JSON), etag, TEST_JSON, now_seconds) 29 | expect(entry.serialize).to eq('1686756435844' + "\n" + etag + "\n" + TEST_JSON) 30 | end 31 | 32 | it "test_invalid_cache_content" do 33 | hook_callbacks = HookCallbacks.new 34 | hooks = Hooks.new(on_error: hook_callbacks.method(:on_error)) 35 | config_json_string = TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' } 36 | config_cache = SingleValueConfigCache.new(ConfigEntry.new( 37 | JSON.parse(config_json_string), 38 | 'test-etag', 39 | config_json_string, 40 | Utils.get_utc_now_seconds_since_epoch).serialize 41 | ) 42 | 43 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, 44 | config_cache: config_cache, 45 | hooks: hooks)) 46 | 47 | expect(client.get_value('testKey', 'default')).to eq('test') 48 | expect(hook_callbacks.error_call_count).to eq(0) 49 | 50 | # Invalid fetch time in cache 51 | config_cache.value = ['text', 'test-etag', TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2}"' }].join("\n") 52 | 53 | expect(client.get_value('testKey', 'default')).to eq('test') 54 | expect(hook_callbacks.error).to include('Error occurred while reading the cache. Invalid fetch time: text') 55 | 56 | # Number of values is fewer than expected 57 | config_cache.value = [Utils.get_utc_now_seconds_since_epoch.to_s, TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2}"' }].join("\n") 58 | 59 | expect(client.get_value('testKey', 'default')).to eq('test') 60 | expect(hook_callbacks.error).to include('Error occurred while reading the cache. Number of values is fewer than expected.') 61 | 62 | # Invalid config JSON 63 | config_cache.value = [Utils.get_utc_now_seconds_since_epoch.to_s, 'test-etag', 'wrong-json'].join("\n") 64 | 65 | expect(client.get_value('testKey', 'default')).to eq('test') 66 | expect(hook_callbacks.error).to include('Error occurred while reading the cache. Invalid config JSON: wrong-json.') 67 | 68 | client.close 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/configcat/configfetcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/configfetcher' 3 | require_relative 'mocks' 4 | 5 | RSpec.describe ConfigCat::ConfigFetcher do 6 | it "test_simple_fetch_success" do 7 | test_json = '{"test": "json"}' 8 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 9 | WebMock.stub_request(:get, uri_template) 10 | .with( 11 | body: "", 12 | headers: { 13 | 'Accept' => '*/*', 14 | 'Content-Type' => 'application/json', 15 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' 16 | } 17 | ) 18 | .to_return(status: 200, body: test_json, headers: {}) 19 | 20 | log = ConfigCatLogger.new(Hooks.new) 21 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 22 | fetch_response = fetcher.get_configuration() 23 | expect(fetch_response.is_fetched()).to be true 24 | expect(fetch_response.entry.config).to eq JSON.parse(test_json) 25 | expect(fetch_response.entry.config_json_string).to eq test_json 26 | end 27 | 28 | it "test_fetch_not_modified_etag" do 29 | etag = "test" 30 | test_json = '{"test": "json"}' 31 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 32 | WebMock.stub_request(:get, uri_template) 33 | .with( 34 | body: "", 35 | headers: { 36 | 'Accept' => '*/*', 37 | 'Content-Type' => 'application/json', 38 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' 39 | } 40 | ) 41 | .to_return(status: 200, body: test_json, headers: { "ETag" => etag }) 42 | log = ConfigCatLogger.new(Hooks.new) 43 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 44 | fetch_response = fetcher.get_configuration() 45 | expect(fetch_response.is_fetched()).to be true 46 | expect(fetch_response.entry.config).to eq JSON.parse(test_json) 47 | expect(fetch_response.entry.config_json_string).to eq test_json 48 | expect(fetch_response.entry.etag).to eq etag 49 | 50 | WebMock.stub_request(:get, uri_template) 51 | .with( 52 | body: "", 53 | headers: { 54 | 'Accept' => '*/*', 55 | 'Content-Type' => 'application/json', 56 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 57 | 'If-None-Match' => etag 58 | } 59 | ) 60 | .to_return(status: 304, body: "", headers: { "ETag" => etag }) 61 | fetch_response = fetcher.get_configuration(etag) 62 | expect(fetch_response.is_fetched()).to be false 63 | expect(fetch_response.is_not_modified()).to be true 64 | 65 | WebMock.reset! 66 | end 67 | 68 | it "test_http_error" do 69 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 70 | WebMock.stub_request(:get, uri_template).to_raise(Net::HTTPError.new("error", nil)) 71 | log = ConfigCatLogger.new(Hooks.new) 72 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 73 | fetch_response = fetcher.get_configuration() 74 | expect(fetch_response.is_failed()).to be true 75 | expect(fetch_response.is_transient_error).to be true 76 | expect(fetch_response.entry.empty?).to be true 77 | end 78 | 79 | it "test_exception" do 80 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 81 | WebMock.stub_request(:get, uri_template).to_raise(Exception.new("error")) 82 | log = ConfigCatLogger.new(Hooks.new) 83 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 84 | fetch_response = fetcher.get_configuration() 85 | expect(fetch_response.is_failed()).to be true 86 | expect(fetch_response.is_transient_error).to be true 87 | expect(fetch_response.entry.empty?).to be true 88 | end 89 | 90 | it "test_404_failed_fetch_response" do 91 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 92 | 93 | WebMock.stub_request(:get, uri_template) 94 | .with( 95 | body: "", 96 | headers: { 97 | 'Accept' => '*/*', 98 | 'Content-Type' => 'application/json', 99 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' 100 | } 101 | ) 102 | .to_return(status: 404, body: "", headers: {}) 103 | log = ConfigCatLogger.new(Hooks.new) 104 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 105 | fetch_response = fetcher.get_configuration() 106 | expect(fetch_response.is_failed()).to be true 107 | expect(fetch_response.is_transient_error).to be false 108 | expect(fetch_response.is_fetched()).to be false 109 | expect(fetch_response.entry.empty?).to be true 110 | end 111 | 112 | it "test_403_failed_fetch_response" do 113 | uri_template = Addressable::Template.new "https://{base_url}/{base_path}/{api_key}/{base_ext}" 114 | 115 | WebMock.stub_request(:get, uri_template) 116 | .with( 117 | body: "", 118 | headers: { 119 | 'Accept' => '*/*', 120 | 'Content-Type' => 'application/json', 121 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' 122 | } 123 | ) 124 | .to_return(status: 403, body: "", headers: {}) 125 | log = ConfigCatLogger.new(Hooks.new) 126 | fetcher = ConfigCat::ConfigFetcher.new("", log, "m") 127 | fetch_response = fetcher.get_configuration() 128 | expect(fetch_response.is_failed()).to be true 129 | expect(fetch_response.is_transient_error).to be false 130 | expect(fetch_response.is_fetched()).to be false 131 | expect(fetch_response.entry.empty?).to be true 132 | end 133 | 134 | it "test_server_side_etag" do 135 | log = ConfigCatLogger.new(Hooks.new) 136 | fetcher = ConfigCat::ConfigFetcher.new("PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ", 137 | log, 138 | "m", 139 | base_url: "https://cdn-eu.configcat.com") 140 | fetch_response = fetcher.get_configuration() 141 | etag = fetch_response.entry.etag 142 | expect(etag).not_to be nil 143 | expect(etag.empty?).to be false 144 | expect(fetch_response.is_fetched()).to be true 145 | expect(fetch_response.is_not_modified()).to be false 146 | 147 | fetch_response = fetcher.get_configuration(etag) 148 | expect(fetch_response.is_fetched()).to be false 149 | expect(fetch_response.is_not_modified()).to be true 150 | 151 | fetch_response = fetcher.get_configuration('') 152 | expect(fetch_response.is_fetched()).to be true 153 | expect(fetch_response.is_not_modified()).to be false 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/configcat/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/configcache' 3 | require_relative 'mocks' 4 | 5 | RSpec.describe 'Hooks test', type: :feature do 6 | 7 | it "test init" do 8 | hook_callbacks = HookCallbacks.new 9 | hooks = Hooks.new( 10 | on_client_ready: hook_callbacks.method(:on_client_ready), 11 | on_config_changed: hook_callbacks.method(:on_config_changed), 12 | on_flag_evaluated: hook_callbacks.method(:on_flag_evaluated), 13 | on_error: hook_callbacks.method(:on_error) 14 | ) 15 | 16 | config_cache = ConfigCacheMock.new 17 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, 18 | config_cache: config_cache, 19 | hooks: hooks)) 20 | 21 | value = client.get_value('testStringKey', '') 22 | 23 | expect(value).to eq('testValue') 24 | expect(hook_callbacks.is_ready).to be true 25 | expect(hook_callbacks.is_ready_call_count).to eq(1) 26 | extended_config = TEST_OBJECT 27 | Config.fixup_config_salt_and_segments(extended_config) 28 | expect(hook_callbacks.changed_config).to eq(extended_config.fetch(FEATURE_FLAGS)) 29 | expect(hook_callbacks.changed_config_call_count).to eq(1) 30 | expect(hook_callbacks.evaluation_details).not_to be nil 31 | expect(hook_callbacks.evaluation_details_call_count).to eq(1) 32 | expect(hook_callbacks.error).to be nil 33 | expect(hook_callbacks.error_call_count).to eq(0) 34 | 35 | client.close 36 | end 37 | 38 | it "test subscribe" do 39 | hook_callbacks = HookCallbacks.new 40 | hooks = Hooks.new 41 | hooks.add_on_client_ready(hook_callbacks.method(:on_client_ready)) 42 | hooks.add_on_config_changed(hook_callbacks.method(:on_config_changed)) 43 | hooks.add_on_flag_evaluated(hook_callbacks.method(:on_flag_evaluated)) 44 | hooks.add_on_error(hook_callbacks.method(:on_error)) 45 | 46 | config_cache = ConfigCacheMock.new 47 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, 48 | config_cache: config_cache, 49 | hooks: hooks)) 50 | 51 | value = client.get_value('testStringKey', '') 52 | 53 | expect(value).to eq('testValue') 54 | expect(hook_callbacks.is_ready).to be true 55 | expect(hook_callbacks.is_ready_call_count).to eq(1) 56 | expect(hook_callbacks.changed_config).to eq(TEST_OBJECT.fetch(FEATURE_FLAGS)) 57 | expect(hook_callbacks.changed_config_call_count).to eq(1) 58 | expect(hook_callbacks.evaluation_details).not_to be nil 59 | expect(hook_callbacks.evaluation_details_call_count).to eq(1) 60 | expect(hook_callbacks.error).to be nil 61 | expect(hook_callbacks.error_call_count).to eq(0) 62 | 63 | client.close 64 | end 65 | 66 | it "test_evaluation" do 67 | WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) 68 | 69 | hook_callbacks = HookCallbacks.new 70 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll)) 71 | client.hooks.add_on_flag_evaluated(hook_callbacks.method(:on_flag_evaluated)) 72 | 73 | client.force_refresh 74 | 75 | user = User.new("test@test1.com") 76 | value = client.get_value("testStringKey", "", user) 77 | expect(value).to eq("fake1") 78 | 79 | details = hook_callbacks.evaluation_details 80 | expect(details.value).to eq("fake1") 81 | expect(details.key).to eq("testStringKey") 82 | expect(details.variation_id).to eq("id1") 83 | expect(details.is_default_value).to be false 84 | expect(details.error).to be nil 85 | expect(details.matched_percentage_option).to be nil 86 | expect(details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE]).to eq("fake1") 87 | expect(details.user.to_s).to eq(user.to_s) 88 | now = Utils.get_utc_now_seconds_since_epoch 89 | expect(details.fetch_time.to_f).to be <= now 90 | expect(details.fetch_time.to_f + 1).to be >= now 91 | 92 | client.close 93 | end 94 | 95 | it "test_callback_exception" do 96 | WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) 97 | 98 | hook_callbacks = HookCallbacks.new 99 | hooks = Hooks.new( 100 | on_client_ready: hook_callbacks.method(:callback_exception), 101 | on_config_changed: hook_callbacks.method(:callback_exception), 102 | on_flag_evaluated: hook_callbacks.method(:callback_exception), 103 | on_error: hook_callbacks.method(:callback_exception) 104 | ) 105 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, hooks: hooks)) 106 | 107 | client.force_refresh 108 | 109 | value = client.get_value("testStringKey", "") 110 | expect(value).to eq("testValue") 111 | 112 | value = client.get_value("", "default") 113 | expect(value).to eq("default") 114 | 115 | client.close 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /spec/configcat/manualpollingcachepolicy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/configcache' 3 | require_relative 'mocks' 4 | 5 | RSpec.describe "ManualPollingCachePolicy" do 6 | before(:each) do 7 | WebMock.reset! 8 | end 9 | 10 | it "test_without_refresh" do 11 | config_fetcher = ConfigFetcherMock.new 12 | config_cache = NullConfigCache.new 13 | hooks = Hooks.new 14 | logger = ConfigCatLogger.new(hooks) 15 | cache_policy = ConfigService.new("", PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) 16 | config, _ = cache_policy.get_config 17 | expect(config).to be nil 18 | expect(config_fetcher.get_call_count).to eq 0 19 | cache_policy.close 20 | end 21 | 22 | it "test_with_refresh" do 23 | config_fetcher = ConfigFetcherMock.new 24 | config_cache = NullConfigCache.new 25 | hooks = Hooks.new 26 | logger = ConfigCatLogger.new(hooks) 27 | cache_policy = ConfigService.new("", PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) 28 | cache_policy.refresh 29 | config, _ = cache_policy.get_config 30 | settings = config.fetch(FEATURE_FLAGS) 31 | expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" 32 | expect(config_fetcher.get_call_count).to eq 1 33 | cache_policy.close 34 | end 35 | 36 | it "test_with_refresh_error" do 37 | config_fetcher = ConfigFetcherWithErrorMock.new(StandardError.new("error")) 38 | config_cache = InMemoryConfigCache.new 39 | hooks = Hooks.new 40 | logger = ConfigCatLogger.new(hooks) 41 | cache_policy = ConfigService.new('', PollingMode.manual_poll, hooks, config_fetcher, logger, config_cache, false) 42 | cache_policy.refresh 43 | config, _ = cache_policy.get_config 44 | expect(config).to be nil 45 | cache_policy.close 46 | end 47 | 48 | it "test_with_failed_refresh" do 49 | WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) 50 | 51 | polling_mode = PollingMode.manual_poll 52 | hooks = Hooks.new 53 | logger = ConfigCatLogger.new(hooks) 54 | config_fetcher = ConfigFetcher.new("", logger, polling_mode.identifier()) 55 | config_cache = NullConfigCache.new 56 | cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) 57 | 58 | cache_policy.refresh 59 | config, _ = cache_policy.get_config 60 | settings = config.fetch(FEATURE_FLAGS) 61 | expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" 62 | 63 | WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 500, body: "", headers: {}) 64 | 65 | cache_policy.refresh 66 | config, _ = cache_policy.get_config 67 | settings = config.fetch(FEATURE_FLAGS) 68 | expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" 69 | 70 | cache_policy.close 71 | end 72 | 73 | it "test_cache" do 74 | stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')) 75 | .to_return(status: 200, body: TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' }, 76 | headers: { 'ETag' => 'test-etag' }) 77 | 78 | polling_mode = PollingMode.manual_poll 79 | hooks = Hooks.new 80 | logger = ConfigCatLogger.new(hooks) 81 | config_fetcher = ConfigFetcher.new("", logger, polling_mode.identifier()) 82 | config_cache = InMemoryConfigCache.new 83 | cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) 84 | 85 | start_time_milliseconds = (Utils.get_utc_now_seconds_since_epoch * 1000).floor 86 | cache_policy.refresh 87 | config, _ = cache_policy.get_config 88 | settings = config.fetch(FEATURE_FLAGS) 89 | expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test" 90 | expect(stub_request).to have_been_made.times(1) 91 | expect(config_cache.value.length).to eq 1 92 | 93 | # Check cache content 94 | cache_tokens = config_cache.value.values[0].split("\n") 95 | expect(cache_tokens.length).to eq(3) 96 | expect(start_time_milliseconds).to be <= cache_tokens[0].to_f 97 | expect((Utils.get_utc_now_seconds_since_epoch * 1000).floor).to be >= cache_tokens[0].to_f 98 | expect(cache_tokens[1]).to eq('test-etag') 99 | expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test"}' }) 100 | 101 | # Update response 102 | WebMock.stub_request(:get, Regexp.new('https://.*')) 103 | .to_return(status: 200, body: TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2"}' }, 104 | headers: { 'ETag' => 'test-etag' }) 105 | 106 | start_time_milliseconds = (Utils.get_utc_now_seconds_since_epoch * 1000).floor 107 | cache_policy.refresh 108 | config, _ = cache_policy.get_config 109 | settings = config.fetch(FEATURE_FLAGS) 110 | expect(settings.fetch("testKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "test2" 111 | expect(stub_request).to have_been_made.times(2) 112 | expect(config_cache.value.length).to eq 1 113 | 114 | # Check cache content 115 | cache_tokens = config_cache.value.values[0].split("\n") 116 | expect(cache_tokens.length).to eq(3) 117 | expect(start_time_milliseconds).to be <= cache_tokens[0].to_f 118 | expect((Utils.get_utc_now_seconds_since_epoch * 1000).floor).to be >= cache_tokens[0].to_f 119 | expect(cache_tokens[1]).to eq('test-etag') 120 | expect(cache_tokens[2]).to eq(TEST_JSON_FORMAT % { value_type: SettingType::STRING, value: '{"s": "test2"}' }) 121 | 122 | cache_policy.close 123 | end 124 | 125 | it "test_online_offline" do 126 | stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) 127 | 128 | polling_mode = PollingMode.manual_poll 129 | hooks = Hooks.new 130 | logger = ConfigCatLogger.new(hooks) 131 | config_fetcher = ConfigFetcher.new("", logger, polling_mode.identifier()) 132 | config_cache = NullConfigCache.new 133 | cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, false) 134 | 135 | expect(cache_policy.offline?).to be false 136 | expect(cache_policy.refresh.success).to be true 137 | config, _ = cache_policy.get_config 138 | settings = config.fetch(FEATURE_FLAGS) 139 | expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" 140 | expect(stub_request).to have_been_made.times(1) 141 | 142 | cache_policy.set_offline 143 | 144 | expect(cache_policy.offline?).to be true 145 | expect(cache_policy.refresh.success).to be false 146 | expect(stub_request).to have_been_made.times(1) 147 | 148 | cache_policy.set_online 149 | 150 | expect(cache_policy.offline?).to be false 151 | expect(cache_policy.refresh.success).to be true 152 | expect(stub_request).to have_been_made.times(2) 153 | 154 | cache_policy.close 155 | end 156 | 157 | it "test_init_offline" do 158 | stub_request = WebMock.stub_request(:get, Regexp.new('https://.*')).to_return(status: 200, body: TEST_OBJECT_JSON, headers: {}) 159 | 160 | polling_mode = PollingMode.manual_poll 161 | hooks = Hooks.new 162 | logger = ConfigCatLogger.new(hooks) 163 | config_fetcher = ConfigFetcher.new("", logger, polling_mode.identifier()) 164 | config_cache = NullConfigCache.new 165 | cache_policy = ConfigService.new("", polling_mode, hooks, config_fetcher, logger, config_cache, true) 166 | 167 | expect(cache_policy.offline?).to be true 168 | expect(cache_policy.refresh.success).to be false 169 | expect(stub_request).to have_been_made.times(0) 170 | 171 | cache_policy.set_online 172 | 173 | expect(cache_policy.offline?).to be false 174 | expect(cache_policy.refresh.success).to be true 175 | config, _ = cache_policy.get_config 176 | settings = config.fetch(FEATURE_FLAGS) 177 | expect(settings.fetch("testStringKey").fetch(VALUE).fetch(STRING_VALUE)).to eq "testValue" 178 | expect(stub_request).to have_been_made.times(1) 179 | 180 | cache_policy.close 181 | end 182 | 183 | end 184 | -------------------------------------------------------------------------------- /spec/configcat/mocks.rb: -------------------------------------------------------------------------------- 1 | require 'configcat/interfaces' 2 | require 'json' 3 | 4 | TEST_SDK_KEY = 'configcat-sdk-test-key/0000000000000000000000' 5 | TEST_SDK_KEY1 = 'configcat-sdk-test-key/0000000000000000000001' 6 | TEST_SDK_KEY2 = 'configcat-sdk-test-key/0000000000000000000002' 7 | 8 | TEST_JSON = '{ 9 | "p": { 10 | "u": "https://cdn-global.configcat.com", 11 | "r": 0 12 | }, 13 | "f": { 14 | "testKey": { "v": { "s": "testValue" }, "t": 1 } 15 | } 16 | }' 17 | 18 | TEST_JSON_FORMAT = '{ "f": { "testKey": { "t": %{value_type}, "v": %{value}, "p": [], "r": [] } } }' 19 | 20 | TEST_JSON2 = '{ 21 | "p": { 22 | "u": "https://cdn-global.configcat.com", 23 | "r": 0 24 | }, 25 | "f": { 26 | "testKey": { "v": { "s": "testValue" }, "t": 1 }, 27 | "testKey2": { "v": { "s": "testValue2" }, "t": 1 } 28 | } 29 | }' 30 | 31 | TEST_OBJECT_JSON = '{ 32 | "p": { 33 | "u": "https://cdn-global.configcat.com", 34 | "r": 0 35 | }, 36 | "s": [ 37 | {"n": "id1", "r": [{"a": "Identifier", "c": 2, "l": ["@test1.com"]}]}, 38 | {"n": "id2", "r": [{"a": "Identifier", "c": 2, "l": ["@test2.com"]}]} 39 | ], 40 | "f": { 41 | "testBoolKey": {"v": {"b": true}, "t": 0}, 42 | "testStringKey": {"v": {"s": "testValue"}, "i": "id", "t": 1, "r": [ 43 | {"c": [{"s": {"s": 0, "c": 0}}], "s": {"v": {"s": "fake1"}, "i": "id1"}}, 44 | {"c": [{"s": {"s": 1, "c": 0}}], "s": {"v": {"s": "fake2"}, "i": "id2"}} 45 | ]}, 46 | "testIntKey": {"v": {"i": 1}, "t": 2}, 47 | "testDoubleKey": {"v": {"d": 1.1}, "t": 3}, 48 | "key1": {"v": {"b": true}, "t": 0, "i": "id3"}, 49 | "key2": {"v": {"s": "fake4"}, "t": 1, "i": "id4", 50 | "r": [ 51 | {"c": [{"s": {"s": 0, "c": 0}}], "p": [ 52 | {"p": 50, "v": {"s": "fake5"}, "i": "id5"}, {"p": 50, "v": {"s": "fake6"}, "i": "id6"} 53 | ]} 54 | ], 55 | "p": [ 56 | {"p": 50, "v": {"s": "fake7"}, "i": "id7"}, {"p": 50, "v": {"s": "fake8"}, "i": "id8"} 57 | ] 58 | } 59 | } 60 | }' 61 | 62 | TEST_OBJECT = JSON.parse(TEST_OBJECT_JSON) 63 | 64 | include ConfigCat 65 | 66 | class FetchResponseMock 67 | def initialize(json) 68 | @json = json 69 | end 70 | 71 | def json 72 | return @json 73 | end 74 | 75 | def is_fetched 76 | return true 77 | end 78 | end 79 | 80 | class ConfigFetcherMock 81 | def initialize 82 | @_call_count = 0 83 | @_fetch_count = 0 84 | @_configuration = TEST_JSON 85 | @_etag = "test_etag" 86 | end 87 | 88 | def get_configuration(etag = "") 89 | @_call_count += 1 90 | if etag != @_etag 91 | @_fetch_count += 1 92 | return FetchResponse.success(ConfigEntry.new(JSON.parse(@_configuration), @_etag, @_configuration, Utils.get_utc_now_seconds_since_epoch)) 93 | end 94 | return FetchResponse.not_modified 95 | end 96 | 97 | def set_configuration_json(value) 98 | if @_configuration != value 99 | @_configuration = value 100 | @_etag += "_etag" 101 | end 102 | end 103 | 104 | def close 105 | end 106 | 107 | def get_call_count 108 | return @_call_count 109 | end 110 | 111 | def get_fetch_count 112 | return @_fetch_count 113 | end 114 | end 115 | 116 | class ConfigFetcherWithErrorMock 117 | def initialize(error) 118 | @_error = error 119 | end 120 | 121 | def get_configuration(*) 122 | return FetchResponse.failure(@_error, true) 123 | end 124 | 125 | def close 126 | end 127 | end 128 | 129 | class ConfigFetcherWaitMock 130 | def initialize(wait_seconds) 131 | @_wait_seconds = wait_seconds 132 | end 133 | 134 | def get_configuration(etag = '') 135 | sleep(@_wait_seconds) 136 | return FetchResponse.success(ConfigEntry.new(JSON.parse(TEST_JSON), etag, TEST_JSON)) 137 | end 138 | 139 | def close 140 | end 141 | end 142 | 143 | class ConfigFetcherCountMock 144 | def initialize 145 | @_value = 0 146 | end 147 | 148 | def get_configuration(etag = '') 149 | @_value += 1 150 | value_string = "{ \"i\": #{@_value} }" 151 | config_json_string = TEST_JSON_FORMAT % { value_type: SettingType::INT, value: value_string } 152 | config = JSON.parse(config_json_string) 153 | return FetchResponse.success(ConfigEntry.new(config, etag, config_json_string)) 154 | end 155 | 156 | def close 157 | end 158 | end 159 | 160 | class ConfigCacheMock < ConfigCache 161 | def get(key) 162 | [Utils::DISTANT_PAST, 'test-etag', JSON.dump(TEST_OBJECT)].join("\n") 163 | end 164 | 165 | def set(key, value) 166 | end 167 | end 168 | 169 | class SingleValueConfigCache < ConfigCache 170 | attr_accessor :value 171 | 172 | def initialize(value) 173 | @value = value 174 | end 175 | 176 | def get(key) 177 | @value 178 | end 179 | 180 | def set(key, value) 181 | @value = value 182 | end 183 | end 184 | 185 | class HookCallbacks 186 | attr_accessor :is_ready, :is_ready_call_count, :changed_config, :changed_config_call_count, :evaluation_details, 187 | :evaluation_details_call_count, :error, :error_call_count, :callback_exception_call_count 188 | 189 | def initialize 190 | @is_ready = false 191 | @is_ready_call_count = 0 192 | @changed_config = nil 193 | @changed_config_call_count = 0 194 | @evaluation_details = nil 195 | @evaluation_details_call_count = 0 196 | @error = nil 197 | @error_call_count = 0 198 | @callback_exception_call_count = 0 199 | end 200 | 201 | def on_client_ready 202 | @is_ready = true 203 | @is_ready_call_count += 1 204 | end 205 | 206 | def on_config_changed(config) 207 | @changed_config = config 208 | @changed_config_call_count += 1 209 | end 210 | 211 | def on_flag_evaluated(evaluation_details) 212 | @evaluation_details = evaluation_details 213 | @evaluation_details_call_count += 1 214 | end 215 | 216 | def on_error(error) 217 | @error = error 218 | @error_call_count += 1 219 | end 220 | 221 | def callback_exception(*args, **kwargs) 222 | @callback_exception_call_count += 1 223 | raise Exception, "error" 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/configcat/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'configcat/user' 3 | require_relative 'mocks' 4 | 5 | RSpec.describe ConfigCat::User do 6 | it "test_empty_or_none_identifier" do 7 | u1 = User.new(nil) 8 | expect(u1.get_identifier()).to eq "" 9 | u2 = User.new("") 10 | expect(u2.get_identifier()).to eq "" 11 | end 12 | 13 | it "test_attribute_case_sensitivity" do 14 | user_id = "id" 15 | email = "test@test.com" 16 | country = "country" 17 | custom = { 'custom' => 'test' } 18 | user = User.new(user_id, email: email, country: country, custom: custom) 19 | 20 | expect(user.get_identifier).to eq user_id 21 | 22 | expect(user.get_attribute("Email")).to eq email 23 | expect(user.get_attribute("EMAIL")).to be nil 24 | expect(user.get_attribute("email")).to be nil 25 | 26 | expect(user.get_attribute("Country")).to eq country 27 | expect(user.get_attribute("COUNTRY")).to be nil 28 | expect(user.get_attribute("country")).to be nil 29 | 30 | expect(user.get_attribute('custom')).to eq 'test' 31 | expect(user.get_attribute('non-existing')).to be_nil 32 | end 33 | 34 | it "to_s" do 35 | user_id = "id" 36 | email = "test@test.com" 37 | country = "country" 38 | custom = { 39 | 'string' => 'test', 40 | 'datetime' => DateTime.new(2023, 9, 19, 11, 1, 35.999), 41 | 'int' => 42, 42 | 'float' => 3.14 43 | } 44 | user = User.new(user_id, email: email, country: country, custom: custom) 45 | 46 | user_json = JSON.parse(user.to_s) 47 | 48 | expect(user_json['Identifier']).to eq user_id 49 | expect(user_json['Email']).to eq email 50 | expect(user_json['Country']).to eq country 51 | expect(user_json['string']).to eq 'test' 52 | expect(user_json['int']).to eq 42 53 | expect(user_json['float']).to eq 3.14 54 | expect(user_json['datetime']).to eq "2023-09-19T11:01:35.999+0000" 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/configcat_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe ConfigCat do 4 | it "has a version number" do 5 | expect(ConfigCat::VERSION).not_to be nil 6 | end 7 | 8 | it "exposes ConfigCat::LocalDictionaryDataSource" do 9 | expect(ConfigCat::LocalDictionaryDataSource).not_to be nil 10 | end 11 | 12 | it "exposes ConfigCat::LocalFileDataSource" do 13 | expect(ConfigCat::LocalFileDataSource).not_to be nil 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/data/evaluation/1_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringContainsDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "1_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "1_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "1_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "1_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule 4 | Returning 'Dog'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/1_targeting_rule/1_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/2_targeting_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringIsInDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "2_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "stringIsInDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "2_rules_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringIsInDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Custom1": "user" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "2_rules_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringIsInDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Custom1": "admin" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "2_rules_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule 7 | Returning 'Dog'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARN [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Cat'. 10 | -------------------------------------------------------------------------------- /spec/data/evaluation/2_targeting_rules/2_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | Returning 'Cat'. 9 | -------------------------------------------------------------------------------- /spec/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/and_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "emailAnd", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "and_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "emailAnd", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345", 16 | "Email": "jane@configcat.com" 17 | }, 18 | "returnValue": "Cat", 19 | "expectedLog": "and_rules_user.txt" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /spec/data/evaluation/and_rules/and_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'emailAnd' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 5 | THEN 'Dog' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/and_rules/and_rules_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 4 | AND User.Email CONTAINS ANY OF ['@'] => true 5 | AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 6 | THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/comparators.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "allinone", 7 | "defaultValue": "", 8 | "user": { 9 | "Identifier": "12345", 10 | "Email": "joe@example.com", 11 | "Country": "[\"USA\"]", 12 | "Version": "1.0.0", 13 | "Number": "1.0", 14 | "Date": "1693497500" 15 | }, 16 | "returnValue": "default", 17 | "expectedLog": "allinone.txt" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /spec/data/evaluation/comparators/allinone.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email EQUALS '' => true 4 | AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions 5 | THEN '1h' => no match 6 | - IF User.Email EQUALS 'joe@example.com' => true 7 | AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions 8 | THEN '1c' => no match 9 | - IF User.Email IS ONE OF [<1 hashed value>] => true 10 | AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 11 | THEN '2h' => no match 12 | - IF User.Email IS ONE OF ['joe@example.com'] => true 13 | AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions 14 | THEN '2c' => no match 15 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 16 | AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 17 | THEN '3h' => no match 18 | - IF User.Email STARTS WITH ANY OF ['joe@'] => true 19 | AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions 20 | THEN '3c' => no match 21 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true 22 | AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 23 | THEN '4h' => no match 24 | - IF User.Email ENDS WITH ANY OF ['@example.com'] => true 25 | AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions 26 | THEN '4c' => no match 27 | - IF User.Email CONTAINS ANY OF ['e@e'] => true 28 | AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions 29 | THEN '5' => no match 30 | - IF User.Version IS ONE OF ['1.0.0'] => true 31 | AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions 32 | THEN '6' => no match 33 | - IF User.Version < '1.0.1' => true 34 | AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions 35 | THEN '7' => no match 36 | - IF User.Version > '0.9.9' => true 37 | AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions 38 | THEN '8' => no match 39 | - IF User.Number = '1' => true 40 | AND User.Number != '1' => false, skipping the remaining AND conditions 41 | THEN '9' => no match 42 | - IF User.Number < '1.1' => true 43 | AND User.Number >= '1.1' => false, skipping the remaining AND conditions 44 | THEN '10' => no match 45 | - IF User.Number > '0.9' => true 46 | AND User.Number <= '0.9' => false, skipping the remaining AND conditions 47 | THEN '11' => no match 48 | - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true 49 | AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions 50 | THEN '12' => no match 51 | - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true 52 | AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 53 | THEN '13h' => no match 54 | - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true 55 | AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions 56 | THEN '13c' => no match 57 | Returning 'default'. 58 | -------------------------------------------------------------------------------- /spec/data/evaluation/epoch_date_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "boolTrueIn202304", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "date_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "2023.04.10" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /spec/data/evaluation/epoch_date_validation/date_error.txt: -------------------------------------------------------------------------------- 1 | WARN [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions 5 | THEN 'true' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'false'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonOverride": "test_list_truncation.json", 3 | "tests": [ 4 | { 5 | "key": "booleanKey1", 6 | "defaultValue": false, 7 | "user": { 8 | "Identifier": "12" 9 | }, 10 | "returnValue": true, 11 | "expectedLog": "list_truncation.txt" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /spec/data/evaluation/list_truncation/list_truncation.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true 4 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true 5 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true 6 | THEN 'true' => MATCH, applying rule 7 | Returning 'true'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/list_truncation/test_list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0, 5 | "s": "test-salt" 6 | }, 7 | "f": { 8 | "booleanKey1": { 9 | "t": 0, 10 | "v": { 11 | "b": false 12 | }, 13 | "r": [ 14 | { 15 | "c": [ 16 | { 17 | "u": { 18 | "a": "Identifier", 19 | "c": 2, 20 | "l": [ 21 | "1", 22 | "2", 23 | "3", 24 | "4", 25 | "5", 26 | "6", 27 | "7", 28 | "8", 29 | "9", 30 | "10" 31 | ] 32 | } 33 | }, 34 | { 35 | "u": { 36 | "a": "Identifier", 37 | "c": 2, 38 | "l": [ 39 | "1", 40 | "2", 41 | "3", 42 | "4", 43 | "5", 44 | "6", 45 | "7", 46 | "8", 47 | "9", 48 | "10", 49 | "11" 50 | ] 51 | } 52 | }, 53 | { 54 | "u": { 55 | "a": "Identifier", 56 | "c": 2, 57 | "l": [ 58 | "1", 59 | "2", 60 | "3", 61 | "4", 62 | "5", 63 | "6", 64 | "7", 65 | "8", 66 | "9", 67 | "10", 68 | "11", 69 | "12" 70 | ] 71 | } 72 | } 73 | ], 74 | "s": { 75 | "v": { 76 | "b": true 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /spec/data/evaluation/number_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", 4 | "tests": [ 5 | { 6 | "key": "number", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "number_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "not_a_number" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /spec/data/evaluation/number_validation/number_error.txt: -------------------------------------------------------------------------------- 1 | WARN [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Default'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_after_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "integer25One25Two25Three25FourAdvancedRules", 7 | "defaultValue": 42, 8 | "returnValue": -1, 9 | "expectedLog": "options_after_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "integer25One25Two25Three25FourAdvancedRules", 13 | "defaultValue": 42, 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": 2, 18 | "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "integer25One25Two25Three25FourAdvancedRules", 22 | "defaultValue": 42, 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": 2, 28 | "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "integer25One25Two25Three25FourAdvancedRules", 32 | "defaultValue": 42, 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": 5, 38 | "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule 4 | Returning '5'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Evaluating % options based on the User.Identifier attribute: 7 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 8 | - Hash value 25 selects % option 2 (25%), '2'. 9 | Returning '2'. 10 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Skipping % options because the User Object is missing. 7 | Returning '-1'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match 4 | Evaluating % options based on the User.Identifier attribute: 5 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 6 | - Hash value 25 selects % option 2 (25%), '2'. 7 | Returning '2'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_custom_attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_custom_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Chicken", 18 | "expectedLog": "no_options_custom_attribute.txt" 19 | }, 20 | { 21 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Country": "US" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "matching_options_custom_attribute.txt" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' 2 | Evaluating % options based on the User.Country attribute: 3 | - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) 4 | - Hash value 70 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' 3 | Skipping % options because the User.Country attribute is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_user_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0Horse", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_user_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0Horse", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_user_attribute_user.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' 2 | Evaluating % options based on the User.Identifier attribute: 3 | - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) 4 | - Hash value 21 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "options_within_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Cat", 38 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" 39 | }, 40 | { 41 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 42 | "defaultValue": "default", 43 | "user": { 44 | "Identifier": "12345", 45 | "Email": "joe@configcat.com", 46 | "Country": "US" 47 | }, 48 | "returnValue": "Cat", 49 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 5 | Skipping % options because the User.Country attribute is missing. 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 4 | Evaluating % options based on the User.Country attribute: 5 | - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) 6 | - Hash value 63 selects % option 1 (75%), 'Cat'. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "dependentFeatureWithUserCondition", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" 10 | }, 11 | { 12 | "key": "dependentFeature", 13 | "defaultValue": "default", 14 | "returnValue": "Chicken", 15 | "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" 16 | }, 17 | { 18 | "key": "dependentFeatureWithUserCondition2", 19 | "defaultValue": "default", 20 | "returnValue": "Frog", 21 | "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" 22 | }, 23 | { 24 | "key": "dependentFeature", 25 | "defaultValue": "default", 26 | "user": { 27 | "Identifier": "12345", 28 | "Email": "kate@configcat.com", 29 | "Country": "USA" 30 | }, 31 | "returnValue": "Horse", 32 | "expectedLog": "prerequisite_flag.txt" 33 | }, 34 | { 35 | "key": "dependentFeatureMultipleLevels", 36 | "defaultValue": "default", 37 | "returnValue": "Dog", 38 | "expectedLog": "prerequisite_flag_multilevel.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag/prerequisite_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'mainFeature' EQUALS 'target' 4 | ( 5 | Evaluating prerequisite flag 'mainFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 8 | THEN 'private' => no match 9 | - IF User.Country IS ONE OF [<1 hashed value>] => true 10 | AND User IS NOT IN SEGMENT 'Beta Users' 11 | ( 12 | Evaluating segment 'Beta Users': 13 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 14 | Segment evaluation result: User IS NOT IN SEGMENT. 15 | Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. 16 | ) => true 17 | AND User IS NOT IN SEGMENT 'Developers' 18 | ( 19 | Evaluating segment 'Developers': 20 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 21 | Segment evaluation result: User IS NOT IN SEGMENT. 22 | Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. 23 | ) => true 24 | THEN 'target' => MATCH, applying rule 25 | Prerequisite flag evaluation result: 'target'. 26 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. 27 | ) 28 | THEN % options => MATCH, applying rule 29 | Evaluating % options based on the User.Identifier attribute: 30 | - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) 31 | - Hash value 78 selects % option 4 (25%), 'Horse'. 32 | Returning 'Horse'. 33 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeatureMultipleLevels' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'intermediateFeature' EQUALS 'true' 4 | ( 5 | Evaluating prerequisite flag 'intermediateFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 8 | ( 9 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 10 | Prerequisite flag evaluation result: 'true'. 11 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 12 | ) => true 13 | AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 14 | ( 15 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 16 | Prerequisite flag evaluation result: 'true'. 17 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 18 | ) => true 19 | THEN 'true' => MATCH, applying rule 20 | Prerequisite flag evaluation result: 'true'. 21 | Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. 22 | ) 23 | THEN 'Dog' => MATCH, applying rule 24 | Returning 'Dog'. 25 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 4 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' 5 | Evaluating targeting rules and applying the first match if any: 6 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | - IF Flag 'mainFeature' EQUALS 'public' 9 | ( 10 | Evaluating prerequisite flag 'mainFeature': 11 | Evaluating targeting rules and applying the first match if any: 12 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 13 | THEN 'private' => cannot evaluate, User Object is missing 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 16 | THEN 'target' => cannot evaluate, User Object is missing 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Prerequisite flag evaluation result: 'public'. 19 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 20 | ) 21 | THEN % options => MATCH, applying rule 22 | Skipping % options because the User Object is missing. 23 | The current targeting rule is ignored and the evaluation continues with the next rule. 24 | - IF Flag 'mainFeature' EQUALS 'public' 25 | ( 26 | Evaluating prerequisite flag 'mainFeature': 27 | Evaluating targeting rules and applying the first match if any: 28 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 29 | THEN 'private' => cannot evaluate, User Object is missing 30 | The current targeting rule is ignored and the evaluation continues with the next rule. 31 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 32 | THEN 'target' => cannot evaluate, User Object is missing 33 | The current targeting rule is ignored and the evaluation continues with the next rule. 34 | Prerequisite flag evaluation result: 'public'. 35 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 36 | ) 37 | THEN 'Frog' => MATCH, applying rule 38 | Returning 'Frog'. 39 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' 7 | ( 8 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 9 | Prerequisite flag evaluation result: 'true'. 10 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. 11 | ) 12 | THEN % options => MATCH, applying rule 13 | Skipping % options because the User Object is missing. 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | Returning 'Chicken'. 16 | -------------------------------------------------------------------------------- /spec/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeature' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF Flag 'mainFeature' EQUALS 'target' 5 | ( 6 | Evaluating prerequisite flag 'mainFeature': 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 9 | THEN 'private' => cannot evaluate, User Object is missing 10 | The current targeting rule is ignored and the evaluation continues with the next rule. 11 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 12 | THEN 'target' => cannot evaluate, User Object is missing 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | Prerequisite flag evaluation result: 'public'. 15 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. 16 | ) 17 | THEN % options => no match 18 | Returning 'Chicken'. 19 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", 4 | "tests": [ 5 | { 6 | "key": "featureWithSegmentTargeting", 7 | "defaultValue": false, 8 | "returnValue": false, 9 | "expectedLog": "segment_no_user.txt" 10 | }, 11 | { 12 | "key": "featureWithSegmentTargetingMultipleConditions", 13 | "defaultValue": false, 14 | "returnValue": false, 15 | "expectedLog": "segment_no_user_multi_conditions.txt" 16 | }, 17 | { 18 | "key": "featureWithNegatedSegmentTargetingCleartext", 19 | "defaultValue": false, 20 | "user": { 21 | "Identifier": "12345" 22 | }, 23 | "returnValue": false, 24 | "expectedLog": "segment_no_targeted_attribute.txt" 25 | }, 26 | { 27 | "key": "featureWithSegmentTargeting", 28 | "defaultValue": false, 29 | "user": { 30 | "Identifier": "12345", 31 | "Email": "jane@example.com" 32 | }, 33 | "returnValue": true, 34 | "expectedLog": "segment_matching.txt" 35 | }, 36 | { 37 | "key": "featureWithNegatedSegmentTargeting", 38 | "defaultValue": false, 39 | "user": { 40 | "Identifier": "12345", 41 | "Email": "jane@example.com" 42 | }, 43 | "returnValue": false, 44 | "expectedLog": "segment_no_matching.txt" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment/segment_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS IN SEGMENT 'Beta users') evaluates to true. 9 | ) 10 | THEN 'true' => MATCH, applying rule 11 | Returning 'true'. 12 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment/segment_no_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS NOT IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. 9 | ) 10 | THEN 'true' => no match 11 | Returning 'false'. 12 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment/segment_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARN [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' 5 | ( 6 | Evaluating segment 'Beta users (cleartext)': 7 | - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions 8 | Segment evaluation result: cannot evaluate, the User.Email attribute is missing. 9 | Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. 10 | ) 11 | THEN 'true' => cannot evaluate, the User.Email attribute is missing 12 | The current targeting rule is ignored and the evaluation continues with the next rule. 13 | Returning 'false'. 14 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment/segment_no_user.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargeting' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users' THEN 'true' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'false'. 7 | -------------------------------------------------------------------------------- /spec/data/evaluation/segment/segment_no_user_multi_conditions.txt: -------------------------------------------------------------------------------- 1 | WARN [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions 5 | THEN 'true' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'false'. 8 | -------------------------------------------------------------------------------- /spec/data/evaluation/semver_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", 4 | "tests": [ 5 | { 6 | "key": "isNotOneOf", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "semver_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "wrong_semver" 13 | } 14 | }, 15 | { 16 | "key": "relations", 17 | "defaultValue": "default", 18 | "returnValue": "Default", 19 | "expectedLog": "semver_relations_error.txt", 20 | "user": { 21 | "Identifier": "12345", 22 | "Custom1": "wrong_semver" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /spec/data/evaluation/semver_validation/semver_error.txt: -------------------------------------------------------------------------------- 1 | WARN [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARN [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Default'. 10 | -------------------------------------------------------------------------------- /spec/data/evaluation/semver_validation/semver_relations_error.txt: -------------------------------------------------------------------------------- 1 | WARN [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARN [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | WARN [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 4 | WARN [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 5 | WARN [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 6 | INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 9 | The current targeting rule is ignored and the evaluation continues with the next rule. 10 | - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 11 | The current targeting rule is ignored and the evaluation continues with the next rule. 12 | - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 15 | The current targeting rule is ignored and the evaluation continues with the next rule. 16 | - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Returning 'Default'. 19 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "boolDefaultFalse", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "off_flag.txt" 10 | }, 11 | { 12 | "key": "boolDefaultTrue", 13 | "defaultValue": false, 14 | "returnValue": true, 15 | "expectedLog": "on_flag.txt" 16 | }, 17 | { 18 | "key": "stringDefaultCat", 19 | "defaultValue": "Default", 20 | "returnValue": "Cat", 21 | "expectedLog": "text_setting.txt" 22 | }, 23 | { 24 | "key": "integerDefaultOne", 25 | "defaultValue": 0, 26 | "returnValue": 1, 27 | "expectedLog": "int_setting.txt" 28 | }, 29 | { 30 | "testName": "double_setting", 31 | "key": "doubleDefaultPi", 32 | "defaultValue": 0.0, 33 | "returnValue": 3.1415, 34 | "expectedLog": "double_setting.txt" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value/double_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'doubleDefaultPi' 2 | Returning '3.1415'. 3 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value/int_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integerDefaultOne' 2 | Returning '1'. 3 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value/off_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultFalse' 2 | Returning 'false'. 3 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value/on_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultTrue' 2 | Returning 'true'. 3 | -------------------------------------------------------------------------------- /spec/data/evaluation/simple_value/text_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringDefaultCat' 2 | Returning 'Cat'. 3 | -------------------------------------------------------------------------------- /spec/data/test-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "flags": { 3 | "disabledFeature": false, 4 | "enabledFeature": true, 5 | "intSetting": 5, 6 | "doubleSetting": 3.14, 7 | "stringSetting": "test" 8 | } 9 | } -------------------------------------------------------------------------------- /spec/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "f": { 3 | "disabledFeature": { 4 | "t": 0, 5 | "v": { "b": false } 6 | }, 7 | "enabledFeature": { 8 | "t": 0, 9 | "v": { "b": true } 10 | }, 11 | "intSetting": { 12 | "t": 2, 13 | "v": { "i": 5 } 14 | }, 15 | "doubleSetting": { 16 | "t": 3, 17 | "v": { "d": 3.14 } 18 | }, 19 | "stringSetting": { 20 | "t": 1, 21 | "v": { "s": "test" } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /spec/data/test_circulardependency_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0 5 | }, 6 | "f": { 7 | "key1": { 8 | "t": 1, 9 | "v": { "s": "key1-value" }, 10 | "r": [ 11 | { 12 | "c": [ 13 | { 14 | "p": { 15 | "f": "key1", 16 | "c": 0, 17 | "v": { "s": "key1-prereq" } 18 | } 19 | } 20 | ], 21 | "s": { "v": { "s": "key1-prereq" } } 22 | } 23 | ] 24 | }, 25 | "key2": { 26 | "t": 1, 27 | "v": { "s": "key2-value" }, 28 | "r": [ 29 | { 30 | "c": [ 31 | { 32 | "p": { 33 | "f": "key3", 34 | "c": 0, 35 | "v": { "s": "key3-prereq" } 36 | } 37 | } 38 | ], 39 | "s": { "v": { "s": "key2-prereq" } } 40 | } 41 | ] 42 | }, 43 | "key3": { 44 | "t": 1, 45 | "v": { "s": "key3-value" }, 46 | "r": [ 47 | { 48 | "c": [ 49 | { 50 | "p": { 51 | "f": "key2", 52 | "c": 0, 53 | "v": { "s": "key2-prereq" } 54 | } 55 | } 56 | ], 57 | "s": { "v": { "s": "key3-prereq" } } 58 | } 59 | ] 60 | }, 61 | "key4": { 62 | "t": 1, 63 | "v": { "s": "key4-value" }, 64 | "r": [ 65 | { 66 | "c": [ 67 | { 68 | "p": { 69 | "f": "key3", 70 | "c": 0, 71 | "v": { "s": "key3-prereq" } 72 | } 73 | } 74 | ], 75 | "s": { "v": { "s": "key4-prereq" } } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spec/data/test_override_flagdependency_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://test-cdn-eu.configcat.com", 4 | "r": 0, 5 | "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" 6 | }, 7 | "f": { 8 | "mainStringFlag": { 9 | "t": 1, 10 | "v": { 11 | "s": "private" 12 | }, 13 | "i": "24c96275" 14 | }, 15 | "stringDependsOnInt": { 16 | "t": 1, 17 | "r": [ 18 | { 19 | "c": [ 20 | { 21 | "p": { 22 | "f": "mainIntFlag", 23 | "c": 0, 24 | "v": { 25 | "i": 42 26 | } 27 | } 28 | } 29 | ], 30 | "s": { 31 | "v": { 32 | "s": "Dog" 33 | }, 34 | "i": "12531eec" 35 | } 36 | } 37 | ], 38 | "v": { 39 | "s": "Cat" 40 | }, 41 | "i": "e227d926" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/data/test_override_segments_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://test-cdn-eu.configcat.com", 4 | "r": 0, 5 | "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" 6 | }, 7 | "s": [ 8 | { 9 | "n": "Beta Users", 10 | "r": [ 11 | { 12 | "a": "Email", 13 | "c": 16, 14 | "l": [ 15 | "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff" 16 | ] 17 | } 18 | ] 19 | }, 20 | { 21 | "n": "Developers", 22 | "r": [ 23 | { 24 | "a": "Email", 25 | "c": 16, 26 | "l": [ 27 | "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" 28 | ] 29 | } 30 | ] 31 | } 32 | ], 33 | "f": { 34 | "developerAndBetaUserSegment": { 35 | "t": 0, 36 | "r": [ 37 | { 38 | "c": [ 39 | { 40 | "s": { 41 | "s": 1, 42 | "c": 0 43 | } 44 | }, 45 | { 46 | "s": { 47 | "s": 0, 48 | "c": 1 49 | } 50 | } 51 | ], 52 | "s": { 53 | "v": { 54 | "b": true 55 | }, 56 | "i": "ddc50638" 57 | } 58 | } 59 | ], 60 | "v": { 61 | "b": false 62 | }, 63 | "i": "6427f4b8" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /spec/data/testmatrix_and_or.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr 2 | ##null##;;;;public;Chicken;Cat;Cat 3 | ;;;;public;Chicken;Cat;Cat 4 | jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane 5 | john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John 6 | a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat 7 | mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark 8 | nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat 9 | stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat 10 | jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane 11 | anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat 12 | jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane 13 | jane;jane;##null##;##null##;public;Chicken;Cat;Cat 14 | @sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 15 | jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 16 | -------------------------------------------------------------------------------- /spec/data/testmatrix_comparators_v6.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat 2 | ##null##;;;;false;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 3 | ;;;;false;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 4 | a@configcat.com;a@configcat.com;##null##;##null##;false;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 5 | b@configcat.com;b@configcat.com;Hungary;0;false;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 6 | c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 7 | anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;true;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 8 | bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;false;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 9 | cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;false;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 10 | reader@configcat.com;reader@configcat.com;Bahamas;read,execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 11 | writer@configcat.com;writer@configcat.com;Belgium;write, execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 12 | reader@configcat.com;reader@configcat.com;Canada;execute, Read;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 13 | writer@configcat.com;writer@configcat.com;China;Write;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 14 | admin@configcat.com;admin@configcat.com;France;read, write,execute;false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 15 | user@configcat.com;user@configcat.com;Greece;,execute;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 16 | reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 17 | writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 18 | reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 19 | writer@configcat.com;writer@configcat.com;China;["Write"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 20 | admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 21 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 22 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];false;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 23 | user@configcat.com;user@configcat.com;Greece;["","execute"];false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 24 | user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;false;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 25 | -------------------------------------------------------------------------------- /spec/data/testmatrix_number.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;numberWithPercentage;number 2 | ##null##;;;;Default;Default 3 | id1;;;0;<2.1;<>5 4 | id1;;;0.0;<2.1;<>5 5 | id1;;;0,0;<2.1;<>5 6 | id1;;;0.2;<2.1;<>5 7 | id2;;;0,2;<2.1;<>5 8 | id3;;;1;<2.1;<>5 9 | id4;;;1.0;<2.1;<>5 10 | id5;;;1,0;<2.1;<>5 11 | id6;;;1.5;<2.1;<>5 12 | id7;;;1,5;<2.1;<>5 13 | id8;;;2.1;<=2,1;<>5 14 | id9;;;2,1;<=2,1;<>5 15 | id10;;;3.50;=3.5;<>5 16 | id11;;;3,50;=3.5;<>5 17 | id12;;;5;>=5;Default 18 | id13;;;5.0;>=5;Default 19 | id14;;;5,0;>=5;Default 20 | id13;;;5.76;>5;<>5 21 | id14;;;5,76;>5;<>5 22 | id15;;;4;<>4.2;<>5 23 | id16;;;4.0;<>4.2;<>5 24 | id17;;;4,0;<>4.2;<>5 25 | id18;;;4.2;80%;<>5 26 | id19;;;4,2;20%;<>5 27 | -------------------------------------------------------------------------------- /spec/data/testmatrix_prerequisite_flag.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse 2 | ##null##;;;;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true 3 | ;;;;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true 4 | john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;false;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;false;42;3.14;true;true;EmptyOn;EmptyOn;true;false 5 | jane@example.com;jane@example.com;##null##;##null##;true;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;true;1;1.1;false;true;EmptyOn;EmptyOn;false;true 6 | -------------------------------------------------------------------------------- /spec/data/testmatrix_segments.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment 2 | ##null##;;;;false;false;false;false 3 | ;;;;false;false;false;false 4 | john@example.com;john@example.com;##null##;##null##;false;false;false;false 5 | jane@example.com;jane@example.com;##null##;##null##;false;false;false;false 6 | kate@example.com;kate@example.com;##null##;##null##;true;true;true;true 7 | -------------------------------------------------------------------------------- /spec/data/testmatrix_segments_old.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext 2 | ##null##;;;;false;false;false;false;false;false;false;false 3 | ;;;;false;false;false;false;false;false;false;false 4 | john@example.com;john@example.com;##null##;##null##;true;true;false;false;false;false;true;true 5 | jane@example.com;jane@example.com;##null##;##null##;true;true;false;false;false;false;true;true 6 | kate@example.com;kate@example.com;##null##;##null##;false;false;true;true;true;true;false;false 7 | -------------------------------------------------------------------------------- /spec/data/testmatrix_semantic.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOf;isOneOfWithPercentage;isNotOneOf;isNotOneOfWithPercentage;lessThanWithPercentage;relations 2 | ##null##;;;;Default;Default;Default;Default;Default;Default 3 | id1;;;0.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 4 | id1;;;0.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 5 | id1;;;0.2.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 6 | id1;;;1;Default;80%;Default;80%;20%;Default 7 | id2;;;1.0;Default;80%;Default;80%;80%;Default 8 | id3;;;1.0.0;Is one of (1.0.0);is one of (1.0.0);Default;80%;80%;<=1.0.0 9 | id4;;;1.0.0.0;Default;80%;Default;20%;20%;Default 10 | id5;;;1.0.0.0.0;Default;80%;Default;80%;80%;Default 11 | id6;;;1.0.1;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;Default 12 | id7;;;1.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 13 | id8;;;1.0.111;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 14 | id9;;;1.0.2;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 15 | id10;;;1.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 16 | id11;;;1.0.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 17 | id12;;;1.0.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 18 | id13;;;1.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 19 | id14;;;1.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 20 | id15;;;1.1.2;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 21 | id16;;;1.1.3;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 22 | id17;;;1.1.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 23 | id18;;;1.1.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 24 | id19;;;1.9.0;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 25 | id20;;;1.9.99;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 26 | id21;;;2.0.0;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);20%;>=2.0.0 27 | id22;;;2.0.1;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 28 | id23;;;2.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 29 | id24;;;2.0.2;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 30 | id25;;;2.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 31 | id26;;;3.0.0;Is one of (3.0.0);80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 32 | id27;;;3.0.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 33 | id28;;;3.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 34 | id28;;;3.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 35 | id29;;;5.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 36 | id30;;;5.99.999;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 37 | -------------------------------------------------------------------------------- /spec/data/testmatrix_semantic_2.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;AppVersion;precedenceTests 2 | dontcare;;;1.9.1-1;< 1.9.1-2 3 | dontcare;;;1.9.1-2;< 1.9.1-10 4 | dontcare;;;1.9.1-10;< 1.9.1-10a 5 | dontcare;;;1.9.1-10a;< 1.9.1-1a 6 | dontcare;;;1.9.1-1a;< 1.9.1-alpha 7 | dontcare;;;1.9.1-alpha;< 1.9.99-alpha 8 | dontcare;;;1.9.99-alpha;= 1.9.99-alpha 9 | dontcare;;;1.9.99-alpha+build1;= 1.9.99-alpha 10 | dontcare;;;1.9.99-alpha+build2;= 1.9.99-alpha 11 | dontcare;;;1.9.99-alpha2;< 1.9.99-beta 12 | dontcare;;;1.9.99-beta;< 1.9.99-rc 13 | dontcare;;;1.9.99-rc;< 1.9.99-rc.1 14 | dontcare;;;1.9.99-rc.1;< 1.9.99-rc.2 15 | dontcare;;;1.9.99-rc.2;< 1.9.99-rc.20 16 | dontcare;;;1.9.99-rc.9;< 1.9.99-rc.20 17 | dontcare;;;1.9.99-rc.20;< 1.9.99-rc.20a 18 | dontcare;;;1.9.99-rc.20a;< 1.9.99-rc.2a 19 | dontcare;;;1.9.99-rc.2a;< 1.9.99 20 | dontcare;;;1.9.99;< 1.9.100 21 | dontcare;;;1.9.100;< 1.10.0-alpha 22 | dontcare;;;1.10.0-alpha;<= 1.10.0-alpha 23 | dontcare;;;1.10.0;<= 1.10.0 24 | dontcare;;;1.10.1;<= 1.10.1 25 | dontcare;;;1.10.2;<= 1.10.3 26 | dontcare;;;2.0.0;= 2.0.0 27 | dontcare;;;2.0.0+build3;= 2.0.0 28 | dontcare;;;2.0.0+001;= 2.0.0 29 | dontcare;;;2.0.0+20130313144700;= 2.0.0 30 | dontcare;;;2.0.0+exp.sha.5114f85;= 2.0.0 31 | dontcare;;;3.0.0;= 3.0.0+build3 32 | dontcare;;;4.0.0;= 4.0.0+001 33 | dontcare;;;5.0.0;= 5.0.0+20130313144700 34 | dontcare;;;6.0.0;= 6.0.0+exp.sha.5114f85 35 | dontcare;;;7.0.0-patch+metadata;= 7.0.0-patch 36 | dontcare;;;8.0.0-patch+metadata;= 8.0.0-patch+anothermetadata 37 | dontcare;;;9.0.0-patch;= 9.0.0-patch+metadata 38 | dontcare;;;10.0.0;DEFAULT-FROM-CC-APP 39 | dontcare;;;104.0.0;> 103.0.0 40 | dontcare;;;103.0.0;>= 103.0.0 41 | dontcare;;;102.0.0;>= 101.0.0 42 | dontcare;;;101.0.0;>= 101.0.0 43 | dontcare;;;90.104.0;> 90.103.0 44 | dontcare;;;90.103.0;>= 90.103.0 45 | dontcare;;;90.102.0;>= 90.101.0 46 | dontcare;;;90.101.0;>= 90.101.0 47 | dontcare;;;80.0.104;> 80.0.103 48 | dontcare;;;80.0.103;>= 80.0.103 49 | dontcare;;;80.0.102;>= 80.0.101 50 | dontcare;;;80.0.101;>= 80.0.101 51 | dontcare;;;73.0.0;>= 73.0.0-beta.2 52 | dontcare;;;72.0.0;> 72.0.0-beta.2 53 | dontcare;;;72.0.0-beta.2;> 72.0.0-beta.1 54 | dontcare;;;72.0.0-beta.1;> 72.0.0-beta 55 | dontcare;;;72.0.0-beta;> 72.0.0-alpha 56 | dontcare;;;72.0.0-alpha;> 72.0.0-1a 57 | dontcare;;;72.0.0-1a;> 72.0.0-10a 58 | dontcare;;;72.0.0-10aa;> 72.0.0-10a 59 | dontcare;;;72.0.0-10a;> 72.0.0-2 60 | dontcare;;;72.0.0-2;> 72.0.0-1 61 | dontcare;;;71.0.0+metadata;>= 71.0.0+anothermetadata 62 | dontcare;;;71.0.0-patch3+metadata;>= 71.0.0-patch3+anothermetadata 63 | dontcare;;;71.0.0-patch2+metadata;>= 71.0.0-patch2 64 | dontcare;;;71.0.0-patch1;>= 71.0.0-patch1+metadata 65 | dontcare;;;60.73.0;>= 60.73.0-beta.2 66 | dontcare;;;60.72.0;> 60.72.0-beta.2 67 | dontcare;;;60.72.0-beta.2;> 60.72.0-beta.1 68 | dontcare;;;60.72.0-beta.1;> 60.72.0-beta 69 | dontcare;;;60.72.0-beta;> 60.72.0-alpha 70 | dontcare;;;60.72.0-alpha;> 60.72.0-1a 71 | dontcare;;;60.72.0-1a;> 60.72.0-10a 72 | dontcare;;;60.72.0-10aa;> 60.72.0-10a 73 | dontcare;;;60.72.0-10a;> 60.72.0-2 74 | dontcare;;;60.72.0-2;> 60.72.0-1 75 | dontcare;;;60.71.0+metadata;>= 60.71.0+anothermetadata 76 | dontcare;;;60.71.0-patch3+metadata;>= 60.71.0-patch3+anothermetadata 77 | dontcare;;;60.71.0-patch2+metadata;>= 60.71.0-patch2 78 | dontcare;;;60.71.0-patch1;>= 60.71.0-patch1+metadata 79 | dontcare;;;50.60.73;>= 50.60.73-beta.2 80 | dontcare;;;50.60.72;> 50.60.72-beta.2 81 | dontcare;;;50.60.72-beta.2;> 50.60.72-beta.1 82 | dontcare;;;50.60.72-beta.1;> 50.60.72-beta 83 | dontcare;;;50.60.72-beta;> 50.60.72-alpha 84 | dontcare;;;50.60.72-alpha;> 50.60.72-1a 85 | dontcare;;;50.60.72-1a;> 50.60.72-10a 86 | dontcare;;;50.60.72-10aa;> 50.60.72-10a 87 | dontcare;;;50.60.72-10a;> 50.60.72-2 88 | dontcare;;;50.60.72-2;> 50.60.72-1 89 | dontcare;;;50.60.71+metadata;>= 50.60.71+anothermetadata 90 | dontcare;;;50.60.71-patch3+metadata;>= 50.60.71-patch3+anothermetadata 91 | dontcare;;;50.60.71-patch2+metadata;>= 50.60.71-patch2 92 | dontcare;;;50.60.71-patch1;>= 50.60.71-patch1+metadata 93 | dontcare;;;50.60.71-patch1+anothermetadata;>= 50.60.71-patch1+metadata 94 | dontcare;;;40.0.0-patch;>= 40.0.0-patch 95 | dontcare;;;30.0.0-beta;>= 30.0.0-alpha 96 | -------------------------------------------------------------------------------- /spec/data/testmatrix_sensitive.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOfSensitive;isNotOneOfSensitive 2 | ##null##;;;;ToAll;ToAll 3 | id1;macska@example.com;;;Macska;Kigyo 4 | Kutya;;;;Allat;ToAll 5 | Sas;;;;ToAll;Kigyo 6 | Kutya;macska@example.com;;;Macska;ToAll 7 | id1;;Scotland;;Britt;Kigyo 8 | Macska;;USA;;ToAll;Ireland -------------------------------------------------------------------------------- /spec/data/testmatrix_unicode.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext 2 | 1;;;ʄǟռƈʏ ȶɛӼȶ;true;true;false;false;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false 3 | 1;;;ʄaռƈʏ ȶɛӼȶ;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false 4 | 1;;;ÁRVÍZTŰRŐ tükörfúrógép;false;false;true;true;true;true;false;false;true;true;false;false;true;true;false;false;true;false;false;false;false;false 5 | 1;;;árvíztűrő tükörfúrógép;false;false;true;true;false;false;true;true;false;false;true;true;true;true;false;false;true;false;false;false;false;false 6 | 1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;false;false;true;true;false;false;true;true;true;true;false;false;false;false;true;true;true;false;false;false;false;false 7 | 1;;;árvíztűrő TÜKÖRFÚRÓGÉP;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false 8 | 1;;;u𝖓𝖎𝖈𝖔𝖉e;false;false;true;true;true;true;false;false;true;true;false;false;true;true;false;false;true;false;false;false;false;false 9 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉e;false;false;true;true;false;false;true;true;false;false;true;true;true;true;false;false;true;false;false;false;false;false 10 | ;;;u𝖓𝖎𝖈𝖔𝖉𝖊;false;false;true;true;false;false;true;true;true;true;false;false;false;false;true;true;true;false;false;false;false;false 11 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;false;true;false;false;false;false 12 | 1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;true;true;false;false 13 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;true;true;false;false 14 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];false;false;true;true;false;false;true;true;false;false;true;true;false;false;true;true;true;false;false;false;true;true 15 | -------------------------------------------------------------------------------- /spec/data/testmatrix_variationId.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolean;decimal;text;whole 2 | ##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; 3 | a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 4 | b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 5 | a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; 6 | b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; 7 | cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; 8 | bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; 9 | -------------------------------------------------------------------------------- /spec/evaluationlog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative 'configcat/mocks' 3 | 4 | RSpec.describe 'Evaluation log test', type: :feature do 5 | 6 | it "test_simple_value" do 7 | expect(test_evaluation_log("./data/evaluation/simple_value.json")).to be true 8 | end 9 | 10 | it "test_1_targeting_rule" do 11 | expect(test_evaluation_log("./data/evaluation/1_targeting_rule.json")).to be true 12 | end 13 | 14 | it "test_2_targeting_rules" do 15 | expect(test_evaluation_log("./data/evaluation/2_targeting_rules.json")).to be true 16 | end 17 | 18 | it "test_options_based_on_user_id" do 19 | expect(test_evaluation_log("./data/evaluation/options_based_on_user_id.json")).to be true 20 | end 21 | 22 | it "test_options_based_on_custom_attr" do 23 | expect(test_evaluation_log("./data/evaluation/options_based_on_custom_attr.json")).to be true 24 | end 25 | 26 | it "test_options_after_targeting_rule" do 27 | expect(test_evaluation_log("./data/evaluation/options_after_targeting_rule.json")).to be true 28 | end 29 | 30 | it "test_options_within_targeting_rule" do 31 | expect(test_evaluation_log("./data/evaluation/options_within_targeting_rule.json")).to be true 32 | end 33 | 34 | it "test_and_rules" do 35 | expect(test_evaluation_log("./data/evaluation/and_rules.json")).to be true 36 | end 37 | 38 | it "test_segment" do 39 | expect(test_evaluation_log("./data/evaluation/segment.json")).to be true 40 | end 41 | 42 | it "test_prerequisite_flag" do 43 | expect(test_evaluation_log("./data/evaluation/prerequisite_flag.json")).to be true 44 | end 45 | 46 | it "test_semver_validation" do 47 | expect(test_evaluation_log("./data/evaluation/semver_validation.json")).to be true 48 | end 49 | 50 | it "test_epoch_date_validation" do 51 | expect(test_evaluation_log("./data/evaluation/epoch_date_validation.json")).to be true 52 | end 53 | 54 | it "test_number_validation" do 55 | expect(test_evaluation_log("./data/evaluation/number_validation.json")).to be true 56 | end 57 | 58 | it "test_comparators_validation" do 59 | expect(test_evaluation_log("./data/evaluation/comparators.json")).to be true 60 | end 61 | 62 | it "test_list_truncation_validation" do 63 | expect(test_evaluation_log("./data/evaluation/list_truncation.json")).to be true 64 | end 65 | 66 | def test_evaluation_log(file_path) 67 | script_dir = File.dirname(__FILE__) 68 | full_file_path = File.join(script_dir, file_path) 69 | expect(File.file?(full_file_path)).to be true 70 | 71 | name = File.basename(file_path, '.json') 72 | file_dir = File.join(File.dirname(full_file_path), name) 73 | 74 | data = JSON.parse(File.read(full_file_path)) 75 | sdk_key = data['sdkKey'] 76 | base_url = data['baseUrl'] 77 | json_override = data['jsonOverride'] 78 | flag_overrides = nil 79 | if json_override 80 | flag_overrides = LocalFileFlagOverrides.new(File.join(file_dir, json_override), OverrideBehaviour::LOCAL_ONLY) 81 | sdk_key ||= TEST_SDK_KEY 82 | end 83 | 84 | begin 85 | # Setup logging 86 | logger = ConfigCat.logger 87 | log_stream = StringIO.new 88 | ConfigCat.logger = Logger.new(log_stream, level: Logger::INFO, formatter: proc do |severity, datetime, progname, msg| 89 | "#{severity} #{msg}\n" 90 | end) 91 | 92 | client = ConfigCatClient.get(sdk_key, ConfigCatOptions.new(polling_mode: PollingMode.manual_poll, 93 | flag_overrides: flag_overrides, 94 | base_url: base_url)) 95 | client.force_refresh 96 | 97 | data['tests'].each do |test| 98 | key = test['key'] 99 | default_value = test['defaultValue'] 100 | return_value = test['returnValue'] 101 | user_data = test['user'] 102 | expected_log_file = test['expectedLog'] 103 | test_name = expected_log_file.sub('.json', '') 104 | 105 | expected_log_file_path = File.join(file_dir, expected_log_file) 106 | user_object = nil 107 | if user_data 108 | identifier = user_data['Identifier'] 109 | email = user_data['Email'] 110 | country = user_data['Country'] 111 | custom = user_data.reject { |k, _| ['Identifier', 'Email', 'Country'].include?(k) } 112 | custom = nil if custom.empty? 113 | user_object = User.new(identifier, email: email, country: country, custom: custom) 114 | end 115 | 116 | # Clear log 117 | log_stream.reopen("") 118 | 119 | value = client.get_value(key, default_value, user_object) 120 | log_stream.rewind 121 | log = log_stream.read 122 | 123 | expect(File.file?(expected_log_file_path)).to be true 124 | expected_log = File.read(expected_log_file_path) 125 | 126 | # Compare logs and values 127 | expect(expected_log.strip).to eq(log.strip), "Log mismatch for test: #{test_name}" 128 | expect(return_value).to eq(value), "Return value mismatch for test: #{test_name}" 129 | end 130 | 131 | return true 132 | ensure 133 | client.close 134 | ConfigCat.logger = logger 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | _SDK_KEY = "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/1cGEJXUwYUGZCBOL-E2sOw" 4 | RSpec.describe 'Integration test: DefaultTests', type: :feature do 5 | it "test_without_sdk_key" do 6 | expect { 7 | ConfigCat.get(nil) 8 | }.to raise_error(ConfigCat::ConfigCatClientException) 9 | end 10 | 11 | it "test_client_works" do 12 | client = ConfigCat.get(_SDK_KEY) 13 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 14 | client.close 15 | end 16 | 17 | it "test_get_all_keys" do 18 | client = ConfigCat.get(_SDK_KEY) 19 | keys = client.get_all_keys 20 | expect(keys.size).to eq 5 21 | expect(keys).to include "keySampleText" 22 | client.close 23 | end 24 | 25 | it "test_force_refresh" do 26 | client = ConfigCat.get(_SDK_KEY) 27 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 28 | client.force_refresh 29 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 30 | client.close 31 | end 32 | end 33 | 34 | RSpec.describe 'Integration test: AutoPollTests', type: :feature do 35 | it "test_without_sdk_key" do 36 | expect { 37 | ConfigCat.get(nil, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll)) 38 | }.to raise_error(ConfigCat::ConfigCatClientException) 39 | end 40 | 41 | it "test_client_works" do 42 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll)) 43 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 44 | client.close 45 | end 46 | 47 | it "test_client_works_valid_base_url" do 48 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll, 49 | base_url: "https://cdn.configcat.com")) 50 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 51 | client.close 52 | end 53 | 54 | it "test_client_works_valid_base_url_trailing_slash" do 55 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll, 56 | base_url: "https://cdn.configcat.com/")) 57 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 58 | client.close 59 | end 60 | 61 | it "test_client_works_invalid_base_url" do 62 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll, 63 | base_url: "https://invalidcdn.configcat.com")) 64 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 65 | client.close 66 | end 67 | 68 | it "test_client_works_invalid_proxy" do 69 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll, 70 | proxy_address: "0.0.0.0", 71 | proxy_port: 0, 72 | proxy_user: "test", 73 | proxy_pass: "test")) 74 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 75 | client.close 76 | end 77 | 78 | it "test_client_works_request_timeout" do 79 | uri = ConfigCat::BASE_URL_GLOBAL + "/" + ConfigCat::BASE_PATH + _SDK_KEY + ConfigCat::BASE_EXTENSION 80 | WebMock.stub_request(:get, uri).to_timeout() 81 | 82 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll)) 83 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 84 | client.close 85 | end 86 | 87 | it "test_force_refresh" do 88 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll)) 89 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 90 | client.force_refresh 91 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 92 | client.close 93 | end 94 | 95 | it "test_wrong_param" do 96 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.auto_poll( 97 | poll_interval_seconds: 0, max_init_wait_time_seconds: -1))) 98 | sleep(2) 99 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 100 | client.close 101 | end 102 | end 103 | 104 | RSpec.describe 'Integration test: LazyLoadingTests', type: :feature do 105 | it "test_without_sdk_key" do 106 | expect { 107 | ConfigCat.get(nil, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.lazy_load)) 108 | }.to raise_error(ConfigCat::ConfigCatClientException) 109 | end 110 | 111 | it "test_client_works" do 112 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.lazy_load)) 113 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 114 | client.close 115 | end 116 | 117 | it "test_client_works_valid_base_url" do 118 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.lazy_load, 119 | base_url: "https://cdn.configcat.com")) 120 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 121 | client.close 122 | end 123 | 124 | it "test_client_works_invalid_base_url" do 125 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.lazy_load, 126 | base_url: "https://invalidcdn.configcat.com")) 127 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 128 | client.close 129 | end 130 | 131 | it "test_wrong_param" do 132 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.lazy_load( 133 | cache_refresh_interval_seconds: 0))) 134 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 135 | client.close 136 | end 137 | end 138 | 139 | RSpec.describe 'Integration test: ManualPollingTests', type: :feature do 140 | it "test_without_sdk_key" do 141 | expect { 142 | ConfigCat.get(nil, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll)) 143 | }.to raise_error(ConfigCat::ConfigCatClientException) 144 | end 145 | 146 | it "test_client_works" do 147 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll)) 148 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 149 | client.force_refresh 150 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 151 | client.close 152 | end 153 | 154 | it "test_client_works_valid_base_url" do 155 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll, 156 | base_url: "https://cdn.configcat.com")) 157 | client.force_refresh 158 | expect(client.get_value("keySampleText", "default value")).to eq "This text came from ConfigCat" 159 | client.close 160 | end 161 | 162 | it "test_client_works_invalid_base_url" do 163 | client = ConfigCat.get(_SDK_KEY, ConfigCat::ConfigCatOptions.new(polling_mode: ConfigCat::PollingMode.manual_poll, 164 | base_url: "https://invalidcdn.configcat.com")) 165 | client.force_refresh 166 | expect(client.get_value("keySampleText", "default value")).to eq "default value" 167 | client.close 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'configcat' 2 | require 'webmock/rspec' 3 | WebMock.allow_net_connect! 4 | ConfigCat.logger.level = Logger::WARN 5 | if ENV['COV'] == 'true' 6 | require 'simplecov' 7 | require 'codecov' 8 | SimpleCov.start 9 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 10 | end 11 | -------------------------------------------------------------------------------- /spec/specialcharacter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Special character test', type: :feature do 4 | before(:all) do 5 | @client = ConfigCat.get('configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g') 6 | end 7 | 8 | after(:all) do 9 | @client.close 10 | end 11 | 12 | it "test_special_characters_works_cleartext" do 13 | actual = @client.get_value("specialCharacters", "NOT_CAT", ConfigCat::User.new('äöüÄÖÜçéèñışğ⢙✓😀')) 14 | expect(actual).to eq('äöüÄÖÜçéèñışğ⢙✓😀') 15 | end 16 | 17 | it "test_special_characters_works_hashed" do 18 | actual = @client.get_value("specialCharactersHashed", "NOT_CAT", ConfigCat::User.new('äöüÄÖÜçéèñışğ⢙✓😀')) 19 | expect(actual).to eq('äöüÄÖÜçéèñışğ⢙✓😀') 20 | end 21 | 22 | end 23 | --------------------------------------------------------------------------------