├── .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 | [](https://github.com/configcat/ruby-sdk/actions/workflows/ruby-ci.yml)
10 | [](https://codecov.io/gh/ConfigCat/ruby-sdk)
11 | [](https://rubygems.org/gems/configcat)
12 | 
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 | 
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 |
--------------------------------------------------------------------------------