├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── dart-ci.yml │ ├── publish.yml │ └── stale.yml ├── .gitignore ├── .pubignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPLOY.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── flutter │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android │ │ ├── .gitignore │ │ ├── app │ │ │ ├── build.gradle │ │ │ └── src │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── flutter_example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values-night │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ └── settings.gradle │ ├── ios │ │ ├── .gitignore │ │ ├── Flutter │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Runner.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── Runner │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ ├── lib │ │ └── main.dart │ ├── pubspec.yaml │ └── web │ │ ├── favicon.png │ │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ │ ├── index.html │ │ └── manifest.json └── lib │ ├── README.md │ └── main.dart ├── lib ├── configcat_client.dart └── src │ ├── configcat_cache.dart │ ├── configcat_client.dart │ ├── configcat_options.dart │ ├── configcat_user.dart │ ├── constants.dart │ ├── data_governance.dart │ ├── entry.dart │ ├── error_reporter.dart │ ├── evaluate_logger.dart │ ├── fetch │ ├── config_fetcher.dart │ ├── config_service.dart │ ├── periodic_executor.dart │ └── refresh_result.dart │ ├── json │ ├── condition.dart │ ├── condition.g.dart │ ├── condition_accessor.dart │ ├── config.dart │ ├── config.g.dart │ ├── percentage_option.dart │ ├── percentage_option.g.dart │ ├── preferences.dart │ ├── preferences.g.dart │ ├── prerequisite_comparator.dart │ ├── prerequisite_flag_condition.dart │ ├── prerequisite_flag_condition.g.dart │ ├── segment.dart │ ├── segment.g.dart │ ├── segment_comparator.dart │ ├── segment_condition.dart │ ├── segment_condition.g.dart │ ├── served_value.dart │ ├── served_value.g.dart │ ├── setting.dart │ ├── setting.g.dart │ ├── setting_type.dart │ ├── setting_value.dart │ ├── setting_value.g.dart │ ├── targeting_rule.dart │ ├── targeting_rule.g.dart │ ├── user_comparator.dart │ ├── user_condition.dart │ └── user_condition.g.dart │ ├── log │ ├── configcat_logger.dart │ ├── default_logger.dart │ └── logger.dart │ ├── mixins.dart │ ├── override │ ├── behaviour.dart │ ├── data_source.dart │ └── flag_overrides.dart │ ├── pair.dart │ ├── platform_spec │ ├── default │ │ ├── platform.dart │ │ └── request_builder.dart │ ├── io │ │ └── platform.dart │ ├── platform.dart │ ├── request_builder.dart │ └── web │ │ └── request_builder.dart │ ├── polling_mode.dart │ ├── rollout_evaluator.dart │ └── utils.dart ├── media └── readme02-3.png ├── pubspec.yaml └── test ├── cache_test.dart ├── cache_test.mocks.dart ├── config_service_test.dart ├── config_v2_evaluation_test.dart ├── configcat_client_test.dart ├── configcat_fetcher_test.dart ├── configcat_user_test.dart ├── evaluation ├── data │ ├── 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 ├── evaluation_data.dart ├── evaluation_data_set.dart ├── evaluation_logger_turn_off_test.dart ├── evaluation_test.dart └── evaluation_test_logger.dart ├── evaluator_trim_test.dart ├── fixtures ├── comparison_attribute_conversion.json ├── test_circulardependency.json ├── trim_comparator_values.json └── trim_user_values.json ├── helpers.dart ├── hooks_test.dart ├── http_adapter.dart ├── logger_test.dart ├── logger_test.mocks.dart ├── matrix ├── testmatrix.csv ├── testmatrix_and_or.csv ├── testmatrix_comparators_v6.csv ├── testmatrix_number.csv ├── testmatrix_prerequisite_flag.csv ├── testmatrix_segment.csv ├── testmatrix_segments_old.csv ├── testmatrix_semantic.csv ├── testmatrix_semantic_2.csv ├── testmatrix_sensitive.csv ├── testmatrix_unicode.csv └── testmatrix_variationId.csv ├── matrix_integration_test.dart ├── override_test.dart ├── user_attribute_convert_test.dart └── variation_id_test.dart /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @configcat/developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "pub" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dart-ci.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ main ] 8 | paths-ignore: 9 | - '**.md' 10 | pull_request: 11 | branches: [ main ] 12 | 13 | workflow_dispatch: 14 | 15 | jobs: 16 | analyze-format: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Dart 22 | uses: dart-lang/setup-dart@v1 23 | 24 | - name: Install dependencies 25 | run: dart pub get 26 | 27 | - name: Verify formatting 28 | run: dart format --output=none --set-exit-if-changed . 29 | 30 | - name: Analyze project source 31 | run: dart analyze 32 | 33 | test: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | sdk-version: [ stable, beta, dev, 2.19.0 ] 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Set up Dart 42 | uses: dart-lang/setup-dart@v1 43 | with: 44 | sdk: ${{ matrix.sdk-version }} 45 | 46 | - name: Install dependencies 47 | run: dart pub get 48 | 49 | - name: Run tests 50 | run: dart test 51 | 52 | publish-dry-run: 53 | needs: [analyze-format, test] 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - name: Set up Dart 59 | uses: dart-lang/setup-dart@v1 60 | 61 | - name: Install dependencies 62 | run: dart pub get 63 | 64 | - name: Publish dry-run 65 | run: dart pub publish --dry-run 66 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Dart SDK Publish 2 | 3 | on: 4 | push: 5 | tags: [ '[0-9]+.[0-9]+.[0-9]+' ] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: google/dart:latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup credentials 17 | run: | 18 | mkdir -p ~/.pub-cache 19 | cat < ~/.pub-cache/credentials.json 20 | { 21 | "accessToken":"${{ secrets.PUB_DEV_PUBLISH_ACCESS_TOKEN }}", 22 | "refreshToken":"${{ secrets.PUB_DEV_PUBLISH_REFRESH_TOKEN }}", 23 | "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", 24 | "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], 25 | "expiration": 1642611244950 26 | } 27 | EOF 28 | - name: Publish package 29 | run: pub publish -f -------------------------------------------------------------------------------- /.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 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | 23 | .idea 24 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | media 2 | .github 3 | build 4 | example/flutter 5 | example/lib/*.md -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ConfigCat SDK for Dart 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/dart-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 | - Install the [Dart](https://dart.dev/get-dart) or [Flutter](https://docs.flutter.dev/get-started/install) SDK. 31 | - Install dependencies: 32 | ```bash 33 | dart pub get 34 | ``` 35 | 36 | ## Running tests 37 | 38 | - With Dart SDK: 39 | 40 | ```bash 41 | dart test 42 | ``` 43 | 44 | - With Flutter SDK: 45 | 46 | ```bash 47 | flutter test 48 | ``` 49 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Steps to deploy 2 | 3 | ## Preparation 4 | 5 | 1. Run tests 6 | 2. Increase the version in the `pubspec.yaml` file. 7 | 3. Increase the version in the `lib/src/constant.dart` file. 8 | 4. Add the description of the new version in `CHANGELOG.md`. 9 | 5. Commit & Push 10 | 11 | ## Publish 12 | 13 | Use the **same version** for the git tag as in the properties file. 14 | 15 | - Via git tag 16 | 1. Create a new version tag. 17 | ```bash 18 | git tag [MAJOR].[MINOR].[PATCH] 19 | ``` 20 | > Example: `git tag 2.5.5` 21 | 2. Push the tag. 22 | ```bash 23 | git push origin --tags 24 | ``` 25 | - Via Github release 26 | 27 | Create a new [Github release](https://github.com/configcat/dart-sdk/releases) with a new version tag and release 28 | notes. 29 | 30 | ## Update import examples in local README.md 31 | 32 | ## Update code examples in ConfigCat Dashboard projects 33 | 34 | `Steps to connect your application` 35 | 36 | 1. Update the `Manual` import example. 37 | 38 | ## Update import examples in Docs 39 | 40 | ## Update samples 41 | 42 | Update and test sample apps with the new SDK version. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 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 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: [example/**] -------------------------------------------------------------------------------- /example/flutter/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/flutter/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/flutter/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat sample Flutter app 2 | 3 | This is a simple application to demonstrate how to use the ConfigCat Dart SDK with Flutter. 4 | 5 | ## Run 6 | 7 | - Open this folder in an [IDE that supports Flutter](https://docs.flutter.dev/get-started/editor). 8 | - [Run](https://docs.flutter.dev/get-started/test-drive) one of the pre-configured apps (iOS, Android, Web). 9 | 10 | ## Documentation 11 | - [Dart (Flutter) SDK](https://configcat.com/docs/sdk-reference/dart) 12 | - [ConfigCat](https://configcat.com) 13 | -------------------------------------------------------------------------------- /example/flutter/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/flutter/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/flutter/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.example.flutter_example" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/kotlin/com/example/flutter_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/flutter/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/flutter/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.2.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/flutter/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/flutter/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip 7 | -------------------------------------------------------------------------------- /example/flutter/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/flutter/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/flutter/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/flutter/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/flutter/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/flutter/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/flutter/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/web/favicon.png -------------------------------------------------------------------------------- /example/flutter/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/flutter/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/flutter/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/flutter/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/example/flutter/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/flutter/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_example", 3 | "short_name": "flutter_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/lib/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat sample console app in Dart 2 | 3 | This is a simple Dart application to demonstrate how to use the ConfigCat Dart SDK. 4 | 5 | ## Run 6 | ```dart 7 | dart main.dart 8 | ``` 9 | 10 | ## Documentation 11 | - [Dart (Flutter) SDK](https://configcat.com/docs/sdk-reference/dart) 12 | - [ConfigCat](https://configcat.com) -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/configcat_client.dart'; 2 | 3 | Future main() async { 4 | final client = ConfigCatClient.get( 5 | sdkKey: 'PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ', 6 | options: ConfigCatOptions( 7 | logger: ConfigCatLogger( 8 | // Info level logging helps to inspect the feature flag evaluation process. 9 | // Use the default Warning level to avoid too detailed logging in your application. 10 | level: LogLevel.info))); 11 | 12 | final isAwesomeFeatureEnabled = await client.getValue( 13 | key: 'isAwesomeFeatureEnabled', defaultValue: false); 14 | 15 | print("isAwesomeFeatureEnabled: $isAwesomeFeatureEnabled"); 16 | 17 | final user = ConfigCatUser( 18 | identifier: '#SOME-USER-ID#', email: 'configcat@example.com'); 19 | 20 | final isPOCFeatureEnabled = await client.getValue( 21 | key: 'isPOCFeatureEnabled', defaultValue: false, user: user); 22 | 23 | print("isPOCFeatureEnabled: $isPOCFeatureEnabled"); 24 | 25 | client.close(); 26 | } 27 | -------------------------------------------------------------------------------- /lib/configcat_client.dart: -------------------------------------------------------------------------------- 1 | /// ConfigCat Dart SDK 2 | /// 3 | /// Dart SDK for ConfigCat. ConfigCat is a hosted feature flag service: https://configcat.com. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs. 4 | 5 | library configcat_client; 6 | 7 | // logging 8 | export 'src/log/logger.dart'; 9 | export 'src/log/default_logger.dart'; 10 | export 'src/log/configcat_logger.dart'; 11 | 12 | // polling modes 13 | export 'src/polling_mode.dart'; 14 | 15 | // core 16 | export 'src/configcat_cache.dart'; 17 | export 'src/configcat_client.dart'; 18 | export 'src/configcat_options.dart'; 19 | export 'src/configcat_user.dart'; 20 | export 'src/data_governance.dart'; 21 | 22 | // overrides 23 | export 'src/override/behaviour.dart'; 24 | export 'src/override/data_source.dart'; 25 | export 'src/override/flag_overrides.dart'; 26 | 27 | // fetch 28 | export 'src/fetch/refresh_result.dart'; 29 | 30 | // json models 31 | export 'src/json/config.dart'; 32 | export 'src/json/preferences.dart'; 33 | export 'src/json/setting.dart'; 34 | export 'src/json/targeting_rule.dart'; 35 | export 'src/json/percentage_option.dart'; 36 | export 'src/json/condition.dart'; 37 | export 'src/json/segment.dart'; 38 | export 'src/json/segment_condition.dart'; 39 | export 'src/json/prerequisite_flag_condition.dart'; 40 | export 'src/json/user_condition.dart'; 41 | export 'src/json/setting_value.dart'; 42 | export 'src/json/served_value.dart'; 43 | -------------------------------------------------------------------------------- /lib/src/configcat_cache.dart: -------------------------------------------------------------------------------- 1 | /// A cache API used to make custom cache implementations. 2 | abstract class ConfigCatCache { 3 | /// Child classes has to implement this method, the [ConfigCatClient] is 4 | /// using it to get the actual value from the cache. 5 | /// 6 | /// [key] is the key of the cache entry. 7 | Future read(String key); 8 | 9 | /// Child classes has to implement this method, the [ConfigCatClient] is 10 | /// using it to set the actual cached value. 11 | /// 12 | /// [key] is the key of the cache entry. 13 | /// [value] is the new value to cache. 14 | Future write(String key, String value); 15 | } 16 | 17 | /// Represents a null cache. 18 | class NullConfigCatCache extends ConfigCatCache { 19 | @override 20 | Future read(String key) { 21 | return Future.value(''); 22 | } 23 | 24 | @override 25 | Future write(String key, String value) { 26 | return Future.value(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/configcat_user.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | /// An object containing attributes to properly identify a given user for variation evaluation. 4 | /// Its only mandatory attribute is the [identifier]. 5 | /// 6 | /// Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) 7 | /// 8 | /// The set of allowed attribute values depends on the comparison type of the condition which references the User Object attribute.
9 | /// [String] values are supported by all comparison types (in some cases they need to be provided in a specific format though).
10 | /// Some of the comparison types work with other types of values, as described below. 11 | /// 12 | /// Text-based comparisons (EQUALS, IS ONE OF, etc.)
13 | /// * accept [String] values, 14 | /// * all other values are automatically converted to string (a warning will be logged but evaluation will continue as normal). 15 | /// 16 | /// SemVer-based comparisons (IS ONE OF, <, >=, etc.)
17 | /// * accept [String] values containing a properly formatted, valid semver value, 18 | /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 19 | /// 20 | /// Number-based comparisons (=, <, >=, etc.)
21 | /// * accept [double] values (except for [double.nan]) and all other numeric values which can safely be converted to [double] 22 | /// * accept [String] values containing a properly formatted, valid [double] value 23 | /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 24 | /// 25 | /// Date time-based comparisons (BEFORE / AFTER)
26 | /// * accept [DateTime] values, which are automatically converted to a second-based Unix timestamp 27 | /// * accept [double] values (except for {@code Double.NaN}) representing a second-based Unix timestamp and all other numeric values which can safely be converted to {@link Double} 28 | /// * accept [String] values containing a properly formatted, valid [double] value 29 | /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 30 | /// 31 | /// String array-based comparisons (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF)
32 | /// * accept [List] of [String] 33 | /// * accept [List] of [dynamic] values, which are automatically converted to [String] 34 | /// * accept [Set] of [dynamic] values, which are automatically converted to [String] 35 | /// * accept [String] values containing a valid JSON string which can be deserialized to an array of [String] 36 | /// * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). 37 | /// 38 | /// In case a non-string attribute value needs to be converted to [String] during evaluation, it will always be done using the same format which is accepted by the comparisons. 39 | class ConfigCatUser { 40 | final Map _attributes = {}; 41 | final String identifier; 42 | 43 | ConfigCatUser( 44 | {required this.identifier, 45 | String? email, 46 | String? country, 47 | Map? custom}) { 48 | _attributes['Identifier'] = identifier; 49 | if (email != null && email.isNotEmpty) { 50 | _attributes['Email'] = email; 51 | } 52 | 53 | if (country != null && country.isNotEmpty) { 54 | _attributes['Country'] = country; 55 | } 56 | 57 | if (custom != null) { 58 | for (MapEntry entry in custom.entries) { 59 | if (entry.key != "Identifier" && 60 | entry.key != "Email" && 61 | entry.key != "Country") { 62 | _attributes[entry.key] = entry.value; 63 | } 64 | } 65 | } 66 | } 67 | 68 | Object? getAttribute(String key) { 69 | return _attributes[key]; 70 | } 71 | 72 | @override 73 | String toString() { 74 | return jsonEncode(_attributes); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | const version = '4.1.1'; 2 | const configJsonCacheVersion = 'v2'; 3 | const configJsonName = 'config_v6.json'; 4 | final DateTime distantPast = DateTime.utc(1970, 01, 01); 5 | final DateTime distantFuture = 6 | DateTime.now().toUtc().add(const Duration(days: 1000 * 365)); 7 | 8 | final String sdkKeyProxyPrefix = "configcat-proxy/"; 9 | final String sdkKeyPrefix = "configcat-sdk-1"; 10 | final int sdkKeySectionLength = 22; 11 | -------------------------------------------------------------------------------- /lib/src/data_governance.dart: -------------------------------------------------------------------------------- 1 | /// Describes the location of your feature flag and setting data within the ConfigCat CDN. 2 | enum DataGovernance { 3 | /// Select this if your feature flags are published to CDN nodes only in the EU. 4 | euOnly, 5 | 6 | /// Select this if your feature flags are published to all global CDN nodes. 7 | global, 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/entry.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/utils.dart'; 2 | 3 | import 'constants.dart'; 4 | import 'json/config.dart'; 5 | 6 | class Entry { 7 | final Config config; 8 | final String configJsonString; 9 | final String eTag; 10 | final DateTime fetchTime; 11 | 12 | Entry(this.configJsonString, this.config, this.eTag, this.fetchTime); 13 | 14 | bool get isEmpty => identical(this, empty); 15 | 16 | Entry withTime(DateTime time) => Entry(configJsonString, config, eTag, time); 17 | 18 | static Entry empty = Entry('', Config.empty, '', distantPast); 19 | 20 | String serialize() { 21 | return '${fetchTime.millisecondsSinceEpoch}\n$eTag\n$configJsonString'; 22 | } 23 | 24 | static Entry fromCached(String cached) { 25 | final timeIndex = cached.indexOf('\n'); 26 | if (timeIndex == -1) { 27 | throw FormatException("Number of values is fewer than expected."); 28 | } 29 | 30 | final eTagIndex = cached.indexOf('\n', timeIndex + 1); 31 | if (eTagIndex == -1) { 32 | throw FormatException("Number of values is fewer than expected."); 33 | } 34 | 35 | final timeString = cached.substring(0, timeIndex); 36 | final time = int.tryParse(timeString); 37 | if (time == null) { 38 | throw FormatException("Invalid fetch time: $timeString"); 39 | } 40 | 41 | final fetchTime = DateTime.fromMillisecondsSinceEpoch(time, isUtc: true); 42 | final eTag = cached.substring(timeIndex + 1, eTagIndex); 43 | final configJson = cached.substring(eTagIndex + 1); 44 | final Config config; 45 | try { 46 | config = Utils.deserializeConfig(configJson); 47 | } catch (e) { 48 | throw ArgumentError("Invalid config JSON content: $configJson"); 49 | } 50 | return Entry(configJson, config, eTag, fetchTime); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/error_reporter.dart: -------------------------------------------------------------------------------- 1 | import 'log/configcat_logger.dart'; 2 | import 'configcat_options.dart'; 3 | 4 | class ErrorReporter { 5 | final ConfigCatLogger _logger; 6 | final Hooks _hooks; 7 | 8 | ErrorReporter(this._logger, this._hooks); 9 | 10 | void error(int eventId, message, [dynamic error, StackTrace? stackTrace]) { 11 | _logger.error(eventId, message, error, stackTrace); 12 | _hooks.invokeError(message, error, stackTrace); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/fetch/periodic_executor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class PeriodicExecutor { 4 | final Future Function() _task; 5 | final Duration _interval; 6 | final Completer _canceller = Completer(); 7 | 8 | CancellableDelayed? _delayed; 9 | 10 | bool _isCancelled = false; 11 | 12 | PeriodicExecutor(this._task, this._interval) { 13 | scheduleMicrotask(_execute); 14 | } 15 | 16 | void cancel() { 17 | if (!_canceller.isCompleted) { 18 | _canceller.complete(); 19 | } 20 | _isCancelled = true; 21 | _delayed?.cancel(); 22 | } 23 | 24 | Future _execute() async { 25 | try { 26 | while (!_isCancelled) { 27 | await _task.call(); 28 | await _delay(_interval); 29 | } 30 | } finally { 31 | if (!_isCancelled) { 32 | scheduleMicrotask(_execute); 33 | } 34 | } 35 | } 36 | 37 | Future _delay(Duration duration) { 38 | final delayed = _delayed = CancellableDelayed(duration); 39 | return Future.any([delayed.future, _canceller.future]); 40 | } 41 | } 42 | 43 | class CancellableDelayed { 44 | final Completer _completer = Completer(); 45 | late final Timer? _timer; 46 | 47 | bool _isCompleted = false; 48 | bool _isCanceled = false; 49 | 50 | Future get future => _completer.future; 51 | 52 | CancellableDelayed(Duration delay) { 53 | _timer = Timer(delay, _complete); 54 | } 55 | 56 | void cancel() { 57 | if (!_isCompleted && !_isCanceled) { 58 | _timer?.cancel(); 59 | _isCanceled = true; 60 | _completer.complete(); 61 | } 62 | } 63 | 64 | void _complete() { 65 | if (!_isCompleted && !_isCanceled) { 66 | _isCompleted = true; 67 | _completer.complete(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/fetch/refresh_result.dart: -------------------------------------------------------------------------------- 1 | class RefreshResult { 2 | final bool isSuccess; 3 | final String? error; 4 | 5 | RefreshResult(this.isSuccess, this.error); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/json/condition.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'condition_accessor.dart'; 4 | import 'user_condition.dart'; 5 | import 'segment_condition.dart'; 6 | import 'prerequisite_flag_condition.dart'; 7 | 8 | part 'condition.g.dart'; 9 | 10 | /// Represents a condition. 11 | @JsonSerializable() 12 | class Condition implements ConditionAccessor { 13 | @override 14 | @JsonKey(name: 'u') 15 | final UserCondition? userCondition; 16 | 17 | @override 18 | @JsonKey(name: 's') 19 | final SegmentCondition? segmentCondition; 20 | 21 | @override 22 | @JsonKey(name: 'p') 23 | final PrerequisiteFlagCondition? prerequisiteFlagCondition; 24 | 25 | Condition(this.userCondition, this.segmentCondition, 26 | this.prerequisiteFlagCondition); 27 | 28 | factory Condition.fromJson(Map json) => 29 | _$ConditionFromJson(json); 30 | Map toJson() => _$ConditionToJson(this); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/json/condition.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'condition.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Condition _$ConditionFromJson(Map json) => Condition( 10 | json['u'] == null 11 | ? null 12 | : UserCondition.fromJson(json['u'] as Map), 13 | json['s'] == null 14 | ? null 15 | : SegmentCondition.fromJson(json['s'] as Map), 16 | json['p'] == null 17 | ? null 18 | : PrerequisiteFlagCondition.fromJson( 19 | json['p'] as Map), 20 | ); 21 | 22 | Map _$ConditionToJson(Condition instance) => { 23 | 'u': instance.userCondition, 24 | 's': instance.segmentCondition, 25 | 'p': instance.prerequisiteFlagCondition, 26 | }; 27 | -------------------------------------------------------------------------------- /lib/src/json/condition_accessor.dart: -------------------------------------------------------------------------------- 1 | import 'prerequisite_flag_condition.dart'; 2 | import 'segment_condition.dart'; 3 | import 'user_condition.dart'; 4 | 5 | abstract class ConditionAccessor { 6 | UserCondition? get userCondition; 7 | 8 | SegmentCondition? get segmentCondition; 9 | 10 | PrerequisiteFlagCondition? get prerequisiteFlagCondition; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/json/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'preferences.dart'; 4 | import 'setting.dart'; 5 | import 'segment.dart'; 6 | 7 | part 'config.g.dart'; 8 | 9 | /// Details of a ConfigCat config. 10 | @JsonSerializable() 11 | class Config { 12 | /// The config preferences. 13 | @JsonKey(name: 'p') 14 | final Preferences preferences; 15 | 16 | /// The map of settings. 17 | @JsonKey(name: 'f') 18 | final Map entries; 19 | 20 | /// The list of segments. 21 | @JsonKey(name: 's', defaultValue: []) 22 | final List segments; 23 | 24 | Config(this.preferences, this.entries, this.segments); 25 | 26 | bool get isEmpty => identical(this, empty); 27 | 28 | static Config empty = Config(Preferences.empty, {}, List.empty()); 29 | 30 | factory Config.fromJson(Map json) => _$ConfigFromJson(json); 31 | Map toJson() => _$ConfigToJson(this); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/json/config.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'config.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Config _$ConfigFromJson(Map json) => Config( 10 | Preferences.fromJson(json['p'] as Map), 11 | (json['f'] as Map).map( 12 | (k, e) => MapEntry(k, Setting.fromJson(e as Map)), 13 | ), 14 | (json['s'] as List?) 15 | ?.map((e) => Segment.fromJson(e as Map)) 16 | .toList() ?? 17 | [], 18 | ); 19 | 20 | Map _$ConfigToJson(Config instance) => { 21 | 'p': instance.preferences, 22 | 'f': instance.entries, 23 | 's': instance.segments, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/src/json/percentage_option.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'setting_value.dart'; 4 | 5 | part 'percentage_option.g.dart'; 6 | 7 | /// Represents a percentage option. 8 | @JsonSerializable() 9 | class PercentageOption { 10 | /// The value associated with the percentage option. 11 | /// Can be a value of the following types: {@link Boolean}, {@link String}, {@link Integer} or {@link Double}. 12 | @JsonKey(name: 'v') 13 | final SettingValue settingValue; 14 | 15 | /// A number between 0 and 100 that represents a randomly allocated fraction of the users. 16 | @JsonKey(name: 'p', defaultValue: 0) 17 | final double percentage; 18 | 19 | /// Variation ID. 20 | @JsonKey(name: 'i') 21 | final String? variationId; 22 | 23 | PercentageOption(this.settingValue, this.percentage, this.variationId); 24 | 25 | factory PercentageOption.fromJson(Map json) => 26 | _$PercentageOptionFromJson(json); 27 | Map toJson() => _$PercentageOptionToJson(this); 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/json/percentage_option.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'percentage_option.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PercentageOption _$PercentageOptionFromJson(Map json) => 10 | PercentageOption( 11 | SettingValue.fromJson(json['v'] as Map), 12 | (json['p'] as num?)?.toDouble() ?? 0, 13 | json['i'] as String?, 14 | ); 15 | 16 | Map _$PercentageOptionToJson(PercentageOption instance) => 17 | { 18 | 'v': instance.settingValue, 19 | 'p': instance.percentage, 20 | 'i': instance.variationId, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/src/json/preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'preferences.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Preferences { 7 | @JsonKey(name: 'u') 8 | final String baseUrl; 9 | 10 | @JsonKey(name: 'r', defaultValue: 0) 11 | final int redirect; 12 | 13 | @JsonKey(name: 's') 14 | final String? salt; 15 | 16 | Preferences(this.baseUrl, this.redirect, this.salt); 17 | 18 | static Preferences empty = Preferences("", 0, ""); 19 | 20 | factory Preferences.fromJson(Map json) => 21 | _$PreferencesFromJson(json); 22 | 23 | Map toJson() => _$PreferencesToJson(this); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/json/preferences.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'preferences.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Preferences _$PreferencesFromJson(Map json) => Preferences( 10 | json['u'] as String, 11 | json['r'] as int? ?? 0, 12 | json['s'] as String?, 13 | ); 14 | 15 | Map _$PreferencesToJson(Preferences instance) => 16 | { 17 | 'u': instance.baseUrl, 18 | 'r': instance.redirect, 19 | 's': instance.salt, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/src/json/prerequisite_comparator.dart: -------------------------------------------------------------------------------- 1 | // Note for maintainers: the order of enum members matters. 2 | // The index of the enum members must correspond to the comparator type number 3 | 4 | enum PrerequisiteComparator { 5 | equals(name: "EQUALS"), 6 | notEquals(name: "NOT EQUALS"); 7 | 8 | final String name; 9 | 10 | const PrerequisiteComparator({required this.name}); 11 | 12 | static PrerequisiteComparator? tryFrom(int value) { 13 | return 0 <= value && value < PrerequisiteComparator.values.length 14 | ? PrerequisiteComparator.values[value] 15 | : null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/json/prerequisite_flag_condition.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'setting_value.dart'; 4 | 5 | part 'prerequisite_flag_condition.g.dart'; 6 | 7 | /// Describes a condition that is based on a prerequisite flag. 8 | @JsonSerializable() 9 | class PrerequisiteFlagCondition { 10 | /// The key of the prerequisite flag that the condition is based on. 11 | @JsonKey(name: 'f') 12 | final String prerequisiteFlagKey; 13 | 14 | /// The operator which defines the relation between the evaluated value of the prerequisite flag and the comparison value. 15 | @JsonKey(name: 'c') 16 | final int prerequisiteComparator; 17 | 18 | /// The value that the evaluated value of the prerequisite flag is compared to. 19 | /// Can be a value of the following types: {@link Boolean}, {@link String}, {@link Integer} or {@link Double}. 20 | @JsonKey(name: 'v') 21 | final SettingValue? value; 22 | 23 | PrerequisiteFlagCondition( 24 | this.prerequisiteFlagKey, this.prerequisiteComparator, this.value); 25 | 26 | factory PrerequisiteFlagCondition.fromJson(Map json) => 27 | _$PrerequisiteFlagConditionFromJson(json); 28 | Map toJson() => _$PrerequisiteFlagConditionToJson(this); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/json/prerequisite_flag_condition.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'prerequisite_flag_condition.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PrerequisiteFlagCondition _$PrerequisiteFlagConditionFromJson( 10 | Map json) => 11 | PrerequisiteFlagCondition( 12 | json['f'] as String, 13 | json['c'] as int, 14 | json['v'] == null 15 | ? null 16 | : SettingValue.fromJson(json['v'] as Map), 17 | ); 18 | 19 | Map _$PrerequisiteFlagConditionToJson( 20 | PrerequisiteFlagCondition instance) => 21 | { 22 | 'f': instance.prerequisiteFlagKey, 23 | 'c': instance.prerequisiteComparator, 24 | 'v': instance.value, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/json/segment.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'user_condition.dart'; 4 | 5 | part 'segment.g.dart'; 6 | 7 | /// ConfigCat segment. 8 | @JsonSerializable() 9 | class Segment { 10 | /// The name of the segment. 11 | @JsonKey(name: 'n') 12 | final String? name; 13 | 14 | /// The list of segment rule conditions (where there is a logical AND relation between the items). 15 | @JsonKey(name: 'r') 16 | final List segmentRules; 17 | 18 | Segment(this.name, this.segmentRules); 19 | 20 | factory Segment.fromJson(Map json) => 21 | _$SegmentFromJson(json); 22 | Map toJson() => _$SegmentToJson(this); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/json/segment.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'segment.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Segment _$SegmentFromJson(Map json) => Segment( 10 | json['n'] as String?, 11 | (json['r'] as List) 12 | .map((e) => UserCondition.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$SegmentToJson(Segment instance) => { 17 | 'n': instance.name, 18 | 'r': instance.segmentRules, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/json/segment_comparator.dart: -------------------------------------------------------------------------------- 1 | // Note for maintainers: the order of enum members matters. 2 | // The index of the enum members must correspond to the comparator type number 3 | 4 | enum SegmentComparator { 5 | isInSegment(name: "IS IN SEGMENT"), 6 | isNotInSegment(name: "IS NOT IN SEGMENT"); 7 | 8 | final String name; 9 | 10 | const SegmentComparator({required this.name}); 11 | 12 | static SegmentComparator? tryFrom(int value) { 13 | return 0 <= value && value < SegmentComparator.values.length 14 | ? SegmentComparator.values[value] 15 | : null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/json/segment_condition.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'segment_condition.g.dart'; 4 | 5 | /// Describes a condition that is based on a segment. 6 | @JsonSerializable() 7 | class SegmentCondition { 8 | /// The index of the segment that the condition is based on. 9 | @JsonKey(name: 's') 10 | final int segmentIndex; 11 | 12 | /// The operator which defines the expected result of the evaluation of the segment. 13 | @JsonKey(name: 'c') 14 | final int segmentComparator; 15 | 16 | SegmentCondition(this.segmentIndex, this.segmentComparator); 17 | 18 | factory SegmentCondition.fromJson(Map json) => 19 | _$SegmentConditionFromJson(json); 20 | Map toJson() => _$SegmentConditionToJson(this); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/json/segment_condition.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'segment_condition.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SegmentCondition _$SegmentConditionFromJson(Map json) => 10 | SegmentCondition( 11 | json['s'] as int, 12 | json['c'] as int, 13 | ); 14 | 15 | Map _$SegmentConditionToJson(SegmentCondition instance) => 16 | { 17 | 's': instance.segmentIndex, 18 | 'c': instance.segmentComparator, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/src/json/served_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'setting_value.dart'; 4 | 5 | part 'served_value.g.dart'; 6 | 7 | @JsonSerializable() 8 | class ServedValue { 9 | @JsonKey(name: 'v') 10 | final SettingValue settingValue; 11 | 12 | @JsonKey(name: 'i') 13 | final String? variationId; 14 | 15 | ServedValue(this.settingValue, this.variationId); 16 | 17 | factory ServedValue.fromJson(Map json) => 18 | _$ServedValueFromJson(json); 19 | Map toJson() => _$ServedValueToJson(this); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/json/served_value.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'served_value.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ServedValue _$ServedValueFromJson(Map json) => ServedValue( 10 | SettingValue.fromJson(json['v'] as Map), 11 | json['i'] as String?, 12 | ); 13 | 14 | Map _$ServedValueToJson(ServedValue instance) => 15 | { 16 | 'v': instance.settingValue, 17 | 'i': instance.variationId, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/src/json/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'percentage_option.dart'; 4 | import 'segment.dart'; 5 | import 'targeting_rule.dart'; 6 | import 'setting_value.dart'; 7 | 8 | part 'setting.g.dart'; 9 | 10 | extension SettingConvert on Object { 11 | /// Creates a basic [Setting] instance from an [Object]. 12 | Setting toSetting() { 13 | SettingValue settingValue; 14 | int settingType; 15 | if (this is bool) { 16 | settingValue = SettingValue(this as bool?, null, null, null); 17 | settingType = 0; 18 | } else if (this is String) { 19 | settingValue = SettingValue(null, this as String?, null, null); 20 | settingType = 1; 21 | } else if (this is int) { 22 | settingValue = SettingValue(null, null, this as int?, null); 23 | settingType = 2; 24 | } else if (this is double) { 25 | settingValue = SettingValue(null, null, null, this as double?); 26 | settingType = 3; 27 | } else { 28 | throw ArgumentError( 29 | "Only String, Integer, Double or Boolean types are supported."); 30 | } 31 | return Setting( 32 | settingValue, settingType, List.empty(), List.empty(), "", ""); 33 | } 34 | } 35 | 36 | /// Feature flag or setting. 37 | @JsonSerializable() 38 | class Setting { 39 | /// Setting value. 40 | /// Can be a value of the following types: {@link Boolean}, {@link String}, {@link Integer} or {@link Double}. 41 | @JsonKey(name: 'v') 42 | final SettingValue settingValue; 43 | 44 | /// Setting type. 45 | @JsonKey(name: 't') 46 | final int type; 47 | 48 | /// The list of percentage options. 49 | @JsonKey(name: 'p', defaultValue: []) 50 | final List percentageOptions; 51 | 52 | /// The list of targeting rules (where there is a logical OR relation between the items). 53 | @JsonKey(name: 'r', defaultValue: []) 54 | final List targetingRules; 55 | 56 | /// Variation ID. 57 | @JsonKey(name: 'i') 58 | final String? variationId; 59 | 60 | /// The User Object attribute which serves as the basis of percentage options evaluation. 61 | @JsonKey(name: 'a') 62 | final String? percentageAttribute; 63 | 64 | @JsonKey(includeFromJson: false, includeToJson: false) 65 | String? salt; 66 | 67 | @JsonKey(includeFromJson: false, includeToJson: false) 68 | List segments = List.empty(); 69 | 70 | Setting(this.settingValue, this.type, this.percentageOptions, 71 | this.targetingRules, this.variationId, this.percentageAttribute); 72 | 73 | factory Setting.fromJson(Map json) => 74 | _$SettingFromJson(json); 75 | Map toJson() => _$SettingToJson(this); 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/json/setting.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'setting.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Setting _$SettingFromJson(Map json) => Setting( 10 | SettingValue.fromJson(json['v'] as Map), 11 | json['t'] as int, 12 | (json['p'] as List?) 13 | ?.map((e) => PercentageOption.fromJson(e as Map)) 14 | .toList() ?? 15 | [], 16 | (json['r'] as List?) 17 | ?.map((e) => TargetingRule.fromJson(e as Map)) 18 | .toList() ?? 19 | [], 20 | json['i'] as String?, 21 | json['a'] as String?, 22 | ); 23 | 24 | Map _$SettingToJson(Setting instance) => { 25 | 'v': instance.settingValue, 26 | 't': instance.type, 27 | 'p': instance.percentageOptions, 28 | 'r': instance.targetingRules, 29 | 'i': instance.variationId, 30 | 'a': instance.percentageAttribute, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/src/json/setting_type.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'dart:core' as core; 3 | 4 | // Note for maintainers: the order of enum members matters. 5 | // The index of the enum members must correspond to the comparator type numbers. 6 | 7 | enum SettingType { 8 | boolean(name: 'Boolean'), 9 | string(name: 'String'), 10 | int(name: 'Int'), 11 | double(name: 'Double'); 12 | 13 | final String name; 14 | 15 | const SettingType({required this.name}); 16 | 17 | static SettingType? tryFrom(core.int value) { 18 | return 0 <= value && value < SettingType.values.length 19 | ? SettingType.values[value] 20 | : null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/json/setting_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/json/setting_type.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'setting_value.g.dart'; 5 | 6 | /// Describes the setting type-specific value of a setting or feature flag. 7 | @JsonSerializable() 8 | class SettingValue { 9 | @JsonKey(name: 'b') 10 | final bool? booleanValue; 11 | 12 | @JsonKey(name: 's') 13 | final String? stringValue; 14 | 15 | @JsonKey(name: 'i') 16 | final int? intValue; 17 | 18 | @JsonKey(name: 'd') 19 | final double? doubleValue; 20 | 21 | SettingValue( 22 | this.booleanValue, this.stringValue, this.intValue, this.doubleValue); 23 | 24 | factory SettingValue.fromJson(Map json) => 25 | _$SettingValueFromJson(json); 26 | 27 | Map toJson() => _$SettingValueToJson(this); 28 | 29 | bool equalsBasedOnSettingType(Object? other, SettingType settingType) { 30 | if (identical(this, other)) { 31 | return true; 32 | } 33 | if (other is SettingValue && runtimeType == other.runtimeType) { 34 | if (settingType == SettingType.boolean) { 35 | return booleanValue == other.booleanValue; 36 | } 37 | if (settingType == SettingType.string) { 38 | return stringValue == other.stringValue; 39 | } 40 | if (settingType == SettingType.int) { 41 | return intValue == other.intValue; 42 | } 43 | if (settingType == SettingType.double) { 44 | return doubleValue == other.doubleValue; 45 | } 46 | throw ArgumentError( 47 | "Setting is of an unsupported type (${settingType.name})."); 48 | } 49 | return false; 50 | } 51 | 52 | @override 53 | String toString() { 54 | if (booleanValue != null) { 55 | return booleanValue.toString(); 56 | } else if (intValue != null) { 57 | return intValue.toString(); 58 | } else if (doubleValue != null) { 59 | return doubleValue.toString(); 60 | } else { 61 | return stringValue ?? ''; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/json/setting_value.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'setting_value.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SettingValue _$SettingValueFromJson(Map json) => SettingValue( 10 | json['b'] as bool?, 11 | json['s'] as String?, 12 | json['i'] as int?, 13 | (json['d'] as num?)?.toDouble(), 14 | ); 15 | 16 | Map _$SettingValueToJson(SettingValue instance) => 17 | { 18 | 'b': instance.booleanValue, 19 | 's': instance.stringValue, 20 | 'i': instance.intValue, 21 | 'd': instance.doubleValue, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/src/json/targeting_rule.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'condition.dart'; 4 | import 'percentage_option.dart'; 5 | import 'served_value.dart'; 6 | 7 | part 'targeting_rule.g.dart'; 8 | 9 | /// Describes a targeting rule. 10 | @JsonSerializable() 11 | class TargetingRule { 12 | /// The list of conditions that are combined with the AND logical operator. 13 | /// Items can be one of the following types: {@link UserCondition}, {@link SegmentCondition} or {@link PrerequisiteFlagCondition}. 14 | @JsonKey(name: 'c') 15 | final List conditions; 16 | 17 | /// The list of percentage options associated with the targeting rule or {@code null} if the targeting rule has a simple value THEN part. 18 | @JsonKey(name: 'p') 19 | final List? percentageOptions; 20 | 21 | /// The simple value associated with the targeting rule or {@code null} if the targeting rule has percentage options THEN part. 22 | @JsonKey(name: 's') 23 | final ServedValue? servedValue; 24 | 25 | TargetingRule(this.conditions, this.percentageOptions, this.servedValue); 26 | 27 | factory TargetingRule.fromJson(Map json) => 28 | _$TargetingRuleFromJson(json); 29 | Map toJson() => _$TargetingRuleToJson(this); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/json/targeting_rule.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'targeting_rule.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TargetingRule _$TargetingRuleFromJson(Map json) => 10 | TargetingRule( 11 | (json['c'] as List) 12 | .map((e) => Condition.fromJson(e as Map)) 13 | .toList(), 14 | (json['p'] as List?) 15 | ?.map((e) => PercentageOption.fromJson(e as Map)) 16 | .toList(), 17 | json['s'] == null 18 | ? null 19 | : ServedValue.fromJson(json['s'] as Map), 20 | ); 21 | 22 | Map _$TargetingRuleToJson(TargetingRule instance) => 23 | { 24 | 'c': instance.conditions, 25 | 'p': instance.percentageOptions, 26 | 's': instance.servedValue, 27 | }; 28 | -------------------------------------------------------------------------------- /lib/src/json/user_comparator.dart: -------------------------------------------------------------------------------- 1 | // Note for maintainers: the order of enum members matters. 2 | // The index of the enum members must correspond to the comparator type numbers. 3 | 4 | enum UserComparator { 5 | isOneOf(name: "IS ONE OF"), 6 | isNotOneOf(name: "IS NOT ONE OF"), 7 | containsAnyOf(name: "CONTAINS ANY OF"), 8 | notContainsAnyOf(name: "NOT CONTAINS ANY OF"), 9 | semverIsOneOf(name: "IS ONE OF"), 10 | semverIsNotOneOf(name: "IS NOT ONE OF"), 11 | semverLess(name: "<"), 12 | semverLessEquals(name: "<="), 13 | semverGreater(name: ">"), 14 | semverGreaterEquals(name: ">="), 15 | numberEquals(name: "="), 16 | numberNotEquals(name: "!="), 17 | numberLess(name: "<"), 18 | numberLessEquals(name: "<="), 19 | numberGreater(name: ">"), 20 | numberGreaterEquals(name: ">="), 21 | sensitiveIsOneOf(name: "IS ONE OF"), 22 | sensitiveIsNotOneOf(name: "IS NOT ONE OF"), 23 | dateBefore(name: "BEFORE"), 24 | dateAfter(name: "AFTER"), 25 | hashedEquals(name: "EQUALS"), 26 | hashedNotEquals(name: "NOT EQUALS"), 27 | hashedStartsWith(name: "STARTS WITH ANY OF"), 28 | hashedNotStartsWith(name: "NOT STARTS WITH ANY OF"), 29 | hashedEndsWith(name: "ENDS WITH ANY OF"), 30 | hashedNotEndsWith(name: "NOT ENDS WITH ANY OF"), 31 | hashedArrayContains(name: "ARRAY CONTAINS ANY OF"), 32 | hashedArrayNotContains(name: "ARRAY NOT CONTAINS ANY OF"), 33 | textEquals(name: "EQUALS"), 34 | textNotEquals(name: "NOT EQUALS"), 35 | textStartsWith(name: "STARTS WITH ANY OF"), 36 | textNotStartsWith(name: "NOT STARTS WITH ANY OF"), 37 | textEndsWith(name: "ENDS WITH ANY OF"), 38 | textNotEndsWith(name: "NOT ENDS WITH ANY OF"), 39 | textArrayContains(name: "ARRAY CONTAINS ANY OF"), 40 | textArrayNotContains(name: "ARRAY NOT CONTAINS ANY OF"); 41 | 42 | final String name; 43 | 44 | const UserComparator({required this.name}); 45 | 46 | static UserComparator? tryFrom(int value) { 47 | return 0 <= value && value < UserComparator.values.length 48 | ? UserComparator.values[value] 49 | : null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/json/user_condition.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/json/condition_accessor.dart'; 2 | import 'package:configcat_client/src/json/prerequisite_flag_condition.dart'; 3 | import 'package:configcat_client/src/json/segment_condition.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | part 'user_condition.g.dart'; 7 | 8 | /// Describes a condition that is based on a User Object attribute. 9 | @JsonSerializable() 10 | class UserCondition implements ConditionAccessor { 11 | /// The User Object attribute that the condition is based on. Can be "Identifier", "Email", "Country" or any custom attribute. 12 | @JsonKey(name: 'a') 13 | final String comparisonAttribute; 14 | 15 | /// The operator which defines the relation between the comparison attribute and the comparison value. 16 | @JsonKey(name: 'c') 17 | final int comparator; 18 | 19 | /// The String value that the User Object attribute is compared or {@code null} if the comparator use a different type of value. 20 | @JsonKey(name: 's') 21 | final String? stringValue; 22 | 23 | /// The Double value that the User Object attribute is compared or {@code null} if the comparator use a different type of value. 24 | @JsonKey(name: 'd') 25 | final double? doubleValue; 26 | 27 | /// The String Array value that the User Object attribute is compared or {@code null} if the comparator use a different type of value. 28 | @JsonKey(name: 'l') 29 | final List? stringArrayValue; 30 | 31 | UserCondition(this.comparisonAttribute, this.comparator, this.stringValue, 32 | this.doubleValue, this.stringArrayValue); 33 | 34 | factory UserCondition.fromJson(Map json) => 35 | _$UserConditionFromJson(json); 36 | 37 | Map toJson() => _$UserConditionToJson(this); 38 | 39 | @override 40 | PrerequisiteFlagCondition? get prerequisiteFlagCondition { 41 | return null; 42 | } 43 | 44 | @override 45 | SegmentCondition? get segmentCondition { 46 | return null; 47 | } 48 | 49 | @override 50 | UserCondition? get userCondition { 51 | return this; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/json/user_condition.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_condition.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | UserCondition _$UserConditionFromJson(Map json) => 10 | UserCondition( 11 | json['a'] as String, 12 | json['c'] as int, 13 | json['s'] as String?, 14 | (json['d'] as num?)?.toDouble(), 15 | (json['l'] as List?)?.map((e) => e as String).toList(), 16 | ); 17 | 18 | Map _$UserConditionToJson(UserCondition instance) => 19 | { 20 | 'a': instance.comparisonAttribute, 21 | 'c': instance.comparator, 22 | 's': instance.stringValue, 23 | 'd': instance.doubleValue, 24 | 'l': instance.stringArrayValue, 25 | }; 26 | -------------------------------------------------------------------------------- /lib/src/log/configcat_logger.dart: -------------------------------------------------------------------------------- 1 | import '../configcat_client.dart'; 2 | import 'default_logger.dart'; 3 | 4 | import 'logger.dart'; 5 | 6 | /// Logger used by [ConfigCatClient]. 7 | class ConfigCatLogger { 8 | late final Logger _internal; 9 | late final LogLevel _globalLevel; 10 | bool _isClosed = false; 11 | 12 | ConfigCatLogger({ 13 | Logger? internalLogger, 14 | LogLevel? level, 15 | }) { 16 | _globalLevel = level ?? LogLevel.warning; 17 | _internal = internalLogger ?? DefaultLogger(); 18 | } 19 | 20 | void debug(message, [dynamic error, StackTrace? stackTrace]) { 21 | if (LogLevel.debug.index >= _globalLevel.index && !_isClosed) { 22 | _internal.debug("[0] $message", error, stackTrace); 23 | } 24 | } 25 | 26 | void error(int eventId, message, [dynamic error, StackTrace? stackTrace]) { 27 | if (LogLevel.error.index >= _globalLevel.index && !_isClosed) { 28 | _internal.error("[$eventId] $message", error, stackTrace); 29 | } 30 | } 31 | 32 | void info(int eventId, message, [dynamic error, StackTrace? stackTrace]) { 33 | if (LogLevel.info.index >= _globalLevel.index && !_isClosed) { 34 | _internal.info("[$eventId] $message", error, stackTrace); 35 | } 36 | } 37 | 38 | void warning(int eventId, message, [dynamic error, StackTrace? stackTrace]) { 39 | if (LogLevel.warning.index >= _globalLevel.index && !_isClosed) { 40 | _internal.warning("[$eventId] $message", error, stackTrace); 41 | } 42 | } 43 | 44 | void close() { 45 | _isClosed = true; 46 | _internal.close(); 47 | } 48 | 49 | LogLevel getLogLevel() { 50 | return _globalLevel; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/log/default_logger.dart: -------------------------------------------------------------------------------- 1 | import 'logger.dart'; 2 | 3 | /// Default [Logger] implementation used by [ConfigCatLogger]. 4 | class DefaultLogger implements Logger { 5 | @override 6 | void debug(message, [dynamic error, StackTrace? stackTrace]) { 7 | _log("[DEBUG]", message, error, stackTrace); 8 | } 9 | 10 | @override 11 | void error(message, [dynamic error, StackTrace? stackTrace]) { 12 | _log("[ERROR]", message, error, stackTrace); 13 | } 14 | 15 | @override 16 | void info(message, [dynamic error, StackTrace? stackTrace]) { 17 | _log("[INFO]", message, error, stackTrace); 18 | } 19 | 20 | @override 21 | void warning(message, [dynamic error, StackTrace? stackTrace]) { 22 | _log("[WARN]", message, error, stackTrace); 23 | } 24 | 25 | @override 26 | void close() {} 27 | 28 | void _log(String prefix, dynamic message, 29 | [dynamic error, StackTrace? stackTrace]) { 30 | var err = error != null ? ' ERROR: $error' : ''; 31 | print('$prefix ${DateTime.now().toIso8601String()}: $message$err'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/log/logger.dart: -------------------------------------------------------------------------------- 1 | /// Defines levels to control logging verbosity. 2 | enum LogLevel { 3 | debug, 4 | info, 5 | warning, 6 | error, 7 | nothing, 8 | } 9 | 10 | /// Describes a logger used by [ConfigCatLogger]. 11 | abstract class Logger { 12 | /// Log a message at level [LogLevel.debug]. 13 | void debug(dynamic message, [dynamic error, StackTrace? stackTrace]); 14 | 15 | /// Log a message at level [LogLevel.info]. 16 | void info(dynamic message, [dynamic error, StackTrace? stackTrace]); 17 | 18 | /// Log a message at level [LogLevel.warning]. 19 | void warning(dynamic message, [dynamic error, StackTrace? stackTrace]); 20 | 21 | /// Log a message at level [LogLevel.error]. 22 | void error(dynamic message, [dynamic error, StackTrace? stackTrace]); 23 | 24 | /// Closes the logger. 25 | void close(); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/mixins.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | /// This mixin can be used to ensure that an asynchronous operation couldn't be 4 | /// initiated multiple times simultaneously. Each caller will wait for the 5 | /// first operation to be completed. 6 | mixin ContinuousFutureSynchronizer { 7 | Future? _future; 8 | 9 | /// Ensure [futureToSync] is not executed multiple times simultaneously. 10 | Future syncFuture(Future Function() futureToSync) async { 11 | // operation is already running 12 | if (_future != null) { 13 | // wait for the completer's result 14 | final result = await _future!; 15 | return result; 16 | } 17 | 18 | // first call, create completer and save it's future for other callers 19 | final completer = Completer(); 20 | _future = completer.future; 21 | 22 | // wait for the operation to finish 23 | final result = await futureToSync(); 24 | 25 | // operation finished, give result to everybody else except the first caller 26 | completer.complete(result); 27 | _future = null; 28 | 29 | // give result to the first caller 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/override/behaviour.dart: -------------------------------------------------------------------------------- 1 | /// Describes how the overrides should behave. 2 | enum 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 | localOnly, 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 | localOverRemote, 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 | remoteOverLocal 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/override/data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/json/setting.dart'; 2 | import 'package:configcat_client/src/override/flag_overrides.dart'; 3 | 4 | /// Describes a data source for [FlagOverrides]. 5 | abstract class OverrideDataSource { 6 | /// Gets all the overrides defined in the given source. 7 | Future> getOverrides(); 8 | 9 | /// Create an [OverrideDataSource] that stores the overrides in a key-value map. 10 | factory OverrideDataSource.map(Map overrides) { 11 | return _MapOverrideDataSource(overrides); 12 | } 13 | } 14 | 15 | class _MapOverrideDataSource implements OverrideDataSource { 16 | late final Map overrides; 17 | 18 | _MapOverrideDataSource(Map mapOverrides) { 19 | overrides = 20 | mapOverrides.map((key, value) => MapEntry(key, value.toSetting())); 21 | } 22 | 23 | @override 24 | Future> getOverrides() { 25 | return Future.value(overrides); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/override/flag_overrides.dart: -------------------------------------------------------------------------------- 1 | import 'data_source.dart'; 2 | import 'behaviour.dart'; 3 | 4 | /// Describes feature flag & setting overrides. 5 | /// 6 | /// [dataSource] contains the flag values in a key-value map. 7 | /// [behaviour] can be used to set preference on whether the local values should 8 | /// override the remote values, or use local values only when a remote value doesn't exist, 9 | /// or use it for local only mode. 10 | class FlagOverrides { 11 | final OverrideDataSource dataSource; 12 | final OverrideBehaviour behaviour; 13 | 14 | FlagOverrides({required this.dataSource, required this.behaviour}); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/pair.dart: -------------------------------------------------------------------------------- 1 | class Pair { 2 | final T1 first; 3 | final T2 second; 4 | 5 | Pair(this.first, this.second); 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/platform_spec/default/platform.dart: -------------------------------------------------------------------------------- 1 | class ActualPlatform { 2 | ActualPlatform._(); 3 | static String get lineTerminator => '\n'; 4 | } 5 | -------------------------------------------------------------------------------- /lib/src/platform_spec/default/request_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class ActualRequestBuilder { 4 | static const _userAgentHeaderName = 'X-ConfigCat-UserAgent'; 5 | static const _ifNoneMatchHeaderName = 'If-None-Match'; 6 | 7 | ActualRequestBuilder._(); 8 | 9 | static RequestOptions build(String sdkInfo, String etag) { 10 | Map headers = { 11 | _userAgentHeaderName: sdkInfo, 12 | if (etag.isNotEmpty) _ifNoneMatchHeaderName: etag 13 | }; 14 | 15 | return RequestOptions(headers: headers); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/platform_spec/io/platform.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class ActualPlatform { 4 | ActualPlatform._(); 5 | static String get lineTerminator => Platform.isWindows ? '\r\n' : '\n'; 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/platform_spec/platform.dart: -------------------------------------------------------------------------------- 1 | import 'default/platform.dart' if (dart.library.io) 'io/platform.dart'; 2 | 3 | class Platform { 4 | Platform._(); 5 | static String get lineTerminator => ActualPlatform.lineTerminator; 6 | } 7 | -------------------------------------------------------------------------------- /lib/src/platform_spec/request_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import 'default/request_builder.dart' 4 | if (dart.library.html) 'web/request_builder.dart'; 5 | 6 | class RequestBuilder { 7 | RequestBuilder._(); 8 | 9 | static RequestOptions build(String sdkInfo, String etag) { 10 | return ActualRequestBuilder.build(sdkInfo, etag); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/platform_spec/web/request_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class ActualRequestBuilder { 4 | static const _sdkInfoName = 'sdk'; 5 | static const _ccEtagName = 'ccetag'; 6 | 7 | ActualRequestBuilder._(); 8 | 9 | static RequestOptions build(String sdkInfo, String etag) { 10 | Map queryParams = { 11 | _sdkInfoName: sdkInfo, 12 | if (etag.isNotEmpty) _ccEtagName: etag 13 | }; 14 | 15 | return RequestOptions(queryParameters: queryParams); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/polling_mode.dart: -------------------------------------------------------------------------------- 1 | /// The base class of a polling mode configuration. 2 | abstract class PollingMode { 3 | const PollingMode._(); 4 | 5 | /// Creates a configured auto polling configuration. 6 | /// 7 | /// [autoPollInterval] sets at least how often this policy should fetch the latest configuration and refresh the cache. 8 | /// [maxInitWaitTime] sets the maximum waiting time between initialization and the first config acquisition in seconds. 9 | factory PollingMode.autoPoll( 10 | {autoPollInterval = const Duration(seconds: 60), 11 | maxInitWaitTime = const Duration(seconds: 5)}) { 12 | return AutoPollingMode._(autoPollInterval, maxInitWaitTime); 13 | } 14 | 15 | /// Creates a configured lazy loading polling configuration. 16 | /// 17 | /// [cacheRefreshInterval] sets how long the cache will store its value before fetching the latest from the network again. 18 | factory PollingMode.lazyLoad( 19 | {cacheRefreshInterval = const Duration(seconds: 60)}) { 20 | return LazyLoadingMode._(cacheRefreshInterval); 21 | } 22 | 23 | /// Creates a configured manual polling configuration. 24 | factory PollingMode.manualPoll() { 25 | return ManualPollingMode._(); 26 | } 27 | 28 | /// Gets the current polling mode's identifier. 29 | /// Used for analytical purposes in HTTP User-Agent headers. 30 | String getPollingIdentifier(); 31 | 32 | /// The default polling mode used by [ConfigCatClient] when no mode is set. 33 | static const defaultMode = 34 | AutoPollingMode._(Duration(seconds: 60), Duration(seconds: 5)); 35 | } 36 | 37 | /// Represents the auto polling mode's configuration. 38 | class AutoPollingMode extends PollingMode { 39 | final Duration autoPollInterval; 40 | final Duration maxInitWaitTime; 41 | 42 | const AutoPollingMode._(this.autoPollInterval, this.maxInitWaitTime) 43 | : super._(); 44 | 45 | @override 46 | String getPollingIdentifier() { 47 | return 'a'; 48 | } 49 | } 50 | 51 | /// Represents the manual polling mode's configuration. 52 | class ManualPollingMode extends PollingMode { 53 | const ManualPollingMode._() : super._(); 54 | 55 | @override 56 | String getPollingIdentifier() { 57 | return 'm'; 58 | } 59 | } 60 | 61 | /// Represents lazy loading mode's configuration. 62 | class LazyLoadingMode extends PollingMode { 63 | final Duration cacheRefreshInterval; 64 | 65 | const LazyLoadingMode._(this.cacheRefreshInterval) : super._(); 66 | 67 | @override 68 | String getPollingIdentifier() { 69 | return 'l'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../configcat_client.dart'; 4 | 5 | class Utils { 6 | Utils._(); 7 | 8 | static Config deserializeConfig(String configJson) { 9 | final decoded = jsonDecode(configJson); 10 | Config config = Config.fromJson(decoded); 11 | String? salt = config.preferences.salt; 12 | List segments = config.segments; 13 | 14 | for (Setting setting in config.entries.values) { 15 | setting.salt = salt; 16 | setting.segments = segments; 17 | } 18 | return config; 19 | } 20 | 21 | // Dart syntax doesn't allow expressions like `T == int?` because that conflicts with ternary operator syntax. 22 | // This is a workaround for that limitation (see also https://stackoverflow.com/a/73120173/8656352) 23 | static bool typesEqual() => T == U; 24 | } 25 | -------------------------------------------------------------------------------- /media/readme02-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/dart-sdk/3e6f918a08911b075bee668b11ced086fccfec85/media/readme02-3.png -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: configcat_client 2 | description: >- 3 | Dart (Flutter) SDK for ConfigCat. ConfigCat is a hosted feature flag service that lets you manage feature toggles across frontend, backend, mobile, desktop apps. 4 | version: 4.1.1 5 | homepage: https://configcat.com/docs/sdk-reference/dart 6 | repository: https://github.com/configcat/dart-sdk 7 | issue_tracker: https://github.com/configcat/dart-sdk/issues 8 | 9 | environment: 10 | sdk: '>=2.19.0 <4.0.0' 11 | 12 | dependencies: 13 | crypto: ^3.0.3 14 | dio: ^5.0.0 15 | pub_semver: ^2.1.4 16 | json_annotation: ^4.8.1 17 | 18 | dev_dependencies: 19 | intl: ">=0.18.1 <0.21.0" 20 | test: ^1.17.10 21 | mockito: ^5.0.15 22 | build_runner: ^2.1.1 23 | json_serializable: ^6.1.3 24 | sprintf: ">=6.0.0 <8.0.0" 25 | lints: ">=1.0.1 <6.0.0" -------------------------------------------------------------------------------- /test/cache_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.4 from annotations 2 | // in configcat_client/test/cache_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i3; 7 | 8 | import 'package:configcat_client/src/configcat_cache.dart' as _i2; 9 | import 'package:mockito/mockito.dart' as _i1; 10 | import 'package:mockito/src/dummies.dart' as _i4; 11 | 12 | // ignore_for_file: type=lint 13 | // ignore_for_file: avoid_redundant_argument_values 14 | // ignore_for_file: avoid_setters_without_getters 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: deprecated_member_use 17 | // ignore_for_file: deprecated_member_use_from_same_package 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | /// A class which mocks [ConfigCatCache]. 26 | /// 27 | /// See the documentation for Mockito's code generation for more information. 28 | class MockConfigCatCache extends _i1.Mock implements _i2.ConfigCatCache { 29 | MockConfigCatCache() { 30 | _i1.throwOnMissingStub(this); 31 | } 32 | 33 | @override 34 | _i3.Future read(String? key) => (super.noSuchMethod( 35 | Invocation.method( 36 | #read, 37 | [key], 38 | ), 39 | returnValue: _i3.Future.value(_i4.dummyValue( 40 | this, 41 | Invocation.method( 42 | #read, 43 | [key], 44 | ), 45 | )), 46 | ) as _i3.Future); 47 | 48 | @override 49 | _i3.Future write( 50 | String? key, 51 | String? value, 52 | ) => 53 | (super.noSuchMethod( 54 | Invocation.method( 55 | #write, 56 | [ 57 | key, 58 | value, 59 | ], 60 | ), 61 | returnValue: _i3.Future.value(), 62 | returnValueForMissingStub: _i3.Future.value(), 63 | ) as _i3.Future); 64 | } 65 | -------------------------------------------------------------------------------- /test/configcat_user_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/configcat_client.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'helpers.dart'; 5 | import 'http_adapter.dart'; 6 | 7 | void main() { 8 | late ConfigCatClient client; 9 | late HttpTestAdapter testAdapter; 10 | setUp(() { 11 | client = ConfigCatClient.get( 12 | sdkKey: testSdkKey, 13 | options: ConfigCatOptions(pollingMode: PollingMode.manualPoll())); 14 | testAdapter = HttpTestAdapter(client.httpClient); 15 | }); 16 | tearDown(() { 17 | ConfigCatClient.closeAll(); 18 | testAdapter.close(); 19 | }); 20 | 21 | test('user attributes case insensitivity', () async { 22 | // Arrange 23 | final user = ConfigCatUser( 24 | identifier: 'id', 25 | email: 'email', 26 | country: 'country', 27 | custom: {'custom': 'test'}); 28 | 29 | // Assert 30 | expect('id', equals(user.identifier)); 31 | expect('email', equals(user.getAttribute('Email'))); 32 | expect('email', isNot(equals(user.getAttribute('EMAIL')))); 33 | expect('email', isNot(equals(user.getAttribute('email')))); 34 | expect('country', equals(user.getAttribute('Country'))); 35 | expect('country', isNot(equals(user.getAttribute('COUNTRY')))); 36 | expect('country', isNot(equals(user.getAttribute('country')))); 37 | expect('test', equals(user.getAttribute('custom'))); 38 | expect(user.getAttribute('not-existing'), isNull); 39 | }); 40 | 41 | test('default user', () async { 42 | // Arrange 43 | testAdapter.enqueueResponse( 44 | getPath(), 200, createTestConfigWithRules().toJson()); 45 | await client.forceRefresh(); 46 | final user1 = ConfigCatUser(identifier: 'test@test1.com'); 47 | final user2 = ConfigCatUser(identifier: 'test@test2.com'); 48 | 49 | // Act 50 | client.setDefaultUser(user1); 51 | var value = await client.getValue(key: 'key1', defaultValue: ''); 52 | 53 | // Assert 54 | expect(value, equals('fake1')); 55 | 56 | // Act 57 | value = await client.getValue(key: 'key1', defaultValue: '', user: user2); 58 | 59 | // Assert 60 | expect(value, equals('fake2')); 61 | 62 | // Act 63 | client.clearDefaultUser(); 64 | value = await client.getValue(key: 'key1', defaultValue: ''); 65 | 66 | // Assert 67 | expect(value, equals('def')); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/evaluation/data/1_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 3 | "tests": [ 4 | { 5 | "key": "stringContainsDogDefaultCat", 6 | "defaultValue": "default", 7 | "returnValue": "Cat", 8 | "expectedLog": "1_rule_no_user.txt" 9 | }, 10 | { 11 | "key": "stringContainsDogDefaultCat", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": "Cat", 17 | "expectedLog": "1_rule_no_targeted_attribute.txt" 18 | }, 19 | { 20 | "key": "stringContainsDogDefaultCat", 21 | "defaultValue": "default", 22 | "user": { 23 | "Identifier": "12345", 24 | "Email": "joe@example.com" 25 | }, 26 | "returnValue": "Cat", 27 | "expectedLog": "1_rule_not_matching_targeted_attribute.txt" 28 | }, 29 | { 30 | "key": "stringContainsDogDefaultCat", 31 | "defaultValue": "default", 32 | "user": { 33 | "Identifier": "12345", 34 | "Email": "joe@configcat.com" 35 | }, 36 | "returnValue": "Dog", 37 | "expectedLog": "1_rule_matching_targeted_attribute.txt" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/1_targeting_rule/1_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/1_targeting_rule/1_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/2_targeting_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 3 | "tests": [ 4 | { 5 | "key": "stringIsInDogDefaultCat", 6 | "defaultValue": "default", 7 | "returnValue": "Cat", 8 | "expectedLog": "2_rules_no_user.txt" 9 | }, 10 | { 11 | "key": "stringIsInDogDefaultCat", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": "Cat", 17 | "expectedLog": "2_rules_no_targeted_attribute.txt" 18 | }, 19 | { 20 | "key": "stringIsInDogDefaultCat", 21 | "defaultValue": "default", 22 | "user": { 23 | "Identifier": "12345", 24 | "Custom1": "user" 25 | }, 26 | "returnValue": "Cat", 27 | "expectedLog": "2_rules_not_matching_targeted_attribute.txt" 28 | }, 29 | { 30 | "key": "stringIsInDogDefaultCat", 31 | "defaultValue": "default", 32 | "user": { 33 | "Identifier": "12345", 34 | "Custom1": "admin" 35 | }, 36 | "returnValue": "Dog", 37 | "expectedLog": "2_rules_matching_targeted_attribute.txt" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/evaluation/data/2_targeting_rules/2_rules_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/2_targeting_rules/2_rules_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/2_targeting_rules/2_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/and_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 3 | "tests": [ 4 | { 5 | "key": "emailAnd", 6 | "defaultValue": "default", 7 | "returnValue": "Cat", 8 | "expectedLog": "and_rules_no_user.txt" 9 | }, 10 | { 11 | "key": "emailAnd", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345", 15 | "Email": "jane@configcat.com" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "and_rules_user.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/evaluation/data/and_rules/and_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/comparators.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 3 | "tests": [ 4 | { 5 | "key": "allinone", 6 | "defaultValue": "", 7 | "user": { 8 | "Identifier": "12345", 9 | "Email": "joe@example.com", 10 | "Country": "[\"USA\"]", 11 | "Version": "1.0.0", 12 | "Number": "1.0", 13 | "Date": "1693497500" 14 | }, 15 | "returnValue": "default", 16 | "expectedLog": "allinone.txt" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/epoch_date_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 3 | "tests": [ 4 | { 5 | "key": "boolTrueIn202304", 6 | "defaultValue": true, 7 | "returnValue": false, 8 | "expectedLog": "date_error.txt", 9 | "user": { 10 | "Identifier": "12345", 11 | "Custom1": "2023.04.10" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/evaluation/data/epoch_date_validation/date_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonOverride": "test_list_truncation.json", 3 | "sdkKey": "configcat-sdk-test-key/0000000000000000000001", 4 | "tests": [ 5 | { 6 | "key": "booleanKey1", 7 | "defaultValue": false, 8 | "user": { 9 | "Identifier": "12" 10 | }, 11 | "returnValue": true, 12 | "expectedLog": "list_truncation.txt" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | "i" : "test-variation-id" 79 | } 80 | } 81 | ], 82 | "i" : "test-variation-id" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/evaluation/data/number_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", 3 | "tests": [ 4 | { 5 | "key": "number", 6 | "defaultValue": "default", 7 | "returnValue": "Default", 8 | "expectedLog": "number_error.txt", 9 | "user": { 10 | "Identifier": "12345", 11 | "Custom1": "not_a_number" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/evaluation/data/number_validation/number_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_after_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 3 | "tests": [ 4 | { 5 | "key": "integer25One25Two25Three25FourAdvancedRules", 6 | "defaultValue": 42, 7 | "returnValue": -1, 8 | "expectedLog": "options_after_targeting_rule_no_user.txt" 9 | }, 10 | { 11 | "key": "integer25One25Two25Three25FourAdvancedRules", 12 | "defaultValue": 42, 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": 2, 17 | "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" 18 | }, 19 | { 20 | "key": "integer25One25Two25Three25FourAdvancedRules", 21 | "defaultValue": 42, 22 | "user": { 23 | "Identifier": "12345", 24 | "Email": "joe@example.com" 25 | }, 26 | "returnValue": 2, 27 | "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" 28 | }, 29 | { 30 | "key": "integer25One25Two25Three25FourAdvancedRules", 31 | "defaultValue": 42, 32 | "user": { 33 | "Identifier": "12345", 34 | "Email": "joe@configcat.com" 35 | }, 36 | "returnValue": 5, 37 | "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_after_targeting_rule/options_after_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_based_on_custom_attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 3 | "tests": [ 4 | { 5 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 6 | "defaultValue": "default", 7 | "returnValue": "Chicken", 8 | "expectedLog": "options_custom_attribute_no_user.txt" 9 | }, 10 | { 11 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": "Chicken", 17 | "expectedLog": "no_options_custom_attribute.txt" 18 | }, 19 | { 20 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 21 | "defaultValue": "default", 22 | "user": { 23 | "Identifier": "12345", 24 | "Country": "US" 25 | }, 26 | "returnValue": "Cat", 27 | "expectedLog": "matching_options_custom_attribute.txt" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_based_on_custom_attr/no_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_based_on_custom_attr/options_custom_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_based_on_user_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 3 | "tests": [ 4 | { 5 | "key": "string75Cat0Dog25Falcon0Horse", 6 | "defaultValue": "default", 7 | "returnValue": "Chicken", 8 | "expectedLog": "options_user_attribute_no_user.txt" 9 | }, 10 | { 11 | "key": "string75Cat0Dog25Falcon0Horse", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": "Cat", 17 | "expectedLog": "options_user_attribute_user.txt" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/evaluation/data/options_based_on_user_id/options_user_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_within_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 3 | "tests": [ 4 | { 5 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 6 | "defaultValue": "default", 7 | "returnValue": "Cat", 8 | "expectedLog": "options_within_targeting_rule_no_user.txt" 9 | }, 10 | { 11 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 12 | "defaultValue": "default", 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": "Cat", 17 | "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" 18 | }, 19 | { 20 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 21 | "defaultValue": "default", 22 | "user": { 23 | "Identifier": "12345", 24 | "Email": "joe@example.com" 25 | }, 26 | "returnValue": "Cat", 27 | "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" 28 | }, 29 | { 30 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 31 | "defaultValue": "default", 32 | "user": { 33 | "Identifier": "12345", 34 | "Email": "joe@configcat.com" 35 | }, 36 | "returnValue": "Cat", 37 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" 38 | }, 39 | { 40 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 41 | "defaultValue": "default", 42 | "user": { 43 | "Identifier": "12345", 44 | "Email": "joe@configcat.com", 45 | "Country": "US" 46 | }, 47 | "returnValue": "Cat", 48 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /test/evaluation/data/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/options_within_targeting_rule/options_within_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/prerequisite_flag.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 3 | "tests": [ 4 | { 5 | "key": "dependentFeatureWithUserCondition", 6 | "defaultValue": "default", 7 | "returnValue": "Chicken", 8 | "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" 9 | }, 10 | { 11 | "key": "dependentFeature", 12 | "defaultValue": "default", 13 | "returnValue": "Chicken", 14 | "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" 15 | }, 16 | { 17 | "key": "dependentFeatureWithUserCondition2", 18 | "defaultValue": "default", 19 | "returnValue": "Frog", 20 | "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" 21 | }, 22 | { 23 | "key": "dependentFeature", 24 | "defaultValue": "default", 25 | "user": { 26 | "Identifier": "12345", 27 | "Email": "kate@configcat.com", 28 | "Country": "USA" 29 | }, 30 | "returnValue": "Horse", 31 | "expectedLog": "prerequisite_flag.txt" 32 | }, 33 | { 34 | "key": "dependentFeatureMultipleLevels", 35 | "defaultValue": "default", 36 | "returnValue": "Dog", 37 | "expectedLog": "prerequisite_flag_multilevel.txt" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARNING [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 `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", 3 | "tests": [ 4 | { 5 | "key": "featureWithSegmentTargeting", 6 | "defaultValue": false, 7 | "returnValue": false, 8 | "expectedLog": "segment_no_user.txt" 9 | }, 10 | { 11 | "key": "featureWithNegatedSegmentTargetingCleartext", 12 | "defaultValue": false, 13 | "user": { 14 | "Identifier": "12345" 15 | }, 16 | "returnValue": false, 17 | "expectedLog": "segment_no_targeted_attribute.txt" 18 | }, 19 | { 20 | "key": "featureWithSegmentTargeting", 21 | "defaultValue": false, 22 | "user": { 23 | "Identifier": "12345", 24 | "Email": "jane@example.com" 25 | }, 26 | "returnValue": true, 27 | "expectedLog": "segment_matching.txt" 28 | }, 29 | { 30 | "key": "featureWithNegatedSegmentTargeting", 31 | "defaultValue": false, 32 | "user": { 33 | "Identifier": "12345", 34 | "Email": "jane@example.com" 35 | }, 36 | "returnValue": false, 37 | "expectedLog": "segment_no_matching.txt" 38 | }, 39 | { 40 | "key": "featureWithSegmentTargetingMultipleConditions", 41 | "defaultValue": false, 42 | "returnValue": false, 43 | "expectedLog": "segment_no_user_multi_conditions.txt" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/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 | -------------------------------------------------------------------------------- /test/evaluation/data/segment/segment_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/segment/segment_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/segment/segment_no_user_multi_conditions.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 `getValue()` 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 | -------------------------------------------------------------------------------- /test/evaluation/data/semver_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", 3 | "tests": [ 4 | { 5 | "key": "isNotOneOf", 6 | "defaultValue": "default", 7 | "returnValue": "Default", 8 | "expectedLog": "semver_error.txt", 9 | "user": { 10 | "Identifier": "12345", 11 | "Custom1": "wrong_semver" 12 | } 13 | }, 14 | { 15 | "key": "relations", 16 | "defaultValue": "default", 17 | "returnValue": "Default", 18 | "expectedLog": "semver_relations_error.txt", 19 | "user": { 20 | "Identifier": "12345", 21 | "Custom1": "wrong_semver" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/evaluation/data/semver_validation/semver_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/semver_validation/semver_relations_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [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 | WARNING [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 | WARNING [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 | WARNING [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 | WARNING [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 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 3 | "tests": [ 4 | { 5 | "key": "boolDefaultFalse", 6 | "defaultValue": true, 7 | "returnValue": false, 8 | "expectedLog": "off_flag.txt" 9 | }, 10 | { 11 | "key": "boolDefaultTrue", 12 | "defaultValue": false, 13 | "returnValue": true, 14 | "expectedLog": "on_flag.txt" 15 | }, 16 | { 17 | "key": "stringDefaultCat", 18 | "defaultValue": "Default", 19 | "returnValue": "Cat", 20 | "expectedLog": "text_setting.txt" 21 | }, 22 | { 23 | "key": "integerDefaultOne", 24 | "defaultValue": 0, 25 | "returnValue": 1, 26 | "expectedLog": "int_setting.txt" 27 | }, 28 | { 29 | "testName": "double_setting", 30 | "key": "doubleDefaultPi", 31 | "defaultValue": 0.0, 32 | "returnValue": 3.1415, 33 | "expectedLog": "double_setting.txt" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value/double_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'doubleDefaultPi' 2 | Returning '3.1415'. 3 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value/int_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integerDefaultOne' 2 | Returning '1'. 3 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value/off_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultFalse' 2 | Returning 'false'. 3 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value/on_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultTrue' 2 | Returning 'true'. 3 | -------------------------------------------------------------------------------- /test/evaluation/data/simple_value/text_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringDefaultCat' 2 | Returning 'Cat'. 3 | -------------------------------------------------------------------------------- /test/evaluation/evaluation_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/configcat_user.dart'; 2 | 3 | class EvaluationData { 4 | final String key; 5 | 6 | final dynamic defaultValue; 7 | 8 | final dynamic returnValue; 9 | 10 | final String expectedLog; 11 | 12 | final ConfigCatUser? user; 13 | 14 | EvaluationData(this.key, this.defaultValue, this.returnValue, 15 | this.expectedLog, this.user); 16 | 17 | static EvaluationData fromJson(Map json) => EvaluationData( 18 | json['key'] as String, 19 | json['defaultValue'] as dynamic, 20 | json['returnValue'] as dynamic, 21 | json['expectedLog'] as String, 22 | json['user'] == null 23 | ? null 24 | : userFromJson(json['user'] as Map)); 25 | 26 | Map toJson() => { 27 | 'key': key, 28 | 'defaultValue': defaultValue, 29 | 'returnValue': returnValue, 30 | 'expectedLog': expectedLog, 31 | 'user': user 32 | }; 33 | 34 | static ConfigCatUser userFromJson(Map json) => ConfigCatUser( 35 | identifier: json['Identifier'] as String, 36 | email: json['Email'] as String?, 37 | country: json['Country'] as String?, 38 | custom: customsFromJson(json)); 39 | 40 | static Map? customsFromJson(Map json) { 41 | Map? customs = {}; 42 | json.forEach((key, value) { 43 | if (key != 'Identifier' || key != 'Email' || key != 'Country') { 44 | customs[key] = value; 45 | } 46 | }); 47 | return customs; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/evaluation/evaluation_data_set.dart: -------------------------------------------------------------------------------- 1 | import '../evaluation/evaluation_data.dart'; 2 | 3 | class EvaluationDataSet { 4 | final String sdkKey; 5 | final String? jsonOverride; 6 | final List tests; 7 | 8 | EvaluationDataSet(this.sdkKey, this.jsonOverride, this.tests); 9 | 10 | static EvaluationDataSet fromJson( 11 | Map json) => 12 | EvaluationDataSet( 13 | json['sdkKey'] as String, 14 | json['jsonOverride'] as String?, 15 | (json['tests'] as List) 16 | .map((e) => EvaluationData.fromJson(e as Map)) 17 | .toList()); 18 | 19 | Map toJson() => { 20 | 'sdkKey': sdkKey, 21 | 'jsonOverride': jsonOverride, 22 | 'tests': tests 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test/evaluation/evaluation_logger_turn_off_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/configcat_client.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'evaluation_test_logger.dart'; 5 | 6 | void main() { 7 | tearDown(() { 8 | ConfigCatClient.closeAll(); 9 | }); 10 | 11 | test('evaluation log level info', () async { 12 | // Arrange 13 | final testLogger = EvaluationTestLogger(); 14 | 15 | final client = ConfigCatClient.get( 16 | sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 17 | options: ConfigCatOptions( 18 | logger: 19 | ConfigCatLogger(internalLogger: testLogger, level: LogLevel.info), 20 | pollingMode: PollingMode.manualPoll(), 21 | )); 22 | 23 | await client.forceRefresh(); 24 | 25 | // Act 26 | final String value = await client.getValue( 27 | key: 'stringContainsDogDefaultCat', defaultValue: 'default'); 28 | 29 | // Assert 30 | expect(value, equals('Cat')); 31 | 32 | var logList = testLogger.getLogList(); 33 | expect(2, equals(logList.length)); 34 | expect(LogLevel.warning, equals(logList[0].logLevel)); 35 | expect(LogLevel.info, equals(logList[1].logLevel)); 36 | }); 37 | 38 | test('evaluation log level warning', () async { 39 | // Arrange 40 | final testLogger = EvaluationTestLogger(); 41 | 42 | final client = ConfigCatClient.get( 43 | sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 44 | options: ConfigCatOptions( 45 | logger: ConfigCatLogger( 46 | internalLogger: testLogger, level: LogLevel.warning), 47 | pollingMode: PollingMode.manualPoll(), 48 | )); 49 | 50 | await client.forceRefresh(); 51 | 52 | // Act 53 | final String value = await client.getValue( 54 | key: 'stringContainsDogDefaultCat', defaultValue: 'default'); 55 | 56 | // Assert 57 | expect(value, equals('Cat')); 58 | 59 | var logList = testLogger.getLogList(); 60 | expect(1, equals(logList.length)); 61 | expect(LogLevel.warning, equals(logList[0].logLevel)); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/evaluation/evaluation_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:configcat_client/configcat_client.dart'; 5 | 6 | import 'package:test/test.dart'; 7 | 8 | import '../http_adapter.dart'; 9 | import 'evaluation_data_set.dart'; 10 | import 'evaluation_test_logger.dart'; 11 | import '../helpers.dart'; 12 | import 'evaluation_data.dart'; 13 | 14 | void main() { 15 | final testData = { 16 | "simple_value", 17 | "1_targeting_rule", 18 | "2_targeting_rules", 19 | "and_rules", 20 | "semver_validation", 21 | "epoch_date_validation", 22 | "number_validation", 23 | "comparators", 24 | "prerequisite_flag", 25 | "segment", 26 | "options_after_targeting_rule", 27 | "options_based_on_user_id", 28 | "options_based_on_custom_attr", 29 | "options_within_targeting_rule", 30 | "list_truncation" 31 | }; 32 | 33 | tearDown(() { 34 | ConfigCatClient.closeAll(); 35 | }); 36 | 37 | for (var element in testData) { 38 | test(element, () async { 39 | await _runTest(element); 40 | }); 41 | } 42 | } 43 | 44 | Future _runTest(String testCaseName) async { 45 | const testSetPath = "test/evaluation/data/"; 46 | const jsonExt = ".json"; 47 | 48 | final testSet = 49 | await File(testSetPath + testCaseName + jsonExt).readAsString(); 50 | EvaluationDataSet dataSet = EvaluationDataSet.fromJson(jsonDecode(testSet)); 51 | 52 | String sdkKey = dataSet.sdkKey; 53 | if (sdkKey.isEmpty) { 54 | sdkKey = "configcat-sdk-test-key/0000000000000000000000"; //DUMMY TEST KEY 55 | } 56 | final testLogger = EvaluationTestLogger(); 57 | 58 | final client = ConfigCatClient.get( 59 | sdkKey: sdkKey, 60 | options: ConfigCatOptions( 61 | logger: 62 | ConfigCatLogger(internalLogger: testLogger, level: LogLevel.info), 63 | pollingMode: PollingMode.manualPoll(), 64 | )); 65 | 66 | var jsonOverride = dataSet.jsonOverride; 67 | 68 | if (jsonOverride != null && jsonOverride.isNotEmpty) { 69 | var jsonOverrideFile = 70 | await File("$testSetPath$testCaseName/$jsonOverride").readAsString(); 71 | final decoded = jsonDecode(jsonOverrideFile); 72 | Config config = Config.fromJson(decoded); 73 | final testAdapter = HttpTestAdapter(client.httpClient); 74 | testAdapter.enqueueResponse(getPath(sdkKey: sdkKey), 200, config); 75 | } 76 | 77 | await client.forceRefresh(); 78 | 79 | List errors = List.empty(growable: true); 80 | 81 | for (EvaluationData test in dataSet.tests) { 82 | var configCatUser = test.user; 83 | var value = await client.getValue( 84 | key: test.key, defaultValue: test.defaultValue, user: configCatUser); 85 | 86 | if (test.returnValue != value) { 87 | errors.add( 88 | "Return value mismatch for test: $testCaseName Test Key: ${test.key} Expected: ${test.returnValue}, Result: $value \n"); 89 | } 90 | 91 | final expectedLog = 92 | (await File("$testSetPath$testCaseName/${test.expectedLog}") 93 | .readAsString()) 94 | .replaceAll("\r\n", "\n"); 95 | 96 | StringBuffer logResultBuffer = StringBuffer(); 97 | 98 | for (LogEvent log in testLogger.getLogList()) { 99 | logResultBuffer.write(log.message.replaceAll("\r\n", "\n")); 100 | logResultBuffer.write("\n"); 101 | } 102 | 103 | String logResult = logResultBuffer.toString(); 104 | 105 | if (expectedLog != logResult) { 106 | errors.add( 107 | "Log mismatch for test: $testCaseName Test Key: ${test.key} Expected:\n$expectedLog\nResult:\n$logResult\n"); 108 | } 109 | 110 | testLogger.reset(); 111 | } 112 | 113 | if (errors.isNotEmpty) { 114 | stderr.writeln("\n == ERRORS == \n"); 115 | for (var element in errors) { 116 | stderr.writeln(element); 117 | } 118 | fail("Errors found: ${errors.length}"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test/evaluation/evaluation_test_logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/src/log/logger.dart'; 2 | 3 | class EvaluationTestLogger implements Logger { 4 | List _logs = List.empty(growable: true); 5 | 6 | final Map _levelMap = { 7 | LogLevel.debug: "DEBUG", 8 | LogLevel.warning: "WARNING", 9 | LogLevel.info: "INFO", 10 | LogLevel.error: "ERROR", 11 | }; 12 | 13 | @override 14 | void close() {} 15 | 16 | @override 17 | void debug(message, [error, StackTrace? stackTrace]) { 18 | _addLog(LogLevel.debug, message, error, stackTrace); 19 | } 20 | 21 | @override 22 | void error(message, [error, StackTrace? stackTrace]) { 23 | _addLog(LogLevel.error, message, error, stackTrace); 24 | } 25 | 26 | @override 27 | void info(message, [error, StackTrace? stackTrace]) { 28 | _addLog(LogLevel.info, message, error, stackTrace); 29 | } 30 | 31 | @override 32 | void warning(message, [error, StackTrace? stackTrace]) { 33 | _addLog(LogLevel.warning, message, error, stackTrace); 34 | } 35 | 36 | void _addLog(LogLevel logLevel, String message, 37 | [error, StackTrace? stackTrace]) { 38 | _logs.add(LogEvent( 39 | logLevel, _enrichMessage(logLevel, message, error, stackTrace))); 40 | } 41 | 42 | List getLogList() { 43 | return _logs; 44 | } 45 | 46 | String _enrichMessage(LogLevel logLevel, String message, 47 | [error, StackTrace? stackTrace]) { 48 | var err = error != null ? ' ERROR: $error' : ''; 49 | return "${_levelMap[logLevel]} $message$err"; 50 | } 51 | 52 | void reset() { 53 | _logs = List.empty(growable: true); 54 | } 55 | } 56 | 57 | class LogEvent { 58 | final LogLevel logLevel; 59 | final String message; 60 | 61 | LogEvent(this.logLevel, this.message); 62 | } 63 | -------------------------------------------------------------------------------- /test/fixtures/test_circulardependency.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0, 5 | "s": "test-salt" 6 | }, 7 | "f": { 8 | "key1": { 9 | "t": 1, 10 | "v": { "s": "key1-value" }, 11 | "r": [ 12 | { 13 | "c": [ 14 | { 15 | "p": { 16 | "f": "key1", 17 | "c": 0, 18 | "v": { "s": "key1-prereq" } 19 | } 20 | } 21 | ], 22 | "s": { 23 | "v": { 24 | "s": "key1-prereq" 25 | }, 26 | "i" : "test-variation-id" 27 | } 28 | } 29 | ], 30 | "i" : "test-variation-id" 31 | }, 32 | "key2": { 33 | "t": 1, 34 | "v": { "s": "key2-value" }, 35 | "r": [ 36 | { 37 | "c": [ 38 | { 39 | "p": { 40 | "f": "key3", 41 | "c": 0, 42 | "v": { 43 | "s": "key3-prereq" 44 | } 45 | } 46 | } 47 | ], 48 | "s": { 49 | "v": { 50 | "s": "key2-prereq" 51 | }, 52 | "i" : "test-variation-id" 53 | } 54 | } 55 | ], 56 | "i" : "test-variation-id" 57 | }, 58 | "key3": { 59 | "t": 1, 60 | "v": { "s": "key3-value" }, 61 | "r": [ 62 | { 63 | "c": [ 64 | { 65 | "p": { 66 | "f": "key2", 67 | "c": 0, 68 | "v": { 69 | "s": "key2-prereq" 70 | } 71 | } 72 | } 73 | ], 74 | "s": { 75 | "v": { 76 | "s": "key3-prereq" 77 | }, 78 | "i" : "test-variation-id" 79 | } 80 | } 81 | ], 82 | "i" : "test-variation-id" 83 | }, 84 | "key4": { 85 | "t": 1, 86 | "v": { "s": "key4-value" }, 87 | "r": [ 88 | { 89 | "c": [ 90 | { 91 | "p": { 92 | "f": "key3", 93 | "c": 0, 94 | "v": { "s": "key3-prereq" } 95 | } 96 | } 97 | ], 98 | "s": { 99 | "v": { 100 | "s": "key4-prereq" 101 | }, 102 | "i" : "test-variation-id" 103 | } 104 | } 105 | ], 106 | "i" : "test-variation-id" 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:configcat_client/configcat_client.dart'; 4 | import 'package:configcat_client/src/fetch/config_fetcher.dart'; 5 | import 'package:configcat_client/src/constants.dart'; 6 | import 'package:configcat_client/src/entry.dart'; 7 | import 'package:sprintf/sprintf.dart'; 8 | 9 | const urlTemplate = '%s/configuration-files/%s/$configJsonName'; 10 | const testSdkKey = 11 | 'configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012'; 12 | const etag = 'test-etag'; 13 | 14 | Config createTestConfig(Map map) { 15 | return Config(Preferences(ConfigFetcher.globalBaseUrl, 0, "test-salt"), 16 | map.map((key, value) => MapEntry(key, value.toSetting())), List.empty()); 17 | } 18 | 19 | Config createTestConfigWithRules() { 20 | return Config( 21 | Preferences(ConfigFetcher.globalBaseUrl, 0, "test-salt"), 22 | { 23 | 'key1': Setting( 24 | SettingValue(null, "def", null, null), //default flag value 25 | 1, 26 | [], 27 | [ 28 | TargetingRule( 29 | [ 30 | Condition( 31 | UserCondition( 32 | "Identifier", 2, null, null, ["@test1.com"]), 33 | null, 34 | null) 35 | ], 36 | [], 37 | ServedValue( 38 | SettingValue(null, "fake1", null, null), "variationId1")), 39 | TargetingRule( 40 | [ 41 | Condition( 42 | UserCondition( 43 | "Identifier", 2, null, null, ["@test2.com"]), 44 | null, 45 | null) 46 | ], 47 | [], 48 | ServedValue( 49 | SettingValue(null, "fake2", null, null), "variationId2")), 50 | ], 51 | 'defaultId', // flag def variationID 52 | "") //percentage attribute 53 | }, 54 | List.empty()); 55 | } 56 | 57 | Entry createTestEntry(Map map) { 58 | Config config = createTestConfig(map); 59 | return Entry(jsonEncode(config.toJson()), config, map[0].toString(), 60 | DateTime.now().toUtc()); 61 | } 62 | 63 | Entry createTestEntryWithTime(Map map, DateTime time) { 64 | Config config = createTestConfig(map); 65 | return Entry(jsonEncode(config.toJson()), config, map[0].toString(), time); 66 | } 67 | 68 | Entry createTestEntryWithETag(Map map, String etag) { 69 | Config config = createTestConfig(map); 70 | return Entry( 71 | jsonEncode(config.toJson()), config, etag, DateTime.now().toUtc()); 72 | } 73 | 74 | String getPath({String sdkKey = testSdkKey}) { 75 | return sprintf(urlTemplate, [ConfigFetcher.globalBaseUrl, sdkKey]); 76 | } 77 | 78 | Future until( 79 | Future Function() predicate, Duration timeout) async { 80 | final start = DateTime.now().toUtc(); 81 | while (!await predicate()) { 82 | await Future.delayed(const Duration(milliseconds: 100)); 83 | if (DateTime.now().toUtc().isAfter(start.add(timeout))) { 84 | throw Exception("Test await timed out."); 85 | } 86 | } 87 | return DateTime.now().toUtc().difference(start); 88 | } 89 | 90 | class CustomCache implements ConfigCatCache { 91 | late String _value; 92 | 93 | CustomCache(String initial) { 94 | _value = initial; 95 | } 96 | 97 | @override 98 | Future read(String key) { 99 | return Future.value(_value); 100 | } 101 | 102 | @override 103 | Future write(String key, String value) { 104 | _value = value; 105 | return Future.value(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/http_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:dio/dio.dart'; 7 | 8 | class HttpTestAdapter implements HttpClientAdapter { 9 | bool _closed = false; 10 | final List _capturedRequests = []; 11 | final Map> _responseQueue = {}; 12 | List get capturedRequests { 13 | return List.unmodifiable(_capturedRequests); 14 | } 15 | 16 | HttpTestAdapter(Dio dio) { 17 | dio.httpClientAdapter = this; 18 | } 19 | 20 | @override 21 | void close({bool force = false}) { 22 | _responseQueue.clear(); 23 | _capturedRequests.clear(); 24 | _closed = true; 25 | } 26 | 27 | @override 28 | Future fetch(RequestOptions options, 29 | Stream? requestStream, Future? cancelFuture) async { 30 | if (_closed) { 31 | throw HttpException("Test adapter closed."); 32 | } 33 | 34 | _capturedRequests.add(options); 35 | final next = _next(options.path); 36 | if (next == null) { 37 | throw HttpException("Response not found for path: ${options.path}."); 38 | } 39 | if (next.delay != null) { 40 | await Future.delayed(next.delay!); 41 | } 42 | if (next.exception != null) { 43 | throw next.exception!; 44 | } 45 | 46 | return ResponseBody( 47 | Stream.fromIterable(utf8 48 | .encode(next.body) 49 | .map((e) => Uint8List.fromList([e])) 50 | .toList()), 51 | next.statusCode, 52 | headers: next.headers?.map((key, value) => MapEntry(key, [value]))); 53 | } 54 | 55 | void enqueueResponse(String path, int statusCode, dynamic body, 56 | {Map? headers, Duration? delay, Exception? exception}) { 57 | final response = _Response(statusCode, 58 | body: jsonEncode(body), 59 | headers: headers, 60 | delay: delay, 61 | exception: exception); 62 | if (_responseQueue.containsKey(path)) { 63 | _responseQueue[path]?.add(response); 64 | } else { 65 | final queue = Queue<_Response>(); 66 | queue.add(response); 67 | _responseQueue.addAll({path: queue}); 68 | } 69 | } 70 | 71 | _Response? _next(String path) { 72 | final nextEntry = _responseQueue[path]; 73 | if (nextEntry == null) { 74 | return null; 75 | } 76 | return nextEntry.length == 1 ? nextEntry.first : nextEntry.removeFirst(); 77 | } 78 | } 79 | 80 | class _Response { 81 | final int statusCode; 82 | final String body; 83 | final Map? headers; 84 | final Duration? delay; 85 | final Exception? exception; 86 | 87 | _Response(this.statusCode, 88 | {this.body = "", 89 | Map? headers, 90 | this.delay, 91 | this.exception}) 92 | : headers = headers ?? {}; 93 | } 94 | -------------------------------------------------------------------------------- /test/logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/configcat_client.dart'; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:test/scaffolding.dart'; 5 | 6 | import 'logger_test.mocks.dart'; 7 | 8 | @GenerateMocks([Logger]) 9 | void main() { 10 | test('logger debug level tests', () { 11 | // Arrange 12 | final internal = MockLogger(); 13 | final logger = 14 | ConfigCatLogger(internalLogger: internal, level: LogLevel.debug); 15 | 16 | // Act 17 | logger.debug('debug'); 18 | logger.info(5000, 'info'); 19 | logger.warning(3000, 'warning'); 20 | logger.error(1000, 'error'); 21 | 22 | // Assert 23 | verify(internal.debug('[0] debug')).called(1); 24 | verify(internal.info('[5000] info')).called(1); 25 | verify(internal.warning('[3000] warning')).called(1); 26 | verify(internal.error('[1000] error')).called(1); 27 | }); 28 | 29 | test('logger info level tests', () { 30 | // Arrange 31 | final internal = MockLogger(); 32 | final logger = 33 | ConfigCatLogger(internalLogger: internal, level: LogLevel.info); 34 | 35 | // Act 36 | logger.debug('debug'); 37 | logger.info(5000, 'info'); 38 | logger.warning(3000, 'warning'); 39 | logger.error(1000, 'error'); 40 | 41 | // Assert 42 | verifyNever(internal.debug('[0] debug')); 43 | verify(internal.info('[5000] info')).called(1); 44 | verify(internal.warning('[3000] warning')).called(1); 45 | verify(internal.error('[1000] error')).called(1); 46 | }); 47 | 48 | test('logger warning level tests', () { 49 | // Arrange 50 | final internal = MockLogger(); 51 | final logger = 52 | ConfigCatLogger(internalLogger: internal, level: LogLevel.warning); 53 | 54 | // Act 55 | logger.debug('debug'); 56 | logger.info(5000, 'info'); 57 | logger.warning(3000, 'warning'); 58 | logger.error(1000, 'error'); 59 | 60 | // Assert 61 | verifyNever(internal.debug('[0] debug')); 62 | verifyNever(internal.info('[5000] info')); 63 | verify(internal.warning('[3000] warning')).called(1); 64 | verify(internal.error('[1000] error')).called(1); 65 | }); 66 | 67 | test('logger error level tests', () { 68 | // Arrange 69 | final internal = MockLogger(); 70 | final logger = 71 | ConfigCatLogger(internalLogger: internal, level: LogLevel.error); 72 | 73 | // Act 74 | logger.debug('debug'); 75 | logger.info(5000, 'info'); 76 | logger.warning(3000, 'warning'); 77 | logger.error(1000, 'error'); 78 | 79 | // Assert 80 | verifyNever(internal.debug('[0] debug')); 81 | verifyNever(internal.info('[5000] info')); 82 | verifyNever(internal.warning('[3000] warning')); 83 | verify(internal.error('[1000] error')).called(1); 84 | }); 85 | 86 | test('logger nothing level tests', () { 87 | // Arrange 88 | final internal = MockLogger(); 89 | final logger = 90 | ConfigCatLogger(internalLogger: internal, level: LogLevel.nothing); 91 | 92 | // Act 93 | logger.debug('debug'); 94 | logger.info(5000, 'info'); 95 | logger.warning(3000, 'warning'); 96 | logger.error(1000, 'error'); 97 | 98 | // Assert 99 | verifyNever(internal.debug('ConfigCat - [0] debug')); 100 | verifyNever(internal.info('ConfigCat - [5000] info')); 101 | verifyNever(internal.warning('ConfigCat - [3000] warning')); 102 | verifyNever(internal.error('ConfigCat - [1000] error')); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /test/logger_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.4 from annotations 2 | // in configcat_client/test/logger_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'package:configcat_client/src/log/logger.dart' as _i2; 7 | import 'package:mockito/mockito.dart' as _i1; 8 | 9 | // ignore_for_file: type=lint 10 | // ignore_for_file: avoid_redundant_argument_values 11 | // ignore_for_file: avoid_setters_without_getters 12 | // ignore_for_file: comment_references 13 | // ignore_for_file: deprecated_member_use 14 | // ignore_for_file: deprecated_member_use_from_same_package 15 | // ignore_for_file: implementation_imports 16 | // ignore_for_file: invalid_use_of_visible_for_testing_member 17 | // ignore_for_file: prefer_const_constructors 18 | // ignore_for_file: unnecessary_parenthesis 19 | // ignore_for_file: camel_case_types 20 | // ignore_for_file: subtype_of_sealed_class 21 | 22 | /// A class which mocks [Logger]. 23 | /// 24 | /// See the documentation for Mockito's code generation for more information. 25 | class MockLogger extends _i1.Mock implements _i2.Logger { 26 | MockLogger() { 27 | _i1.throwOnMissingStub(this); 28 | } 29 | 30 | @override 31 | void debug( 32 | dynamic message, [ 33 | dynamic error, 34 | StackTrace? stackTrace, 35 | ]) => 36 | super.noSuchMethod( 37 | Invocation.method( 38 | #debug, 39 | [ 40 | message, 41 | error, 42 | stackTrace, 43 | ], 44 | ), 45 | returnValueForMissingStub: null, 46 | ); 47 | 48 | @override 49 | void info( 50 | dynamic message, [ 51 | dynamic error, 52 | StackTrace? stackTrace, 53 | ]) => 54 | super.noSuchMethod( 55 | Invocation.method( 56 | #info, 57 | [ 58 | message, 59 | error, 60 | stackTrace, 61 | ], 62 | ), 63 | returnValueForMissingStub: null, 64 | ); 65 | 66 | @override 67 | void warning( 68 | dynamic message, [ 69 | dynamic error, 70 | StackTrace? stackTrace, 71 | ]) => 72 | super.noSuchMethod( 73 | Invocation.method( 74 | #warning, 75 | [ 76 | message, 77 | error, 78 | stackTrace, 79 | ], 80 | ), 81 | returnValueForMissingStub: null, 82 | ); 83 | 84 | @override 85 | void error( 86 | dynamic message, [ 87 | dynamic error, 88 | StackTrace? stackTrace, 89 | ]) => 90 | super.noSuchMethod( 91 | Invocation.method( 92 | #error, 93 | [ 94 | message, 95 | error, 96 | stackTrace, 97 | ], 98 | ), 99 | returnValueForMissingStub: null, 100 | ); 101 | 102 | @override 103 | void close() => super.noSuchMethod( 104 | Invocation.method( 105 | #close, 106 | [], 107 | ), 108 | returnValueForMissingStub: null, 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /test/matrix/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 | -------------------------------------------------------------------------------- /test/matrix/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 -------------------------------------------------------------------------------- /test/matrix/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 | -------------------------------------------------------------------------------- /test/matrix/testmatrix_segment.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 | -------------------------------------------------------------------------------- /test/matrix/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 | -------------------------------------------------------------------------------- /test/matrix/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 -------------------------------------------------------------------------------- /test/matrix/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 | -------------------------------------------------------------------------------- /test/matrix/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 | -------------------------------------------------------------------------------- /test/override_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:configcat_client/configcat_client.dart'; 2 | import 'package:configcat_client/src/fetch/config_fetcher.dart'; 3 | import 'package:sprintf/sprintf.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'helpers.dart'; 7 | import 'http_adapter.dart'; 8 | 9 | void main() { 10 | tearDown(() { 11 | ConfigCatClient.closeAll(); 12 | }); 13 | 14 | test('local only', () async { 15 | // Arrange 16 | final client = ConfigCatClient.get( 17 | sdkKey: testSdkKey, 18 | options: ConfigCatOptions( 19 | override: FlagOverrides( 20 | dataSource: OverrideDataSource.map( 21 | {'enabled': true, 'local-only': true}), 22 | behaviour: OverrideBehaviour.localOnly))); 23 | final testAdapter = HttpTestAdapter(client.httpClient); 24 | final body = createTestConfig({'enabled': false, 'remote': 'rem'}).toJson(); 25 | final path = 26 | sprintf(urlTemplate, [ConfigFetcher.globalBaseUrl, 'localhost']); 27 | testAdapter.enqueueResponse(path, 200, body); 28 | 29 | // Act 30 | final found = await client.getValue(key: 'enabled', defaultValue: false); 31 | final localOnly = 32 | await client.getValue(key: 'local-only', defaultValue: false); 33 | final notFound = 34 | await client.getValue(key: 'remote', defaultValue: null); 35 | 36 | // Assert 37 | expect(found, isTrue); 38 | expect(localOnly, isTrue); 39 | expect(notFound, isNull); 40 | }); 41 | 42 | test('local over remote', () async { 43 | // Arrange 44 | final client = ConfigCatClient.get( 45 | sdkKey: testSdkKey, 46 | options: ConfigCatOptions( 47 | override: FlagOverrides( 48 | dataSource: OverrideDataSource.map( 49 | {'enabled': true, 'local-only': true}), 50 | behaviour: OverrideBehaviour.localOverRemote))); 51 | final testAdapter = HttpTestAdapter(client.httpClient); 52 | final body = createTestConfig({'enabled': false, 'remote': 'rem'}).toJson(); 53 | final path = 54 | sprintf(urlTemplate, [ConfigFetcher.globalBaseUrl, testSdkKey]); 55 | testAdapter.enqueueResponse(path, 200, body); 56 | 57 | // Act 58 | final enabled = await client.getValue(key: 'enabled', defaultValue: false); 59 | final localOnly = 60 | await client.getValue(key: 'local-only', defaultValue: false); 61 | final remote = await client.getValue(key: 'remote', defaultValue: ''); 62 | 63 | // Assert 64 | expect(enabled, isTrue); 65 | expect(localOnly, isTrue); 66 | expect(remote, equals('rem')); 67 | }); 68 | 69 | test('remote over local', () async { 70 | // Arrange 71 | final client = ConfigCatClient.get( 72 | sdkKey: testSdkKey, 73 | options: ConfigCatOptions( 74 | override: FlagOverrides( 75 | dataSource: OverrideDataSource.map( 76 | {'enabled': true, 'local-only': true}), 77 | behaviour: OverrideBehaviour.remoteOverLocal))); 78 | final testAdapter = HttpTestAdapter(client.httpClient); 79 | final body = createTestConfig({'enabled': false, 'remote': 'rem'}).toJson(); 80 | final path = 81 | sprintf(urlTemplate, [ConfigFetcher.globalBaseUrl, testSdkKey]); 82 | testAdapter.enqueueResponse(path, 200, body); 83 | 84 | // Act 85 | final enabled = await client.getValue(key: 'enabled', defaultValue: true); 86 | final localOnly = 87 | await client.getValue(key: 'local-only', defaultValue: false); 88 | final remote = await client.getValue(key: 'remote', defaultValue: ''); 89 | 90 | // Assert 91 | expect(enabled, isFalse); 92 | expect(localOnly, isTrue); 93 | expect(remote, equals('rem')); 94 | }); 95 | } 96 | --------------------------------------------------------------------------------