From ccf321cc77f20e7797613053a6a259680e1ac9cb Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Thu, 21 Mar 2024 11:00:50 +0100 Subject: [PATCH] Road to configV6 (#41) * Road to configV6 * Update ConfigFetcher.swift * Fix warnings * Update project.pbxproj * SDK key validation * Update RolloutEvaluator.swift * Small fixes * Update doc comments * Add privacy manifest * Update swift-ci.yml * Update swift-ci.yml * Update Config.swift * Update swift-ci.yml * Update swift-ci.yml * Try to run tests on watchos * Try to fix watchos tests * Update project.pbxproj * Update doc comments on enums * Enable visionOS support * Update swift-ci.yml * Update README.md * Update swift-ci.yml * Update README.md * Update README.md * Update README.md * Update Package.swift * Update Package.swift * Update RolloutEvaluator.swift * Update RolloutEvaluator.swift * Minor fixes * Fix coverage * Fix coverage * Update README.md * PR comments * Fix namings * Clean up duplicated functionality * Update README.md * Update README.md * Update FlagEvaluator.swift * PR fixes * Centralize JSON handling * PR comments * Update ConfigCatSnapshot.swift --- .github/workflows/swift-ci.yml | 55 +- .slather.yml | 4 +- ConfigCat.podspec | 14 +- ConfigCat.xcconfig | 31 +- ConfigCat.xcodeproj/project.pbxproj | 162 ++- Package.swift | 9 +- README.md | 79 +- Sources/ConfigCat/Config.swift | 953 ++++++++++++--- Sources/ConfigCat/ConfigCatClient.swift | 162 ++- .../ConfigCat/ConfigCatClientProtocol.swift | 40 +- Sources/ConfigCat/ConfigCatOptions.swift | 23 +- Sources/ConfigCat/ConfigCatSnapshot.swift | 10 +- Sources/ConfigCat/ConfigCatUser.swift | 108 +- Sources/ConfigCat/ConfigFetcher.swift | 28 +- Sources/ConfigCat/ConfigService.swift | 75 +- Sources/ConfigCat/EvaluationDetails.swift | 47 +- Sources/ConfigCat/EvaluationLogger.swift | 66 ++ Sources/ConfigCat/Extensions.swift | 26 +- Sources/ConfigCat/FlagEvaluator.swift | 84 +- .../ConfigCat/LocalDictionaryDataSource.swift | 2 +- Sources/ConfigCat/Log.swift | 95 +- .../ConfigCat/Resources/PrivacyInfo.xcprivacy | 23 + Sources/ConfigCat/RolloutEvaluator.swift | 1018 ++++++++++++----- Sources/ConfigCat/Utils.swift | 65 +- Tests/ConfigCatTests/AsyncAwaitTests.swift | 32 +- Tests/ConfigCatTests/AutoPollingTests.swift | 78 +- Tests/ConfigCatTests/CacheTest.swift | 6 +- .../ConfigCatClientIntegrationTests.swift | 16 +- .../ConfigCatTests/ConfigCatClientTests.swift | 151 ++- Tests/ConfigCatTests/ConfigFetcherTests.swift | 14 +- .../ConfigCatTests/DataGovernanceTests.swift | 4 +- .../EvaluationDetailsExtensionTests.swift | 16 +- Tests/ConfigCatTests/EvaluationLogTests.swift | 197 ++++ Tests/ConfigCatTests/EvaluationTests.swift | 460 ++++++++ Tests/ConfigCatTests/Helpers.swift | 141 ++- Tests/ConfigCatTests/LazyLoadingTests.swift | 54 +- Tests/ConfigCatTests/LocalTests.swift | 6 +- Tests/ConfigCatTests/ManualPollingTests.swift | 44 +- .../evaluationlog/1_targeting_rule.json | 41 + .../1_rule_matching_targeted_attribute.txt | 4 + .../1_rule_no_targeted_attribute.txt | 6 + .../1_targeting_rule/1_rule_no_user.txt | 6 + ...1_rule_not_matching_targeted_attribute.txt | 4 + .../evaluationlog/2_targeting_rules.json | 41 + .../2_rules_matching_targeted_attribute.txt | 7 + .../2_rules_no_targeted_attribute.txt | 9 + .../2_targeting_rules/2_rules_no_user.txt | 8 + ..._rules_not_matching_targeted_attribute.txt | 7 + .../_overrides/test_list_truncation.json | 83 ++ .../Resources/evaluationlog/and_rules.json | 22 + .../and_rules/and_rules_no_user.txt | 7 + .../and_rules/and_rules_user.txt | 7 + .../Resources/evaluationlog/comparators.json | 20 + .../evaluationlog/comparators/allinone.txt | 57 + .../evaluationlog/epoch_date_validation.json | 16 + .../epoch_date_validation/date_error.txt | 7 + .../evaluationlog/list_truncation.json | 14 + .../list_truncation/list_truncation.txt | 7 + .../evaluationlog/number_validation.json | 16 + .../number_validation/number_error.txt | 6 + .../options_after_targeting_rule.json | 41 + ...eting_rule_matching_targeted_attribute.txt | 4 + ...r_targeting_rule_no_targeted_attribute.txt | 9 + .../options_after_targeting_rule_no_user.txt | 7 + ...g_rule_not_matching_targeted_attribute.txt | 7 + .../options_based_on_custom_attr.json | 31 + .../matching_options_custom_attribute.txt | 5 + .../no_options_custom_attribute.txt | 4 + .../options_custom_attribute_no_user.txt | 4 + .../options_based_on_user_id.json | 21 + .../options_user_attribute_no_user.txt | 4 + .../options_user_attribute_user.txt | 5 + .../options_within_targeting_rule.json | 52 + ...argeted_attribute_no_options_attribute.txt | 7 + ...g_targeted_attribute_options_attribute.txt | 7 + ...n_targeting_rule_no_targeted_attribute.txt | 6 + .../options_within_targeting_rule_no_user.txt | 6 + ...g_rule_not_matching_targeted_attribute.txt | 4 + .../evaluationlog/prerequisite_flag.json | 41 + .../prerequisite_flag/prerequisite_flag.txt | 32 + .../prerequisite_flag_multilevel.txt | 24 + ...erequisite_flag_no_user_needed_by_both.txt | 38 + ...rerequisite_flag_no_user_needed_by_dep.txt | 15 + ...equisite_flag_no_user_needed_by_prereq.txt | 18 + .../Resources/evaluationlog/segment.json | 47 + .../segment/segment_matching.txt | 11 + .../segment/segment_no_matching.txt | 11 + .../segment/segment_no_targeted_attribute.txt | 13 + .../evaluationlog/segment/segment_no_user.txt | 6 + .../segment_no_user_multi_conditions.txt | 7 + .../evaluationlog/semver_validation.json | 26 + .../semver_validation/semver_error.txt | 9 + .../semver_relations_error.txt | 18 + .../Resources/evaluationlog/simple_value.json | 37 + .../simple_value/double_setting.txt | 2 + .../simple_value/int_setting.txt | 2 + .../evaluationlog/simple_value/off_flag.txt | 2 + .../evaluationlog/simple_value/on_flag.txt | 2 + .../simple_value/text_setting.txt | 2 + .../json/comparison_attribute_conversion.json | 789 +++++++++++++ .../json/comparison_attribute_trimming.json | 985 ++++++++++++++++ .../json/comparison_value_trimming.json | 777 +++++++++++++ .../json/test_circulardependency_v6.json | 80 ++ .../json/test_override_flagdependency_v6.json | 44 + Tests/ConfigCatTests/Resources/testmatrix.csv | 460 ++++---- .../Resources/testmatrix_and_or.csv | 15 + .../Resources/testmatrix_comparators_v6.csv | 24 + .../testmatrix_prerequisite_flag.csv | 5 + .../Resources/testmatrix_segments.csv | 6 + .../Resources/testmatrix_segments_old.csv | 6 + .../Resources/testmatrix_unicode.csv | 14 + ....swift => RolloutIntegrationV1Tests.swift} | 93 +- .../RolloutIntegrationV2Tests.swift | 223 ++++ Tests/ConfigCatTests/SnapshotTests.swift | 10 +- Tests/ConfigCatTests/VariationIdTests.swift | 90 +- sonar-project.properties | 16 + 116 files changed, 7693 insertions(+), 1324 deletions(-) create mode 100644 Sources/ConfigCat/EvaluationLogger.swift create mode 100644 Sources/ConfigCat/Resources/PrivacyInfo.xcprivacy create mode 100644 Tests/ConfigCatTests/EvaluationLogTests.swift create mode 100644 Tests/ConfigCatTests/EvaluationTests.swift create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/_overrides/test_list_truncation.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/and_rules.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/comparators.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/comparators/allinone.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation/date_error.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/list_truncation.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/list_truncation/list_truncation.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/number_validation.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/number_validation/number_error.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_matching.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_matching.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_targeted_attribute.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user_multi_conditions.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/semver_validation.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_error.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_relations_error.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value.json create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value/double_setting.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value/int_setting.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value/off_flag.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value/on_flag.txt create mode 100644 Tests/ConfigCatTests/Resources/evaluationlog/simple_value/text_setting.txt create mode 100644 Tests/ConfigCatTests/Resources/json/comparison_attribute_conversion.json create mode 100644 Tests/ConfigCatTests/Resources/json/comparison_attribute_trimming.json create mode 100644 Tests/ConfigCatTests/Resources/json/comparison_value_trimming.json create mode 100644 Tests/ConfigCatTests/Resources/json/test_circulardependency_v6.json create mode 100644 Tests/ConfigCatTests/Resources/json/test_override_flagdependency_v6.json create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_and_or.csv create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_comparators_v6.csv create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_prerequisite_flag.csv create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_segments.csv create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_segments_old.csv create mode 100644 Tests/ConfigCatTests/Resources/testmatrix_unicode.csv rename Tests/ConfigCatTests/{RolloutIntegrationTests.swift => RolloutIntegrationV1Tests.swift} (57%) create mode 100644 Tests/ConfigCatTests/RolloutIntegrationV2Tests.swift create mode 100644 sonar-project.properties diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml index c7bd8fc..09b2b54 100644 --- a/.github/workflows/swift-ci.yml +++ b/.github/workflows/swift-ci.yml @@ -5,6 +5,8 @@ on: - cron: '0 0 * * *' push: branches: [ master ] + paths-ignore: + - '**.md' tags: [ '[0-9]+.[0-9]+.[0-9]+' ] pull_request: branches: [ master ] @@ -12,27 +14,29 @@ on: workflow_dispatch: env: - DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_15.2.app jobs: test-ios: - runs-on: macos-latest + runs-on: macos-14 strategy: matrix: include: - - sdk: iphonesimulator15.2 - destination: OS=15.2,name=iPhone 8 + - destination: platform=iOS Simulator,name=iPhone 8 platform: iphone - - sdk: appletvsimulator15.2 - destination: OS=15.2,name=Apple TV + - destination: platform=tvOS Simulator,name=Apple TV platform: appletv + - destination: platform=watchOS Simulator,name=Apple Watch Series 8 (45mm) + platform: watch + - destination: platform=visionOS Simulator,name=Apple Vision Pro + platform: watch steps: - uses: actions/checkout@v4 - name: Tests run: | set -o pipefail - xcodebuild build-for-testing -sdk ${{ matrix.sdk }} -project ConfigCat.xcodeproj -scheme "ConfigCat" -destination "${{ matrix.destination }}" CODE_SIGNING_REQUIRED=NO -quiet - xcodebuild test-without-building -sdk ${{ matrix.sdk }} -project ConfigCat.xcodeproj -scheme "ConfigCat" -destination "${{ matrix.destination }}" CODE_SIGNING_REQUIRED=NO -resultBundlePath tests/${{ matrix.platform }}.xcresult + xcodebuild build-for-testing -project ConfigCat.xcodeproj -scheme "ConfigCat" -destination "${{ matrix.destination }}" CODE_SIGNING_REQUIRED=NO -quiet + xcodebuild test-without-building -project ConfigCat.xcodeproj -scheme "ConfigCat" -destination "${{ matrix.destination }}" CODE_SIGNING_REQUIRED=NO -resultBundlePath tests/${{ matrix.platform }}.xcresult - name: Archive results if: failure() uses: actions/upload-artifact@v4 @@ -41,11 +45,11 @@ jobs: path: tests test-osx: - runs-on: macos-latest + runs-on: macos-14 strategy: matrix: include: - - sdk: macosx12.1 + - sdk: macosx14.2 destination: arch=x86_64 platform: macos steps: @@ -65,24 +69,37 @@ jobs: coverage: needs: [test-ios, test-osx] - runs-on: macos-latest + runs-on: macos-14 + strategy: + matrix: + include: + - sdk: macosx14.2 + destination: arch=x86_64 steps: - uses: actions/checkout@v4 - name: Install Slather - run: gem install slather + run: | + gem install slather + brew install sonar-scanner - name: Execute Coverage run: | set -o pipefail - xcodebuild build-for-testing -sdk macosx12.1 -project ConfigCat.xcodeproj -scheme "ConfigCat Coverage" -destination "arch=x86_64" -quiet - xcodebuild test-without-building -sdk macosx12.1 -project ConfigCat.xcodeproj -scheme "ConfigCat Coverage" -destination "arch=x86_64" -quiet + xcodebuild build-for-testing -sdk ${{ matrix.sdk }} -project ConfigCat.xcodeproj -scheme "ConfigCat Coverage" -destination "${{ matrix.destination }}" -quiet + xcodebuild test-without-building -sdk ${{ matrix.sdk }} -project ConfigCat.xcodeproj -scheme "ConfigCat Coverage" -destination "${{ matrix.destination }}" -quiet slather - bash <(curl -s https://codecov.io/bash) -f ./cobertura.xml - lint: + - name: Run Sonar + run: | + git fetch --unshallow --no-tags + sonar-scanner -Dsonar.token=${{ secrets.SONAR_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pod-lint: needs: coverage - runs-on: macos-latest + runs-on: macos-14 steps: - uses: actions/checkout@v4 @@ -93,8 +110,8 @@ jobs: run: pod lib lint publish: - needs: lint - runs-on: macos-latest + needs: pod-lint + runs-on: macos-14 if: startsWith(github.ref, 'refs/tags') steps: - uses: actions/checkout@v4 diff --git a/.slather.yml b/.slather.yml index bcc0aff..81951b3 100644 --- a/.slather.yml +++ b/.slather.yml @@ -1,6 +1,6 @@ -coverage_service: cobertura_xml +coverage_service: sonarqube_xml xcodeproj: ./ConfigCat.xcodeproj scheme: 'ConfigCat Coverage' -output_directory: ./ +output_directory: ./reports ignore: - "Version/*.swift" \ No newline at end of file diff --git a/ConfigCat.podspec b/ConfigCat.podspec index 25d9569..4adb830 100755 --- a/ConfigCat.podspec +++ b/ConfigCat.podspec @@ -1,22 +1,24 @@ Pod::Spec.new do |spec| spec.name = "ConfigCat" - spec.version = "10.0.0" + spec.version = "11.0.0" spec.summary = "ConfigCat Swift SDK" - spec.swift_version = "4.2" + spec.swift_version = "5.0" spec.description = "Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages. Alternative to LaunchDarkly. Host yourself, or use the hosted management app at https://configcat.com." spec.homepage = "https://github.com/configcat/swift-sdk" spec.license = { :type => "MIT", :file => "LICENSE" } spec.author = { "ConfigCat" => "developer@configcat.com" } - spec.ios.deployment_target = "10.0" - spec.watchos.deployment_target = "3.0" - spec.tvos.deployment_target = "10.0" - spec.osx.deployment_target = '10.12' + spec.ios.deployment_target = "12.0" + spec.watchos.deployment_target = "4.0" + spec.tvos.deployment_target = "12.0" + spec.osx.deployment_target = "10.13" + spec.visionos.deployment_target = "1.0" spec.source = { :git => "https://github.com/configcat/swift-sdk.git", :tag => spec.version } spec.source_files = "Sources/ConfigCat/*.swift", "Sources/Version/*.swift" + spec.resource_bundles = { 'ConfigCat' => 'Sources/ConfigCat/Resources/PrivacyInfo.xcprivacy' } spec.requires_arc = true spec.module_name = "ConfigCat" spec.documentation_url = "https://configcat.com/docs/sdk-reference/ios" diff --git a/ConfigCat.xcconfig b/ConfigCat.xcconfig index 8371559..7b83b1d 100644 --- a/ConfigCat.xcconfig +++ b/ConfigCat.xcconfig @@ -4,12 +4,13 @@ // `UIDeviceFamily` key it adds to the target's `Info.plist` file. This also drives the // --target-device flag to actool, which determines the idioms selected during catalog // compilation. -TARGETED_DEVICE_FAMILY = 1,2,3,4 +TARGETED_DEVICE_FAMILY = 1,2,3,4,7 -IPHONEOS_DEPLOYMENT_TARGET = 10.0 -MACOSX_DEPLOYMENT_TARGET = 10.12 -TVOS_DEPLOYMENT_TARGET = 10.0 -WATCHOS_DEPLOYMENT_TARGET = 3.0 +IPHONEOS_DEPLOYMENT_TARGET = 12.0 +MACOSX_DEPLOYMENT_TARGET = 10.13 +TVOS_DEPLOYMENT_TARGET = 12.0 +WATCHOS_DEPLOYMENT_TARGET = 4.0 +XROS_DEPLOYMENT_TARGET = 1.0 // Product Bundle Identifier // @@ -24,27 +25,17 @@ PRODUCT_BUNDLE_IDENTIFIER[sdk=iphonesimulator*] = com.configcat.iOS.client PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*] = com.configcat.macOS.client PRODUCT_BUNDLE_IDENTIFIER[sdk=watchos*] = com.configcat.watchOS.client PRODUCT_BUNDLE_IDENTIFIER[sdk=watchsimulator*] = com.configcat.watchOS.client - -// Base SDK -// -// The name or path of the base SDK being used during the build. The product will be -// built against the headers and libraries located inside the indicated SDK. This path -// will be prepended to all search paths, and will be passed through the environment to -// the compiler and linker. Additional SDKs can be specified in the `ADDITIONAL_SDKS` -// setting. -SDKROOT[sdk=macosx*] = macosx -SDKROOT[sdk=iphoneos*] = iphoneos -SDKROOT[sdk=watchos*] = watchos -SDKROOT[sdk=appletvos*] = appletvos +PRODUCT_BUNDLE_IDENTIFIER[sdk=xros*] = com.configcat.visionOS.client +PRODUCT_BUNDLE_IDENTIFIER[sdk=xrsimulator*] = com.configcat.visionOS.client // Supported Platforms // // The list of supported platforms from which a base SDK can be used. This setting is // used if the product can be built for multiple platforms using different SDKs. -SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator +SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator appletvos appletvsimulator xros xrsimulator // Swift Language Version -SWIFT_VERSION = 4.2 +SWIFT_VERSION = 5.0 // ConfigCat SDK version -MARKETING_VERSION = 10.0.0 +MARKETING_VERSION = 11.0.0 diff --git a/ConfigCat.xcodeproj/project.pbxproj b/ConfigCat.xcodeproj/project.pbxproj index 77f7f95..a4c70d9 100755 --- a/ConfigCat.xcodeproj/project.pbxproj +++ b/ConfigCat.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -22,7 +22,7 @@ B4BD2AC5258CA6FF007371E2 /* ConfigCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880AA1207BF1B100087A6B /* ConfigCache.swift */; }; B4BD2AC9258CA6FF007371E2 /* ConfigCat.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F880A2C207BE8F000087A6B /* ConfigCat.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */; }; - B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */; }; + B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationV1Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9B16216973B000F490CD /* RolloutIntegrationV1Tests.swift */; }; B4BD2AE8258CA7DF007371E2 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AEDBE8223876064008803E7 /* Config.swift */; }; B4BD2AEC258CA7DF007371E2 /* ConfigFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */; }; B4BD2AED258CA7DF007371E2 /* DataGovernanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10F787D2528950D0021F468 /* DataGovernanceTests.swift */; }; @@ -42,12 +42,6 @@ B4BD2B02258CA7DF007371E2 /* ManualPollingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8EDF9720840FE900906339 /* ManualPollingTests.swift */; }; B4BD2B03258CA7DF007371E2 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E1180A257532D700DA245A /* Log.swift */; }; B4BD2B04258CA7DF007371E2 /* ConfigCatUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F9AF62169176A00F490CD /* ConfigCatUser.swift */; }; - B4BD2B08258CA7DF007371E2 /* testmatrix_variationId.csv in Resources */ = {isa = PBXBuildFile; fileRef = C4D34D3A249B6F2900908D76 /* testmatrix_variationId.csv */; }; - B4BD2B09258CA7DF007371E2 /* testmatrix_semantic_2.csv in Resources */ = {isa = PBXBuildFile; fileRef = 1A65477E23A450A700EA53B8 /* testmatrix_semantic_2.csv */; }; - B4BD2B0A258CA7DF007371E2 /* testmatrix_sensitive.csv in Resources */ = {isa = PBXBuildFile; fileRef = 1A92102224030C7F0059B111 /* testmatrix_sensitive.csv */; }; - B4BD2B0B258CA7DF007371E2 /* testmatrix.csv in Resources */ = {isa = PBXBuildFile; fileRef = F15F9B122169738100F490CD /* testmatrix.csv */; }; - B4BD2B0C258CA7DF007371E2 /* testmatrix_number.csv in Resources */ = {isa = PBXBuildFile; fileRef = 1AEDBE99238761BA008803E7 /* testmatrix_number.csv */; }; - B4BD2B0D258CA7DF007371E2 /* testmatrix_semantic.csv in Resources */ = {isa = PBXBuildFile; fileRef = 1AEDBE98238761BA008803E7 /* testmatrix_semantic.csv */; }; C40CF51227B5533800D9F88A /* LocalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CF51127B5533800D9F88A /* LocalTests.swift */; }; C40CF51427B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CF51327B557EE00D9F88A /* LocalDictionaryDataSource.swift */; }; C40CF51527B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CF51327B557EE00D9F88A /* LocalDictionaryDataSource.swift */; }; @@ -75,11 +69,31 @@ F17DEE2D288876F7009C3E48 /* ConfigService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE26288876F6009C3E48 /* ConfigService.swift */; }; F17DEE2E288876F7009C3E48 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE27288876F6009C3E48 /* Utils.swift */; }; F17DEE2F288876F7009C3E48 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17DEE27288876F6009C3E48 /* Utils.swift */; }; + F183A19E2B8F824F0015967E /* testmatrix_variationid.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1922B8F824F0015967E /* testmatrix_variationid.csv */; }; + F183A19F2B8F824F0015967E /* testmatrix_segments_old.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1932B8F824F0015967E /* testmatrix_segments_old.csv */; }; + F183A1A02B8F824F0015967E /* testmatrix_semantic_2.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1942B8F824F0015967E /* testmatrix_semantic_2.csv */; }; + F183A1A12B8F824F0015967E /* testmatrix_semantic.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1952B8F824F0015967E /* testmatrix_semantic.csv */; }; + F183A1A22B8F824F0015967E /* testmatrix_comparators_v6.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1962B8F824F0015967E /* testmatrix_comparators_v6.csv */; }; + F183A1A32B8F824F0015967E /* testmatrix_and_or.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1972B8F824F0015967E /* testmatrix_and_or.csv */; }; + F183A1A42B8F824F0015967E /* testmatrix_sensitive.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1982B8F824F0015967E /* testmatrix_sensitive.csv */; }; + F183A1A52B8F824F0015967E /* testmatrix_segments.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A1992B8F824F0015967E /* testmatrix_segments.csv */; }; + F183A1A62B8F824F0015967E /* testmatrix_prerequisite_flag.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A19A2B8F824F0015967E /* testmatrix_prerequisite_flag.csv */; }; + F183A1A72B8F824F0015967E /* testmatrix.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A19B2B8F824F0015967E /* testmatrix.csv */; }; + F183A1A82B8F824F0015967E /* testmatrix_unicode.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A19C2B8F824F0015967E /* testmatrix_unicode.csv */; }; + F183A1A92B8F824F0015967E /* testmatrix_number.csv in Resources */ = {isa = PBXBuildFile; fileRef = F183A19D2B8F824F0015967E /* testmatrix_number.csv */; }; + F183A1AB2B8F836A0015967E /* RolloutIntegrationV2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F183A1AA2B8F836A0015967E /* RolloutIntegrationV2Tests.swift */; }; + F183A2032B8FAC090015967E /* evaluationlog in Resources */ = {isa = PBXBuildFile; fileRef = F183A2012B8FAC090015967E /* evaluationlog */; }; + F183A2042B8FAC090015967E /* json in Resources */ = {isa = PBXBuildFile; fileRef = F183A2022B8FAC090015967E /* json */; }; + F183A2062B8FBABD0015967E /* EvaluationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F183A2052B8FBABD0015967E /* EvaluationTests.swift */; }; + F183A2082B9096A30015967E /* EvaluationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F183A2072B9096A30015967E /* EvaluationLogTests.swift */; }; + F183A20B2BA342990015967E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F183A2092BA273AC0015967E /* PrivacyInfo.xcprivacy */; }; F1B1D8B528FF2C830034165E /* ConfigCatOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B1D8B428FF2C830034165E /* ConfigCatOptions.swift */; }; F1B1D8B628FF2C830034165E /* ConfigCatOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B1D8B428FF2C830034165E /* ConfigCatOptions.swift */; }; F1BC414728E1D54800F2230A /* EvaluationDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BC414428E1D54800F2230A /* EvaluationDetails.swift */; }; F1BC414828E1D54800F2230A /* EvaluationDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BC414428E1D54800F2230A /* EvaluationDetails.swift */; }; F1BC414C28E1D6C900F2230A /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BC414B28E1D6C900F2230A /* Helpers.swift */; }; + F1CBE12F2B8CC81700CD2FF9 /* EvaluationLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CBE12E2B8CC81700CD2FF9 /* EvaluationLogger.swift */; }; + F1CBE1302B8CC81700CD2FF9 /* EvaluationLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CBE12E2B8CC81700CD2FF9 /* EvaluationLogger.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -94,17 +108,12 @@ /* Begin PBXFileReference section */ 1A3B096423909055002A3A62 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; - 1A65477E23A450A700EA53B8 /* testmatrix_semantic_2.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_semantic_2.csv; path = Resources/testmatrix_semantic_2.csv; sourceTree = ""; }; - 1A92102224030C7F0059B111 /* testmatrix_sensitive.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_sensitive.csv; path = Resources/testmatrix_sensitive.csv; sourceTree = ""; }; 1AEDBE8223876064008803E7 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; - 1AEDBE98238761BA008803E7 /* testmatrix_semantic.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_semantic.csv; path = Resources/testmatrix_semantic.csv; sourceTree = ""; }; - 1AEDBE99238761BA008803E7 /* testmatrix_number.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_number.csv; path = Resources/testmatrix_number.csv; sourceTree = ""; }; 3F029E89207EB3BD0019942F /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; 3F1F2C6023E103C600AFA7D2 /* PollingMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollingMode.swift; sourceTree = ""; }; 3F1F2C6523E10BF300AFA7D2 /* PollingModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollingModes.swift; sourceTree = ""; }; 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigFetcherTests.swift; sourceTree = ""; }; 3F880A2C207BE8F000087A6B /* ConfigCat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConfigCat.h; sourceTree = ""; }; - 3F880A39207BE8F000087A6B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Resources/Info.plist; sourceTree = ""; }; 3F880A9F207BF1B100087A6B /* ConfigFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigFetcher.swift; sourceTree = ""; }; 3F880AA1207BF1B100087A6B /* ConfigCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigCache.swift; sourceTree = ""; }; 3F880AA2207BF1B100087A6B /* Synced.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Synced.swift; sourceTree = ""; }; @@ -122,7 +131,6 @@ C41F8AB324AD38F70004CF03 /* VariationIdTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariationIdTests.swift; sourceTree = ""; }; C45414AE24AF2BF2004E66E0 /* KeyValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyValue.swift; sourceTree = ""; }; C480E82B2768144400916320 /* ConfigCatClientIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigCatClientIntegrationTests.swift; sourceTree = ""; }; - C4D34D3A249B6F2900908D76 /* testmatrix_variationId.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_variationId.csv; path = Resources/testmatrix_variationId.csv; sourceTree = ""; }; C4FA1B3C278D919900BFA8C3 /* OverrideDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideDataSource.swift; sourceTree = ""; }; C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideBehaviour.swift; sourceTree = ""; }; F10AC6CF2A93B02B006FA496 /* CacheTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheTest.swift; sourceTree = ""; }; @@ -135,16 +143,34 @@ F14961CE28FF71400095A72A /* EvaluationDetailsExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvaluationDetailsExtensionTests.swift; sourceTree = ""; }; F15F9AF62169176A00F490CD /* ConfigCatUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatUser.swift; sourceTree = ""; }; F15F9AFB216922F000F490CD /* RolloutEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutEvaluator.swift; sourceTree = ""; }; - F15F9B122169738100F490CD /* testmatrix.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix.csv; path = Resources/testmatrix.csv; sourceTree = ""; }; - F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutIntegrationTests.swift; sourceTree = ""; }; + F15F9B16216973B000F490CD /* RolloutIntegrationV1Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutIntegrationV1Tests.swift; sourceTree = ""; }; F17DEE22288876AE009C3E48 /* LazyLoadingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyLoadingTests.swift; sourceTree = ""; }; F17DEE24288876F6009C3E48 /* MutableQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableQueue.swift; sourceTree = ""; }; F17DEE25288876F6009C3E48 /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; F17DEE26288876F6009C3E48 /* ConfigService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigService.swift; sourceTree = ""; }; F17DEE27288876F6009C3E48 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + F183A1922B8F824F0015967E /* testmatrix_variationid.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_variationid.csv; path = Resources/testmatrix_variationid.csv; sourceTree = ""; }; + F183A1932B8F824F0015967E /* testmatrix_segments_old.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_segments_old.csv; path = Resources/testmatrix_segments_old.csv; sourceTree = ""; }; + F183A1942B8F824F0015967E /* testmatrix_semantic_2.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_semantic_2.csv; path = Resources/testmatrix_semantic_2.csv; sourceTree = ""; }; + F183A1952B8F824F0015967E /* testmatrix_semantic.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_semantic.csv; path = Resources/testmatrix_semantic.csv; sourceTree = ""; }; + F183A1962B8F824F0015967E /* testmatrix_comparators_v6.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_comparators_v6.csv; path = Resources/testmatrix_comparators_v6.csv; sourceTree = ""; }; + F183A1972B8F824F0015967E /* testmatrix_and_or.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_and_or.csv; path = Resources/testmatrix_and_or.csv; sourceTree = ""; }; + F183A1982B8F824F0015967E /* testmatrix_sensitive.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_sensitive.csv; path = Resources/testmatrix_sensitive.csv; sourceTree = ""; }; + F183A1992B8F824F0015967E /* testmatrix_segments.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_segments.csv; path = Resources/testmatrix_segments.csv; sourceTree = ""; }; + F183A19A2B8F824F0015967E /* testmatrix_prerequisite_flag.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_prerequisite_flag.csv; path = Resources/testmatrix_prerequisite_flag.csv; sourceTree = ""; }; + F183A19B2B8F824F0015967E /* testmatrix.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix.csv; path = Resources/testmatrix.csv; sourceTree = ""; }; + F183A19C2B8F824F0015967E /* testmatrix_unicode.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_unicode.csv; path = Resources/testmatrix_unicode.csv; sourceTree = ""; }; + F183A19D2B8F824F0015967E /* testmatrix_number.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = testmatrix_number.csv; path = Resources/testmatrix_number.csv; sourceTree = ""; }; + F183A1AA2B8F836A0015967E /* RolloutIntegrationV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolloutIntegrationV2Tests.swift; sourceTree = ""; }; + F183A2012B8FAC090015967E /* evaluationlog */ = {isa = PBXFileReference; lastKnownFileType = folder; name = evaluationlog; path = Resources/evaluationlog; sourceTree = ""; }; + F183A2022B8FAC090015967E /* json */ = {isa = PBXFileReference; lastKnownFileType = folder; name = json; path = Resources/json; sourceTree = ""; }; + F183A2052B8FBABD0015967E /* EvaluationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvaluationTests.swift; sourceTree = ""; }; + F183A2072B9096A30015967E /* EvaluationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvaluationLogTests.swift; sourceTree = ""; }; + F183A2092BA273AC0015967E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; F1B1D8B428FF2C830034165E /* ConfigCatOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigCatOptions.swift; sourceTree = ""; }; F1BC414428E1D54800F2230A /* EvaluationDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EvaluationDetails.swift; sourceTree = ""; }; F1BC414B28E1D6C900F2230A /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + F1CBE12E2B8CC81700CD2FF9 /* EvaluationLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvaluationLogger.swift; sourceTree = ""; }; F1E1180A257532D700DA245A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -199,6 +225,7 @@ isa = PBXGroup; children = ( B4BD2B64258CA9A2007371E2 /* Info.plist */, + F183A2092BA273AC0015967E /* PrivacyInfo.xcprivacy */, 3F880A2C207BE8F000087A6B /* ConfigCat.h */, ); path = Resources; @@ -232,6 +259,7 @@ 3F880A41207BE91300087A6B /* Resources */, F10AC6D12A950197006FA496 /* FlagEvaluator.swift */, F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */, + F1CBE12E2B8CC81700CD2FF9 /* EvaluationLogger.swift */, ); name = Sources; path = Sources/ConfigCat; @@ -240,6 +268,7 @@ 3F880A43207BE92000087A6B /* Tests */ = { isa = PBXGroup; children = ( + F183A18F2B8F82290015967E /* Data */, F14961CE28FF71400095A72A /* EvaluationDetailsExtensionTests.swift */, F1BC414B28E1D6C900F2230A /* Helpers.swift */, F11F76BD288AE7540097939F /* SnapshotTests.swift */, @@ -248,26 +277,43 @@ C40CF51127B5533800D9F88A /* LocalTests.swift */, C480E82B2768144400916320 /* ConfigCatClientIntegrationTests.swift */, C41F8AB324AD38F70004CF03 /* VariationIdTests.swift */, - C4D34D3A249B6F2900908D76 /* testmatrix_variationId.csv */, - 1A92102224030C7F0059B111 /* testmatrix_sensitive.csv */, - 1A65477E23A450A700EA53B8 /* testmatrix_semantic_2.csv */, - 1AEDBE99238761BA008803E7 /* testmatrix_number.csv */, - 1AEDBE98238761BA008803E7 /* testmatrix_semantic.csv */, - F15F9B122169738100F490CD /* testmatrix.csv */, - 3F880A39207BE8F000087A6B /* Info.plist */, 3F029E89207EB3BD0019942F /* Mock.swift */, 3F4D40C4207EC17000BBAEC6 /* ConfigFetcherTests.swift */, 3F8EDF9720840FE900906339 /* ManualPollingTests.swift */, 3F8EDF9B208415FC00906339 /* AutoPollingTests.swift */, 3F8EDF9F2084194700906339 /* ConfigCatClientTests.swift */, - F15F9B16216973B000F490CD /* RolloutIntegrationTests.swift */, + F15F9B16216973B000F490CD /* RolloutIntegrationV1Tests.swift */, F10F787D2528950D0021F468 /* DataGovernanceTests.swift */, F10AC6CF2A93B02B006FA496 /* CacheTest.swift */, + F183A1AA2B8F836A0015967E /* RolloutIntegrationV2Tests.swift */, + F183A2052B8FBABD0015967E /* EvaluationTests.swift */, + F183A2072B9096A30015967E /* EvaluationLogTests.swift */, ); name = Tests; path = Tests/ConfigCatTests; sourceTree = ""; }; + F183A18F2B8F82290015967E /* Data */ = { + isa = PBXGroup; + children = ( + F183A2012B8FAC090015967E /* evaluationlog */, + F183A2022B8FAC090015967E /* json */, + F183A1972B8F824F0015967E /* testmatrix_and_or.csv */, + F183A1962B8F824F0015967E /* testmatrix_comparators_v6.csv */, + F183A19D2B8F824F0015967E /* testmatrix_number.csv */, + F183A19A2B8F824F0015967E /* testmatrix_prerequisite_flag.csv */, + F183A1932B8F824F0015967E /* testmatrix_segments_old.csv */, + F183A1992B8F824F0015967E /* testmatrix_segments.csv */, + F183A1942B8F824F0015967E /* testmatrix_semantic_2.csv */, + F183A1952B8F824F0015967E /* testmatrix_semantic.csv */, + F183A1982B8F824F0015967E /* testmatrix_sensitive.csv */, + F183A19C2B8F824F0015967E /* testmatrix_unicode.csv */, + F183A1922B8F824F0015967E /* testmatrix_variationid.csv */, + F183A19B2B8F824F0015967E /* testmatrix.csv */, + ); + name = Data; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -336,7 +382,7 @@ }; }; buildConfigurationList = 3F8809CF207BE3F100087A6B /* Build configuration list for PBXProject "ConfigCat" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 12.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -359,6 +405,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F183A20B2BA342990015967E /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -366,12 +413,20 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - B4BD2B08258CA7DF007371E2 /* testmatrix_variationId.csv in Resources */, - B4BD2B09258CA7DF007371E2 /* testmatrix_semantic_2.csv in Resources */, - B4BD2B0A258CA7DF007371E2 /* testmatrix_sensitive.csv in Resources */, - B4BD2B0B258CA7DF007371E2 /* testmatrix.csv in Resources */, - B4BD2B0C258CA7DF007371E2 /* testmatrix_number.csv in Resources */, - B4BD2B0D258CA7DF007371E2 /* testmatrix_semantic.csv in Resources */, + F183A1A42B8F824F0015967E /* testmatrix_sensitive.csv in Resources */, + F183A1A32B8F824F0015967E /* testmatrix_and_or.csv in Resources */, + F183A1A92B8F824F0015967E /* testmatrix_number.csv in Resources */, + F183A2032B8FAC090015967E /* evaluationlog in Resources */, + F183A1A02B8F824F0015967E /* testmatrix_semantic_2.csv in Resources */, + F183A19E2B8F824F0015967E /* testmatrix_variationid.csv in Resources */, + F183A1A72B8F824F0015967E /* testmatrix.csv in Resources */, + F183A1A62B8F824F0015967E /* testmatrix_prerequisite_flag.csv in Resources */, + F183A1A52B8F824F0015967E /* testmatrix_segments.csv in Resources */, + F183A1A82B8F824F0015967E /* testmatrix_unicode.csv in Resources */, + F183A2042B8FAC090015967E /* json in Resources */, + F183A1A12B8F824F0015967E /* testmatrix_semantic.csv in Resources */, + F183A19F2B8F824F0015967E /* testmatrix_segments_old.csv in Resources */, + F183A1A22B8F824F0015967E /* testmatrix_comparators_v6.csv in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -383,6 +438,7 @@ buildActionMask = 2147483647; files = ( B4BD2AB0258CA6FF007371E2 /* ConfigCatClient.swift in Sources */, + F1CBE12F2B8CC81700CD2FF9 /* EvaluationLogger.swift in Sources */, B4BD2AB1258CA6FF007371E2 /* Version.swift in Sources */, B4BD2AB2258CA6FF007371E2 /* PollingModes.swift in Sources */, F17DEE2C288876F7009C3E48 /* ConfigService.swift in Sources */, @@ -420,9 +476,10 @@ F10AC6D62A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */, F10AC6D22A950197006FA496 /* FlagEvaluator.swift in Sources */, B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */, - B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationTests.swift in Sources */, + B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationV1Tests.swift in Sources */, B4BD2AE8258CA7DF007371E2 /* Config.swift in Sources */, F11F76C0288AE7650097939F /* Extensions.swift in Sources */, + F183A2062B8FBABD0015967E /* EvaluationTests.swift in Sources */, B4BD2AEC258CA7DF007371E2 /* ConfigFetcherTests.swift in Sources */, B4BD2AED258CA7DF007371E2 /* DataGovernanceTests.swift in Sources */, B4BD2AEE258CA7DF007371E2 /* PollingMode.swift in Sources */, @@ -442,11 +499,14 @@ B4BD2AF8258CA7DF007371E2 /* ConfigCatClientTests.swift in Sources */, B4BD2AF9258CA7DF007371E2 /* ConfigCatClient.swift in Sources */, F14961CF28FF71400095A72A /* EvaluationDetailsExtensionTests.swift in Sources */, + F183A1AB2B8F836A0015967E /* RolloutIntegrationV2Tests.swift in Sources */, B4BD2AFA258CA7DF007371E2 /* ConfigCatClientProtocol.swift in Sources */, B4BD2AFB258CA7DF007371E2 /* Synced.swift in Sources */, C480E82C2768144400916320 /* ConfigCatClientIntegrationTests.swift in Sources */, B4BD2AFC258CA7DF007371E2 /* AutoPollingTests.swift in Sources */, B4BD2AFF258CA7DF007371E2 /* VariationIdTests.swift in Sources */, + F1CBE1302B8CC81700CD2FF9 /* EvaluationLogger.swift in Sources */, + F183A2082B9096A30015967E /* EvaluationLogTests.swift in Sources */, B4BD2B00258CA7DF007371E2 /* Mock.swift in Sources */, F11F76BE288AE7540097939F /* SnapshotTests.swift in Sources */, C40CF51227B5533800D9F88A /* LocalTests.swift in Sources */, @@ -593,7 +653,11 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/ConfigCat/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -601,7 +665,6 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INCLUDE_PATHS = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2,3,6"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -657,13 +720,17 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/ConfigCat/Resources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_INCLUDE_PATHS = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2,3,6"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -722,15 +789,18 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/ConfigCatTests/Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.configcat.Tests.client; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2,3,6"; + WATCHOS_DEPLOYMENT_TARGET = 9.1; }; name = Debug; }; @@ -780,14 +850,18 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Tests/ConfigCatTests/Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.configcat.Tests.client; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2,3,6"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 9.1; }; name = Release; }; diff --git a/Package.swift b/Package.swift index 2997e8b..9d5e59f 100644 --- a/Package.swift +++ b/Package.swift @@ -5,10 +5,10 @@ import PackageDescription let package = Package( name: "ConfigCat", platforms: [ - .iOS(.v10), - .watchOS(.v3), - .tvOS(.v10), - .macOS(.v10_12) + .iOS(.v12), + .watchOS(.v4), + .tvOS(.v12), + .macOS(.v10_13) ], products: [ .library(name: "ConfigCat", targets: ["ConfigCat"]) @@ -19,6 +19,7 @@ let package = Package( .target(name: "ConfigCat", dependencies: ["Version"], exclude: ["Resources/ConfigCat.h", "Resources/Info.plist"], + resources: [.copy("Resources/PrivacyInfo.xcprivacy")], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ]), diff --git a/README.md b/README.md index a7fb83b..c7a39f9 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,61 @@ # ConfigCat SDK for Swift -https://configcat.com - -ConfigCat SDK for Swift provides easy integration for your application to ConfigCat. - -ConfigCat is a feature flag and configuration management service that lets you separate releases from deployments. You can turn your features ON/OFF using ConfigCat Dashboard even after they are deployed. ConfigCat lets you target specific groups of users based on region, email or any other custom user attribute. - -ConfigCat is a hosted feature flag service. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs. [![Build Status](https://github.com/configcat/swift-sdk/actions/workflows/swift-ci.yml/badge.svg?branch=master)](https://github.com/configcat/swift-sdk/actions/workflows/swift-ci.yml) -[![Coverage Status](https://img.shields.io/codecov/c/github/ConfigCat/swift-sdk.svg)](https://codecov.io/gh/ConfigCat/swift-sdk) [![CocoaPods](https://img.shields.io/cocoapods/v/ConfigCat.svg)](https://cocoapods.org/pods/ConfigCat) -[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Supported Platforms](https://img.shields.io/cocoapods/p/ConfigCat.svg?style=flat)](https://configcat.com/docs/sdk-reference/ios) +[![Coverage Status](https://img.shields.io/sonar/coverage/configcat_swift-sdk?logo=SonarCloud&server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/project/overview?id=configcat_swift-sdk) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=configcat_swift-sdk&metric=alert_status)](https://sonarcloud.io/dashboard?id=configcat_swift-sdk) + +ConfigCat SDK for Swift provides easy integration for your application to [ConfigCat](https://configcat.com). + +The following device platform versions are supported: + +| Platform | Version | +| -------- | ------- | +| iOS | 12.0 | +| watchOS | 4.0 | +| tvOS | 12.0 | +| macOS | 10.13 | +| visionOS | 1.0 | ## Getting started ### 1. Install the package -**CocoaPods:** +- ### CocoaPods -Add the following to your `Podfile`: -```ruby -target '' do -pod 'ConfigCat' -end -``` -Then, run the following command to install your dependencies: -```bash -pod install -``` + Add the following to your `Podfile`: + ```ruby + target '' do + pod 'ConfigCat' + end + ``` + Then, run the following command to install your dependencies: + ```bash + pod install + ``` -**Carthage:** +- ### Swift Package Manager -Add the following to your `Cartfile`: -``` -github "configcat/swift-sdk" -``` -Then, run the `carthage update` command and then follow the [Carthage integration steps](https://github.com/Carthage/Carthage#getting-started) to link the framework with your project. + You can add ConfigCat to an Xcode project by adding it as a package dependency. -**Swift Package Manager:** + > https://github.com/configcat/swift-sdk -You can add ConfigCat to an Xcode project by adding it as a package dependency. + If you want to use ConfigCat in a [SwiftPM](https://swift.org/package-manager/) project, it's as simple as adding a `dependencies` clause to your `Package.swift`: -> https://github.com/configcat/swift-sdk + ``` swift + dependencies: [ + .package(url: "https://github.com/configcat/swift-sdk", from: "11.0.0") + ] + ``` -If you want to use ConfigCat in a [SwiftPM](https://swift.org/package-manager/) project, it's as simple as adding a `dependencies` clause to your `Package.swift`: +- ### Carthage -``` swift -dependencies: [ - .package(url: "https://github.com/configcat/swift-sdk", from: "10.0.0") -] -``` + Add the following to your `Cartfile`: + ``` + github "configcat/swift-sdk" + ``` + Then, run the `carthage update` command and then follow the [Carthage integration steps](https://github.com/Carthage/Carthage#getting-started) to link the framework with your project. ### 2. Go to the ConfigCat Dashboard to get your *SDK Key*: ![SDK-KEY](https://raw.githubusercontent.com/ConfigCat/swift-sdk/master/media/readme02-3.png "SDK-KEY") @@ -127,6 +132,10 @@ https://configcat.com/support Contributions are welcome. For more info please read the [Contribution Guideline](CONTRIBUTING.md). ## About ConfigCat +ConfigCat is a feature flag and configuration management service that lets you separate releases from deployments. You can turn your features ON/OFF using ConfigCat Dashboard even after they are deployed. ConfigCat lets you target specific groups of users based on region, email or any other custom user attribute. + +ConfigCat is a hosted feature flag service. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs. + - [Official ConfigCat SDKs for other platforms](https://github.com/configcat) - [Documentation](https://configcat.com/docs) - [Blog](https://configcat.com/blog) diff --git a/Sources/ConfigCat/Config.swift b/Sources/ConfigCat/Config.swift index b47fc5d..165df7e 100644 --- a/Sources/ConfigCat/Config.swift +++ b/Sources/ConfigCat/Config.swift @@ -1,5 +1,206 @@ import Foundation +enum RedirectMode: Int { + case unknown = -1 + case noRedirect = 0 + case shouldRedirect = 1 + case forceRedirect = 2 +} + +@objc public enum SegmentComparator: Int { + case unknown = -1 + /// Checks whether the conditions of the specified segment are evaluated to true. + case isIn = 0 + /// Checks whether the conditions of the specified segment are evaluated to false. + case isNotIn = 1 + + var text: String { + return self == .isIn ? "IS IN SEGMENT" : "IS NOT IN SEGMENT" + } +} + +@objc public enum PrerequisiteFlagComparator: Int { + case unknown = -1 + /// Checks whether the evaluated value of the specified prerequisite flag is equal to the comparison value. + case eq = 0 + /// Checks whether the evaluated value of the specified prerequisite flag is not equal to the comparison value. + case notEq = 1 + + var text: String { + return self == .eq ? "EQUALS" : "NOT EQUALS" + } +} + +@objc public enum SettingType: Int { + case unknown = -1 + /// The on/off type (feature flag). + case bool = 0 + /// The text setting type. + case string = 1 + /// The whole number setting type. + case int = 2 + /// The decimal number setting type. + case double = 3 + + var text: String { + switch self { + case .unknown: + return "Unknown" + case .bool: + return "Bool" + case .string: + return "String" + case .int: + return "Int" + case .double: + return "Double" + } + } +} + +@objc public enum UserComparator: Int { + case unknown = -1 + /// Checks whether the comparison attribute is equal to any of the comparison values. + case oneOf = 0 + /// Checks whether the comparison attribute is not equal to any of the comparison values. + case notOneOf = 1 + /// Checks whether the comparison attribute contains any comparison values as a substring. + case contains = 2 + /// Checks whether the comparison attribute does not contain any comparison values as a substring. + case notContains = 3 + /// Checks whether the comparison attribute interpreted as a semantic version is equal to any of the comparison values. + case oneOfSemver = 4 + /// Checks whether the comparison attribute interpreted as a semantic version is not equal to any of the comparison values. + case notOneOfSemver = 5 + /// Checks whether the comparison attribute interpreted as a semantic version is less than the comparison value. + case lessSemver = 6 + /// Checks whether the comparison attribute interpreted as a semantic version is less than or equal to the comparison value. + case lessEqSemver = 7 + /// Checks whether the comparison attribute interpreted as a semantic version is greater than the comparison value. + case greaterSemver = 8 + /// Checks whether the comparison attribute interpreted as a semantic version is greater than or equal to the comparison value. + case greaterEqSemver = 9 + /// Checks whether the comparison attribute interpreted as a decimal number is equal to the comparison value. + case eqNum = 10 + /// Checks whether the comparison attribute interpreted as a decimal number is not equal to the comparison value. + case notEqNum = 11 + /// Checks whether the comparison attribute interpreted as a decimal number is less than the comparison value. + case lessNum = 12 + /// Checks whether the comparison attribute interpreted as a decimal number is less than or equal to the comparison value. + case lessEqNum = 13 + /// Checks whether the comparison attribute interpreted as a decimal number is greater than the comparison value. + case greaterNum = 14 + /// Checks whether the comparison attribute interpreted as a decimal number is greater than or equal to the comparison value. + case greaterEqNum = 15 + /// Checks whether the comparison attribute is equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case oneOfHashed = 16 + /// Checks whether the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case notOneOfHashed = 17 + /// Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is less than the comparison value. + case beforeDateTime = 18 + /// Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. + case afterDateTime = 19 + /// Checks whether the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). + case eqHashed = 20 + /// Checks whether the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). + case notEqHashed = 21 + /// Checks whether the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case startsWithAnyOfHashed = 22 + /// Checks whether the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case notStartsWithAnyOfHashed = 23 + /// Checks whether the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case endsWithAnyOfHashed = 24 + /// Checks whether the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case notEndsWithAnyOfHashed = 25 + /// Checks whether the comparison attribute interpreted as a string list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case arrayContainsAnyOfHashed = 26 + /// Checks whether the comparison attribute interpreted as a string list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). + case arrayNotContainsAnyOfHashed = 27 + //// Checks whether the comparison attribute is equal to the comparison value. + case eq = 28 + /// Checks whether the comparison attribute is not equal to the comparison value. + case notEq = 29 + /// Checks whether the comparison attribute starts with any of the comparison values. + case startsWithAnyOf = 30 + /// Checks whether the comparison attribute does not start with any of the comparison values. + case notStartsWithAnyOf = 31 + /// Checks whether the comparison attribute ends with any of the comparison values. + case endsWithAnyOf = 32 + /// Checks whether the comparison attribute does not end with any of the comparison values. + case notEndsWithAnyOf = 33 + /// Checks whether the comparison attribute interpreted as a string list contains any of the comparison values. + case arrayContainsAnyOf = 34 + /// Checks whether the comparison attribute interpreted as a string list does not contain any of the comparison values. + case arrayNotContainsAnyOf = 35 + + var isSensitive: Bool { + switch self { + case .oneOfHashed, .notOneOfHashed, .eqHashed, .notEqHashed, .startsWithAnyOfHashed, .notStartsWithAnyOfHashed, + .endsWithAnyOfHashed, .notEndsWithAnyOfHashed, .arrayContainsAnyOfHashed, .arrayNotContainsAnyOfHashed: + return true + default: + return false + } + } + + var isStartsWith: Bool { + switch self { + case .startsWithAnyOf, .startsWithAnyOfHashed, .notStartsWithAnyOf, .notStartsWithAnyOfHashed: + return true + default: + return false + } + } + + var isDateTime: Bool { + switch self { + case .afterDateTime, .beforeDateTime: + return true + default: + return false + } + } +} + +let comparatorTexts: [UserComparator: String] = [ + .oneOf: "IS ONE OF", + .notOneOf: "IS NOT ONE OF", + .contains: "CONTAINS ANY OF", + .notContains: "NOT CONTAINS ANY OF", + .oneOfSemver: "IS ONE OF", + .notOneOfSemver: "IS NOT ONE OF", + .lessSemver: "<", + .lessEqSemver: "<=", + .greaterSemver: ">", + .greaterEqSemver: ">=", + .eqNum: "=", + .notEqNum: "!=", + .lessNum: "<", + .lessEqNum: "<=", + .greaterNum: ">", + .greaterEqNum: ">=", + .oneOfHashed: "IS ONE OF", + .notOneOfHashed: "IS NOT ONE OF", + .beforeDateTime: "BEFORE", + .afterDateTime: "AFTER", + .eqHashed: "EQUALS", + .notEqHashed: "NOT EQUALS", + .startsWithAnyOfHashed: "STARTS WITH ANY OF", + .notStartsWithAnyOfHashed: "NOT STARTS WITH ANY OF", + .endsWithAnyOfHashed: "ENDS WITH ANY OF", + .notEndsWithAnyOfHashed: "NOT ENDS WITH ANY OF", + .arrayContainsAnyOfHashed: "ARRAY CONTAINS ANY OF", + .arrayNotContainsAnyOfHashed: "ARRAY NOT CONTAINS ANY OF", + .eq: "EQUALS", + .notEq: "NOT EQUALS", + .startsWithAnyOf: "STARTS WITH ANY OF", + .notStartsWithAnyOf: "NOT STARTS WITH ANY OF", + .endsWithAnyOf: "ENDS WITH ANY OF", + .notEndsWithAnyOf: "NOT ENDS WITH ANY OF", + .arrayContainsAnyOf: "ARRAY CONTAINS ANY OF", + .arrayNotContainsAnyOf: "ARRAY NOT CONTAINS ANY OF", +] + protocol JsonSerializable { func toJsonMap() -> [String: Any] } @@ -30,18 +231,10 @@ class ConfigEntry: Equatable { } static func fromConfigJson(json: String, eTag: String, fetchTime: Date) -> Result { - do { - guard let data = json.data(using: .utf8) else { - return .failure(ParseError(message: "Decode to utf8 data failed.")) - } - guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - return .failure(ParseError(message: "Convert to [String: Any] map failed.")) - } - - return .success(ConfigEntry(config: Config.fromJson(json: jsonObject), configJson: json, eTag: eTag, fetchTime: fetchTime)) - } catch { - return .failure(error) + guard let jsonObject: [String: Any] = Utils.fromJson(json: json) else { + return .failure(ParseError(message: "Config JSON parsing failed.")) } + return .success(ConfigEntry(config: .fromJson(json: jsonObject), configJson: json, eTag: eTag, fetchTime: fetchTime)) } static func fromCached(cached: String) -> Result { @@ -77,226 +270,726 @@ class ConfigEntry: Equatable { static let empty = ConfigEntry(eTag: "empty") } -class Config: JsonSerializable { +public class Config: NSObject, JsonSerializable { static let preferencesKey = "p" - static let entriesKey = "f" - - let preferences: Preferences? - let entries: [String: Setting] + static let settingsKey = "f" + static let segmentsKey = "s" + + /// The dictionary of settings. + @objc public let settings: [String: Setting] + /// The list of segments. + @objc public let segments: [Segment] + /// The salt that was used to hash sensitive comparison values. + @objc public let salt: String? + + let preferences: Preferences - init(preferences: Preferences? = nil, entries: [String: Setting] = [:]) { + init(preferences: Preferences, settings: [String: Setting] = [:], segments: [Segment] = []) { self.preferences = preferences - self.entries = entries + self.settings = settings + self.segments = segments + self.salt = preferences.salt } static func fromJson(json: [String: Any]) -> Config { - let entriesMap = json[Config.entriesKey] as? [String: Any] ?? [:] - let entries = entriesMap.mapValues { entry in - Setting.fromJson(json: entry as? [String: Any] ?? [:]) + let settingsMap = json[self.settingsKey] as? [String: Any] ?? [:] + let settings: [String: Setting] = settingsMap.mapValues { setting in + .fromJson(json: setting as? [String: Any] ?? [:]) + } + let segmentsMap = json[self.segmentsKey] as? [[String: Any]] ?? [] + let segments: [Segment] = segmentsMap.map { segment in + .fromJson(json: segment) + } + var preferences: Preferences = .empty + if let pref = json[Config.preferencesKey] as? [String: Any] { + preferences = .fromJson(json: pref) + } + for setting in settings { + setting.value.salt = preferences.salt + for rule in setting.value.targetingRules { + for condition in rule.conditions { + if let cond = condition.segmentCondition { + cond.segment = segments.count > cond.index ? segments[cond.index] : nil + } + } + } } if let preferences = json[Config.preferencesKey] as? [String: Any] { - return Config(preferences: Preferences.fromJson(json: preferences), entries: entries) + return Config(preferences: .fromJson(json: preferences), settings: settings, segments: segments) } - return Config(preferences: nil, entries: entries) + return Config(preferences: .empty, settings: settings, segments: segments) } func toJsonMap() -> [String: Any] { var result: [String: Any] = [ - Config.entriesKey: entries.mapValues { setting in + Config.settingsKey: settings.mapValues { setting in setting.toJsonMap() }, ] - if let pref = preferences { - result[Config.preferencesKey] = pref.toJsonMap() + if !preferences.isEmpty { + result[Config.preferencesKey] = preferences.toJsonMap() } return result } var isEmpty: Bool { get { - entries.isEmpty + settings.isEmpty } } - static let empty = Config() + static let empty = Config(preferences: .empty) } class Preferences: JsonSerializable { static let preferencesUrlKey = "u" static let preferencesRedirectKey = "r" + static let saltKey = "s" let preferencesUrl: String - let preferencesRedirect: Int + let preferencesRedirect: RedirectMode + let salt: String? - init(preferencesUrl: String, preferencesRedirect: Int) { + init(preferencesUrl: String, preferencesRedirect: RedirectMode, salt: String?) { self.preferencesUrl = preferencesUrl self.preferencesRedirect = preferencesRedirect + self.salt = salt } static func fromJson(json: [String: Any]) -> Preferences { - Preferences(preferencesUrl: json[Preferences.preferencesUrlKey] as? String ?? "", - preferencesRedirect: json[Preferences.preferencesRedirectKey] as? Int ?? 0) + Preferences(preferencesUrl: json[self.preferencesUrlKey] as? String ?? "", + preferencesRedirect: RedirectMode(rawValue: (json[self.preferencesRedirectKey] as? Int ?? -1)) ?? .noRedirect, + salt: json[self.saltKey] as? String) } + + static let empty = Preferences(preferencesUrl: "", preferencesRedirect: .noRedirect, salt: nil) + var isEmpty: Bool { + get { + self === Preferences.empty + } + } + func toJsonMap() -> [String: Any] { [ Preferences.preferencesUrlKey: preferencesUrl, - Preferences.preferencesRedirectKey: preferencesRedirect, + Preferences.preferencesRedirectKey: preferencesRedirect.rawValue, + Preferences.saltKey: salt as Any, ] } } public final class Setting: NSObject, JsonSerializable { static let valueKey = "v" - static let percentageItemsKey = "p" - static let rolloutRulesKey = "r" + static let percentageAttributeKey = "a" + static let settingTypeKey = "t" + static let percentageOptionsKey = "p" + static let targetingRulesKey = "r" static let variationIdKey = "i" - /// Value of the feature flag / setting. - @objc public let value: Any + /// The value that is returned when none of the targeting rules or percentage options yield a result. + @objc public let value: SettingValue - /// Collection of percentage rules that belongs to the feature flag / setting. - @objc public let percentageItems: [PercentageRule] + /// The list of percentage options. + @objc public let percentageOptions: [PercentageOption] - /// Collection of targeting rules that belongs to the feature flag / setting. - @objc public let rolloutRules: [RolloutRule] + /// The list of targeting rules (where there is a logical OR relation between the items). + @objc public let targetingRules: [TargetingRule] /// Variation ID (for analytical purposes). - @objc public let variationId: String + @objc public let variationId: String? + + /// The User Object attribute which serves as the basis of percentage options evaluation. + @objc public let percentageAttribute: String + + /// The setting's type. It can be `bool`, `string`, `int` or `double`. + @objc public let settingType: SettingType + + var salt: String? - init(value: Any, variationId: String, percentageItems: [PercentageRule], rolloutRules: [RolloutRule]) { + init(value: SettingValue, variationId: String?, percentageAttribute: String, settingType: SettingType, percentageOptions: [PercentageOption], targetingRules: [TargetingRule]) { self.value = value - self.percentageItems = percentageItems - self.rolloutRules = rolloutRules + self.percentageAttribute = percentageAttribute + self.settingType = settingType self.variationId = variationId; + self.percentageOptions = percentageOptions + self.targetingRules = targetingRules } static func fromJson(json: [String: Any]) -> Setting { - let rolloutRules = json[Setting.rolloutRulesKey] as? [[String: Any]] ?? [] - let percentageRules = json[Setting.percentageItemsKey] as? [[String: Any]] ?? [] - - return Setting(value: json[Setting.valueKey] ?? "", - variationId: json[Setting.variationIdKey] as? String ?? "", - percentageItems: percentageRules.map { rule in - PercentageRule.fromJson(json: rule) - }, - rolloutRules: rolloutRules.map { rule in - RolloutRule.fromJson(json: rule) - }) + let targetingRules = json[self.targetingRulesKey] as? [[String: Any]] ?? [] + let percentageOptions = json[self.percentageOptionsKey] as? [[String: Any]] ?? [] + + return Setting(value: .fromJson(json: json[self.valueKey] as? [String: Any] ?? [:]), + variationId: json[self.variationIdKey] as? String, + percentageAttribute: json[self.percentageAttributeKey] as? String ?? ConfigCatUser.idKey, + settingType: SettingType(rawValue: (json[self.settingTypeKey] as? Int ?? -1)) ?? .unknown, + percentageOptions: percentageOptions.map { opt in + .fromJson(json: opt) + }, + targetingRules: targetingRules.map { rule in + .fromJson(json: rule) + }) } func toJsonMap() -> [String: Any] { [ - Setting.valueKey: value, - Setting.variationIdKey: variationId, - Setting.percentageItemsKey: percentageItems.map { rule in - rule.toJsonMap() + Setting.valueKey: value.toJsonMap(), + Setting.variationIdKey: variationId as Any, + Setting.percentageAttributeKey: percentageAttribute, + Setting.settingTypeKey: settingType.rawValue, + Setting.percentageOptionsKey: percentageOptions.map { opt in + opt.toJsonMap() }, - Setting.rolloutRulesKey: rolloutRules.map { rule in + Setting.targetingRulesKey: targetingRules.map { rule in rule.toJsonMap() }, ] } + + static func fromAnyValue(value: Any?) -> Setting { + switch value { + case let val as String: + return Setting(value: SettingValue(boolValue: nil, stringValue: val, doubleValue: nil, intValue: nil), variationId: nil, percentageAttribute: "", settingType: .string, percentageOptions: [], targetingRules: []) + case let val as Bool: + return Setting(value: SettingValue(boolValue: val, stringValue: nil, doubleValue: nil, intValue: nil), variationId: nil, percentageAttribute: "", settingType: .bool, percentageOptions: [], targetingRules: []) + case let val as Double: + return Setting(value: SettingValue(boolValue: nil, stringValue: nil, doubleValue: val, intValue: nil), variationId: nil, percentageAttribute: "", settingType: .double, percentageOptions: [], targetingRules: []) + case let val as Int: + return Setting(value: SettingValue(boolValue: nil, stringValue: nil, doubleValue: nil, intValue: val), variationId: nil, percentageAttribute: "", settingType: .int, percentageOptions: [], targetingRules: []) + default: + return Setting(value: SettingValue(invalidValue: value), variationId: "", percentageAttribute: "", settingType: .unknown, percentageOptions: [], targetingRules: []) + } + } } -public final class RolloutRule: NSObject, JsonSerializable { - static let valueKey = "v" - static let comparatorKey = "t" +public final class Segment: NSObject, JsonSerializable { + static let nameKey = "n" + static let conditionsKey = "r" + + /// The name of the segment. + @objc public let name: String? + + /// The list of segment rule conditions (has a logical AND relation between the items). + @objc public let conditions: [UserCondition] + + init(name: String?, conditions: [UserCondition]) { + self.name = name + self.conditions = conditions + } + + static func fromJson(json: [String: Any]) -> Segment { + let conditions = json[self.conditionsKey] as? [[String: Any]] ?? [] + + return Segment(name: json[self.nameKey] as? String, + conditions: conditions.map { cond in + .fromJson(json: cond) + }) + } + + func toJsonMap() -> [String: Any] { + [ + Segment.nameKey: name as Any, + Segment.conditionsKey: conditions.map { cond in + cond.toJsonMap() + } + ] + } +} + +public final class TargetingRule: NSObject, JsonSerializable { + static let valueKey = "s" + static let conditionsKey = "c" + static let percentageOptionsKey = "p" + + /// The value associated with the targeting rule or nil if the targeting rule has percentage options THEN part. + @objc public let servedValue: ServedValue? + + /// The list of conditions that are combined with the AND logical operator. + @objc public let conditions: [Condition] + + /// The list of percentage options associated with the targeting rule or empty if the targeting rule has a served value THEN part. + @objc public let percentageOptions: [PercentageOption] + + init(servedValue: ServedValue?, conditions: [Condition], percentageOptions: [PercentageOption]) { + self.servedValue = servedValue + self.conditions = conditions + self.percentageOptions = percentageOptions + } + + static func fromJson(json: [String: Any]) -> TargetingRule { + let conditions = json[self.conditionsKey] as? [[String: Any]] ?? [] + let percentageOptions = json[self.percentageOptionsKey] as? [[String: Any]] ?? [] + let servedValueJson = json[self.valueKey] as? [String: Any] + + return TargetingRule(servedValue: servedValueJson != nil ? .fromJson(json: servedValueJson!) : nil, + conditions: conditions.map { cond in + .fromJson(json: cond) + }, + percentageOptions: percentageOptions.map { opt in + .fromJson(json: opt) + }) + } + + func toJsonMap() -> [String: Any] { + var result: [String: Any] = [ + TargetingRule.conditionsKey: conditions.map { cond in + cond.toJsonMap() + }, + ] + if let sv = servedValue { + result[TargetingRule.valueKey] = sv.toJsonMap() + } else { + result[TargetingRule.percentageOptionsKey] = percentageOptions.map { opt in + opt.toJsonMap() + } + } + return result + } +} + +public final class Condition: NSObject, JsonSerializable { + static let userKey = "u" + static let segmentKey = "s" + static let prereqKey = "p" + + /// Describes a condition that works with User Object attributes. + @objc public let userCondition: UserCondition? + + /// Describes a condition that works with a segment. + @objc public let segmentCondition: SegmentCondition? + + /// Describes a condition that works with a prerequisite flag. + @objc public let prerequisiteFlagCondition: PrerequisiteFlagCondition? + + init(userCondition: UserCondition?, segmentCondition: SegmentCondition?, prerequisiteFlagCondition: PrerequisiteFlagCondition?) { + self.userCondition = userCondition + self.segmentCondition = segmentCondition + self.prerequisiteFlagCondition = prerequisiteFlagCondition + } + + static func fromJson(json: [String: Any]) -> Condition { + if let cond = json[self.userKey] as? [String: Any] { + return Condition(userCondition: .fromJson(json: cond), segmentCondition: nil, prerequisiteFlagCondition: nil) + } + if let cond = json[self.segmentKey] as? [String: Any] { + return Condition(userCondition: nil, segmentCondition: .fromJson(json: cond), prerequisiteFlagCondition: nil) + } + if let cond = json[self.prereqKey] as? [String: Any] { + return Condition(userCondition: nil, segmentCondition: nil, prerequisiteFlagCondition: .fromJson(json: cond)) + } + return Condition(userCondition: nil, segmentCondition: nil, prerequisiteFlagCondition: nil) + } + + func toJsonMap() -> [String: Any] { + var result: [String: Any] = [:] + if let cond = userCondition { + result[Condition.userKey] = cond.toJsonMap() + } + if let cond = segmentCondition { + result[Condition.segmentKey] = cond.toJsonMap() + } + if let cond = prerequisiteFlagCondition { + result[Condition.prereqKey] = cond.toJsonMap() + } + return result + } +} + +public final class UserCondition: NSObject, JsonSerializable { + static let stringListMaxLength = 10 + static let comparatorKey = "c" static let comparisonAttributeKey = "a" - static let comparisonValueKey = "c" - static let variationIdKey = "i" + static let stringValKey = "s" + static let doubleValKey = "d" + static let stringArrValKey = "l" - /// Value served when the rule is selected during evaluation. - @objc public let value: Any - - /// The rule's variation ID (for analytical purposes). - @objc public let variationId: String - - /// The operator used in the comparison. - /// - /// 0 -> 'IS ONE OF', - /// 1 -> 'IS NOT ONE OF', - /// 2 -> 'CONTAINS', - /// 3 -> 'DOES NOT CONTAIN', - /// 4 -> 'IS ONE OF (SemVer)', - /// 5 -> 'IS NOT ONE OF (SemVer)', - /// 6 -> '< (SemVer)', - /// 7 -> '<= (SemVer)', - /// 8 -> '> (SemVer)', - /// 9 -> '>= (SemVer)', - /// 10 -> '= (Number)', - /// 11 -> '<> (Number)', - /// 12 -> '< (Number)', - /// 13 -> '<= (Number)', - /// 14 -> '> (Number)', - /// 15 -> '>= (Number)', - /// 16 -> 'IS ONE OF (Sensitive)', - /// 17 -> 'IS NOT ONE OF (Sensitive)' - @objc public let comparator: Int - - /// The user attribute used in the comparison during evaluation. - @objc public let comparisonAttribute: String - - /// The comparison value compared to the given user attribute. - @objc public let comparisonValue: String - - init(value: Any, variationId: String, comparator: Int, comparisonAttribute: String, comparisonValue: String) { - self.value = value + /// The value that the User Object attribute is compared to, when the comparator works with a single text comparison value. + @objc public let stringValue: String? + + /// The value that the User Object attribute is compared to, when the comparator works with a numeric comparison value. + public let doubleValue: Double? + + /// The value in Objective-C format that the User Object attribute is compared to, when the comparator works with a numeric comparison value. + @objc public let doubleValueObjC: NSNumber? + + /// The value that the User Object attribute is compared to, when the comparator works with an array of text comparison value. + @objc public let stringArrayValue: [String]? + + /// The operator which defines the relation between the comparison attribute and the comparison value. + @objc public let comparator: UserComparator + + /// The User Object attribute that the condition is based on. Can be "Identifier", "Email", "Country" or any custom attribute. + @objc public let comparisonAttribute: String? + + init(stringValue: String?, doubleValue: Double?, stringArrayValue: [String]?, comparator: UserComparator, comparisonAttribute: String?) { + self.stringValue = stringValue + self.doubleValue = doubleValue + self.stringArrayValue = stringArrayValue self.comparator = comparator self.comparisonAttribute = comparisonAttribute - self.comparisonValue = comparisonValue - self.variationId = variationId; + if let val = doubleValue { + self.doubleValueObjC = NSNumber(value: val) + } else { + self.doubleValueObjC = nil + } } - static func fromJson(json: [String: Any]) -> RolloutRule { - RolloutRule(value: json[RolloutRule.valueKey] ?? "", - variationId: json[RolloutRule.variationIdKey] as? String ?? "", - comparator: json[RolloutRule.comparatorKey] as? Int ?? 0, - comparisonAttribute: json[RolloutRule.comparisonAttributeKey] as? String ?? "", - comparisonValue: json[RolloutRule.comparisonValueKey] as? String ?? "") + static func fromJson(json: [String: Any]) -> UserCondition { + UserCondition(stringValue: json[self.stringValKey] as? String, + doubleValue: json[self.doubleValKey] as? Double, + stringArrayValue: json[self.stringArrValKey] as? [String], + comparator: UserComparator(rawValue: (json[self.comparatorKey] as? Int ?? -1)) ?? .unknown, + comparisonAttribute: json[self.comparisonAttributeKey] as? String) } func toJsonMap() -> [String: Any] { [ - RolloutRule.valueKey: value, - RolloutRule.variationIdKey: variationId, - RolloutRule.comparatorKey: comparator, - RolloutRule.comparisonAttributeKey: comparisonAttribute, - RolloutRule.comparisonValueKey: comparisonValue + UserCondition.stringValKey: stringValue as Any, + UserCondition.doubleValKey: doubleValue as Any, + UserCondition.stringArrValKey: stringArrayValue as Any, + UserCondition.comparatorKey: comparator.rawValue, + UserCondition.comparisonAttributeKey: comparisonAttribute as Any, ] } + + public override var description: String { + let res = "User.\(unwrappedComparisonAttribute) \(comparatorTexts[comparator] ?? "") " + if stringValue == nil && doubleValue == nil && stringArrayValue == nil { + return res + "" + } + if let number = doubleValue { + return res + (comparator.isDateTime ? String(format: "'%.0f' (\(Date(timeIntervalSince1970: number)) UTC)", number) : "'\(String(format: "%g", number))'") + } + if let text = stringValue { + return res + "'\(comparator.isSensitive ? "" : text)'" + } + if let arr = stringArrayValue, !arr.isEmpty { + if comparator.isSensitive { + let valText = arr.count > 1 ? "values" : "value" + return res + "[<\(arr.count) hashed \(valText)>]" + } else { + let valText = arr.count - UserCondition.stringListMaxLength > 1 ? "values" : "value" + let limit = arr.count > UserCondition.stringListMaxLength ? UserCondition.stringListMaxLength : arr.count + var arrText = "" + for (index, item) in arr.enumerated() { + arrText += "'"+item+"'" + if index < limit-1 { + arrText += ", " + } else if arr.count > UserCondition.stringListMaxLength { + arrText += ", ... <\(arr.count - UserCondition.stringListMaxLength) more \(valText)>" + break + } + } + return res + "[\(arrText)]" + } + } + return res + } + + var unwrappedComparisonAttribute: String { + return comparisonAttribute ?? "" + } } -public final class PercentageRule: NSObject, JsonSerializable { +public final class SegmentCondition: NSObject, JsonSerializable { + static let indexKey = "s" + static let comparatorKey = "c" + + /// Identifies the segment that the condition is based on. + @objc public let index: Int + + /// The operator which defines the expected result of the evaluation of the segment. + @objc public let segmentComparator: SegmentComparator + + var segment: Segment? + + init(index: Int, segmentComparator: SegmentComparator) { + self.index = index + self.segmentComparator = segmentComparator + } + + static func fromJson(json: [String: Any]) -> SegmentCondition { + SegmentCondition(index: json[self.indexKey] as? Int ?? -1, + segmentComparator: SegmentComparator(rawValue: (json[self.comparatorKey] as? Int ?? -1)) ?? .unknown) + } + + func toJsonMap() -> [String: Any] { + [ + SegmentCondition.indexKey: index, + SegmentCondition.comparatorKey: segmentComparator.rawValue, + ] + } + + public override var description: String { + return "User \(segmentComparator.text) '\(segment?.name ?? "")'" + } +} + +public final class PrerequisiteFlagCondition: NSObject, JsonSerializable { + static let flagKeyKey = "f" + static let comparatorKey = "c" + static let valueKey = "v" + + /// The key of the prerequisite flag that the condition is based on. + @objc public let flagKey: String? + + /// The operator which defines the relation between the evaluated value of the prerequisite flag and the comparison value. + @objc public let prerequisiteComparator: PrerequisiteFlagComparator + + /// The evaluated value of the prerequisite flag is compared to. + @objc public let flagValue: SettingValue + + init(flagKey: String?, prerequisiteComparator: PrerequisiteFlagComparator, flagValue: SettingValue) { + self.flagKey = flagKey + self.prerequisiteComparator = prerequisiteComparator + self.flagValue = flagValue + } + + static func fromJson(json: [String: Any]) -> PrerequisiteFlagCondition { + PrerequisiteFlagCondition(flagKey: json[self.flagKeyKey] as? String, + prerequisiteComparator: PrerequisiteFlagComparator(rawValue: (json[self.comparatorKey] as? Int ?? -1)) ?? .unknown, + flagValue: .fromJson(json: json[self.valueKey] as? [String: Any] ?? [:])) + } + + func toJsonMap() -> [String: Any] { + [ + PrerequisiteFlagCondition.flagKeyKey: flagKey as Any, + PrerequisiteFlagCondition.comparatorKey: prerequisiteComparator.rawValue, + PrerequisiteFlagCondition.valueKey: flagValue.toJsonMap() + ] + } + + public override var description: String { + return "Flag '\(flagKey ?? "")' \(prerequisiteComparator.text) '\(flagValue.anyValue ?? "")'" + } +} + +enum ValueResult { + case success(Any) + case error(String) +} + +public final class SettingValue: NSObject, JsonSerializable { + static let boolKey = "b" + static let stringKey = "s" + static let doubleKey = "d" + static let intKey = "i" + + static let settingValueMissingMessage = "Setting value is missing" + + /// Holds a bool feature flag's value. + public let boolValue: Bool? + + @objc public let boolValueObjC: NSNumber? + + /// Holds a string setting's value. + @objc public let stringValue: String? + + /// Holds a decimal number setting's value. + public let doubleValue: Double? + + @objc public let doubleValueObjC: NSNumber? + + /// Holds a whole number setting's value. + public let intValue: Int? + + @objc public let intValueObjC: NSNumber? + + private let invalidValue: Any? + + init(boolValue: Bool?, stringValue: String?, doubleValue: Double?, intValue: Int?, invalidValue: Any? = nil) { + self.boolValue = boolValue + self.stringValue = stringValue + self.doubleValue = doubleValue + self.intValue = intValue + self.invalidValue = invalidValue + + if let val = boolValue { + self.boolValueObjC = NSNumber(value: val) + } else { + self.boolValueObjC = nil + } + if let val = doubleValue { + self.doubleValueObjC = NSNumber(value: val) + } else { + self.doubleValueObjC = nil + } + if let val = intValue { + self.intValueObjC = NSNumber(value: val) + } else { + self.intValueObjC = nil + } + } + + convenience init(invalidValue: Any?) { + self.init(boolValue: nil, stringValue: nil, doubleValue: nil, intValue: nil, invalidValue: invalidValue) + } + + static func fromJson(json: [String: Any]) -> SettingValue { + SettingValue(boolValue: json[self.boolKey] as? Bool, + stringValue: json[self.stringKey] as? String, + doubleValue: json[self.doubleKey] as? Double, + intValue: json[self.intKey] as? Int) + } + + func toJsonMap() -> [String: Any] { + [ + SettingValue.boolKey: boolValue as Any, + SettingValue.stringKey: stringValue as Any, + SettingValue.doubleKey: doubleValue as Any, + SettingValue.intKey: intValue as Any, + ] + } + + static func fromAnyValue(value: Any?) -> SettingValue { + switch value { + case let val as String: + return SettingValue(boolValue: nil, stringValue: val, doubleValue: nil, intValue: nil) + case let val as Bool: + return SettingValue(boolValue: val, stringValue: nil, doubleValue: nil, intValue: nil) + case let val as Double: + return SettingValue(boolValue: nil, stringValue: nil, doubleValue: val, intValue: nil) + case let val as Int: + return SettingValue(boolValue: nil, stringValue: nil, doubleValue: nil, intValue: val) + default: + return SettingValue(invalidValue: value) + } + } + + var settingType: SettingType { + if boolValue != nil { + return .bool + } + if stringValue != nil { + return .string + } + if doubleValue != nil { + return .double + } + if intValue != nil { + return .int + } + return .unknown + } + + var anyValue: Any? { + if let val = boolValue { + return val + } + if let val = stringValue { + return val + } + if let val = doubleValue { + return val + } + if let val = intValue { + return val + } + return invalidValue + } + + var isValid: Bool { + return boolValue != nil || stringValue != nil || doubleValue != nil || intValue != nil + } + + var isEmpty: Bool { + return !isValid && invalidValue == nil + } + + func toAnyChecked(settingType: SettingType) -> ValueResult { + if isEmpty { + return .error(SettingValue.settingValueMissingMessage) + } + if let inv = invalidValue { + return .error("Setting value '\(inv)' is of an unsupported type (\(type(of: inv))") + } + switch settingType { + case .bool: + guard let val = boolValue else { + return .error(SettingValue.settingValueMissingMessage) + } + return .success(val) + case .string: + guard let val = stringValue else { + return .error(SettingValue.settingValueMissingMessage) + } + return .success(val) + case .int: + guard let val = intValue else { + return .error(SettingValue.settingValueMissingMessage) + } + return .success(val) + case .double: + guard let val = doubleValue else { + return .error(SettingValue.settingValueMissingMessage) + } + return .success(val) + default: + return .error("Setting value is missing or invalid") + } + } +} + +public final class ServedValue: NSObject, JsonSerializable { + static let valueKey = "v" + static let idKey = "i" + + /// The value associated with the targeting rule. + @objc public let value: SettingValue + + /// Variation ID (for analytical purposes). + @objc public let variationId: String? + + init(value: SettingValue, variationId: String?) { + self.value = value + self.variationId = variationId + } + + static func fromJson(json: [String: Any]) -> ServedValue { + ServedValue(value: .fromJson(json: json[self.valueKey] as? [String: Any] ?? [:]), + variationId: json[self.idKey] as? String) + } + + func toJsonMap() -> [String: Any] { + [ + ServedValue.valueKey: value.toJsonMap(), + ServedValue.idKey: variationId as Any, + ] + } +} + + +public final class PercentageOption: NSObject, JsonSerializable { static let valueKey = "v" static let percentageKey = "p" static let variationIdKey = "i" - /// Value served when the rule is selected during evaluation. - @objc public let value: Any + /// The served value of the percentage option. + @objc public let servedValue: SettingValue - /// The rule's percentage value. + /// A number between 0 and 100 that represents a randomly allocated fraction of the users. @objc public let percentage: Int - /// The rule's variation ID (for analytical purposes). - @objc public let variationId: String + /// Variation ID (for analytical purposes). + @objc public let variationId: String? - init(value: Any, percentage: Int, variationId: String) { - self.value = value + init(servedValue: SettingValue, percentage: Int, variationId: String?) { + self.servedValue = servedValue self.percentage = percentage self.variationId = variationId; } - static func fromJson(json: [String: Any]) -> PercentageRule { - PercentageRule(value: json[PercentageRule.valueKey] ?? "", - percentage: json[PercentageRule.percentageKey] as? Int ?? 0, - variationId: json[PercentageRule.variationIdKey] as? String ?? "") + static func fromJson(json: [String: Any]) -> PercentageOption { + PercentageOption(servedValue: .fromJson(json: json[self.valueKey] as? [String: Any] ?? [:]), + percentage: json[self.percentageKey] as? Int ?? 0, + variationId: json[self.variationIdKey] as? String) } func toJsonMap() -> [String: Any] { [ - PercentageRule.valueKey: value, - PercentageRule.percentageKey: percentage, - PercentageRule.variationIdKey: variationId + PercentageOption.valueKey: servedValue.toJsonMap(), + PercentageOption.percentageKey: percentage, + PercentageOption.variationIdKey: variationId as Any ] } } diff --git a/Sources/ConfigCat/ConfigCatClient.swift b/Sources/ConfigCat/ConfigCatClient.swift index 35bc9ff..98f3ed3 100755 --- a/Sources/ConfigCat/ConfigCatClient.swift +++ b/Sources/ConfigCat/ConfigCatClient.swift @@ -9,9 +9,9 @@ import os.log case euOnly } -/// A client for handling configurations provided by ConfigCat. +/// ConfigCat SDK client. public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { - private let log: Logger + private let log: InternalLogger private let flagEvaluator: FlagEvaluator private let configService: ConfigService? private let sdkKey: String @@ -24,6 +24,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { init(sdkKey: String, pollingMode: PollingMode, + logger: ConfigCatLogger, httpEngine: HttpEngine?, hooks: Hooks? = nil, configCache: ConfigCache? = nil, @@ -31,15 +32,13 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { dataGovernance: DataGovernance = DataGovernance.global, flagOverrides: OverrideDataSource? = nil, defaultUser: ConfigCatUser? = nil, - logLevel: LogLevel = .warning, + logLevel: ConfigCatLogLevel = .warning, offline: Bool = false) { - assert(!sdkKey.isEmpty, "sdkKey cannot be empty") - self.sdkKey = sdkKey self.hooks = hooks ?? Hooks() self.defaultUser = defaultUser - log = Logger(level: logLevel, hooks: self.hooks) + log = InternalLogger(log: logger, level: logLevel, hooks: self.hooks) overrideDataSource = flagOverrides flagEvaluator = FlagEvaluator(log: log, evaluator: RolloutEvaluator(logger: log), hooks: self.hooks) @@ -47,6 +46,10 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { // configService is not needed in localOnly mode configService = nil hooks?.invokeOnReady(state: .hasLocalOverrideFlagDataOnly) + } else if !Utils.validateSdkKey(sdkKey: sdkKey, isCustomUrl: !baseUrl.isEmpty) { + log.error(eventId: 0, message: "ConfigCat SDK Key '\(sdkKey)' is invalid.") + configService = nil + hooks?.invokeOnReady(state: .noFlagData) } else { let fetcher = ConfigFetcher(httpEngine: httpEngine ?? URLSessionEngine(session: URLSession(configuration: URLSessionConfiguration.default)), logger: log, @@ -66,17 +69,22 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } /** - Creates a new or gets an already existing ConfigCatClient for the given sdkKey. + Creates a new or gets an already existing `ConfigCatClient` for the given sdkKey. - Parameters: - - sdkKey: the SDK Key for to communicate with the ConfigCat services. - - options: the configuration options. - - Returns: the ConfigCatClient instance. + - sdkKey: The SDK Key for to communicate with the ConfigCat services. + - options: The configuration options. + - Returns: The ConfigCatClient instance. */ @objc public static func get(sdkKey: String, options: ConfigCatOptions? = nil) -> ConfigCatClient { mutex.lock() defer { mutex.unlock() } + let isCustomUrl = !(options?.baseUrl ?? "").isEmpty + if options?.flagOverrides == nil || options?.flagOverrides?.behaviour != .localOnly { + assert(Utils.validateSdkKey(sdkKey: sdkKey, isCustomUrl: isCustomUrl), "invalid 'sdkKey' passed to the ConfigCatClient") + } + if let client = instances[sdkKey]?.get() { if options != nil { client.log.warning(eventId: 3000, message: String(format: "There is an existing client instance for the specified SDK Key. " @@ -86,18 +94,19 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } return client } - let opts = options ?? ConfigCatOptions.default + let opts = options ?? .default let client = ConfigCatClient(sdkKey: sdkKey, - pollingMode: opts.pollingMode, - httpEngine: URLSessionEngine(session: URLSession(configuration: opts.sessionConfiguration)), - hooks: opts.hooks, - configCache: opts.configCache, - baseUrl: opts.baseUrl, - dataGovernance: opts.dataGovernance, - flagOverrides: opts.flagOverrides, - defaultUser: opts.defaultUser, - logLevel: opts.logLevel, - offline: opts.offline) + pollingMode: opts.pollingMode, + logger: opts.logger, + httpEngine: URLSessionEngine(session: URLSession(configuration: opts.sessionConfiguration)), + hooks: opts.hooks, + configCache: opts.configCache, + baseUrl: opts.baseUrl, + dataGovernance: opts.dataGovernance, + flagOverrides: opts.flagOverrides, + defaultUser: opts.defaultUser, + logLevel: opts.logLevel, + offline: opts.offline) instances[sdkKey] = Weak(value: client) return client } @@ -106,9 +115,9 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { Creates a new or gets an already existing ConfigCatClient for the given sdkKey. - Parameters: - - sdkKey: the SDK Key for to communicate with the ConfigCat services. - - configurator: the configuration callback. - - Returns: the ConfigCatClient instance. + - sdkKey: The SDK Key for to communicate with the ConfigCat services. + - configurator: The configuration callback. + - Returns: The ConfigCatClient instance. */ @objc public static func get(sdkKey: String, configurator: (ConfigCatOptions) -> ()) -> ConfigCatClient { let options = ConfigCatOptions.default @@ -156,12 +165,12 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { // MARK: ConfigCatClientProtocol /** - Gets the value of a feature flag or setting identified by the given `key`. + Gets the value of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - - Parameter key: the identifier of the feature flag or setting. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter key: The identifier of the feature flag or setting. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ public func getValue(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (Value) -> ()) { assert(!key.isEmpty, "key cannot be empty") @@ -179,12 +188,12 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } /** - Gets the value and evaluation details of a feature flag or setting identified by the given `key`. + Gets the value and evaluation details of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - - Parameter key: the identifier of the feature flag or setting. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter key: The identifier of the feature flag or setting. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ public func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (TypedEvaluationDetails) -> ()) { assert(!key.isEmpty, "key cannot be empty") @@ -204,8 +213,8 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { /** Gets the values along with evaluation details of all feature flags and settings. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ @objc public func getAllValueDetails(user: ConfigCatUser? = nil, completion: @escaping ([EvaluationDetails]) -> ()) { getSettings { result in @@ -219,8 +228,9 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { guard let setting = result.settings[key] else { continue } - let details = self.flagEvaluator.evaluateRules(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) - detailsResult.append(details) + if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime, settings: result.settings) { + detailsResult.append(details) + } } completion(detailsResult) } @@ -247,19 +257,19 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { return } for (key, setting) in result.settings { - if variationId == setting.variationId { - completion(KeyValue(key: key, value: setting.value)) + if setting.settingType == .unknown { + self.log.error(eventId: 1002, message: "Error occurred in the `getKeyAndValue` method: Setting type of '\(key)' is invalid.") + completion(nil) return } - for rule in setting.rolloutRules { - if variationId == rule.variationId { - completion(KeyValue(key: key, value: rule.value)) + if let valResult = self.getValueForVariationId(variationId: variationId, setting: setting) { + switch valResult { + case .success(let val): + completion(KeyValue(key: key, value: val)) return - } - } - for rule in setting.percentageItems { - if variationId == rule.variationId { - completion(KeyValue(key: key, value: rule.value)) + case .error(let err): + self.log.error(eventId: 1002, message: "Error occurred in the `getKeyAndValue` method: \(err).") + completion(nil) return } } @@ -269,6 +279,33 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { completion(nil) } } + + func getValueForVariationId(variationId: String, setting: Setting) -> ValueResult? { + if variationId == setting.variationId { + return setting.value.toAnyChecked(settingType: setting.settingType) + } + for rule in setting.targetingRules { + if let servedValue = rule.servedValue { + if variationId == servedValue.variationId { + return servedValue.value.toAnyChecked(settingType: setting.settingType) + } + } else if !rule.percentageOptions.isEmpty { + for opt in rule.percentageOptions { + if variationId == opt.variationId { + return opt.servedValue.toAnyChecked(settingType: setting.settingType) + } + } + } else { + return .error("Targeting rule THEN part is missing or invalid") + } + } + for opt in setting.percentageOptions { + if variationId == opt.variationId { + return opt.servedValue.toAnyChecked(settingType: setting.settingType) + } + } + return nil + } /// Gets the values of all feature flags or settings asynchronously. @objc public func getAllValues(user: ConfigCatUser? = nil, completion: @escaping ([String: Any]) -> ()) { @@ -283,8 +320,9 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { guard let setting = result.settings[key] else { continue } - let details = self.flagEvaluator.evaluateRules(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime) - allValues[key] = details.value + if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime, settings: result.settings) { + allValues[key] = details.value + } } completion(allValues) } @@ -293,7 +331,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { /** Initiates a force refresh asynchronously on the cached configuration. - - Parameter completion: the function which will be called when refresh completed successfully. + - Parameter completion: The function which will be called when refresh completed successfully. */ @objc public func forceRefresh(completion: @escaping (RefreshResult) -> ()) { if let configService = configService { @@ -325,19 +363,19 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } #endif - func getSettings(completion: @escaping (SettingResult) -> Void) { + func getSettings(completion: @escaping (SettingsResult) -> Void) { if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { - completion(SettingResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast)) + completion(SettingsResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast)) return } guard let configService = configService else { - completion(SettingResult.empty) + completion(.empty) return } if let overrideDataSource = overrideDataSource { if overrideDataSource.behaviour == .localOverRemote { configService.settings { result in - completion(SettingResult(settings: result.settings.merging(overrideDataSource.getOverrides()) { (_, new) in + completion(SettingsResult(settings: result.settings.merging(overrideDataSource.getOverrides()) { (_, new) in new }, fetchTime: result.fetchTime)) } @@ -345,7 +383,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } if overrideDataSource.behaviour == .remoteOverLocal { configService.settings { result in - completion(SettingResult(settings: result.settings.merging(overrideDataSource.getOverrides()) { (current, _) in + completion(SettingsResult(settings: result.settings.merging(overrideDataSource.getOverrides()) { (current, _) in current }, fetchTime: result.fetchTime)) } @@ -357,30 +395,30 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol { } } - func getInMemorySettings() -> SettingResult { + func getInMemorySettings() -> SettingsResult { if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly { - return SettingResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast) + return SettingsResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast) } guard let configService = configService else { - return SettingResult.empty + return .empty } let inMemory = configService.inMemory if let overrideDataSource = overrideDataSource { if overrideDataSource.behaviour == .localOverRemote { - return SettingResult(settings: inMemory.config.entries.merging(overrideDataSource.getOverrides()) { (_, new) in + return SettingsResult(settings: inMemory.config.settings.merging(overrideDataSource.getOverrides()) { (_, new) in new }, fetchTime: inMemory.fetchTime) } if overrideDataSource.behaviour == .remoteOverLocal { - return SettingResult(settings: inMemory.config.entries.merging(overrideDataSource.getOverrides()) { (current, _) in + return SettingsResult(settings: inMemory.config.settings.merging(overrideDataSource.getOverrides()) { (current, _) in current }, fetchTime: inMemory.fetchTime) } } - return SettingResult(settings: inMemory.config.entries, fetchTime: inMemory.fetchTime) + return SettingsResult(settings: inMemory.config.settings, fetchTime: inMemory.fetchTime) } /// Sets the default user. diff --git a/Sources/ConfigCat/ConfigCatClientProtocol.swift b/Sources/ConfigCat/ConfigCatClientProtocol.swift index 6287a94..1aefdab 100755 --- a/Sources/ConfigCat/ConfigCatClientProtocol.swift +++ b/Sources/ConfigCat/ConfigCatClientProtocol.swift @@ -3,30 +3,30 @@ import Foundation /// Defines the public protocol of the `ConfigCatClient`. public protocol ConfigCatClientProtocol { /** - Gets the value of a feature flag or setting identified by the given `key`. + Gets the value of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - - Parameter key: the identifier of the feature flag or setting. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter key: The identifier of the feature flag or setting. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (Value) -> ()) /** - Gets the value and evaluation details of a feature flag or setting identified by the given `key`. + Gets the value and evaluation details of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - - Parameter key: the identifier of the feature flag or setting. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter key: The identifier of the feature flag or setting. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser?, completion: @escaping (TypedEvaluationDetails) -> ()) /** Gets the values along with evaluation details of all feature flags and settings. - - Parameter user: the user object to identify the caller. - - Parameter completion: the function which will be called when the feature flag or setting is evaluated. + - Parameter user: The user object to identify the caller. + - Parameter completion: The function which will be called when the feature flag or setting is evaluated. */ func getAllValueDetails(user: ConfigCatUser?, completion: @escaping ([EvaluationDetails]) -> ()) @@ -57,7 +57,7 @@ public protocol ConfigCatClientProtocol { /** Initiates a force refresh asynchronously on the cached configuration. - - Parameter completion: the function which will be called when refresh completed. + - Parameter completion: The function which will be called when refresh completed. */ func forceRefresh(completion: @escaping (RefreshResult) -> ()) @@ -70,9 +70,9 @@ public protocol ConfigCatClientProtocol { /** Gets a value asynchronously as `Value` from the configuration identified by the given `key`. - - Parameter key: the identifier of the configuration value. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. + - Parameter key: The identifier of the configuration value. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. */ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getValue(for key: String, defaultValue: Value, user: ConfigCatUser?) async -> Value @@ -80,9 +80,9 @@ public protocol ConfigCatClientProtocol { /** Gets the value and evaluation details of a feature flag or setting identified by the given `key`. - - Parameter key: the identifier of the feature flag or setting. - - Parameter defaultValue: in case of any failure, this value will be returned. - - Parameter user: the user object to identify the caller. + - Parameter key: The identifier of the feature flag or setting. + - Parameter defaultValue: In case of any failure, this value will be returned. + - Parameter user: The user object to identify the caller. */ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser?) async -> TypedEvaluationDetails @@ -90,7 +90,7 @@ public protocol ConfigCatClientProtocol { /** Gets the values along with evaluation details of all feature flags and settings. - - Parameter user: the user object to identify the caller. + - Parameter user: The user object to identify the caller. */ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func getAllValueDetails(user: ConfigCatUser?) async -> [EvaluationDetails] diff --git a/Sources/ConfigCat/ConfigCatOptions.swift b/Sources/ConfigCat/ConfigCatOptions.swift index 5a04eb4..81d31b5 100644 --- a/Sources/ConfigCat/ConfigCatOptions.swift +++ b/Sources/ConfigCat/ConfigCatOptions.swift @@ -16,7 +16,7 @@ public final class ConfigCatOptions: NSObject { @objc public var pollingMode: PollingMode = PollingModes.autoPoll() /// Custom `URLSessionConfiguration` used by the HTTP calls. - @objc public var sessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default + @objc public var sessionConfiguration: URLSessionConfiguration = .default /// The base ConfigCat CDN url. @objc public var baseUrl: String = "" @@ -25,7 +25,10 @@ public final class ConfigCatOptions: NSObject { @objc public var flagOverrides: OverrideDataSource? = nil /// Default: `LogLevel.warning`. The internal log level. - @objc public var logLevel: LogLevel = .warning + @objc public var logLevel: ConfigCatLogLevel = .warning + + /// The logger used by the SDK. + @objc public var logger: ConfigCatLogger = OSLogger() /// The default user, used as fallback when there's no user parameter is passed to the getValue() method. @objc public var defaultUser: ConfigCatUser? = nil @@ -62,12 +65,12 @@ public final class Hooks: NSObject { private var readyState: ClientReadyState? private var onReady: [(ClientReadyState) -> ()] = [] private var onFlagEvaluated: [(EvaluationDetails) -> ()] = [] - private var onConfigChanged: [([String: Setting]) -> ()] = [] + private var onConfigChanged: [(Config) -> ()] = [] private var onError: [(String) -> ()] = [] /** Subscribes a handler to the `onReady` hook. - - Parameter handler: the handler to subscribe. + - Parameter handler: The handler to subscribe. */ @objc public func addOnReady(handler: @escaping (ClientReadyState) -> ()) { mutex.lock() @@ -81,7 +84,7 @@ public final class Hooks: NSObject { /** Subscribes a handler to the `onFlagEvaluated` hook. - - Parameter handler: the handler to subscribe. + - Parameter handler: The handler to subscribe. */ @objc public func addOnFlagEvaluated(handler: @escaping (EvaluationDetails) -> ()) { mutex.lock() @@ -91,9 +94,9 @@ public final class Hooks: NSObject { /** Subscribes a handler to the `onConfigChanged` hook. - - Parameter handler: the handler to subscribe. + - Parameter handler: The handler to subscribe. */ - @objc public func addOnConfigChanged(handler: @escaping ([String: Setting]) -> ()) { + @objc public func addOnConfigChanged(handler: @escaping (Config) -> ()) { mutex.lock() defer { mutex.unlock() } onConfigChanged.append(handler) @@ -101,7 +104,7 @@ public final class Hooks: NSObject { /** Subscribes a handler to the `onError` hook. - - Parameter handler: the handler to subscribe. + - Parameter handler: The handler to subscribe. */ @objc public func addOnError(handler: @escaping (String) -> ()) { mutex.lock() @@ -118,11 +121,11 @@ public final class Hooks: NSObject { } } - func invokeOnConfigChanged(settings: [String: Setting]) { + func invokeOnConfigChanged(config: Config) { mutex.lock() defer { mutex.unlock() } for item in onConfigChanged { - item(settings); + item(config); } } diff --git a/Sources/ConfigCat/ConfigCatSnapshot.swift b/Sources/ConfigCat/ConfigCatSnapshot.swift index 018c9ae..a809b86 100644 --- a/Sources/ConfigCat/ConfigCatSnapshot.swift +++ b/Sources/ConfigCat/ConfigCatSnapshot.swift @@ -2,11 +2,11 @@ import Foundation public final class ConfigCatSnapshot: NSObject { private let flagEvaluator: FlagEvaluator - private let settingsSnapshot: SettingResult + private let settingsSnapshot: SettingsResult private let defaultUser: ConfigCatUser? - private let log: Logger + private let log: InternalLogger - init(flagEvaluator: FlagEvaluator, settingsSnapshot: SettingResult, defaultUser: ConfigCatUser?, log: Logger) { + init(flagEvaluator: FlagEvaluator, settingsSnapshot: SettingsResult, defaultUser: ConfigCatUser?, log: InternalLogger) { self.flagEvaluator = flagEvaluator self.settingsSnapshot = settingsSnapshot self.defaultUser = defaultUser @@ -14,7 +14,7 @@ public final class ConfigCatSnapshot: NSObject { } /** - Gets the value of a feature flag or setting identified by the given `key`. + Gets the value of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - Parameter key: the identifier of the feature flag or setting. - Parameter defaultValue: in case of any failure, this value will be returned. @@ -34,7 +34,7 @@ public final class ConfigCatSnapshot: NSObject { } /** - Gets the value and evaluation details of a feature flag or setting identified by the given `key`. + Gets the value and evaluation details of a feature flag or setting identified by the given `key`. The generic parameter `Value` represents the type of the desired feature flag or setting. Only the following types are allowed: `String`, `Bool`, `Int`, `Double`, `Any` (both nullable and non-nullable). - Parameter key: the identifier of the feature flag or setting. - Parameter defaultValue: in case of any failure, this value will be returned. diff --git a/Sources/ConfigCat/ConfigCatUser.swift b/Sources/ConfigCat/ConfigCatUser.swift index de94cab..90210f1 100644 --- a/Sources/ConfigCat/ConfigCatUser.swift +++ b/Sources/ConfigCat/ConfigCatUser.swift @@ -1,63 +1,105 @@ import Foundation -/// An object containing attributes to properly identify a given user for rollout evaluation. +/// User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. public final class ConfigCatUser: NSObject { - private var attributes: [String: String] + static let idKey: String = "Identifier" + static let emailKey: String = "Email" + static let countryKey: String = "Country" + + private var attributes: [String: Any] private(set) var identifier: String - + /** - Initializes a new `User`. + Initializes a new `ConfigCatUser`. + + - Parameter identifier: The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) + - Parameter email: Optional, email address of the user. + - Parameter country: Optional, country of the user. + - Parameter custom: Optional, custom attributes of the user for advanced targeting rule definitions (e.g. role, subscription type, etc.) + - Returns: A new `ConfigCatUser`. + + All comparators support `String` values as User Object attribute (in some cases they need to be provided in a specific format though, see below), + but some of them also support other types of values. It depends on the comparator how the values will be handled. The following rules apply: + + **Text-based comparators** (EQUALS, IS ONE OF, etc.) + * accept `String` values, + * all other values are automatically converted to `String` (a warning will be logged but evaluation will continue as normal). + + **SemVer-based comparators** (IS ONE OF, <, >=, etc.) + * accept `String` values containing a properly formatted, valid semver value, + * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). - - Parameter identifier: the SDK Key for to communicate with the ConfigCat services. - - Parameter email: optional, sets the email of the user. - - Parameter country: optional, sets the country of the user. - - Parameter custom: optional, sets the custom attributes of a user. - - Returns: A new `User`. + **Number-based comparators** (=, <, >=, etc.) + * accept `Int`, `UInt`, `Double`, or `Float` values, + * accept `String` values containing a properly formatted, valid `Double` value, + * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + + **Date time-based comparators** (BEFORE / AFTER) + * accept `Date` values, which are automatically converted to a second-based Unix timestamp, + * accept `Int`, `UInt`, `Double`, or `Float` values representing a second-based Unix timestamp, + * accept `String` values containing a properly formatted, valid `Double` value, + * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + + **String array-based comparators** (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF) + * accept arrays of `String`, + * accept `String` values containing a valid JSON string which can be deserialized to an array of `String`, + * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). */ @objc public init(identifier: String, email: String? = nil, country: String? = nil, - custom: [String: String]? = nil) { - + custom: [String: Any]? = nil) { + attributes = [:] self.identifier = identifier - attributes["Identifier"] = identifier - + attributes[ConfigCatUser.idKey] = identifier + if let email = email { - attributes["Email"] = email + attributes[ConfigCatUser.emailKey] = email } - + if let country = country { - attributes["Country"] = country + attributes[ConfigCatUser.countryKey] = country } - + if let custom = custom { for (key, value) in custom { - attributes[key] = value + if !ConfigCatUser.isPredefinedKey(key: key) { + attributes[key] = value + } } } } - - func getAttribute(for key: String) -> String? { + + init(custom: [String: Any]) { + self.attributes = custom + self.identifier = custom[ConfigCatUser.idKey] as? String ?? "" + } + + func attribute(for key: String) -> Any? { if key.isEmpty { assert(false, "key cannot be empty") } - - if let value = attributes[key] { - return value + guard let value = attributes[key] else { + return nil } - - return nil + return value } - + + static func isPredefinedKey(key: String) -> Bool { + return key == ConfigCatUser.idKey || key == ConfigCatUser.emailKey || key == ConfigCatUser.countryKey + } + public override var description: String { - let jsonEncoder = JSONEncoder() - jsonEncoder.outputFormatting = .prettyPrinted - do { - let jsonData = try jsonEncoder.encode(attributes) - return String(data: jsonData, encoding: .utf8) ?? "" - } catch { - return "" + var map = [String: Any]() + for (key, value) in attributes { + switch value { + case is String, is [String], is Int, is Int8, is Int16, is Int32, is Int64, is UInt, is UInt8, is UInt16, is UInt32, is UInt64, is Float, is Float32, is Float64, is Double: + map[key] = value + default: + map[key] = "\(value)" + } } + return Utils.toJson(obj: map) } } diff --git a/Sources/ConfigCat/ConfigFetcher.swift b/Sources/ConfigCat/ConfigFetcher.swift index 9ca8272..3b688c5 100755 --- a/Sources/ConfigCat/ConfigFetcher.swift +++ b/Sources/ConfigCat/ConfigFetcher.swift @@ -1,11 +1,5 @@ import Foundation -enum RedirectMode: Int { - case noRedirect - case shouldRedirect - case forceRedirect -} - enum FetchResponse: Equatable { case fetched(ConfigEntry) case notModified @@ -51,14 +45,14 @@ class URLSessionEngine: HttpEngine { } class ConfigFetcher: NSObject { - private let log: Logger + private let log: InternalLogger private let httpEngine: HttpEngine @Synced private var baseUrl: String private let mode: String private let sdkKey: String private let urlIsCustom: Bool - init(httpEngine: HttpEngine, logger: Logger, sdkKey: String, mode: String, + init(httpEngine: HttpEngine, logger: InternalLogger, sdkKey: String, mode: String, dataGovernance: DataGovernance, baseUrl: String = "") { log = logger self.httpEngine = httpEngine @@ -75,7 +69,7 @@ class ConfigFetcher: NSObject { func fetch(eTag: String, completion: @escaping (FetchResponse) -> Void) { let cachedUrl = baseUrl executeFetch(url: cachedUrl, eTag: eTag, executionCount: 2) { response in - if let newUrl = response.entry?.config.preferences?.preferencesUrl, !newUrl.isEmpty && newUrl != cachedUrl { + if let newUrl = response.entry?.config.preferences.preferencesUrl, !newUrl.isEmpty && newUrl != cachedUrl { self._baseUrl.testAndSet(expect: cachedUrl, new: newUrl) } completion(response) @@ -88,23 +82,21 @@ class ConfigFetcher: NSObject { completion(response) return } - guard let newUrl = entry.config.preferences?.preferencesUrl, !newUrl.isEmpty, newUrl != url else { - completion(response) - return - } - guard let redirect = entry.config.preferences?.preferencesRedirect else { + let newUrl = entry.config.preferences.preferencesUrl + if newUrl.isEmpty || newUrl == url { completion(response) return } - if self.urlIsCustom && redirect != RedirectMode.forceRedirect.rawValue { + let redirect = entry.config.preferences.preferencesRedirect + if self.urlIsCustom && redirect != .forceRedirect { completion(response) return } - if redirect == RedirectMode.noRedirect.rawValue { + if redirect == .noRedirect { completion(response) return } - if redirect == RedirectMode.shouldRedirect.rawValue { + if redirect == .shouldRedirect { self.log.warning(eventId: 3002, message: "The `dataGovernance` parameter specified at the client initialization is not in sync with the preferences on the ConfigCat Dashboard. " + "Read more: https://configcat.com/docs/advanced/data-governance/") } @@ -127,7 +119,7 @@ class ConfigFetcher: NSObject { self.log.error(eventId: 1102, message: message) } else { - message = String(format: "Unexpected error occurred while trying to fetch config JSON. %@", error.localizedDescription) + message = String(format: "Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP. %@", error.localizedDescription) self.log.error(eventId: 1103, message: message) } completion(.failure(message: message, isTransient: true)) diff --git a/Sources/ConfigCat/ConfigService.swift b/Sources/ConfigCat/ConfigService.swift index a4452a1..94ba104 100644 --- a/Sources/ConfigCat/ConfigService.swift +++ b/Sources/ConfigCat/ConfigService.swift @@ -1,6 +1,6 @@ import Foundation -class SettingResult { +class SettingsResult { let settings: [String: Setting] let fetchTime: Date @@ -11,11 +11,11 @@ class SettingResult { var isEmpty: Bool { get { - self === SettingResult.empty + self === SettingsResult.empty } } - static let empty = SettingResult(settings: [:], fetchTime: .distantPast) + static let empty = SettingsResult(settings: [:], fetchTime: .distantPast) } public final class RefreshResult: NSObject { @@ -34,14 +34,14 @@ enum FetchResult { } class ConfigService { - private let log: Logger + private let log: InternalLogger private let fetcher: ConfigFetcher private let mutex: Mutex = Mutex(recursive: true) private let cache: ConfigCache? private let pollingMode: PollingMode private let hooks: Hooks private let cacheKey: String - private var initialized: Bool = false + @Synced private var initialized: Bool = false private var offline: Bool = false private var completions: MutableQueue<(FetchResult) -> Void>? private var cachedEntry: ConfigEntry = .empty @@ -49,7 +49,7 @@ class ConfigService { private var pollTimer: DispatchSourceTimer? = nil private var initTimer: DispatchSourceTimer? = nil - init(log: Logger, fetcher: ConfigFetcher, cache: ConfigCache?, pollingMode: PollingMode, hooks: Hooks, sdkKey: String, offline: Bool) { + init(log: InternalLogger, fetcher: ConfigFetcher, cache: ConfigCache?, pollingMode: PollingMode, hooks: Hooks, sdkKey: String, offline: Bool) { self.log = log self.fetcher = fetcher self.cache = cache @@ -74,9 +74,8 @@ class ConfigService { this.mutex.lock() defer { this.mutex.unlock() } // Max wait time expired without result, notify subscribers with the cached config. - if !this.initialized { + if this._initialized.testAndSet(expect: false, new: true) { this.log.warning(eventId: 4200, message: String(format: "`maxInitWaitTimeInSeconds` for the very first fetch reached (%ds). Returning cached config.", autoPoll.maxInitWaitTimeInSeconds)) - this.initialized = true hooks.invokeOnReady(state: this.determineReadyState()) this.callCompletions(result: .success(this.cachedEntry)) this.completions = nil @@ -99,28 +98,28 @@ class ConfigService { initTimer?.cancel() } - func settings(completion: @escaping (SettingResult) -> Void) { + func settings(completion: @escaping (SettingsResult) -> Void) { switch pollingMode { case let lazyMode as LazyLoadingMode: - fetchIfOlder(time: Date().subtract(seconds: lazyMode.cacheRefreshIntervalInSeconds)!) { result in + fetchIfOlder(threshold: Date().subtract(seconds: lazyMode.cacheRefreshIntervalInSeconds)!) { result in switch result { case .success(let entry): completion(!entry.isEmpty - ? SettingResult(settings: entry.config.entries, fetchTime: entry.fetchTime) - : SettingResult.empty) + ? SettingsResult(settings: entry.config.settings, fetchTime: entry.fetchTime) + : .empty) case .failure(_, let entry): completion(!entry.isEmpty - ? SettingResult(settings: entry.config.entries, fetchTime: entry.fetchTime) - : SettingResult.empty) + ? SettingsResult(settings: entry.config.settings, fetchTime: entry.fetchTime) + : .empty) } } default: - fetchIfOlder(time: .distantPast, preferCache: true) { result in + fetchIfOlder(threshold: .distantPast, preferCache: initialized) { result in switch result { case .success(let entry): completion(!entry.isEmpty - ? SettingResult(settings: entry.config.entries, fetchTime: entry.fetchTime) - : SettingResult.empty) + ? SettingsResult(settings: entry.config.settings, fetchTime: entry.fetchTime) + : .empty) case .failure(_, let entry): completion(!entry.isEmpty - ? SettingResult(settings: entry.config.entries, fetchTime: entry.fetchTime) - : SettingResult.empty) + ? SettingsResult(settings: entry.config.settings, fetchTime: entry.fetchTime) + : .empty) } } } @@ -134,7 +133,7 @@ class ConfigService { return } - fetchIfOlder(time: .distantFuture) { result in + fetchIfOlder(threshold: .distantFuture) { result in switch result { case .success: completion(RefreshResult(success: true)) case .failure(let error, _): completion(RefreshResult(success: false, error: error)) @@ -189,32 +188,23 @@ class ConfigService { } } - private func fetchIfOlder(time: Date, preferCache: Bool = false, completion: @escaping (FetchResult) -> Void) { + private func fetchIfOlder(threshold: Date, preferCache: Bool = false, completion: @escaping (FetchResult) -> Void) { mutex.lock() defer { mutex.unlock() } // Sync up with the cache and use it when it's not expired. - if cachedEntry.isEmpty || cachedEntry.fetchTime > time { - let entry = readCache() - if !entry.isEmpty && entry != cachedEntry { - cachedEntry = entry - hooks.invokeOnConfigChanged(settings: entry.config.entries) - } - // Cache isn't expired - if cachedEntry.fetchTime > time { - setInitialized() - completion(.success(cachedEntry)) - return - } + let entry = readCache() + if cachedEntry.isEmpty && entry != cachedEntry { + cachedEntry = entry + hooks.invokeOnConfigChanged(config: entry.config) } - // Use cache anyway (get calls on auto & manual poll must not initiate fetch). - // The initialized check ensures that we subscribe for the ongoing fetch during the - // max init wait time window in case of auto poll. - if preferCache && initialized { + // Cache isn't expired + if cachedEntry.fetchTime > threshold { + setInitialized() completion(.success(cachedEntry)) return } - // If we are in offline mode we are not allowed to initiate fetch. - if offline { + // If we are in offline mode or the caller prefers cached values, do not initiate fetch. + if offline || preferCache { completion(.success(cachedEntry)) return } @@ -239,7 +229,7 @@ class ConfigService { case .fetched(let entry): cachedEntry = entry writeCache(entry: entry) - hooks.invokeOnConfigChanged(settings: entry.config.entries) + hooks.invokeOnConfigChanged(config: entry.config) callCompletions(result: .success(entry)) case .notModified: cachedEntry = cachedEntry.withFetchTime(time: Date()) @@ -257,8 +247,7 @@ class ConfigService { } private func setInitialized() { - if !initialized { - initialized = true + if _initialized.testAndSet(expect: false, new: true) { hooks.invokeOnReady(state: determineReadyState()) } } @@ -283,7 +272,7 @@ class ConfigService { return } this.log.debug(message: "Polling for config.json changes.") - this.fetchIfOlder(time: Date().subtract(seconds: ageThreshold)!) { _ in + this.fetchIfOlder(threshold: Date().subtract(seconds: ageThreshold)!) { _ in // we don't have to do anything with the result in the timer ticks. } }) diff --git a/Sources/ConfigCat/EvaluationDetails.swift b/Sources/ConfigCat/EvaluationDetails.swift index ac3d3f0..42fb487 100644 --- a/Sources/ConfigCat/EvaluationDetails.swift +++ b/Sources/ConfigCat/EvaluationDetails.swift @@ -1,14 +1,22 @@ import Foundation public class EvaluationDetailsBase: NSObject { + /// Key of the feature flag or setting. @objc public let key: String + /// Variation ID of the feature flag or setting (if available). @objc public let variationId: String? + /// The User Object used for the evaluation (if available). @objc public let user: ConfigCatUser? + /// Indicates whether the default value passed to the setting evaluation methods is used as the result of the evaluation. @objc public let isDefaultValue: Bool + /// Error message in case evaluation failed. @objc public let error: String? + /// Time of last successful config download. @objc public let fetchTime: Date - @objc public let matchedEvaluationRule: RolloutRule? - @objc public let matchedEvaluationPercentageRule: PercentageRule? + /// The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value. + @objc public let matchedTargetingRule: TargetingRule? + /// The percentage option (if any) that was used to select the evaluated value. + @objc public let matchedPercentageOption: PercentageOption? init(key: String, variationId: String?, @@ -16,16 +24,16 @@ public class EvaluationDetailsBase: NSObject { user: ConfigCatUser? = nil, isDefaultValue: Bool = false, error: String? = nil, - matchedEvaluationRule: RolloutRule? = nil, - matchedEvaluationPercentageRule: PercentageRule? = nil) { + matchedTargetingRule: TargetingRule? = nil, + matchedPercentageOption: PercentageOption? = nil) { self.key = key self.variationId = variationId self.user = user self.fetchTime = fetchTime self.isDefaultValue = isDefaultValue self.error = error - self.matchedEvaluationRule = matchedEvaluationRule - self.matchedEvaluationPercentageRule = matchedEvaluationPercentageRule + self.matchedTargetingRule = matchedTargetingRule + self.matchedPercentageOption = matchedPercentageOption } } @@ -39,10 +47,10 @@ public final class EvaluationDetails: EvaluationDetailsBase { user: ConfigCatUser? = nil, isDefaultValue: Bool = false, error: String? = nil, - matchedEvaluationRule: RolloutRule? = nil, - matchedEvaluationPercentageRule: PercentageRule? = nil) { + matchedTargetingRule: TargetingRule? = nil, + matchedPercentageOption: PercentageOption? = nil) { self.value = value - super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedEvaluationRule: matchedEvaluationRule, matchedEvaluationPercentageRule: matchedEvaluationPercentageRule) + super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedTargetingRule: matchedTargetingRule, matchedPercentageOption: matchedPercentageOption) } static func fromError(key: String, value: Any, error: String, user: ConfigCatUser?) -> EvaluationDetails { @@ -51,46 +59,51 @@ public final class EvaluationDetails: EvaluationDetailsBase { } public final class StringEvaluationDetails: EvaluationDetailsBase { + /// Evaluated value of the feature flag or setting. @objc public let value: String init(value: String, base: EvaluationDetailsBase) { self.value = value - super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedEvaluationRule: base.matchedEvaluationRule, matchedEvaluationPercentageRule: base.matchedEvaluationPercentageRule) + super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedTargetingRule: base.matchedTargetingRule, matchedPercentageOption: base.matchedPercentageOption) } } public final class BoolEvaluationDetails: EvaluationDetailsBase { + /// Evaluated value of the feature flag or setting. @objc public let value: Bool init(value: Bool, base: EvaluationDetailsBase) { self.value = value - super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedEvaluationRule: base.matchedEvaluationRule, matchedEvaluationPercentageRule: base.matchedEvaluationPercentageRule) + super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedTargetingRule: base.matchedTargetingRule, matchedPercentageOption: base.matchedPercentageOption) } } public final class IntEvaluationDetails: EvaluationDetailsBase { + /// Evaluated value of the feature flag or setting. @objc public let value: Int init(value: Int, base: EvaluationDetailsBase) { self.value = value - super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedEvaluationRule: base.matchedEvaluationRule, matchedEvaluationPercentageRule: base.matchedEvaluationPercentageRule) + super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedTargetingRule: base.matchedTargetingRule, matchedPercentageOption: base.matchedPercentageOption) } } public final class DoubleEvaluationDetails: EvaluationDetailsBase { + /// Evaluated value of the feature flag or setting. @objc public let value: Double init(value: Double, base: EvaluationDetailsBase) { self.value = value - super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedEvaluationRule: base.matchedEvaluationRule, matchedEvaluationPercentageRule: base.matchedEvaluationPercentageRule) + super.init(key: base.key, variationId: base.variationId, fetchTime: base.fetchTime, user: base.user, isDefaultValue: base.isDefaultValue, error: base.error, matchedTargetingRule: base.matchedTargetingRule, matchedPercentageOption: base.matchedPercentageOption) } } public final class TypedEvaluationDetails: EvaluationDetailsBase { + /// Evaluated value of the feature flag or setting. public let value: Value init(key: String, @@ -100,13 +113,13 @@ public final class TypedEvaluationDetails: EvaluationDetailsBase { user: ConfigCatUser? = nil, isDefaultValue: Bool = false, error: String? = nil, - matchedEvaluationRule: RolloutRule? = nil, - matchedEvaluationPercentageRule: PercentageRule? = nil) { + matchedTargetingRule: TargetingRule? = nil, + matchedPercentageOption: PercentageOption? = nil) { self.value = value - super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedEvaluationRule: matchedEvaluationRule, matchedEvaluationPercentageRule: matchedEvaluationPercentageRule) + super.init(key: key, variationId: variationId, fetchTime: fetchTime, user: user, isDefaultValue: isDefaultValue, error: error, matchedTargetingRule: matchedTargetingRule, matchedPercentageOption: matchedPercentageOption) } - static func fromError(key: String, value: Value, error: String, user: ConfigCatUser?) -> TypedEvaluationDetails { + static func fromError(key: String, value: Value, error: String, user: ConfigCatUser?) -> TypedEvaluationDetails { TypedEvaluationDetails(key: key, value: value, variationId: "", user: user, isDefaultValue: true, error: error) } diff --git a/Sources/ConfigCat/EvaluationLogger.swift b/Sources/ConfigCat/EvaluationLogger.swift new file mode 100644 index 0000000..24bfdcd --- /dev/null +++ b/Sources/ConfigCat/EvaluationLogger.swift @@ -0,0 +1,66 @@ +import Foundation + +class EvaluationLogger { + static let newLineChar: String = "\n" + static let indentSeq: String = " " + + var content: String = "" + var indent: Int = 0 + + @discardableResult + func resetIndent() -> EvaluationLogger { + indent = 0 + return self + } + + @discardableResult + func incIndent() -> EvaluationLogger { + indent += 1 + return self + } + + @discardableResult + func decIndent() -> EvaluationLogger { + indent -= 1 + return self + } + + @discardableResult + func newLine(msg: String? = nil) -> EvaluationLogger { + content += EvaluationLogger.newLineChar + String(repeating: EvaluationLogger.indentSeq, count: indent) + if let message = msg { + content += message + } + return self + } + + @discardableResult + func append(value: Any) -> EvaluationLogger { + content += "\(value)" + return self + } + + @discardableResult + func appendThen(newLine: Bool, result: EvalConditionResult, targetingRule: TargetingRule) -> EvaluationLogger { + self.incIndent() + + if newLine { + self.newLine() + } else { + self.append(value: " ") + } + self.append(value: "THEN") + + if let sv = targetingRule.servedValue { + self.append(value: " '\(sv.value.anyValue ?? RolloutEvaluator.invalidValueText)'") + } else { + self.append(value: " % options") + } + + self.append(value: " => ").append(value: result.isSuccess ? result.isMatch ? "MATCH, applying rule" : "no match" : result.err) + + self.decIndent() + + return self + } +} diff --git a/Sources/ConfigCat/Extensions.swift b/Sources/ConfigCat/Extensions.swift index 15775c9..40587fb 100644 --- a/Sources/ConfigCat/Extensions.swift +++ b/Sources/ConfigCat/Extensions.swift @@ -30,8 +30,8 @@ extension ConfigCatClient { user: user, isDefaultValue: details.isDefaultValue, error: details.error, - matchedEvaluationRule: details.matchedEvaluationRule, - matchedEvaluationPercentageRule: details.matchedEvaluationPercentageRule)) + matchedTargetingRule: details.matchedTargetingRule, + matchedPercentageOption: details.matchedPercentageOption)) } } @@ -68,6 +68,15 @@ extension ConfigCatClient { } } } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getAnyValue(for key: String, defaultValue: Any?, user: ConfigCatUser? = nil) async -> Any? { + await withCheckedContinuation { continuation in + getValue(for: key, defaultValue: defaultValue, user: user) { value in + continuation.resume(returning: value) + } + } + } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func getValueDetails(for key: String, defaultValue: Value, user: ConfigCatUser? = nil) async -> TypedEvaluationDetails { @@ -77,6 +86,15 @@ extension ConfigCatClient { } } } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func getAnyValueDetails(for key: String, defaultValue: Any?, user: ConfigCatUser? = nil) async -> TypedEvaluationDetails { + await withCheckedContinuation { continuation in + getValueDetails(for: key, defaultValue: defaultValue, user: user) { details in + continuation.resume(returning: details) + } + } + } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func getAllValueDetails(user: ConfigCatUser? = nil) async -> [EvaluationDetails] { @@ -156,8 +174,8 @@ extension ConfigCatSnapshot { user: user, isDefaultValue: details.isDefaultValue, error: details.error, - matchedEvaluationRule: details.matchedEvaluationRule, - matchedEvaluationPercentageRule: details.matchedEvaluationPercentageRule) + matchedTargetingRule: details.matchedTargetingRule, + matchedPercentageOption: details.matchedPercentageOption) } @objc public func getStringValueDetails(for key: String, defaultValue: String, user: ConfigCatUser?) -> StringEvaluationDetails { diff --git a/Sources/ConfigCat/FlagEvaluator.swift b/Sources/ConfigCat/FlagEvaluator.swift index ef4b6b2..c3a71ba 100644 --- a/Sources/ConfigCat/FlagEvaluator.swift +++ b/Sources/ConfigCat/FlagEvaluator.swift @@ -1,11 +1,11 @@ import Foundation class FlagEvaluator { - private let log: Logger + private let log: InternalLogger private let evaluator: RolloutEvaluator private let hooks: Hooks - init(log: Logger, evaluator: RolloutEvaluator, hooks: Hooks) { + init(log: InternalLogger, evaluator: RolloutEvaluator, hooks: Hooks) { self.log = log self.evaluator = evaluator self.hooks = hooks @@ -22,7 +22,7 @@ class FlagEvaluator { of != Bool?.self && of != Any.self && of != Any?.self { - let message = "Only String, Integer, Double, Bool or Any types are supported." + let message = "Only the following types are supported: String, Int, Double, Bool, and Any (both nullable and non-nullable)." log.error(eventId: 2022, message: message) hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, @@ -34,7 +34,7 @@ class FlagEvaluator { return nil } - func evaluateFlag(result: SettingResult, key: String, defaultValue: Value, user: ConfigCatUser?) -> TypedEvaluationDetails { + func evaluateFlag(result: SettingsResult, key: String, defaultValue: Value, user: ConfigCatUser?) -> TypedEvaluationDetails { if result.settings.isEmpty { let message = String(format: "Config JSON is not present when evaluating setting '%@'. Returning the `defaultValue` parameter that you specified in your application: '%@'.", key, "\(defaultValue)") @@ -57,43 +57,61 @@ class FlagEvaluator { user: user)) return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) } - - let evaluationResult = self.evaluateRules(for: setting, key: key, user: user, fetchTime: result.fetchTime) - - guard let typedValue = evaluationResult.value as? Value else { - let message = String(format: "Failed to evaluate setting '%@' (the value '%@' cannot be converted to the requested type). " - + "Returning the `defaultValue` parameter that you specified in your application: '%@'.", - key, "\(evaluationResult.value)", "\(defaultValue)") - self.log.error(eventId: 2002, message: message) + let evaluationResult = evaluator.evaluate(setting: setting, key: key, user: user, settings: result.settings, defaultValue: defaultValue) + switch evaluationResult { + case .success(let value, let variationId, let rule, let option): + guard let typedValue = value as? Value else { + let message = "The type of a setting must match the type of the specified default value. Setting's type was \(setting.settingType.text) but the default value's type was \(Value.self). Please use a default value which corresponds to the setting type \(setting.settingType.text). Learn more: https://configcat.com/docs/sdk-reference/ios/#setting-type-mapping" + self.log.error(eventId: 2002, message: message) + self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, + value: defaultValue, + error: message, + user: user)) + return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) + } + + hooks.invokeOnFlagEvaluated(details: EvaluationDetails(key: key, + value: typedValue, + variationId: variationId, + fetchTime: result.fetchTime, + user: user, + matchedTargetingRule: rule, + matchedPercentageOption: option)) + return TypedEvaluationDetails(key: key, + value: typedValue, + variationId: variationId, + fetchTime: result.fetchTime, + user: user, + matchedTargetingRule: rule, + matchedPercentageOption: option) + case .error(let err): + let message = "Failed to evaluate setting '\(key)' (\(err))" + self.log.error(eventId: 1002, message: message) self.hooks.invokeOnFlagEvaluated(details: EvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user)) return TypedEvaluationDetails.fromError(key: key, value: defaultValue, error: message, user: user) } - - return TypedEvaluationDetails(key: key, - value: typedValue, - variationId: evaluationResult.variationId ?? "", - fetchTime: result.fetchTime, - user: user, - matchedEvaluationRule: evaluationResult.matchedEvaluationRule, - matchedEvaluationPercentageRule: evaluationResult.matchedEvaluationPercentageRule) } - func evaluateRules(for setting: Setting, key: String, user: ConfigCatUser?, fetchTime: Date) -> EvaluationDetails { - let (value, variationId, evaluateLog, rolloutRule, percentageRule): (Any, String?, String?, RolloutRule?, PercentageRule?) = evaluator.evaluate(setting: setting, key: key, user: user) - if let evaluateLog = evaluateLog { - log.info(eventId: 5000, message: evaluateLog) + func evaluateFlag(for setting: Setting, key: String, user: ConfigCatUser?, fetchTime: Date, settings: [String: Setting]) -> EvaluationDetails? { + let evaluationResult = evaluator.evaluate(setting: setting, key: key, user: user, settings: settings, defaultValue: nil) + switch evaluationResult { + case .success(let value, let variationId, let rule, let option): + let details = EvaluationDetails(key: key, + value: value, + variationId: variationId, + fetchTime: fetchTime, + user: user, + matchedTargetingRule: rule, + matchedPercentageOption: option) + hooks.invokeOnFlagEvaluated(details: details) + return details + case .error(let err): + let message = "Failed to evaluate setting '\(key)' (\(err))" + self.log.error(eventId: 1002, message: message) + return nil } - let details = EvaluationDetails(key: key, - value: value, - variationId: variationId, - fetchTime: fetchTime, - user: user, - matchedEvaluationRule: rolloutRule, - matchedEvaluationPercentageRule: percentageRule) - hooks.invokeOnFlagEvaluated(details: details) - return details } } diff --git a/Sources/ConfigCat/LocalDictionaryDataSource.swift b/Sources/ConfigCat/LocalDictionaryDataSource.swift index 992231c..a995ec0 100755 --- a/Sources/ConfigCat/LocalDictionaryDataSource.swift +++ b/Sources/ConfigCat/LocalDictionaryDataSource.swift @@ -6,7 +6,7 @@ public class LocalDictionaryDataSource: OverrideDataSource { @objc public init(source: [String: Any], behaviour: OverrideBehaviour) { super.init(behaviour: behaviour) for (key, value) in source { - settings[key] = Setting(value: value, variationId: "", percentageItems: [], rolloutRules: []) + settings[key] = Setting.fromAnyValue(value: value) } } diff --git a/Sources/ConfigCat/Log.swift b/Sources/ConfigCat/Log.swift index 0e629a9..a1ae6ba 100644 --- a/Sources/ConfigCat/Log.swift +++ b/Sources/ConfigCat/Log.swift @@ -2,7 +2,7 @@ import os.log import os import Foundation -@objc public enum LogLevel: Int { +@objc public enum ConfigCatLogLevel: Int { case debug case info case warning @@ -10,52 +10,89 @@ import Foundation case nolog } -class Logger { - static let noLogger: Logger = Logger(level: .nolog, hooks: Hooks()) - private static let log: OSLog = OSLog(subsystem: "com.configcat", category: "main") - private let level: LogLevel - private let hooks: Hooks +@objc public protocol ConfigCatLogger { + func debug(message: String) + func warning(message: String) + func info(message: String) + func error(message: String) +} - init(level: LogLevel, hooks: Hooks) { +class InternalLogger { + static let noLogger: InternalLogger = InternalLogger(log: NoLogger(), level: .nolog, hooks: Hooks()) + let log: ConfigCatLogger + let level: ConfigCatLogLevel + let hooks: Hooks + + init(log: ConfigCatLogger, level: ConfigCatLogLevel, hooks: Hooks) { + self.log = log self.level = level self.hooks = hooks } - + func debug(message: String) { - log(message: "[0] \(message)", currentLevel: .debug) + if ConfigCatLogLevel.debug.rawValue >= level.rawValue { + log.debug(message: "[0] \(message)") + } } func warning(eventId: Int, message: String) { - log(message: "[\(eventId)] \(message)", currentLevel: .warning) + if ConfigCatLogLevel.warning.rawValue >= level.rawValue { + log.warning(message: "[\(eventId)] \(message)") + } } func info(eventId: Int, message: String) { - log(message: "[\(eventId)] \(message)", currentLevel: .info) + if ConfigCatLogLevel.info.rawValue >= level.rawValue { + log.info(message: "[\(eventId)] \(message)") + } } func error(eventId: Int, message: String) { hooks.invokeOnError(error: message) - log(message: "[\(eventId)] \(message)", currentLevel: .error) + if ConfigCatLogLevel.error.rawValue >= level.rawValue { + log.error(message: "[\(eventId)] \(message)") + } + } + + func enabled(level: ConfigCatLogLevel) -> Bool { + return level.rawValue >= self.level.rawValue } +} - func log(message: String, currentLevel: LogLevel) { - if currentLevel.rawValue >= level.rawValue { - os_log("%{public}@", log: Logger.log, type: getLogType(level: currentLevel), message) - } +class OSLogger: ConfigCatLogger { + private static let log: OSLog = OSLog(subsystem: "com.configcat", category: "main") + + func debug(message: String) { + os_log("%{public}@", log: OSLogger.log, type: .debug, message) + } + + func warning(message: String) { + os_log("%{public}@", log: OSLogger.log, type: .info, message) + } + + func info(message: String) { + os_log("%{public}@", log: OSLogger.log, type: .info, message) } + + func error(message: String) { + os_log("%{public}@", log: OSLogger.log, type: .error, message) + } +} - func getLogType(level: LogLevel) -> OSLogType { - switch level { - case .debug: - return OSLogType.debug - case .error: - return OSLogType.error - case .warning: - return OSLogType.info - case .info: - return OSLogType.info - default: - return OSLogType.default - } +class NoLogger: ConfigCatLogger { + func debug(message: String) { + // do nothing + } + + func warning(message: String) { + // do nothing + } + + func info(message: String) { + // do nothing + } + + func error(message: String) { + // do nothing } } diff --git a/Sources/ConfigCat/Resources/PrivacyInfo.xcprivacy b/Sources/ConfigCat/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..62fc53c --- /dev/null +++ b/Sources/ConfigCat/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + + + NSPrivacyTrackingDomains + + + diff --git a/Sources/ConfigCat/RolloutEvaluator.swift b/Sources/ConfigCat/RolloutEvaluator.swift index 7aae4ed..3a121c8 100644 --- a/Sources/ConfigCat/RolloutEvaluator.swift +++ b/Sources/ConfigCat/RolloutEvaluator.swift @@ -6,311 +6,776 @@ import os.log import Version #endif -class RolloutEvaluator { - private static let comparatorTexts = [ - "IS ONE OF", - "IS NOT ONE OF", - "CONTAINS", - "DOES NOT CONTAIN", - "IS ONE OF (SemVer)", - "IS NOT ONE OF (SemVer)", - "< (SemVer)", - "<= (SemVer)", - "> (SemVer)", - ">= (SemVer)", - "= (Number)", - "<> (Number)", - "< (Number)", - "<= (Number)", - "> (Number)", - ">= (Number", - "IS ONE OF (Sensitive)", - "IS NOT ONE OF (Sensitive)", - ] - private let log: Logger; - - - init(logger: Logger) { - log = logger +enum EvalConditionResult { + case success(Bool) + case noUser + case attributeMissing(UserCondition) + case attributeInvalid(String, UserCondition) + case compValueInvalid(String?) + case fatal(String) + + var isSuccess: Bool { + if case .success(_) = self { + return true + } + return false } - - - func evaluate(setting: Setting, key: String, user: ConfigCatUser?) -> (value: Any, variationId: String?, evaluateLog: String?, rollout: RolloutRule?, percentage: PercentageRule?) { - guard let user = user else { - if setting.rolloutRules.count > 0 || setting.percentageItems.count > 0 { - log.warning(eventId: 3001, message: String(format: "Cannot evaluate targeting rules and %% options for setting '%@' (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/", - key)) - } - - return (setting.value, setting.variationId, nil, nil, nil) + + var isMatch: Bool { + if case .success(let match) = self { + return match } + return false + } + + var isAttributeMissing: Bool { + if case .attributeMissing(_) = self { + return true + } + return false + } + + var err: String { + switch self { + case .success(_): + return "" + case .noUser: + return "cannot evaluate, User Object is missing" + case .attributeMissing(let cond): + return "cannot evaluate, the User.\(cond.unwrappedComparisonAttribute) attribute is missing" + case .attributeInvalid(let reason, let cond): + return "cannot evaluate, the User.\(cond.unwrappedComparisonAttribute) attribute is invalid (\(reason))" + case .compValueInvalid(let err): + return "cannot evaluate, (\(err ?? "comparison value is missing or invalid"))" + case .fatal(let err): + return "cannot evaluate (\(err))" + } + } +} - var evaluateLog = String(format: "Evaluating getValue(%@).\nUser object: %@.", key, user) - - for rule in setting.rolloutRules { - let comparisonAttribute = rule.comparisonAttribute - let comparisonValue = rule.comparisonValue - let comparator = rule.comparator - - if let userValue = user.getAttribute(for: comparisonAttribute) { - if comparisonValue.isEmpty || userValue.isEmpty { - evaluateLog += "\n" + formatNoMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue) - continue - } - - let ruleValue = rule.value - let ruleVariationId = rule.variationId +enum EvalPercentageResult { + case success(PercentageOption) + case userAttrMissing(String) + case fatal(String) +} - switch comparator { - // IS ONE OF - case 0: - let split = comparisonValue.components(separatedBy: ",") - .map { val in - val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - } +enum EvalResult { + case success(Any, String?, TargetingRule?, PercentageOption?) + case error(String) +} - if split.contains(userValue) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - // IS NOT ONE OF - case 1: - let split = comparisonValue.components(separatedBy: ",") - .map { val in - val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - } +class RolloutEvaluator { + static let ruleIgnoredMessage = "The current targeting rule is ignored and the evaluation continues with the next rule." + static let saltMissingMessage = "Config JSON salt is missing" + static let invalidValueText = "" + private let log: InternalLogger; - if !split.contains(userValue) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - // CONTAINS - case 2: - if userValue.contains(comparisonValue) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - // DOES NOT CONTAIN - case 3: - if !userValue.contains(comparisonValue) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - // IS ONE OF (Semantic version), IS NOT ONE OF (Semantic version) - case 4...5: - let split = comparisonValue.components(separatedBy: ",") - .map { val in - val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - } - .filter { val -> Bool in - !val.isEmpty - } + init(logger: InternalLogger) { + log = logger + } - // The rule will be ignored if we found an invalid semantic version - if let invalidValue = (split.first { val -> Bool in - Version(val) == nil - }) { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(invalidValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - if Version(userValue) == nil { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(userValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue + func evaluate(setting: Setting, key: String, user: ConfigCatUser?, settings: [String: Setting], defaultValue: Any?) -> EvalResult { + let evalLogger = log.enabled(level: .info) ? EvaluationLogger() : nil + var cycleTracker: [String] = [] + + evalLogger?.append(value: "Evaluating '\(key)'") + if let usr = user { + evalLogger?.append(value: " for User '\(usr.description)'") + } + evalLogger?.incIndent() + + let result = setting.settingType == .unknown ? .error("Setting type is invalid") : evalSetting(setting: setting, key: key, user: user, evalLogger: evalLogger, settings: settings, cycleTracker: &cycleTracker) + + if case .success(let val, _, _, _) = result { + evalLogger?.newLine(msg: "Returning '\(val)'.") + } else { + evalLogger?.resetIndent().incIndent() + evalLogger?.newLine(msg: "Returning '\(defaultValue ?? "nil")'.") + } + + evalLogger?.decIndent() + if log.enabled(level: .info) { + log.info(eventId: 5000, message: evalLogger?.content ?? "") + } + + return result + } + + private func evalSetting(setting: Setting, key: String, user: ConfigCatUser?, evalLogger: EvaluationLogger?, settings: [String: Setting], cycleTracker: inout [String]) -> EvalResult { + var userMissingLogged = false + + if !setting.targetingRules.isEmpty { + evalLogger?.newLine(msg: "Evaluating targeting rules and applying the first match if any:") + for rule in setting.targetingRules { + let result = evalConditions(targetingRule: rule, key: key, user: user, salt: setting.salt, ctxSalt: key, evalLogger: evalLogger, settings: settings, cycleTracker: &cycleTracker) + if !result.isSuccess { + evalLogger?.incIndent().newLine(msg: RolloutEvaluator.ruleIgnoredMessage).decIndent() + } + switch result { + case .success(true): + if let servedValue = rule.servedValue { + return evalResult(value: servedValue.value, settingType: setting.settingType, variationId: servedValue.variationId, rule: rule, opt: nil) } - - if comparator == 4 { // IS ONE OF - if Version(userValue) == nil { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(userValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - - if let userValueVersion = Version(userValue) { - if (split.first { val -> Bool in - userValueVersion == Version(val) - } != nil) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) + evalLogger?.incIndent() + if !rule.percentageOptions.isEmpty { + if let usr = user { + let percResult = evalPercentageOptions(opts: rule.percentageOptions, user: usr, key: key, percentageAttr: setting.percentageAttribute, evalLogger: evalLogger) + switch percResult { + case .success(let opt): + evalLogger?.decIndent() + return evalResult(value: opt.servedValue, settingType: setting.settingType, variationId: opt.variationId, rule: rule, opt: opt) + case .userAttrMissing(let attr): + logAttributeMissing(key: key, attr: attr) + case .fatal(let err): + return .error(err) } - } - } else { // IS NOT ONE OF - if Version(userValue) == nil { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(userValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - - if let userValueVersion = Version(userValue) { - if let invalidValue = (split.first { val -> Bool in - userValueVersion == Version(val) - }) { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(invalidValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - } - // LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version) - case 6...9: - let comparison = comparisonValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - if Version(userValue) == nil { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(userValue)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - - if Version(comparison) == nil { - let message = formatValidationErrorRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, - error: "Invalid semantic version: \(comparison)") - log.warning(eventId: 0, message: message) - evaluateLog += "\n" + message - continue - } - if let userValueVersion = Version(userValue), - let comparisonValueVersion = Version(comparison) { - let userValueVersionWithoutMetadata = Version(userValueVersion.major, - userValueVersion.minor, - userValueVersion.patch, - pre: userValueVersion.prereleaseIdentifiers) - let comparisonValueVersionWithoutMetadata = Version(comparisonValueVersion.major, - comparisonValueVersion.minor, - comparisonValueVersion.patch, - pre: comparisonValueVersion.prereleaseIdentifiers) - if (comparator == 6 && userValueVersionWithoutMetadata < comparisonValueVersionWithoutMetadata) - || (comparator == 7 && userValueVersionWithoutMetadata <= comparisonValueVersionWithoutMetadata) - || (comparator == 8 && userValueVersionWithoutMetadata > comparisonValueVersionWithoutMetadata) - || (comparator == 9 && userValueVersionWithoutMetadata >= comparisonValueVersionWithoutMetadata) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - } - case 10...15: - if let userValueFloat = Float(userValue.replacingOccurrences(of: ",", with: ".")), - let comparisonValueFloat = Float(comparisonValue.replacingOccurrences(of: ",", with: ".")) { - if (comparator == 10 && userValueFloat == comparisonValueFloat) - || (comparator == 11 && userValueFloat != comparisonValueFloat) - || (comparator == 12 && userValueFloat < comparisonValueFloat) - || (comparator == 13 && userValueFloat <= comparisonValueFloat) - || (comparator == 14 && userValueFloat > comparisonValueFloat) - || (comparator == 15 && userValueFloat >= comparisonValueFloat) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } - } - // IS ONE OF (Sensitive) - case 16: - let split = comparisonValue.components(separatedBy: ",") - .map { val in - val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } else { + if !userMissingLogged { + logUserObjectMissing(key: key) + userMissingLogged = true } - - if let userValueHash = userValue.sha1hex { - if split.contains(userValueHash) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValueHash, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) + evalLogger?.newLine(msg: "Skipping % options because the User Object is missing.") } + } else { + return .error("Targeting rule THEN part is missing or invalid") } - // IS NOT ONE OF (Sensitive) - case 17: - let split = comparisonValue.components(separatedBy: ",") - .map { val in - val.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - } - - if let userValueHash = userValue.sha1hex { - if !split.contains(userValueHash) { - let returnValue = ruleValue - evaluateLog += "\n" + formatMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValueHash, comparator: comparator, comparisonValue: comparisonValue, value: returnValue) - return (returnValue, ruleVariationId, evaluateLog, rule, nil) - } + evalLogger?.newLine(msg: RolloutEvaluator.ruleIgnoredMessage).decIndent() + case .success(false): + continue + case .fatal(let err): + return .error(err) + case .noUser: + if !userMissingLogged { + logUserObjectMissing(key: key) + userMissingLogged = true } - default: continue + case .attributeMissing(let cond): + logAttributeMissing(key: key, cond: cond) + continue + case .attributeInvalid(let reason, let cond): + logAttributeInvalid(key: key, reason: reason, cond: cond) + continue + case .compValueInvalid(let err): + return .error(err ?? "Comparison value is missing or invalid") } - - evaluateLog += "\n" + formatNoMatchRule(comparisonAttribute: comparisonAttribute, userValue: userValue, comparator: comparator, comparisonValue: comparisonValue) } } - - if (setting.percentageItems.count > 0) { - let hashCandidate = key + user.identifier - if let hash = hashCandidate.sha1hex?.prefix(7) { - let hashString = String(hash) - if let num = Int(hashString, radix: 16) { - let scaled = num % 100 - - var bucket = 0 - for rule in setting.percentageItems { - bucket += rule.percentage - if scaled < bucket { - evaluateLog += "\n" + String(format: "Evaluating %@ options. Returning %@", "%", rule.value as? String ?? "") - return (rule.value, rule.variationId, evaluateLog, nil, rule) - } - } + + if !setting.percentageOptions.isEmpty { + if let usr = user { + let percResult = evalPercentageOptions(opts: setting.percentageOptions, user: usr, key: key, percentageAttr: setting.percentageAttribute, evalLogger: evalLogger) + switch percResult { + case .success(let opt): + return evalResult(value: opt.servedValue, settingType: setting.settingType, variationId: opt.variationId, rule: nil, opt: opt) + case .userAttrMissing(let attr): + logAttributeMissing(key: key, attr: attr) + case .fatal(let err): + return .error(err) } + } else { + if !userMissingLogged { + logUserObjectMissing(key: key) + } + evalLogger?.newLine(msg: "Skipping % options because the User Object is missing.") } } - - evaluateLog += "\n" + String(format: "Returning %@", setting.value as? String ?? "") - return (setting.value, setting.variationId, evaluateLog, nil, nil) + return evalResult(value: setting.value, settingType: setting.settingType, variationId: setting.variationId, rule: nil, opt: nil) } - - private func formatMatchRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String, value: Any) -> String { - let format = String(format: "Evaluating rule: [%@:%@] [%@] [%@] => match, returning: ", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) - - return format + "\(value)" + + private func evalResult(value: SettingValue, settingType: SettingType, variationId: String?, rule: TargetingRule?, opt: PercentageOption?) -> EvalResult { + let valResult = value.toAnyChecked(settingType: settingType) + switch valResult { + case .success(let val): + return .success(val, variationId, rule, opt) + case .error(let err): + return .error(err) + } } - - private func formatNoMatchRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String) -> String { - String(format: "Evaluating rule: [%@:%@] [%@] [%@] => no match", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue) + + private func evalPercentageOptions(opts: [PercentageOption], user: ConfigCatUser, key: String, percentageAttr: String, evalLogger: EvaluationLogger?) -> EvalPercentageResult { + guard let attrVal = user.attribute(for: percentageAttr) else { + evalLogger?.newLine(msg: "Skipping % options because the User.\(percentageAttr) attribute is missing.") + return .userAttrMissing(percentageAttr) + } + evalLogger?.newLine(msg: "Evaluating % options based on the User.\(percentageAttr) attribute:") + let (stringAttrVal, _) = asString(value: attrVal) + let hashCandidate = key + stringAttrVal + let hash = hashCandidate.sha1hex.prefix(7) + let hashString = String(hash) + if let num = Int(hashString, radix: 16) { + let scaled = num % 100 + evalLogger?.newLine(msg: "- Computing hash in the [0..99] range from User.\(percentageAttr) => \(scaled) (this value is sticky and consistent across all SDKs)") + var bucket = 0 + for (index, opt) in opts.enumerated() { + bucket += opt.percentage + if scaled < bucket { + evalLogger?.newLine(msg: "- Hash value \(scaled) selects % option \(index + 1) (\(opt.percentage)%), '\(opt.servedValue.anyValue ?? RolloutEvaluator.invalidValueText)'.") + return .success(opt) + } + } + } + return .fatal("Sum of percentage option percentages is less than 100") + } + + private func evalConditions(targetingRule: TargetingRule, key: String, user: ConfigCatUser?, salt: String?, ctxSalt: String, evalLogger: EvaluationLogger?, settings: [String: Setting], cycleTracker: inout [String]) -> EvalConditionResult { + evalLogger?.newLine(msg: "- ") + var newLineBeforeThen = false + + for (index, cond) in targetingRule.conditions.enumerated() { + var condResult: EvalConditionResult = .fatal("Condition isn't a type of user, segment, or prerequisite flag condition") + var matched = true + if index == 0 { + evalLogger?.append(value: "IF ").incIndent() + } else { + evalLogger?.incIndent().newLine(msg: "AND ") + } + + if let userCondition = cond.userCondition { + evalLogger?.append(value: userCondition) + if let usr = user { + condResult = evalUserCondition(cond: userCondition, key: key, user: usr, salt: salt, ctxSalt: ctxSalt) + } else { + condResult = .noUser + } + newLineBeforeThen = targetingRule.conditions.count > 1 + } else if let segmentCondition = cond.segmentCondition { + evalLogger?.append(value: segmentCondition) + if let usr = user { + condResult = evalSegmentCondition(cond: segmentCondition, key: key, salt: salt, user: usr, evalLogger: evalLogger) + } else { + condResult = .noUser + } + newLineBeforeThen = condResult.isSuccess || condResult.isAttributeMissing || targetingRule.conditions.count > 1 + } else if let prerequisiteFlagCondition = cond.prerequisiteFlagCondition { + condResult = evalPrerequisiteCondition(cond: prerequisiteFlagCondition, key: key, user: user, evalLogger: evalLogger, settings: settings, cycleTracker: &cycleTracker) + newLineBeforeThen = true + } + + if targetingRule.conditions.count > 1 { + evalLogger?.append(value: " => ").append(value: condResult.isMatch ? "true" : "false").append(value: condResult.isMatch ? "" : ", skipping the remaining AND conditions") + } + + evalLogger?.decIndent() + + if case .success(let match) = condResult { + matched = match + } else { + matched = false + } + + if !matched { + evalLogger?.appendThen(newLine: newLineBeforeThen, result: condResult, targetingRule: targetingRule) + return condResult + } + } + evalLogger?.appendThen(newLine: newLineBeforeThen, result: .success(true), targetingRule: targetingRule) + return .success(true) + } + + private func evalSegmentCondition(cond: SegmentCondition, key: String, salt: String?, user: ConfigCatUser, evalLogger: EvaluationLogger?) -> EvalConditionResult { + guard let userConditions = cond.segment?.conditions else { + return .fatal("Segment reference is invalid") + } + guard let name = cond.segment?.name else { + return .fatal("Segment name is missing") + } + if cond.segmentComparator == .unknown { + return .fatal("Comparison operator is invalid") + } + + evalLogger?.newLine(msg: "(").incIndent().newLine(msg: "Evaluating segment '\(name)':") + var result: EvalConditionResult = .fatal("") + let needsTrue = cond.segmentComparator == .isIn + + for (index, userCondition) in userConditions.enumerated() { + evalLogger?.newLine(msg: "- ") + if index == 0 { + evalLogger?.append(value: "IF ").incIndent() + } else { + evalLogger?.incIndent().newLine(msg: "AND ") + } + evalLogger?.append(value: userCondition) + result = evalUserCondition(cond: userCondition, key: key, user: user, salt: salt, ctxSalt: name) + evalLogger?.append(value: " => ").append(value: result.isMatch ? "true" : "false").append(value: result.isMatch ? "" : ", skipping the remaining AND conditions").decIndent() + + if !result.isSuccess || !result.isMatch { + break + } + } + + evalLogger?.newLine(msg: "Segment evaluation result: ") + .append(value: !result.isSuccess ? result.err : "User \(result.isMatch ? SegmentComparator.isIn.text : SegmentComparator.isNotIn.text)") + .append(value: ".") + .newLine(msg: "Condition (").append(value: cond).append(value: ")") + .append(value: !result.isSuccess ? " failed to evaluate" : " evaluates to \(result.isMatch == needsTrue ? "true" : "false")") + .append(value: ".") + .decIndent() + .newLine(msg: ")") + + if case let .success(match) = result { + return .success(match == needsTrue) + } + return result + } + + private func evalPrerequisiteCondition(cond: PrerequisiteFlagCondition, key: String, user: ConfigCatUser?, evalLogger: EvaluationLogger?, settings: [String: Setting], cycleTracker: inout [String]) -> EvalConditionResult { + evalLogger?.append(value: cond) + guard let flagKey = cond.flagKey else { + return .fatal("Prerequisite flag key is missing") + } + guard let prereq = settings[flagKey] else { + return .fatal("Prerequisite flag is missing") + } + if cond.prerequisiteComparator == .unknown { + return .fatal("Comparison operator is invalid") + } + + let compValChecked = cond.flagValue.toAnyChecked(settingType: prereq.settingType) + var compVal: Any? = nil + if case .error(_) = compValChecked, prereq.settingType != .unknown { + return .fatal("Type mismatch between comparison value '\(cond.flagValue.anyValue ?? "")' and prerequisite flag '\(flagKey)'.") + } + if case .success(let val) = compValChecked { + compVal = val + } + cycleTracker.append(key) + if cycleTracker.contains(flagKey) { + cycleTracker.append(flagKey) + let output = cycleTracker.map { c in + return "'"+c+"'" + }.joined(separator: " -> ") + return .fatal("Circular dependency detected between the following depending flags: [\(output)].") + } + + let needsTrue = cond.prerequisiteComparator == .eq + evalLogger?.newLine(msg: "(").incIndent().newLine(msg: "Evaluating prerequisite flag '\(flagKey)':") + + let evalResult = evalSetting(setting: prereq, key: flagKey, user: user, evalLogger: evalLogger, settings: settings, cycleTracker: &cycleTracker) + cycleTracker.removeLast() + + switch evalResult { + case .success(let val, _, _, _): + let match = needsTrue == (Utils.anyEq(a: compVal, b: val)) + evalLogger?.newLine(msg: "Prerequisite flag evaluation result: '\(val)'.") + .newLine(msg: "Condition (").append(value: cond).append(value: ") evaluates to ").append(value: "\(match ? "true" : "false").") + .decIndent() + .newLine(msg: ")") + return .success(match) + case .error(let err): + return .fatal(err) + } + } + + private func evalUserCondition(cond: UserCondition, key: String, user: ConfigCatUser, salt: String?, ctxSalt: String) -> EvalConditionResult { + guard let comparisonAttribute = cond.comparisonAttribute else { + return .fatal("Comparison attribute is missing") + } + guard let userAnyVal = user.attribute(for: comparisonAttribute) else { + return .attributeMissing(cond) + } + switch cond.comparator { + case .eq, .notEq, .eqHashed, .notEqHashed: + guard let compVal = cond.stringValue else { + return .compValueInvalid(nil) + } + let (userVal, converted) = asString(value: userAnyVal) + if converted { + logConverted(cond: cond, key: key, attrValue: userVal) + } + return evalTextEq(comparisonValue: compVal, userValue: userVal, comp: cond.comparator, salt: salt, ctxSalt: ctxSalt) + + case .oneOf, .notOneOf, .oneOfHashed, .notOneOfHashed: + guard let compVal = cond.stringArrayValue else { + return .compValueInvalid(nil) + } + let (userVal, converted) = asString(value: userAnyVal) + if converted { + logConverted(cond: cond, key: key, attrValue: userVal) + } + return evalOneOf(comparisonValue: compVal, userValue: userVal, comp: cond.comparator, salt: salt, ctxSalt: ctxSalt) + + case .startsWithAnyOf, .startsWithAnyOfHashed, .notStartsWithAnyOf, .notStartsWithAnyOfHashed, .endsWithAnyOf, .endsWithAnyOfHashed, .notEndsWithAnyOf, .notEndsWithAnyOfHashed: + guard let compVal = cond.stringArrayValue else { + return .compValueInvalid(nil) + } + let (userVal, converted) = asString(value: userAnyVal) + if converted { + logConverted(cond: cond, key: key, attrValue: userVal) + } + return evalStartsEndsWith(comparisonValue: compVal, userValue: userVal, comp: cond.comparator, salt: salt, ctxSalt: ctxSalt) + + case .contains, .notContains: + guard let compVal = cond.stringArrayValue else { + return .compValueInvalid(nil) + } + let (userVal, converted) = asString(value: userAnyVal) + if converted { + logConverted(cond: cond, key: key, attrValue: userVal) + } + return evalContains(comparisonValue: compVal, userValue: userVal, comp: cond.comparator) + + case .oneOfSemver, .notOneOfSemver: + guard let compVal = cond.stringArrayValue else { + return .compValueInvalid(nil) + } + guard let userVal = asSemver(value: userAnyVal) else { + return .attributeInvalid("'\(userAnyVal)' is not a valid semantic version", cond) + } + return evalSemverIsOneOf(comparisonValue: compVal, userValue: userVal, comp: cond.comparator) + + case .lessSemver, .lessEqSemver, .greaterSemver, .greaterEqSemver: + guard let compVal = cond.stringValue else { + return .compValueInvalid(nil) + } + guard let userVal = asSemver(value: userAnyVal) else { + return .attributeInvalid("'\(userAnyVal)' is not a valid semantic version", cond) + } + return evalSemverCompare(comparisonValue: compVal, userValue: userVal, comp: cond.comparator) + + case .eqNum, .notEqNum, .lessNum, .lessEqNum, .greaterNum, .greaterEqNum: + guard let compVal = cond.doubleValue else { + return .compValueInvalid(nil) + } + guard let userVal = asDouble(value: userAnyVal) else { + return .attributeInvalid("'\(userAnyVal)' is not a valid decimal number", cond) + } + return evalNumberCompare(comparisonValue: compVal, userValue: userVal, comp: cond.comparator) + + case .beforeDateTime, .afterDateTime: + guard let compVal = cond.doubleValue else { + return .compValueInvalid(nil) + } + guard let userVal = asDateDouble(value: userAnyVal) else { + return .attributeInvalid("'\(userAnyVal)' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)", cond) + } + return evalDateTime(comparisonValue: compVal, userValue: userVal, comp: cond.comparator) + + case .arrayContainsAnyOf, .arrayNotContainsAnyOf, .arrayContainsAnyOfHashed, .arrayNotContainsAnyOfHashed: + guard let compVal = cond.stringArrayValue else { + return .compValueInvalid(nil) + } + guard let userVal = asSlice(value: userAnyVal) else { + return .attributeInvalid("'\(userAnyVal)' is not a valid string array", cond) + } + return evalArrayContains(comparisonValue: compVal, userValue: userVal, comp: cond.comparator, salt: salt, ctxSalt: ctxSalt) + default: + return .fatal("Comparison operator is invalid") + } } - private func formatValidationErrorRule(comparisonAttribute: String, userValue: String, comparator: Int, comparisonValue: String, error: String) -> String { - String(format: "Evaluating rule: [%@:%@] [%@] [%@] => SKIP rule. Validation error: %@", - comparisonAttribute, userValue, RolloutEvaluator.comparatorTexts[comparator], comparisonValue, error) + private func evalTextEq(comparisonValue: String, userValue: String, comp: UserComparator, salt: String?, ctxSalt: String) -> EvalConditionResult { + let needsTrue = comp.isSensitive ? comp == .eqHashed : comp == .eq + var userVal = userValue + if comp.isSensitive { + guard let salt = salt else { + return .fatal(RolloutEvaluator.saltMissingMessage) + } + userVal = userValue.sha256hex(salt: salt, contextSalt: ctxSalt) + } + return .success((comparisonValue == userVal) == needsTrue) + } + + private func evalOneOf(comparisonValue: [String], userValue: String, comp: UserComparator, salt: String?, ctxSalt: String) -> EvalConditionResult { + let needsTrue = comp.isSensitive ? comp == .oneOfHashed : comp == .oneOf + var userVal = userValue + if comp.isSensitive { + guard let salt = salt else { + return .fatal(RolloutEvaluator.saltMissingMessage) + } + userVal = userValue.sha256hex(salt: salt, contextSalt: ctxSalt) + } + for value in comparisonValue { + if value == userVal { + return .success(needsTrue) + } + } + return .success(!needsTrue) + } + + private func evalStartsEndsWith(comparisonValue: [String], userValue: String, comp: UserComparator, salt: String?, ctxSalt: String) -> EvalConditionResult { + let needsTrue = comp.isStartsWith ? comp.isSensitive ? comp == .startsWithAnyOfHashed : comp == .startsWithAnyOf : + comp.isSensitive ? comp == .endsWithAnyOfHashed : comp == .endsWithAnyOf + let userValData = Data(userValue.utf8) + for value in comparisonValue { + if comp.isSensitive { + guard let salt = salt else { + return .fatal(RolloutEvaluator.saltMissingMessage) + } + let parts = value.components(separatedBy: "_") + if parts.count < 2 || parts[1].isEmpty { + return .fatal("Comparison value is missing or invalid") + } + guard let length = Int(parts[0].trimmingCharacters(in: .whitespaces)), length >= 0 else { + return .fatal("Comparison value is missing or invalid") + } + if length > userValData.count { + continue + } + let chunk = comp.isStartsWith ? userValData[.. EvalConditionResult { + let needsTrue = comp == .contains + for value in comparisonValue { + if userValue.contains(value) { + return .success(needsTrue) + } + } + return .success(!needsTrue) + } + + private func evalSemverIsOneOf(comparisonValue: [String], userValue: Version, comp: UserComparator) -> EvalConditionResult { + let needsTrue = comp == .oneOfSemver + var matched = false + for value in comparisonValue { + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + continue + } + guard let compVer = trimmed.toVersion() else { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. + return .success(false) + } + if userValue == compVer { + matched = true + } + } + return .success(matched == needsTrue) + } + + private func evalSemverCompare(comparisonValue: String, userValue: Version, comp: UserComparator) -> EvalConditionResult { + guard let compVer = comparisonValue.toVersion() else { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. + return .success(false) + } + switch comp { + case .greaterSemver: + return .success(userValue > compVer) + case .greaterEqSemver: + return .success(userValue >= compVer) + case .lessSemver: + return .success(userValue < compVer) + case .lessEqSemver: + return .success(userValue <= compVer) + default: + return .fatal("wrong semver comparator") // shouldn't happen + } + } + + private func evalNumberCompare(comparisonValue: Double, userValue: Double, comp: UserComparator) -> EvalConditionResult { + switch comp { + case .eqNum: + return .success(userValue == comparisonValue) + case .notEqNum: + return .success(userValue != comparisonValue) + case .greaterNum: + return .success(userValue > comparisonValue) + case .greaterEqNum: + return .success(userValue >= comparisonValue) + case .lessNum: + return .success(userValue < comparisonValue) + case .lessEqNum: + return .success(userValue <= comparisonValue) + default: + return .fatal("wrong number comparator") // shouldn't happen + } + } + + private func evalDateTime(comparisonValue: Double, userValue: Double, comp: UserComparator) -> EvalConditionResult { + return comp == .beforeDateTime ? .success(userValue < comparisonValue) : .success(userValue > comparisonValue) + } + + private func evalArrayContains(comparisonValue: [String], userValue: [String], comp: UserComparator, salt: String?, ctxSalt: String) -> EvalConditionResult { + let needsTrue = comp.isSensitive ? comp == .arrayContainsAnyOfHashed : comp == .arrayContainsAnyOf + for userItem in userValue { + var userVal = userItem + if comp.isSensitive { + guard let salt = salt else { + return .fatal(RolloutEvaluator.saltMissingMessage) + } + userVal = userItem.sha256hex(salt: salt, contextSalt: ctxSalt) + } + for value in comparisonValue { + if userVal == value { + return .success(needsTrue) + } + } + } + return .success(!needsTrue) + } + + private func asString(value: Any) -> (String, Bool) { + switch value { + case let val as String: + return (val, false) + case let val as [String]: + return (Utils.toJson(obj: val), true) + case let val as Int: + return (val.description, true) + case let val as Int8: + return (val.description, true) + case let val as Int16: + return (val.description, true) + case let val as Int32: + return (val.description, true) + case let val as Int64: + return (val.description, true) + case let val as UInt: + return (val.description, true) + case let val as UInt8: + return (val.description, true) + case let val as UInt16: + return (val.description, true) + case let val as UInt32: + return (val.description, true) + case let val as UInt64: + return (val.description, true) + case let val as Float: + return val.isNaN ? ("NaN", true) : val.isInfinite ? (val < 0 ? "-Infinity" : "Infinity", true) : (val.description, true) + case let val as Float32: + return val.isNaN ? ("NaN", true) : val.isInfinite ? (val < 0 ? "-Infinity" : "Infinity", true) : (val.description, true) + case let val as Float64: + return val.isNaN ? ("NaN", true) : val.isInfinite ? (val < 0 ? "-Infinity" : "Infinity", true) : (val.description, true) + case let val as Double: + return val.isNaN ? ("NaN", true) : val.isInfinite ? (val < 0 ? "-Infinity" : "Infinity", true) : (val.description, true) + case let val as Date: + return (val.timeIntervalSince1970.description, true) + default: + return ("\(value)", true) + } + } + + private func asDouble(value: Any) -> Double? { + switch value { + case let val as String: + let stringVal = val.trimmingCharacters(in: .whitespacesAndNewlines) + switch stringVal { + case "Infinity", "+Infinity": + return Double.infinity + case "-Infinity": + return -Double.infinity + case "NaN": + return Double.nan + default: + return Double(stringVal.replacingOccurrences(of: ",", with: ".")) + } + case let val as Int: + return Double(val) + case let val as Int8: + return Double(val) + case let val as Int16: + return Double(val) + case let val as Int32: + return Double(val) + case let val as Int64: + return Double(val) + case let val as UInt: + return Double(val) + case let val as UInt8: + return Double(val) + case let val as UInt16: + return Double(val) + case let val as UInt32: + return Double(val) + case let val as UInt64: + return Double(val) + case let val as Float: + return Double(val) + case let val as Float32: + return Double(val) + case let val as Float64: + return Double(val) + case let val as Double: + return Double(val) + default: + return nil + } + } + + private func asDateDouble(value: Any) -> Double? { + if case let val as Date = value { + return val.timeIntervalSince1970 + } + return asDouble(value: value) + } + + private func asSemver(value: Any) -> Version? { + if case let val as String = value { + return val.toVersion() + } + return nil + } + + private func asSlice(value: Any) -> [String]? { + switch value { + case let val as [String]: + return val + case let val as String: + return Utils.fromJson(json: val) + default: + return nil + } + } + + private func logConverted(cond: UserCondition, key: String, attrValue: String) { + log.warning(eventId: 3005, message: "Evaluation of condition (\(cond)) for setting '\(key)' may not produce the expected result (the User.\(cond.unwrappedComparisonAttribute) attribute is not a string value, thus it was automatically converted to the string value '\(attrValue)'). Please make sure that using a non-string value was intended.") + } + + private func logUserObjectMissing(key: String) { + log.warning(eventId: 3001, message: "Cannot evaluate targeting rules and % options for setting '\(key)' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/") + } + + private func logAttributeMissing(key:String, cond: UserCondition) { + log.warning(eventId: 3003, message: "Cannot evaluate condition (\(cond)) for setting '\(key)' (the User.\(cond.unwrappedComparisonAttribute) attribute is missing). You should set the User.\(cond.unwrappedComparisonAttribute) attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/") + } + + private func logAttributeMissing(key:String, attr: String) { + log.warning(eventId: 3003, message: "Cannot evaluate % options for setting '\(key)' (the User.\(attr) attribute is missing). You should set the User.\(attr) attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/") + } + + private func logAttributeInvalid(key:String, reason: String, cond: UserCondition) { + log.warning(eventId: 3004, message: "Cannot evaluate condition (\(cond)) for setting '\(key)' (\(reason)). Please check the User.\(cond.unwrappedComparisonAttribute) attribute and make sure that its value corresponds to the comparison operator.") } } internal extension String { - var sha1hex: String? { - if let utf8Data = data(using: .utf8, allowLossyConversion: false) { - return utf8Data.digestSHA1.hexString + var sha1hex: String { + return Data(self.utf8).digestSHA1.hexString + } + + func sha256hex(salt: String, contextSalt: String) -> String { + return Data((self + salt + contextSalt).utf8).digestSHA256.hexString + } + + func toVersion() -> Version? { + if let semver = Version(self.trimmingCharacters(in: .whitespacesAndNewlines)) { + return Version(semver.major, semver.minor, semver.patch, pre: semver.prereleaseIdentifiers) } return nil } } internal extension Data { + func sha256hex(salt: String, contextSalt: String) -> String { + var copy = Data(self) + copy.append(Data((salt + contextSalt).utf8)) + return copy.digestSHA256.hexString + } + var digestSHA1: Data { var bytes: [UInt8] = Array(repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) withUnsafeBytes { @@ -318,11 +783,18 @@ internal extension Data { } return Data(_: bytes) } + + var digestSHA256: Data { + var bytes: [UInt8] = Array(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(count), &bytes) + } + return Data(_: bytes) + } var hexString: String { map { String(format: "%02x", UInt8($0)) - } - .joined() + }.joined() } } diff --git a/Sources/ConfigCat/Utils.swift b/Sources/ConfigCat/Utils.swift index f0c6725..0ddf848 100644 --- a/Sources/ConfigCat/Utils.swift +++ b/Sources/ConfigCat/Utils.swift @@ -37,17 +37,76 @@ extension Date { } } +extension Equatable { + func isEqual(_ other: any Equatable) -> Bool { + guard let other = other as? Self else { + return false + } + return self == other + } +} + class Constants { - static let version: String = "10.0.0" - static let configJsonName: String = "config_v5.json" + static let version: String = "11.0.0" + static let configJsonName: String = "config_v6.json" static let configJsonCacheVersion: String = "v2" static let globalBaseUrl: String = "https://cdn-global.configcat.com" static let euOnlyBaseUrl: String = "https://cdn-eu.configcat.com" + static let proxyPrefix: String = "configcat-proxy/" + static let sdkKeyCompSize: Int = 22 } class Utils { public static func generateCacheKey(sdkKey: String) -> String { let keyToHash = sdkKey + "_" + Constants.configJsonName + "_" + Constants.configJsonCacheVersion - return String(keyToHash.sha1hex ?? keyToHash) + return String(keyToHash.sha1hex) + } + + static func validateSdkKey(sdkKey: String, isCustomUrl: Bool) -> Bool { + if isCustomUrl && sdkKey.count > Constants.proxyPrefix.count && sdkKey.hasPrefix(Constants.proxyPrefix) { + return true + } + let comps = sdkKey.split(separator: "/") + switch comps.count { + case 2: + return comps[0].count == Constants.sdkKeyCompSize && comps[1].count == Constants.sdkKeyCompSize + case 3: + return comps[0] == "configcat-sdk-1" && comps[1].count == Constants.sdkKeyCompSize && comps[2].count == Constants.sdkKeyCompSize + default: + return false + } + } + + static func anyEq(a: Any?, b: Any?) -> Bool { + if a == nil && b == nil { + return true + } + guard let eq1 = a as? any Equatable, let eq2 = b as? any Equatable else { + return false + } + return eq1.isEqual(eq2) + } + + static func toJson(obj: Any) -> String { + do { + let jsonData = try JSONSerialization.data(withJSONObject: obj, options: []) + return String(data: jsonData, encoding: .utf8) ?? "" + } catch { + return "" + } + } + + static func fromJson(json: String) -> T? { + do { + guard let data = json.data(using: .utf8) else { + return nil + } + guard let result = try JSONSerialization.jsonObject(with: data, options: []) as? T else { + return nil + } + return result + } catch { + return nil + } } } diff --git a/Tests/ConfigCatTests/AsyncAwaitTests.swift b/Tests/ConfigCatTests/AsyncAwaitTests.swift index 9c839c5..91437f4 100644 --- a/Tests/ConfigCatTests/AsyncAwaitTests.swift +++ b/Tests/ConfigCatTests/AsyncAwaitTests.swift @@ -3,7 +3,7 @@ import XCTest class AsyncAwaitTests: XCTestCase { #if compiler(>=5.5) && canImport(_Concurrency) - let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}] } } }"# + let testJsonMultiple = #"{"f":{"key1":{"t":0,"v":{"b":true},"i":"fakeId1"},"key2":{"t":0,"r":[{"c":[{"u":{"a":"Email","c":2,"l":["@example.com"]}}],"s":{"v":{"b":true},"i":"9f21c24c"}}],"v":{"b":false},"i":"fakeId2"}}}"# let user = ConfigCatUser(identifier: "id", email: "test@example.com") @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -11,7 +11,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let value = await client.getValue(for: "key1", defaultValue: false) XCTAssertTrue(value) let value2 = await client.getValue(for: "key2", defaultValue: false, user: user) @@ -23,7 +23,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let details = await client.getValueDetails(for: "key1", defaultValue: false) XCTAssertEqual("fakeId1", details.variationId) let details2 = await client.getValueDetails(for: "key2", defaultValue: false, user: user) @@ -35,7 +35,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let id = await client.getKeyAndValue(for: "fakeId1") XCTAssertEqual(true, id?.value as? Bool) XCTAssertEqual("key1", id?.key) @@ -46,7 +46,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let keys = await client.getAllKeys() XCTAssertEqual(2, keys.count) } @@ -56,7 +56,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let values = await client.getAllValues() XCTAssertEqual(2, values.count) } @@ -66,7 +66,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let values = await client.getAllValueDetails() XCTAssertEqual(2, values.count) } @@ -76,7 +76,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let result = await client.forceRefresh() let value = await client.getValue(for: "key2", defaultValue: true) XCTAssertTrue(result.success) @@ -89,7 +89,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) await client.forceRefresh() let value = await client.getValue(for: "key2", defaultValue: true) XCTAssertFalse(value) @@ -101,7 +101,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let details = await client.getValueDetails(for: "key2", defaultValue: true) XCTAssertFalse(details.isDefaultValue) XCTAssertFalse(details.value) @@ -116,7 +116,7 @@ class AsyncAwaitTests: XCTestCase { let initValue = testJsonMultiple.asEntryString() let cache = SingleValueCache(initValue: initValue) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, configCache: cache) let state = await client.waitForReady() @@ -132,7 +132,7 @@ class AsyncAwaitTests: XCTestCase { let initValue = testJsonMultiple.asEntryString(date: Date().subtract(minutes: 5)!) let cache = SingleValueCache(initValue: initValue) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, configCache: cache) let state = await client.waitForReady() @@ -148,7 +148,7 @@ class AsyncAwaitTests: XCTestCase { let initValue = testJsonMultiple.asEntryString(date: Date().subtract(minutes: 5)!) let cache = SingleValueCache(initValue: initValue) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, configCache: cache) let state = await client.waitForReady() @@ -161,7 +161,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let state = await client.waitForReady() @@ -174,7 +174,7 @@ class AsyncAwaitTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 400)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let state = await client.waitForReady() @@ -190,7 +190,7 @@ class AsyncAwaitTests: XCTestCase { let initValue = testJsonMultiple.asEntryString() let cache = SingleValueCache(initValue: initValue) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine, configCache: cache) let state = await client.waitForReady() diff --git a/Tests/ConfigCatTests/AutoPollingTests.swift b/Tests/ConfigCatTests/AutoPollingTests.swift index 186a958..30899a7 100755 --- a/Tests/ConfigCatTests/AutoPollingTests.swift +++ b/Tests/ConfigCatTests/AutoPollingTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ConfigCat class AutoPollingTests: XCTestCase { - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# + private let testJsonFormat = #"{ "f": { "fakeKey": { "t": 1, "v": { "s": "%@" } } } }"# func testGet() throws { let engine = MockEngine() @@ -10,12 +10,12 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -24,7 +24,7 @@ class AutoPollingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -36,12 +36,12 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 500)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -50,7 +50,7 @@ class AutoPollingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -67,8 +67,8 @@ class AutoPollingTests: XCTestCase { hooks.addOnConfigChanged { _ in called = true } - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: hooks, sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: hooks, sdkKey: "", offline: false) sleep(1) @@ -78,7 +78,7 @@ class AutoPollingTests: XCTestCase { let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -89,8 +89,8 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200, delay: 3)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) sleep(2) @@ -100,7 +100,7 @@ class AutoPollingTests: XCTestCase { let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -112,8 +112,8 @@ class AutoPollingTests: XCTestCase { let start = Date() let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 60, maxInitWaitTimeInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -135,12 +135,12 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -152,7 +152,7 @@ class AutoPollingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -167,12 +167,12 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -181,7 +181,7 @@ class AutoPollingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -194,12 +194,12 @@ class AutoPollingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -216,8 +216,8 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) Thread.sleep(forTimeInterval: 1.5) @@ -241,8 +241,8 @@ class AutoPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) XCTAssertTrue(service.isOffline) Thread.sleep(forTimeInterval: 2) @@ -265,8 +265,8 @@ class AutoPollingTests: XCTestCase { let cache = SingleValueCache(initValue: initValue) let start = Date() let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 60, maxInitWaitTimeInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -293,8 +293,8 @@ class AutoPollingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 60, maxInitWaitTimeInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: hooks, sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: hooks, sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -315,12 +315,12 @@ class AutoPollingTests: XCTestCase { let cache = SingleValueCache(initValue: initValue) let start = Date() let mode = PollingModes.autoPoll(autoPollIntervalInSeconds: 60, maxInitWaitTimeInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) diff --git a/Tests/ConfigCatTests/CacheTest.swift b/Tests/ConfigCatTests/CacheTest.swift index a007e1b..3e28f85 100644 --- a/Tests/ConfigCatTests/CacheTest.swift +++ b/Tests/ConfigCatTests/CacheTest.swift @@ -3,12 +3,12 @@ import XCTest class CacheTests: XCTestCase { func testCacheKeys() { - XCTAssertEqual("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", Utils.generateCacheKey(sdkKey: "test1")) - XCTAssertEqual("c09513b1756de9e4bc48815ec7a142b2441ed4d5", Utils.generateCacheKey(sdkKey: "test2")) + XCTAssertEqual("f83ba5d45bceb4bb704410f51b704fb6dfa19942", Utils.generateCacheKey(sdkKey: "configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012")) + XCTAssertEqual("da7bfd8662209c8ed3f9db96daed4f8d91ba5876", Utils.generateCacheKey(sdkKey: "configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012")) } func testPayloads() { - let testJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0},\"f\":{\"testKey\":{\"v\":\"testValue\",\"t\":1,\"p\":[],\"r\":[]}}}" + let testJson = "{\"p\":{\"u\":\"https://cdn-global.configcat.com\",\"r\":0,\"s\":\"FUkC6RADjzF0vXrDSfJn7BcEBag9afw1Y6jkqjMP9BA=\"},\"f\":{\"testKey\":{\"t\":1,\"v\":{\"s\":\"testValue\"}}}}" let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" let time = formatter.date(from: "2023-06-14T15:27:15.8440000Z")! diff --git a/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift b/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift index 8db1cf5..e3d7065 100755 --- a/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift +++ b/Tests/ConfigCatTests/ConfigCatClientIntegrationTests.swift @@ -52,10 +52,10 @@ class ConfigCatClientIntegrationTests: XCTestCase { XCTAssertFalse(details.isDefaultValue) XCTAssertNil(details.error) XCTAssertEqual("d0cd8f06", details.variationId) - XCTAssertEqual("Email", details.matchedEvaluationRule?.comparisonAttribute) - XCTAssertEqual("@configcat.com", details.matchedEvaluationRule?.comparisonValue) - XCTAssertNil(details.matchedEvaluationPercentageRule) - XCTAssertEqual(2, details.matchedEvaluationRule?.comparator) + XCTAssertEqual("Email", details.matchedTargetingRule?.conditions[0].userCondition?.comparisonAttribute) + XCTAssertEqual("@configcat.com", details.matchedTargetingRule?.conditions[0].userCondition?.stringArrayValue?.first) + XCTAssertNil(details.matchedPercentageOption) + XCTAssertEqual(2, details.matchedTargetingRule?.conditions[0].userCondition?.comparator.rawValue) XCTAssertEqual(user.identifier, details.user?.identifier) expectation.fulfill() } @@ -73,10 +73,10 @@ class ConfigCatClientIntegrationTests: XCTestCase { XCTAssertFalse(details.isDefaultValue) XCTAssertNil(details.error) XCTAssertEqual("d0cd8f06", details.variationId) - XCTAssertEqual("Email", details.matchedEvaluationRule?.comparisonAttribute) - XCTAssertEqual("@configcat.com", details.matchedEvaluationRule?.comparisonValue) - XCTAssertNil(details.matchedEvaluationPercentageRule) - XCTAssertEqual(2, details.matchedEvaluationRule?.comparator) + XCTAssertEqual("Email", details.matchedTargetingRule?.conditions[0].userCondition?.comparisonAttribute) + XCTAssertEqual("@configcat.com", details.matchedTargetingRule?.conditions[0].userCondition?.stringArrayValue?.first) + XCTAssertNil(details.matchedPercentageOption) + XCTAssertEqual(2, details.matchedTargetingRule?.conditions[0].userCondition?.comparator.rawValue) XCTAssertEqual(user.identifier, details.user?.identifier) called = true } diff --git a/Tests/ConfigCatTests/ConfigCatClientTests.swift b/Tests/ConfigCatTests/ConfigCatClientTests.swift index 919d553..7369461 100755 --- a/Tests/ConfigCatTests/ConfigCatClientTests.swift +++ b/Tests/ConfigCatTests/ConfigCatClientTests.swift @@ -2,12 +2,13 @@ import XCTest @testable import ConfigCat class ConfigCatClientTests: XCTestCase { - let testJsonFormat = #"{ "f": { "fakeKey": { "v": %@, "p": [], "r": [] } } }"# - let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [] } } }"# + let testJsonFormat = #"{ "f": { "fakeKey": { "v": { "%@": %@ }, "t": %@ } } }"# + let testStringJson = #"{ "f": { "fakeKey": { "v": { "s": "fake" }, "t": 1 } } }"# + let testJsonMultiple = #"{ "f": { "key1": { "v": { "b": true }, "t":0, "i": "fakeId1" }, "key2": { "v": { "b": false }, "i": "fakeId2", "t":0 } } }"# func testGetIntValue() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "43"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "i", "43", "2"), statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -21,7 +22,7 @@ class ConfigCatClientTests: XCTestCase { func testGetIntValueFailed() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "fake"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "i", "fake", "2"), statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -49,7 +50,7 @@ class ConfigCatClientTests: XCTestCase { func testGetStringValue() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -63,7 +64,7 @@ class ConfigCatClientTests: XCTestCase { func testGetStringValueFailed() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "33"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "s", "33", "1"), statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -77,7 +78,7 @@ class ConfigCatClientTests: XCTestCase { func testGetDoubleValue() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "43.56"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "d", "43.56", "3"), statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -105,7 +106,7 @@ class ConfigCatClientTests: XCTestCase { func testGetBoolValue() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "true"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "b", "true", "0"), statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -133,7 +134,7 @@ class ConfigCatClientTests: XCTestCase { func testGetValueWithInvalidTypeFailed() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "fake"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -147,7 +148,7 @@ class ConfigCatClientTests: XCTestCase { func testGetLatestOnFail() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "55"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "i", "55", "2"), statusCode: 200)) engine.enqueueResponse(response: Response(body: "", statusCode: 500)) let client = createClient(http: engine) let expectation1 = self.expectation(description: "wait for response") @@ -171,10 +172,10 @@ class ConfigCatClientTests: XCTestCase { func testForceRefreshLazy() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test\""), statusCode: 200)) - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test2\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "s", "\"test\"", "1"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "s", "\"test2\"", "1"), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine) let expectation1 = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -195,10 +196,10 @@ class ConfigCatClientTests: XCTestCase { func testForceRefreshAuto() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test\""), statusCode: 200)) - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"test2\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "s", "\"test\"", "1"), statusCode: 200)) + engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "s", "\"test2\"", "1"), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine) let expectation1 = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -220,7 +221,7 @@ class ConfigCatClientTests: XCTestCase { func testFailingAutoPoll() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine) let expectation1 = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in XCTAssertEqual("", value) @@ -232,12 +233,12 @@ class ConfigCatClientTests: XCTestCase { func testFromCacheOnly() throws { let engine = MockEngine() let cache = InMemoryConfigCache() - let sdkKey = "test" + let sdkKey = randomSdkKey() let cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) - try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().serialize()) + try cache.write(for: cacheKey, value: testStringJson.toEntryFromConfigString().serialize()) engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine, configCache: cache) let expectation = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in XCTAssertEqual("fake", value) @@ -249,12 +250,12 @@ class ConfigCatClientTests: XCTestCase { func testFromCacheOnlyRefresh() throws { let engine = MockEngine() let cache = InMemoryConfigCache() - let sdkKey = "test" + let sdkKey = randomSdkKey() let cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) - try cache.write(for: cacheKey, value: String(format: testJsonFormat, "\"fake\"").toEntryFromConfigString().serialize()) + try cache.write(for: cacheKey, value: testStringJson.toEntryFromConfigString().serialize()) engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine, configCache: cache) + let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine, configCache: cache) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -268,7 +269,7 @@ class ConfigCatClientTests: XCTestCase { func testFailingAutoPollRefresh() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(autoPollIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine) let expectation1 = self.expectation(description: "wait for response") client.forceRefresh { _ in client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -282,7 +283,7 @@ class ConfigCatClientTests: XCTestCase { func testFailingExpiringCache() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 120), logger: NoLogger(), httpEngine: engine) let expectation1 = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in XCTAssertEqual("", value) @@ -309,8 +310,8 @@ class ConfigCatClientTests: XCTestCase { func testAutoPollUserAgentHeader() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -324,8 +325,8 @@ class ConfigCatClientTests: XCTestCase { func testLazyUserAgentHeader() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.lazyLoad(), httpEngine: engine) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.lazyLoad(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -340,7 +341,7 @@ class ConfigCatClientTests: XCTestCase { func testGetValueDetailsWithError() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: "", statusCode: 500)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.lazyLoad(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.lazyLoad(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.getValueDetails(for: "fakeKey", defaultValue: "") { details in XCTAssertEqual("", details.value) @@ -353,8 +354,8 @@ class ConfigCatClientTests: XCTestCase { func testManualPollUserAgentHeader() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in client.getValue(for: "fakeKey", defaultValue: "") { value in @@ -368,7 +369,7 @@ class ConfigCatClientTests: XCTestCase { func testOnlineOffline() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) let client = createClient(http: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -404,7 +405,7 @@ class ConfigCatClientTests: XCTestCase { func testInitOffline() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) let client = createClient(http: engine, offline: true) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in @@ -436,7 +437,7 @@ class ConfigCatClientTests: XCTestCase { func testInitOfflineCallsReady() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) var ready = false var state = ClientReadyState.hasUpToDateFlagData let hooks = Hooks() @@ -444,7 +445,7 @@ class ConfigCatClientTests: XCTestCase { ready = true state = st } - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine, hooks: hooks, offline: true) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, hooks: hooks, offline: true) let expectation = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { _ in expectation.fulfill() @@ -459,7 +460,7 @@ class ConfigCatClientTests: XCTestCase { func testDefaultUser() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: createTestConfigWithRules().toJsonString(), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") client.forceRefresh { _ in expectation.fulfill() @@ -497,7 +498,7 @@ class ConfigCatClientTests: XCTestCase { func testHooks() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) engine.enqueueResponse(response: Response(body: "", statusCode: 404)) var error = "" var changed = false @@ -513,7 +514,7 @@ class ConfigCatClientTests: XCTestCase { hooks.addOnConfigChanged { _ in changed = true } - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine, hooks: hooks) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine, hooks: hooks) let expectation = self.expectation(description: "wait for response") client.forceRefresh { r in XCTAssertTrue(r.success) @@ -547,13 +548,13 @@ class ConfigCatClientTests: XCTestCase { func testHooksSub() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) engine.enqueueResponse(response: Response(body: "", statusCode: 404)) var error = "" var changed = false var ready = false let hooks = Hooks() - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine, hooks: hooks) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine, hooks: hooks) client.hooks.addOnError { e in error = e } @@ -585,8 +586,8 @@ class ConfigCatClientTests: XCTestCase { func testReadyHookGetValue() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") @@ -603,8 +604,8 @@ class ConfigCatClientTests: XCTestCase { func testReadyHookGetValueSnapshot() { let engine = MockEngine() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for response") @@ -622,8 +623,10 @@ class ConfigCatClientTests: XCTestCase { func testLazyCache() throws { let engine = MockEngine() let cache = InMemoryConfigCache() - engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "\"fake\""), statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test1", pollingMode: PollingModes.lazyLoad(), httpEngine: engine, configCache: cache) + let sdkKey = randomSdkKey() + let cacheKey = Utils.generateCacheKey(sdkKey: sdkKey) + engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) + let client = ConfigCatClient(sdkKey: sdkKey, pollingMode: PollingModes.lazyLoad(), logger: NoLogger(), httpEngine: engine, configCache: cache) let expectation = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { r in @@ -640,7 +643,7 @@ class ConfigCatClientTests: XCTestCase { wait(for: [expectation2], timeout: 5) XCTAssertEqual(1, engine.requests.count) - try XCTAssertFalse(cache.read(for: "147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6").isEmpty) + try XCTAssertFalse(cache.read(for: cacheKey).isEmpty) } func testOnFlagEvaluationError() { @@ -654,7 +657,7 @@ class ConfigCatClientTests: XCTestCase { XCTAssertTrue(details.isDefaultValue) called = true } - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.lazyLoad(), httpEngine: engine, hooks: hooks) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.lazyLoad(), logger: NoLogger(), httpEngine: engine, hooks: hooks) let expectation = self.expectation(description: "wait for response") client.getValue(for: "fakeKey", defaultValue: "") { value in XCTAssertEqual("", value) @@ -667,34 +670,70 @@ class ConfigCatClientTests: XCTestCase { } func testSingleton() { - var client1 = ConfigCatClient.get(sdkKey: "test") - let client2 = ConfigCatClient.get(sdkKey: "test") + let sdkKey = randomSdkKey() + var client1 = ConfigCatClient.get(sdkKey: sdkKey) + let client2 = ConfigCatClient.get(sdkKey: sdkKey) XCTAssertEqual(client1, client2) ConfigCatClient.closeAll() - client1 = ConfigCatClient.get(sdkKey: "test") + client1 = ConfigCatClient.get(sdkKey: sdkKey) XCTAssertNotEqual(client1, client2) } func testSingletonRemovesOnlyTheClosingInstance() { - let client1 = ConfigCatClient.get(sdkKey: "test") + let sdkKey = randomSdkKey() + + let client1 = ConfigCatClient.get(sdkKey: sdkKey) client1.close() - let client2 = ConfigCatClient.get(sdkKey: "test") + let client2 = ConfigCatClient.get(sdkKey: sdkKey) XCTAssertNotEqual(client1, client2) client1.close() - let client3 = ConfigCatClient.get(sdkKey: "test") + let client3 = ConfigCatClient.get(sdkKey: sdkKey) XCTAssertEqual(client2, client3) } + + func testSdkKeyValidation() { + let tests = [ + ("sdk-key-90123456789012", false, false), + ("sdk-key-9012345678901/1234567890123456789012", false, false), + ("sdk-key-90123456789012/123456789012345678901", false, false), + ("sdk-key-90123456789012/12345678901234567890123", false, false), + ("sdk-key-901234567890123/1234567890123456789012", false, false), + ("sdk-key-90123456789012/1234567890123456789012", false, true), + ("configcat-sdk-1/sdk-key-90123456789012", false, false), + ("configcat-sdk-1/sdk-key-9012345678901/1234567890123456789012", false, false), + ("configcat-sdk-1/sdk-key-90123456789012/123456789012345678901", false, false), + ("configcat-sdk-1/sdk-key-90123456789012/12345678901234567890123", false, false), + ("configcat-sdk-1/sdk-key-901234567890123/1234567890123456789012", false, false), + ("configcat-sdk-1/sdk-key-90123456789012/1234567890123456789012", false, true), + ("configcat-sdk-2/sdk-key-90123456789012/1234567890123456789012", false, false), + ("configcat-proxy/", false, false), + ("configcat-proxy/", true, false), + ("configcat-proxy/sdk-key-90123456789012", false, false), + ("configcat-proxy/sdk-key-90123456789012", true, true), + ] + + for test in tests { + let logger = RecordingLogger() + let customUrl = test.1 ? "https://my-configcat-proxy" : "" + let client = ConfigCatClient(sdkKey: test.0, pollingMode: PollingModes.manualPoll(), logger: logger, httpEngine: nil, baseUrl: customUrl) + + XCTAssertEqual(test.2, !client.isOffline) + if !test.2 { + XCTAssertEqual("ERROR [0] ConfigCat SDK Key '\(test.0)' is invalid.", logger.entries.last) + } + } + } private func createClient(http: HttpEngine, offline: Bool = false) -> ConfigCatClient { - ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: http, offline: offline) + ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: http, offline: offline) } } diff --git a/Tests/ConfigCatTests/ConfigFetcherTests.swift b/Tests/ConfigCatTests/ConfigFetcherTests.swift index a3da1ef..329843d 100755 --- a/Tests/ConfigCatTests/ConfigFetcherTests.swift +++ b/Tests/ConfigCatTests/ConfigFetcherTests.swift @@ -4,14 +4,14 @@ import XCTest class ConfigFetcherTests: XCTestCase { func testSimpleFetchSuccess() throws { let engine = MockEngine() - let testBody = #"{ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } }"# + let testBody = #"{ "f": { "fakeKey": { "v": { "s": "fakeValue" }, "t":1 } } }"# engine.enqueueResponse(response: Response(body: testBody, statusCode: 200)) let expectation = self.expectation(description: "wait for response") - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) fetcher.fetch(eTag: "") { response in XCTAssertEqual(.fetched(.empty), response) - XCTAssertEqual("fakeValue", response.entry?.config.entries["fakeKey"]?.value as? String) + XCTAssertEqual("fakeValue", response.entry?.config.settings["fakeKey"]?.value.stringValue) expectation.fulfill() } wait(for: [expectation], timeout: 5) @@ -22,7 +22,7 @@ class ConfigFetcherTests: XCTestCase { engine.enqueueResponse(response: Response(body: "", statusCode: 304)) let expectation = self.expectation(description: "wait for response") - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) fetcher.fetch(eTag: "") { response in XCTAssertEqual(.notModified, response) XCTAssertNil(response.entry) @@ -36,7 +36,7 @@ class ConfigFetcherTests: XCTestCase { engine.enqueueResponse(response: Response(body: "", statusCode: 404)) let expectation = self.expectation(description: "wait for response") - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) fetcher.fetch(eTag: "") { response in XCTAssertEqual(.failure(message: "", isTransient: false), response) XCTAssertNil(response.entry) @@ -48,11 +48,11 @@ class ConfigFetcherTests: XCTestCase { func testFetchNotModifiedEtag() throws { let engine = MockEngine() let etag = "test" - let testBody = #"{ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } }"# + let testBody = #"{ "f": { "fakeKey": { "v": { "s": "fakeValue" }, "t":1 } } }"# engine.enqueueResponse(response: Response(body: testBody, statusCode: 200, headers: ["Etag": etag])) engine.enqueueResponse(response: Response(body: "", statusCode: 304)) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: "m", dataGovernance: DataGovernance.global) let expectation = self.expectation(description: "wait for response") fetcher.fetch(eTag: "") { response in XCTAssertEqual(.fetched(.empty), response) diff --git a/Tests/ConfigCatTests/DataGovernanceTests.swift b/Tests/ConfigCatTests/DataGovernanceTests.swift index 10a1f5a..c2b0dbd 100644 --- a/Tests/ConfigCatTests/DataGovernanceTests.swift +++ b/Tests/ConfigCatTests/DataGovernanceTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ConfigCat class DataGovernanceTests: XCTestCase { - private let jsonTemplate: String = #"{ "p": { "u": "%@", "r": %@ }, "f": {} }"# + private let jsonTemplate: String = #"{ "p": { "u": "%@", "r": %@, "s":"" }, "f": {} }"# private let customCdnUrl: String = "https://custom-cdn.configcat.com" func testShouldStayOnServer() throws { @@ -215,7 +215,7 @@ class DataGovernanceTests: XCTestCase { private func createFetcher(http: HttpEngine, url: String = "") -> ConfigFetcher { ConfigFetcher(httpEngine: http, - logger: Logger.noLogger, + logger: InternalLogger.noLogger, sdkKey: "", mode: "", dataGovernance: DataGovernance.global, diff --git a/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift b/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift index c6d387f..50c708f 100644 --- a/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift +++ b/Tests/ConfigCatTests/EvaluationDetailsExtensionTests.swift @@ -2,16 +2,16 @@ import XCTest @testable import ConfigCat class EvaluationDetailsExtensionTests: XCTestCase { - let testBoolJson = #"{ "f": { "key": { "v": true, "i": "fakeId1", "p": [], "r": [] } } }"# - let testIntJson = #"{ "f": { "key": { "v": 42, "i": "fakeId1", "p": [], "r": [] } } }"# - let testDoubleJson = #"{ "f": { "key": { "v": 3.14, "i": "fakeId1", "p": [], "r": [] } } }"# - let testStringJson = #"{ "f": { "key": { "v": "fake", "i": "fakeId1", "p": [], "r": [] } } }"# + let testBoolJson = #"{ "f": { "key": { "v": { "b": true }, "i": "fakeId1", "t":0 } } }"# + let testIntJson = #"{ "f": { "key": { "v": { "i": 42 }, "i": "fakeId1", "t":2 } } }"# + let testDoubleJson = #"{ "f": { "key": { "v": { "d": 3.14 }, "i": "fakeId1", "t":3 } } }"# + let testStringJson = #"{ "f": { "key": { "v": { "s": "fake" }, "i": "fakeId1", "t":1 } } }"# func testBoolDetails() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testBoolJson, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let refreshExpectation = expectation(description: "wait for refresh") client.forceRefresh { RefreshResult in refreshExpectation.fulfill() @@ -32,7 +32,7 @@ class EvaluationDetailsExtensionTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testIntJson, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let refreshExpectation = expectation(description: "wait for refresh") client.forceRefresh { RefreshResult in refreshExpectation.fulfill() @@ -53,7 +53,7 @@ class EvaluationDetailsExtensionTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testDoubleJson, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let refreshExpectation = expectation(description: "wait for refresh") client.forceRefresh { RefreshResult in refreshExpectation.fulfill() @@ -74,7 +74,7 @@ class EvaluationDetailsExtensionTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testStringJson, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: engine) let refreshExpectation = expectation(description: "wait for refresh") client.forceRefresh { RefreshResult in refreshExpectation.fulfill() diff --git a/Tests/ConfigCatTests/EvaluationLogTests.swift b/Tests/ConfigCatTests/EvaluationLogTests.swift new file mode 100644 index 0000000..f41ac82 --- /dev/null +++ b/Tests/ConfigCatTests/EvaluationLogTests.swift @@ -0,0 +1,197 @@ +import XCTest +@testable import ConfigCat + +class EvaluationLogTests: XCTestCase { + lazy var testBundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: type(of: self)) + #endif + }() + + #if compiler(>=5.5) && canImport(_Concurrency) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func test1TargetingRule() async { + await runEvalLogTest(suiteName: "1_targeting_rule") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func test2TargetingRules() async { + await runEvalLogTest(suiteName: "2_targeting_rules") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testAndRules() async { + await runEvalLogTest(suiteName: "and_rules") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testComparators() async { + await runEvalLogTest(suiteName: "comparators") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testDateValidation() async { + await runEvalLogTest(suiteName: "epoch_date_validation") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testListTruncation() async { + await runEvalLogTest(suiteName: "list_truncation") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testNumberValidation() async { + await runEvalLogTest(suiteName: "number_validation") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testOptionsAfterTargetingRule() async { + await runEvalLogTest(suiteName: "options_after_targeting_rule") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testOptionsOnCustomAttr() async { + await runEvalLogTest(suiteName: "options_based_on_custom_attr") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testOptionsOnUserId() async { + await runEvalLogTest(suiteName: "options_based_on_user_id") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testOptionsWithinTargetingRule() async { + await runEvalLogTest(suiteName: "options_within_targeting_rule") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testPrerequisiteFlag() async { + await runEvalLogTest(suiteName: "prerequisite_flag") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testSegment() async { + await runEvalLogTest(suiteName: "segment") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testSemverValidation() async { + await runEvalLogTest(suiteName: "semver_validation") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testSimpleValue() async { + await runEvalLogTest(suiteName: "simple_value") + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func runEvalLogTest(suiteName: String) async { + guard let jsonContent = loadResource(bundle: testBundle, path: "evaluationlog/" + suiteName + ".json") else { + XCTFail() + return + } + guard let suite = TestSuite.fromJsonString(json: jsonContent) else { + XCTFail() + return + } + let logger = RecordingLogger() + var localSource: OverrideDataSource? + if let override = suite.override { + guard let overrideJson = loadResource(bundle: testBundle, path: "evaluationlog/_overrides/" + override) else { + XCTFail() + return + } + localSource = try! BundleResourceDataSource(json: overrideJson, behaviour: .localOnly) + } + let client = ConfigCatClient(sdkKey: suite.sdkKey ?? randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: logger, httpEngine: nil, flagOverrides: localSource, logLevel: .info) + await client.forceRefresh() + + for test in suite.tests { + logger.reset() + guard let expLogContent = loadResource(bundle: testBundle, path: "evaluationlog/" + suiteName + "/" + test.expectedLog) else { + XCTFail() + return + } + let user = test.user == nil ? nil : ConfigCatUser(custom: test.user!) + let res = await client.getAnyValue(for: test.key, defaultValue: test.defaultVal, user: user) + + let exp = (test.user?.count ?? 0) > 1 ? expLogContent.removeTrailingNewLine().trimUserSection() : expLogContent.removeTrailingNewLine() + let logs = (test.user?.count ?? 0) > 1 ? logger.entries.joined(separator: "\n").trimUserSection() : logger.entries.joined(separator: "\n") + + XCTAssertTrue(Utils.anyEq(a: test.returnVal, b: res), "\(test.returnVal) is not equal to \(res ?? "invalid")") + XCTAssertEqual(exp, logs) + } + } + #endif +} + +class TestCase { + let key: String + let defaultVal: Any + let returnVal: Any + let expectedLog: String + let user: [String: Any]? + + init(key: String, defaultVal: Any, returnVal: Any, expectedLog: String, user: [String : Any]?) { + self.key = key + self.defaultVal = defaultVal + self.returnVal = returnVal + self.expectedLog = expectedLog + self.user = user + } + + static func fromJson(json: [String: Any]) -> TestCase { + TestCase(key: json["key"] as? String ?? "", + defaultVal: json["defaultValue"] as Any, + returnVal: json["returnValue"] as Any, + expectedLog: json["expectedLog"] as? String ?? "", + user: json["user"] as? [String : Any]) + } +} + +class TestSuite { + let configUrl: String + let sdkKey: String? + let override: String? + let tests: [TestCase] + + init(configUrl: String, sdkKey: String?, override: String?, tests: [TestCase]) { + self.configUrl = configUrl + self.sdkKey = sdkKey + self.override = override + self.tests = tests + } + + static func fromJsonString(json: String) -> TestSuite? { + guard let jsonObject: [String: Any] = Utils.fromJson(json: json) else { + return nil + } + return .fromJson(json: jsonObject) + } + + static func fromJson(json: [String: Any]) -> TestSuite { + let testsMap = json["tests"] as? [[String: Any]] ?? [] + return TestSuite(configUrl: json["configUrl"] as? String ?? "", + sdkKey: json["sdkKey"] as? String, + override: json["jsonOverride"] as? String, + tests: testsMap.map { testCase in + return TestCase.fromJson(json: testCase) + }) + } +} + +extension String { + func trimUserSection() -> String { + if let range = self.range(of: "for User") { + let rest = self[range.upperBound...] + if let newLineIndex = rest.firstIndex(of: "\n") { + var copy = String(self) + copy.removeSubrange(range.lowerBound..=5.5) && canImport(_Concurrency) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testPrerequisiteCircularDependencies() async { + let tests = [ + ("key1", "'key1' -> 'key1'"), + ("key2", "'key2' -> 'key3' -> 'key2'"), + ("key4", "'key4' -> 'key3' -> 'key2' -> 'key3'"), + ] + + guard let jsonContent = loadResource(bundle: testBundle, path: "json/test_circulardependency_v6.json") else { + XCTFail() + return + } + + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: jsonContent, statusCode: 200)) + + for test in tests { + let logger = RecordingLogger() + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: engine) + let _ = await client.getValue(for: test.0, defaultValue: "") + + XCTAssertTrue(logger.entries.last?.contains(test.1) ?? false) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testPrerequisiteFlagComparisonValueTypeMismatch() async { + let tests: [(String, String, Any?, Any?)] = [ + ("stringDependsOnBool", "mainBoolFlag", true, "Dog"), + ("stringDependsOnBool", "mainBoolFlag", false, "Cat"), + ("stringDependsOnBool", "mainBoolFlag", "1", nil), + ("stringDependsOnBool", "mainBoolFlag", 1, nil), + ("stringDependsOnBool", "mainBoolFlag", 1.0, nil), + ("stringDependsOnBool", "mainBoolFlag", [true], nil), + ("stringDependsOnBool", "mainBoolFlag", nil, nil), + ("stringDependsOnString", "mainStringFlag", "private", "Dog"), + ("stringDependsOnString", "mainStringFlag", "Private", "Cat"), + ("stringDependsOnString", "mainStringFlag", true, nil), + ("stringDependsOnString", "mainStringFlag", 1, nil), + ("stringDependsOnString", "mainStringFlag", 1.0, nil), + ("stringDependsOnString", "mainStringFlag", ["private"], nil), + ("stringDependsOnString", "mainStringFlag", nil, nil), + ("stringDependsOnInt", "mainIntFlag", 2, "Dog"), + ("stringDependsOnInt", "mainIntFlag", 1, "Cat"), + ("stringDependsOnInt", "mainIntFlag", "2", nil), + ("stringDependsOnInt", "mainIntFlag", true, nil), + ("stringDependsOnInt", "mainIntFlag", 2.0, nil), + ("stringDependsOnInt", "mainIntFlag", [2], nil), + ("stringDependsOnInt", "mainIntFlag", nil, nil), + ("stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog"), + ("stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat"), + ("stringDependsOnDouble", "mainDoubleFlag", "0.1", nil), + ("stringDependsOnDouble", "mainDoubleFlag", true, nil), + ("stringDependsOnDouble", "mainDoubleFlag", 1, nil), + ("stringDependsOnDouble", "mainDoubleFlag", [0.1], nil), + ("stringDependsOnDouble", "mainDoubleFlag", nil, nil), + ] + + for test in tests { + let logger = RecordingLogger() + let client = ConfigCatClient(sdkKey: "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: nil, flagOverrides: TestDictionaryDataSource(source: [test.1: test.2], behaviour: .localOverRemote)) + + let res = await client.getAnyValue(for: test.0, defaultValue: nil) + + XCTAssertTrue(Utils.anyEq(a: test.3, b: res)) + + if test.3 == nil { + let type = SettingValue.fromAnyValue(value: test.2).settingType + if test.2 == nil { + XCTAssertTrue(logger.entries.last?.contains("Setting value is missing") ?? false, logger.entries.last ?? "") + } else if type == .unknown { + XCTAssertTrue(logger.entries.last?.range(of: "Setting value '[^']+' is of an unsupported type", options: .regularExpression) != nil, logger.entries.last ?? "") + } else { + XCTAssertTrue(logger.entries.last?.range(of: "Type mismatch between comparison value '[^']+' and prerequisite flag '[^']+'", options: .regularExpression) != nil) + } + } + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testPrerequisiteFlagOverrides() async { + let tests: [(String, String, String, OverrideBehaviour?, String?)] = [ + ("stringDependsOnString", "1", "john@sensitivecompany.com", nil, "Dog"), + ("stringDependsOnString", "1", "john@sensitivecompany.com", .remoteOverLocal, "Dog"), + ("stringDependsOnString", "1", "john@sensitivecompany.com", .localOverRemote, "Dog"), + ("stringDependsOnString", "1", "john@sensitivecompany.com", .localOnly, nil), + ("stringDependsOnString", "2", "john@notsensitivecompany.com", nil, "Cat"), + ("stringDependsOnString", "2", "john@notsensitivecompany.com", .remoteOverLocal, "Cat"), + ("stringDependsOnString", "2", "john@notsensitivecompany.com", .localOverRemote, "Dog"), + ("stringDependsOnString", "2", "john@notsensitivecompany.com", .localOnly, nil), + ("stringDependsOnInt", "1", "john@sensitivecompany.com", nil, "Dog"), + ("stringDependsOnInt", "1", "john@sensitivecompany.com", .remoteOverLocal, "Dog"), + ("stringDependsOnInt", "1", "john@sensitivecompany.com", .localOverRemote, "Cat"), + ("stringDependsOnInt", "1", "john@sensitivecompany.com", .localOnly, nil), + ("stringDependsOnInt", "2", "john@notsensitivecompany.com", nil, "Cat"), + ("stringDependsOnInt", "2", "john@notsensitivecompany.com", .remoteOverLocal, "Cat"), + ("stringDependsOnInt", "2", "john@notsensitivecompany.com", .localOverRemote, "Dog"), + ("stringDependsOnInt", "2", "john@notsensitivecompany.com", .localOnly, nil), + ] + + guard let jsonContent = loadResource(bundle: testBundle, path: "json/test_override_flagdependency_v6.json") else { + XCTFail() + return + } + + for test in tests { + let logger = NoLogger() + let source = test.3 != nil ? try! BundleResourceDataSource(json: jsonContent, behaviour: test.3!) : nil + let client = ConfigCatClient(sdkKey: "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: nil, flagOverrides: source, logLevel: .info) + + let user = ConfigCatUser(identifier: test.1, email: test.2) + let res = await client.getAnyValue(for: test.0, defaultValue: nil, user: user) + + XCTAssertTrue(Utils.anyEq(a: test.4, b: res)) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testMatchedEvaluationRuleAndPercantageOption() async { + let tests: [(String, String, String?, String?, String?, String, Bool, Bool)] = [ + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", nil, nil, nil, "Cat", false, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", nil, nil, "Cat", false, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@example.com", nil, "Dog", true, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", nil, "Cat", false, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "", "Frog", true, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "US", "Fish", true, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", nil, "Cat", false, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "", "Falcon", false, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "US", "Spider", false, true), + ] + + var clientCache: [String: ConfigCatClient] = [:] + + for test in tests { + let logger = NoLogger() + var client = clientCache[test.0] + if client == nil { + client = ConfigCatClient(sdkKey: test.0, pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: nil) + clientCache[test.0] = client + } + + let user = test.2 != nil ? ConfigCatUser(identifier: test.2!, email: test.3, custom: test.4 == nil ? nil : ["PercentageBase": test.4!]) : nil + let res = await client!.getAnyValueDetails(for: test.1, defaultValue: nil, user: user) + + XCTAssertTrue(Utils.anyEq(a: test.5, b: res.value), "\(test.5) is not equal to \(String(describing: res.value))") + XCTAssertEqual(test.6, res.matchedTargetingRule != nil) + XCTAssertEqual(test.7, res.matchedPercentageOption != nil) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testObjectAttributeValueConversion() async { + let logger = RecordingLogger() + let client = ConfigCatClient(sdkKey: "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: nil) + + let user = ConfigCatUser(identifier: "12345", custom: ["Custom1": 42]) + let _ = await client.getAnyValueDetails(for: "boolTextEqualsNumber", defaultValue: nil, user: user) + + XCTAssertTrue(logger.entries.last?.contains("Evaluation of condition (User.Custom1 EQUALS '42') for setting 'boolTextEqualsNumber' may not produce the expected result (the User.Custom1 attribute is not a string value, thus it was automatically converted to the string value '42'). Please make sure that using a non-string value was intended.") ?? false) + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testUserObjectAttributeValueConversion() async { + let tests: [(String, String, String, String, Any, Any)] = [ + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.0", "20%"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.9.9", "< 1.0.0"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.0.0", "20%"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.1", "20%"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0, "20%"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0.9, "20%"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 2, "20%"), + // Number-based comparisons + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int8(-1), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int8(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int8(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int8(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt8(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt8(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt8(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt16(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt16(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt16(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1, "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2, "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3, "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5, ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int64.min, "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int64(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int64(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int64(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Int64.max, ">5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt64(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt64(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt64(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", UInt64.max, ">5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -Float.infinity, "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float32(-1), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float32(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float32(2.1), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float32(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float32(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float.infinity, ">5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Float.nan, "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -Double.infinity, "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double(-1), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double(2), "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double(2.1), "<=2,1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double(3), "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double(5), ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double.infinity, ">5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", Double.nan, "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-Infinity", "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2,1", "<=2,1"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "<>4.2"), + ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaNa", "80%"), + // Date time-based comparisons + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDate(val: "2023-03-31T23:59:59.999Z"), false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDateTZ(val: "2023-04-01T01:59:59.999+02:00"), false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDate(val: "2023-04-01T00:00:00.001Z"), true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDateTZ(val: "2023-04-01T02:00:00.0010000+02:00"), true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDate(val: "2023-04-30T23:59:59.999Z"), true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDateTZ(val: "2023-05-01T01:59:59.999+02:00"), true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDate(val: "2023-05-01T00:00:00.001Z"), false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", parseDateTZ(val: "2023-05-01T02:00:00.001+02:00"), false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", -Double.infinity, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199.999, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307200.001, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199.999, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899200.001, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", Double.infinity, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", Double.nan, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307201, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-Infinity", false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", true), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+Infinity", false), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", false), + // String array-based comparisons + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "read"], "Dog"), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "Read"], "Cat"), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"read\"]", "Dog"), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"Read\"]", "Cat"), + ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "x, read", "Cat"), + ] + + var clientCache: [String: ConfigCatClient] = [:] + + for test in tests { + let logger = NoLogger() + var client = clientCache[test.0] + if client == nil { + client = ConfigCatClient(sdkKey: test.0, pollingMode: PollingModes.autoPoll(), logger: logger, httpEngine: nil) + clientCache[test.0] = client + } + + let user = ConfigCatUser(identifier: test.2, custom: [test.3: test.4]) + let res = await client!.getAnyValue(for: test.1, defaultValue: nil, user: user) + + XCTAssertTrue(Utils.anyEq(a: test.5, b: res)) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testComparisonAttributeConversionToCanonicalStringRepresentation() async { + let tests: [(String, Any, String)] = [ + ("numberToStringConversion", 0.12345, "1"), + ("numberToStringConversionInt", Int8(125), "4"), + ("numberToStringConversionInt", UInt8(125), "4"), + ("numberToStringConversionInt", Int8(125), "4"), + ("numberToStringConversionInt", UInt16(125), "4"), + ("numberToStringConversionInt", 125, "4"), + ("numberToStringConversionInt", UInt(125), "4"), + ("numberToStringConversionInt", Int64(125), "4"), + ("numberToStringConversionInt", UInt64(125), "4"), + ("numberToStringConversionPositiveExp", -1.23456789e96, "2"), + ("numberToStringConversionNegativeExp", -12345.6789E-100, "4"), + ("numberToStringConversionNaN", Double.nan, "3"), + ("numberToStringConversionPositiveInf", Double.infinity, "4"), + ("numberToStringConversionNegativeInf", -Double.infinity, "3"), + ("dateToStringConversion", parseDate(val: "2023-03-31T23:59:59.999Z"), "3"), + ("dateToStringConversion", 1680307199.999, "3"), + ("dateToStringConversionNaN", Double.nan, "3"), + ("dateToStringConversionPositiveInf", Double.infinity, "1"), + ("dateToStringConversionNegativeInf", -Double.infinity, "5"), + ("stringArrayToStringConversion", ["read", "Write", " eXecute "], "4"), + ("stringArrayToStringConversionEmpty", [String](), "5"), + //("stringArrayToStringConversionSpecialChars", ["+<>%\"'\\/\t\r\n"], "3"), We'll fix this with the .withoutEscapingSlashes JSONEncoder option after macOS 10.15 + ("stringArrayToStringConversionUnicode", ["äöüÄÖÜçéèñışğ⢙✓😀"], "2") + ] + + guard let jsonContent = loadResource(bundle: testBundle, path: "json/comparison_attribute_conversion.json") else { + XCTFail() + return + } + + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: jsonContent, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) + + for test in tests { + let user = ConfigCatUser(identifier: "12345", custom: ["Custom1": test.1]) + let res = await client.getValue(for: test.0, defaultValue: "default", user: user) + + XCTAssertEqual(test.2, res, "\(test.0) \(test.1)") + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testSpecialCharacters() async { + let tests: [(String, String, String)] = [ + ("specialCharacters", "äöüÄÖÜçéèñışğ⢙✓😀", "äöüÄÖÜçéèñışğ⢙✓😀"), + ("specialCharactersHashed", "äöüÄÖÜçéèñışğ⢙✓😀", "äöüÄÖÜçéèñışğ⢙✓😀"), + ] + + let client = ConfigCatClient(sdkKey: "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g", pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: nil) + + + for test in tests { + let res = await client.getValue(for: test.0, defaultValue: "NOT_CAT", user: ConfigCatUser(identifier: test.1)) + + XCTAssertTrue(Utils.anyEq(a: test.2, b: res)) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testComparisonAttributeTrimming() async { + let tests: [(String, String)] = [ + ("isoneof", "no trim"), + ("isnotoneof", "no trim"), + ("isoneofhashed", "no trim"), + ("isnotoneofhashed", "no trim"), + ("equalshashed", "no trim"), + ("notequalshashed", "no trim"), + ("arraycontainsanyofhashed", "no trim"), + ("arraynotcontainsanyofhashed", "no trim"), + ("equals", "no trim"), + ("notequals", "no trim"), + ("startwithanyof", "no trim"), + ("notstartwithanyof", "no trim"), + ("endswithanyof", "no trim"), + ("notendswithanyof", "no trim"), + ("arraycontainsanyof", "no trim"), + ("arraynotcontainsanyof", "no trim"), + ("startwithanyofhashed", "no trim"), + ("notstartwithanyofhashed", "no trim"), + ("endswithanyofhashed", "no trim"), + ("notendswithanyofhashed", "no trim"), + ("semverisoneof", "4 trim"), + ("semverisnotoneof", "5 trim"), + ("semverless", "6 trim"), + ("semverlessequals", "7 trim"), + ("semvergreater", "8 trim"), + ("semvergreaterequals", "9 trim"), + ("numberequals", "10 trim"), + ("numbernotequals", "11 trim"), + ("numberless", "12 trim"), + ("numberlessequals", "13 trim"), + ("numbergreater", "14 trim"), + ("numbergreaterequals", "15 trim"), + ("datebefore", "18 trim"), + ("dateafter", "19 trim"), + ("containsanyof", "no trim"), + ("notcontainsanyof", "no trim"), + ] + + guard let jsonContent = loadResource(bundle: testBundle, path: "json/comparison_attribute_trimming.json") else { + XCTFail() + return + } + + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: jsonContent, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) + + for test in tests { + let user = ConfigCatUser(identifier: " 12345 ", country: "[\" USA \"]", custom: ["Version": " 1.0.0 ", "Number": " 3 ", "Date": " 1705253400 "]) + let res = await client.getValue(for: test.0, defaultValue: "default", user: user) + + XCTAssertEqual(test.1, res) + } + } + + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func testComparisonValueTrimming() async { + let tests: [(String, String)] = [ + ("isoneof", "no trim"), + ("isnotoneof", "no trim"), + ("containsanyof", "no trim"), + ("notcontainsanyof", "no trim"), + ("isoneofhashed", "no trim"), + ("isnotoneofhashed", "no trim"), + ("equalshashed", "no trim"), + ("notequalshashed", "no trim"), + ("arraycontainsanyofhashed", "no trim"), + ("arraynotcontainsanyofhashed", "no trim"), + ("equals", "no trim"), + ("notequals", "no trim"), + ("startwithanyof", "no trim"), + ("notstartwithanyof", "no trim"), + ("endswithanyof", "no trim"), + ("notendswithanyof", "no trim"), + ("arraycontainsanyof", "no trim"), + ("arraynotcontainsanyof", "no trim"), + ("startwithanyofhashed", "no trim"), + ("notstartwithanyofhashed", "no trim"), + ("endswithanyofhashed", "no trim"), + ("notendswithanyofhashed", "no trim"), + ("semverisoneof", "4 trim"), + ("semverisnotoneof", "5 trim"), + ("semverless", "6 trim"), + ("semverlessequals", "7 trim"), + ("semvergreater", "8 trim"), + ("semvergreaterequals", "9 trim"), + ] + + guard let jsonContent = loadResource(bundle: testBundle, path: "json/comparison_value_trimming.json") else { + XCTFail() + return + } + + let engine = MockEngine() + engine.enqueueResponse(response: Response(body: jsonContent, statusCode: 200)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) + + for test in tests { + let user = ConfigCatUser(identifier: "12345", country: "[\"USA\"]", custom: ["Version": "1.0.0", "Number": "3", "Date": "1705253400"]) + let res = await client.getValue(for: test.0, defaultValue: "default", user: user) + + XCTAssertEqual(test.1, res, "\(test.0) \(test.1)") + } + } + #endif +} diff --git a/Tests/ConfigCatTests/Helpers.swift b/Tests/ConfigCatTests/Helpers.swift index c71e0c9..f0a4b12 100644 --- a/Tests/ConfigCatTests/Helpers.swift +++ b/Tests/ConfigCatTests/Helpers.swift @@ -10,21 +10,84 @@ extension String { func asEntryString(date: Date = Date()) -> String { toEntryFromConfigString().withFetchTime(time: date).serialize() } + + func removeTrailingNewLine() -> String { + if self.hasSuffix("\n") { + return String(self.dropLast()) + } + return self + } + + static func random(len: Int) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. String { - let jsonMap = toJsonMap() - let json = try! JSONSerialization.data(withJSONObject: jsonMap, options: []) - return String(data: json, encoding: .utf8)! + return Utils.toJson(obj: toJsonMap()) } } func createTestConfigWithRules() -> Config { - Config(entries: ["key": Setting(value: "def", variationId: "defVar", percentageItems: [], rolloutRules: [ - RolloutRule(value: "fake1", variationId: "id1", comparator: 2, comparisonAttribute: "Identifier", comparisonValue: "@test1.com"), - RolloutRule(value: "fake2", variationId: "id2", comparator: 2, comparisonAttribute: "Identifier", comparisonValue: "@test2.com") - ])]) + Config(preferences: .empty, settings: [ + "key": Setting(value: SettingValue(boolValue: nil, + stringValue: "def", + doubleValue: nil, + intValue: nil), + variationId: "defVar", + percentageAttribute: "", + settingType: .string, + percentageOptions: [], + targetingRules: [ + TargetingRule(servedValue: ServedValue(value: SettingValue(boolValue: nil, stringValue: "fake1", doubleValue: nil, intValue: nil), variationId: "id1"), + conditions: [ + Condition(userCondition: UserCondition(stringValue: nil, + doubleValue: nil, + stringArrayValue: ["@test1.com"], + comparator: .contains, + comparisonAttribute: "Identifier"), + segmentCondition: nil, + prerequisiteFlagCondition: nil) + ], + percentageOptions: []), + TargetingRule(servedValue: ServedValue(value: SettingValue(boolValue: nil, stringValue: "fake2", doubleValue: nil, intValue: nil), variationId: "id2"), + conditions: [ + Condition(userCondition: UserCondition(stringValue: nil, + doubleValue: nil, + stringArrayValue: ["@test2.com"], + comparator: .contains, + comparisonAttribute: "Identifier"), + segmentCondition: nil, + prerequisiteFlagCondition: nil) + ], + percentageOptions: []) + ] + )]) +} + +func randomSdkKey() -> String { + return "\(String.random(len: 22))/\(String.random(len: 22))" +} + +func loadResource(bundle: Bundle, path: String) -> String? { + guard let url = bundle.url(forResource: path, withExtension: nil), let matrixData = try? Data(contentsOf: url), let content = String(bytes: matrixData, encoding: .utf8) else { + return nil + } + return content +} + +func parseDate(val: String) -> Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + return dateFormatter.date(from:val)! +} + +func parseDateTZ(val: String) -> Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" + return dateFormatter.date(from:val)! } extension XCTestCase { @@ -40,3 +103,67 @@ extension XCTestCase { } } } + +extension Array { + func skip(count: Int) -> [Element] { + [Element](self[count.. [String: Setting] { + settings + } +} + +class BundleResourceDataSource: OverrideDataSource { + private var settings: [String: Setting] = [:] + + public init(json: String, behaviour: OverrideBehaviour) throws { + super.init(behaviour: behaviour) + let result = ConfigEntry.fromConfigJson(json: json, eTag: "", fetchTime: Date()) + switch result { + case .success(let entry): + self.settings = entry.config.settings + case .failure(let err): + throw err + } + } + + public override func getOverrides() -> [String: Setting] { + settings + } +} diff --git a/Tests/ConfigCatTests/LazyLoadingTests.swift b/Tests/ConfigCatTests/LazyLoadingTests.swift index 2b1cc35..8a55d01 100755 --- a/Tests/ConfigCatTests/LazyLoadingTests.swift +++ b/Tests/ConfigCatTests/LazyLoadingTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ConfigCat class LazyLoadingTests: XCTestCase { - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# + private let testJsonFormat = #"{ "f": { "fakeKey": { "t": 1, "v": { "s": "%@" } } } }"# func testGet() throws { let engine = MockEngine() @@ -10,19 +10,19 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200, delay: 2)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) let expectation2 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -34,7 +34,7 @@ class LazyLoadingTests: XCTestCase { let expectation3 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test2", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", result.settings["fakeKey"]?.value.stringValue) expectation3.fulfill() } wait(for: [expectation3], timeout: 4) @@ -46,19 +46,19 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 500)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) let expectation2 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -70,7 +70,7 @@ class LazyLoadingTests: XCTestCase { let expectation3 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation3.fulfill() } wait(for: [expectation3], timeout: 5) @@ -83,12 +83,12 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -101,7 +101,7 @@ class LazyLoadingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test2", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", result.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -116,12 +116,12 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 2) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", result.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } wait(for: [expectation1], timeout: 5) @@ -131,7 +131,7 @@ class LazyLoadingTests: XCTestCase { let expectation2 = expectation(description: "wait for settings") service.settings { result in - XCTAssertEqual("test2", result.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", result.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } wait(for: [expectation2], timeout: 5) @@ -144,8 +144,8 @@ class LazyLoadingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -189,8 +189,8 @@ class LazyLoadingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -232,8 +232,8 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in @@ -273,8 +273,8 @@ class LazyLoadingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) let mode = PollingModes.lazyLoad(cacheRefreshIntervalInSeconds: 1) - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: .global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) let expectation1 = expectation(description: "wait for settings") service.settings { settingsResult in diff --git a/Tests/ConfigCatTests/LocalTests.swift b/Tests/ConfigCatTests/LocalTests.swift index e4c3035..24d00f5 100755 --- a/Tests/ConfigCatTests/LocalTests.swift +++ b/Tests/ConfigCatTests/LocalTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ConfigCat class LocalTests: XCTestCase { - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": %@, "p": [], "r": [] } } }"# + private let testJsonFormat = #"{ "f": { "fakeKey": { "t": 0, "v": { "b": %@ } } } }"# func testDictionary() throws { let dictionary: [String: Any] = [ @@ -35,7 +35,7 @@ class LocalTests: XCTestCase { "fakeKey": true, "nonexisting": true ] - let client = ConfigCatClient(sdkKey: "testKey", pollingMode: PollingModes.autoPoll(), httpEngine: engine, hooks: Hooks(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .localOverRemote)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, hooks: Hooks(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .localOverRemote)) let expectation = self.expectation(description: "wait for response") client.getAllValues { values in XCTAssertTrue(values["fakeKey"] as? Bool ?? false) @@ -53,7 +53,7 @@ class LocalTests: XCTestCase { "fakeKey": true, "nonexisting": true ] - let client = ConfigCatClient(sdkKey: "testKey", pollingMode: PollingModes.autoPoll(), httpEngine: engine, hooks: Hooks(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .remoteOverLocal)) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine, hooks: Hooks(), flagOverrides: LocalDictionaryDataSource(source: dictionary, behaviour: .remoteOverLocal)) let expectation = self.expectation(description: "wait for response") client.getAllValues { values in XCTAssertFalse(values["fakeKey"] as? Bool ?? true) diff --git a/Tests/ConfigCatTests/ManualPollingTests.swift b/Tests/ConfigCatTests/ManualPollingTests.swift index a4766bc..056894b 100755 --- a/Tests/ConfigCatTests/ManualPollingTests.swift +++ b/Tests/ConfigCatTests/ManualPollingTests.swift @@ -2,7 +2,7 @@ import XCTest @testable import ConfigCat class ManualPollingTests: XCTestCase { - private let testJsonFormat = #"{ "f": { "fakeKey": { "v": "%@", "p": [], "r": [] } } }"# + private let testJsonFormat = #"{ "f": { "fakeKey": { "t": 1, "v": { "s": "%@" } } } }"# func testGet() throws { let engine = MockEngine() @@ -10,15 +10,15 @@ class ManualPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200, delay: 2)) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: nil, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = self.expectation(description: "wait for response") service.refresh { result in XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } } @@ -29,7 +29,7 @@ class ManualPollingTests: XCTestCase { XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } } @@ -47,7 +47,7 @@ class ManualPollingTests: XCTestCase { called = true XCTAssertTrue(error.starts(with: "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey.")) } - let logger = Logger(level: .warning, hooks: hooks) + let logger = InternalLogger(log: OSLogger(), level: .warning, hooks: hooks) let mode = PollingModes.manualPoll() let fetcher = ConfigFetcher(httpEngine: engine, logger: logger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) @@ -58,7 +58,7 @@ class ManualPollingTests: XCTestCase { XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } } @@ -69,7 +69,7 @@ class ManualPollingTests: XCTestCase { XCTAssertFalse(result.success) XCTAssertTrue(result.error?.starts(with: "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey.") ?? false && result.error?.contains("404") ?? false) service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } } @@ -87,15 +87,15 @@ class ManualPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: mockCache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = self.expectation(description: "wait for response") service.refresh { result in XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } } @@ -109,7 +109,7 @@ class ManualPollingTests: XCTestCase { XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } } @@ -125,15 +125,15 @@ class ManualPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test2"), statusCode: 200)) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = self.expectation(description: "wait for response") service.refresh { result in XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test", settingsResult.settings["fakeKey"]?.value.stringValue) expectation1.fulfill() } } @@ -144,7 +144,7 @@ class ManualPollingTests: XCTestCase { XCTAssertTrue(result.success) XCTAssertNil(result.error) service.settings { settingsResult in - XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value as? String) + XCTAssertEqual("test2", settingsResult.settings["fakeKey"]?.value.stringValue) expectation2.fulfill() } } @@ -156,8 +156,8 @@ class ManualPollingTests: XCTestCase { engine.enqueueResponse(response: Response(body: String(format: testJsonFormat, "test"), statusCode: 200)) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: FailingCache(), pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = self.expectation(description: "wait for response") service.settings { settingsResult in @@ -176,8 +176,8 @@ class ManualPollingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: false) let expectation1 = self.expectation(description: "wait for response") service.refresh { result in @@ -221,8 +221,8 @@ class ManualPollingTests: XCTestCase { let initValue = String(format: testJsonFormat, "test").asEntryString() let cache = SingleValueCache(initValue: initValue) let mode = PollingModes.manualPoll() - let fetcher = ConfigFetcher(httpEngine: engine, logger: Logger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) - let service = ConfigService(log: Logger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) + let fetcher = ConfigFetcher(httpEngine: engine, logger: InternalLogger.noLogger, sdkKey: "", mode: mode.identifier, dataGovernance: DataGovernance.global) + let service = ConfigService(log: InternalLogger.noLogger, fetcher: fetcher, cache: cache, pollingMode: mode, hooks: Hooks(), sdkKey: "", offline: true) let expectation1 = self.expectation(description: "wait for response") service.refresh { result in diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule.json b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule.json new file mode 100644 index 0000000..596bd2b --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "1_rule_no_user.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Dog", + "expectedLog": "1_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..f05c6f6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..80702e9 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +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/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_user.txt new file mode 100644 index 0000000..5b0f37b --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_no_user.txt @@ -0,0 +1,6 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..49d1252 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules.json b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules.json new file mode 100644 index 0000000..5cf8a3c --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "2_rules_no_user.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_no_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "user" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_not_matching_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "admin" + }, + "returnValue": "Dog", + "expectedLog": "2_rules_matching_targeted_attribute.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt new file mode 100644 index 0000000..d124a4f --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +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/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt new file mode 100644 index 0000000..0e02076 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +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/ +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/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_user.txt new file mode 100644 index 0000000..79d83e9 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_no_user.txt @@ -0,0 +1,8 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..72217b2 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +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/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/_overrides/test_list_truncation.json b/Tests/ConfigCatTests/Resources/evaluationlog/_overrides/test_list_truncation.json new file mode 100644 index 0000000..6fdde45 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/_overrides/test_list_truncation.json @@ -0,0 +1,83 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "test-salt" + }, + "f": { + "booleanKey1": { + "t": 0, + "v": { + "b": false + }, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } + } + ] + } + } +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/and_rules.json b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules.json new file mode 100644 index 0000000..c6ed879 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules.json @@ -0,0 +1,22 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "emailAnd", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "and_rules_no_user.txt" + }, + { + "key": "emailAnd", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "jane@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "and_rules_user.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_no_user.txt new file mode 100644 index 0000000..eedbc2e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_no_user.txt @@ -0,0 +1,7 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'emailAnd' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_user.txt new file mode 100644 index 0000000..92c59ce --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/and_rules/and_rules_user.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email CONTAINS ANY OF ['@'] => true + AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => no match + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/comparators.json b/Tests/ConfigCatTests/Resources/evaluationlog/comparators.json new file mode 100644 index 0000000..5d5631e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/comparators.json @@ -0,0 +1,20 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "allinone", + "defaultValue": "", + "user": { + "Identifier": "12345", + "Email": "joe@example.com", + "Country": "[\"USA\"]", + "Version": "1.0.0", + "Number": "1.0", + "Date": "1693497500" + }, + "returnValue": "default", + "expectedLog": "allinone.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/comparators/allinone.txt b/Tests/ConfigCatTests/Resources/evaluationlog/comparators/allinone.txt new file mode 100644 index 0000000..21a51c2 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/comparators/allinone.txt @@ -0,0 +1,57 @@ +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email EQUALS '' => true + AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions + THEN '1h' => no match + - IF User.Email EQUALS 'joe@example.com' => true + AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions + THEN '1c' => no match + - IF User.Email IS ONE OF [<1 hashed value>] => true + AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '2h' => no match + - IF User.Email IS ONE OF ['joe@example.com'] => true + AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions + THEN '2c' => no match + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '3h' => no match + - IF User.Email STARTS WITH ANY OF ['joe@'] => true + AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions + THEN '3c' => no match + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '4h' => no match + - IF User.Email ENDS WITH ANY OF ['@example.com'] => true + AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions + THEN '4c' => no match + - IF User.Email CONTAINS ANY OF ['e@e'] => true + AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions + THEN '5' => no match + - IF User.Version IS ONE OF ['1.0.0'] => true + AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions + THEN '6' => no match + - IF User.Version < '1.0.1' => true + AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions + THEN '7' => no match + - IF User.Version > '0.9.9' => true + AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions + THEN '8' => no match + - IF User.Number = '1' => true + AND User.Number != '1' => false, skipping the remaining AND conditions + THEN '9' => no match + - IF User.Number < '1.1' => true + AND User.Number >= '1.1' => false, skipping the remaining AND conditions + THEN '10' => no match + - IF User.Number > '0.9' => true + AND User.Number <= '0.9' => false, skipping the remaining AND conditions + THEN '11' => no match + - IF User.Date BEFORE '1693497600' (2023-08-31 16:00:00 +0000 UTC) => true + AND User.Date AFTER '1693497600' (2023-08-31 16:00:00 +0000 UTC) => false, skipping the remaining AND conditions + THEN '12' => no match + - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true + AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '13h' => no match + - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true + AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions + THEN '13c' => no match + Returning 'default'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation.json b/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation.json new file mode 100644 index 0000000..e916d21 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "boolTrueIn202304", + "defaultValue": true, + "returnValue": false, + "expectedLog": "date_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "2023.04.10" + } + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation/date_error.txt b/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation/date_error.txt new file mode 100644 index 0000000..18291e4 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/epoch_date_validation/date_error.txt @@ -0,0 +1,7 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01 00:00:00 +0000 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. +INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 AFTER '1680307200' (2023-04-01 00:00:00 +0000 UTC) => false, skipping the remaining AND conditions + 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)) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation.json b/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation.json new file mode 100644 index 0000000..64e9426 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation.json @@ -0,0 +1,14 @@ +{ + "jsonOverride": "test_list_truncation.json", + "tests": [ + { + "key": "booleanKey1", + "defaultValue": false, + "user": { + "Identifier": "12" + }, + "returnValue": true, + "expectedLog": "list_truncation.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation/list_truncation.txt b/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation/list_truncation.txt new file mode 100644 index 0000000..10a0195 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/list_truncation/list_truncation.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/number_validation.json b/Tests/ConfigCatTests/Resources/evaluationlog/number_validation.json new file mode 100644 index 0000000..640cf3d --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/number_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", + "tests": [ + { + "key": "number", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "number_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "not_a_number" + } + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/number_validation/number_error.txt b/Tests/ConfigCatTests/Resources/evaluationlog/number_validation/number_error.txt new file mode 100644 index 0000000..f936809 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/number_validation/number_error.txt @@ -0,0 +1,6 @@ +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. +INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule.json b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule.json new file mode 100644 index 0000000..803840e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "returnValue": -1, + "expectedLog": "options_after_targeting_rule_no_user.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": 5, + "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..6815fa3 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule + Returning '5'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..8e6facb --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +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/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt new file mode 100644 index 0000000..49de0d0 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -0,0 +1,7 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Skipping % options because the User Object is missing. + Returning '-1'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..c412e5a --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr.json b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr.json new file mode 100644 index 0000000..5f8d1c6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr.json @@ -0,0 +1,31 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_custom_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Chicken", + "expectedLog": "no_options_custom_attribute.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "matching_options_custom_attribute.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt new file mode 100644 index 0000000..2621086 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/matching_options_custom_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) + - Hash value 70 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt new file mode 100644 index 0000000..c92c5bc --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -0,0 +1,4 @@ +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/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' + Skipping % options because the User.Country attribute is missing. + Returning 'Chicken'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt new file mode 100644 index 0000000..7550655 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -0,0 +1,4 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id.json b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id.json new file mode 100644 index 0000000..442f575 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id.json @@ -0,0 +1,21 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_user_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_user_attribute_user.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt new file mode 100644 index 0000000..cc929fb --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_no_user.txt @@ -0,0 +1,4 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt new file mode 100644 index 0000000..dac8dd6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_based_on_user_id/options_user_attribute_user.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) + - Hash value 21 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule.json b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule.json new file mode 100644 index 0000000..4c6c533 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule.json @@ -0,0 +1,52 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_user.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt new file mode 100644 index 0000000..db721f5 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -0,0 +1,7 @@ +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/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Skipping % options because the User.Country attribute is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt new file mode 100644 index 0000000..8129521 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) + - Hash value 63 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..74f812f --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +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/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt new file mode 100644 index 0000000..141c3ad --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -0,0 +1,6 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..dd6032e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag.json b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag.json new file mode 100644 index 0000000..9c35c00 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "dependentFeatureWithUserCondition", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" + }, + { + "key": "dependentFeatureWithUserCondition2", + "defaultValue": "default", + "returnValue": "Frog", + "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "kate@configcat.com", + "Country": "USA" + }, + "returnValue": "Horse", + "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag.txt b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag.txt new file mode 100644 index 0000000..1d9022b --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag.txt @@ -0,0 +1,32 @@ +INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => no match + - IF User.Country IS ONE OF [<1 hashed value>] => true + AND User IS NOT IN SEGMENT 'Beta Users' + ( + Evaluating segment 'Beta Users': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. + ) => true + AND User IS NOT IN SEGMENT 'Developers' + ( + Evaluating segment 'Developers': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. + ) => true + THEN 'target' => MATCH, applying rule + Prerequisite flag evaluation result: 'target'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. + ) + THEN % options => MATCH, applying rule + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) + - Hash value 78 selects % option 4 (25%), 'Horse'. + Returning 'Horse'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_multilevel.txt @@ -0,0 +1,24 @@ +INFO [5000] Evaluating 'dependentFeatureMultipleLevels' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'intermediateFeature' EQUALS 'true' + ( + Evaluating prerequisite flag 'intermediateFeature': + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + THEN 'true' => MATCH, applying rule + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. + ) + THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt new file mode 100644 index 0000000..a9ff3a5 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt @@ -0,0 +1,38 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN 'Frog' => MATCH, applying rule + Returning 'Frog'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt new file mode 100644 index 0000000..f629cc7 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt @@ -0,0 +1,15 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Chicken'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt new file mode 100644 index 0000000..400cf5a --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt @@ -0,0 +1,18 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeature' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. + ) + THEN % options => no match + Returning 'Chicken'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment.json b/Tests/ConfigCatTests/Resources/evaluationlog/segment.json new file mode 100644 index 0000000..1bb4df5 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment.json @@ -0,0 +1,47 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", + "tests": [ + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user.txt" + }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" + }, + { + "key": "featureWithNegatedSegmentTargetingCleartext", + "defaultValue": false, + "user": { + "Identifier": "12345" + }, + "returnValue": false, + "expectedLog": "segment_no_targeted_attribute.txt" + }, + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": true, + "expectedLog": "segment_matching.txt" + }, + { + "key": "featureWithNegatedSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": false, + "expectedLog": "segment_no_matching.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_matching.txt b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_matching.txt new file mode 100644 index 0000000..9065aae --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS IN SEGMENT 'Beta users') evaluates to true. + ) + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_matching.txt b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_matching.txt new file mode 100644 index 0000000..0d04d83 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. + ) + THEN 'true' => no match + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_targeted_attribute.txt b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_targeted_attribute.txt new file mode 100644 index 0000000..6c7cf7e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_targeted_attribute.txt @@ -0,0 +1,13 @@ +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/ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' + ( + Evaluating segment 'Beta users (cleartext)': + - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions + Segment evaluation result: cannot evaluate, the User.Email attribute is missing. + Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. + ) + THEN 'true' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user.txt b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user.txt new file mode 100644 index 0000000..355dcce --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user.txt @@ -0,0 +1,6 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargeting' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user_multi_conditions.txt b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..ea01ccc --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +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()`/`getValueDetails()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation.json b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation.json new file mode 100644 index 0000000..3a14fc6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation.json @@ -0,0 +1,26 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", + "tests": [ + { + "key": "isNotOneOf", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + }, + { + "key": "relations", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_relations_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_error.txt b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_error.txt new file mode 100644 index 0000000..e14cc95 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_error.txt @@ -0,0 +1,9 @@ +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. +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. +INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_relations_error.txt b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_relations_error.txt new file mode 100644 index 0000000..8198c85 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/semver_validation/semver_relations_error.txt @@ -0,0 +1,18 @@ +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. +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. +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. +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. +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. +INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + - 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) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value.json b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value.json new file mode 100644 index 0000000..070d6f5 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value.json @@ -0,0 +1,37 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "boolDefaultFalse", + "defaultValue": true, + "returnValue": false, + "expectedLog": "off_flag.txt" + }, + { + "key": "boolDefaultTrue", + "defaultValue": false, + "returnValue": true, + "expectedLog": "on_flag.txt" + }, + { + "key": "stringDefaultCat", + "defaultValue": "Default", + "returnValue": "Cat", + "expectedLog": "text_setting.txt" + }, + { + "key": "integerDefaultOne", + "defaultValue": 0, + "returnValue": 1, + "expectedLog": "int_setting.txt" + }, + { + "testName": "double_setting", + "key": "doubleDefaultPi", + "defaultValue": 0.0, + "returnValue": 3.1415, + "expectedLog": "double_setting.txt" + } + ] +} diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/double_setting.txt b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/double_setting.txt new file mode 100644 index 0000000..4a632f7 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/double_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/int_setting.txt b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/int_setting.txt new file mode 100644 index 0000000..1361843 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/int_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/off_flag.txt b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/off_flag.txt new file mode 100644 index 0000000..4580685 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/off_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'false'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/on_flag.txt b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/on_flag.txt new file mode 100644 index 0000000..274c990 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/on_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'true'. diff --git a/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/text_setting.txt b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/text_setting.txt new file mode 100644 index 0000000..831d7c6 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/evaluationlog/simple_value/text_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'. diff --git a/Tests/ConfigCatTests/Resources/json/comparison_attribute_conversion.json b/Tests/ConfigCatTests/Resources/json/comparison_attribute_conversion.json new file mode 100644 index 0000000..5a900ae --- /dev/null +++ b/Tests/ConfigCatTests/Resources/json/comparison_attribute_conversion.json @@ -0,0 +1,789 @@ +{ + "p": { + "u": "https://test-cdn-global.configcat.com", + "r": 0, + "s": "uM29sy1rjx71ze3ehr\u002BqCnoIpx8NZgL8V//MN7OL1aM=" + }, + "f": { + "numberToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "0.12345" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionInt": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "125" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e+96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e-96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "1680307199.999" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"read\",\"Write\",\" eXecute \"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionEmpty": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionSpecialChars": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"+<>%\\\"'\\\\/\\t\\r\\n\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionUnicode": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"äöüÄÖÜçéèñışğ⢙✓😀\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + } + } +} diff --git a/Tests/ConfigCatTests/Resources/json/comparison_attribute_trimming.json b/Tests/ConfigCatTests/Resources/json/comparison_attribute_trimming.json new file mode 100644 index 0000000..a42df5f --- /dev/null +++ b/Tests/ConfigCatTests/Resources/json/comparison_attribute_trimming.json @@ -0,0 +1,985 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "VjBfGYcmyHzLBv5EINgSBbX6/rYevYGWQhF3Zk5t8i4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + "09d5761537a8136eb7fc45a53917b51cb9dcd2bb9b62ffa24ace0e8a7600a3c7" + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + "99d06b6b3669b906803c285267f76fe4e2ccc194b00801ab07f2fd49939b6960" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + "5_7eb158c29b48b62cec860dffc459171edbfeef458bcc8e8bb62956d823eef3df" + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": "ea0d05859bb737105eea40bc605f6afd542c8f50f8497cd21ace38e731d7eef0" + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + "1765b470044971bbc19e7bed10112199c5da9c626455f86be109fef96e747911" + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + "5_2a338d3beb8ebe2e711d198420d04e2627e39501c2fcc7d5b3b8d93540691097" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": "650fe0e8e86030b5f73ccd77e6532f307adf82506048a22f02d95386206ecea1" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + "5_586ab2ec61946cb1457d4af170d88e7f14e655d9debf352b4ab6bf5bf77df3f7" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + "1.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + "5_67a323069ee45fef4ccd8365007d4713f7a3bc87764943b1139e8e50d1aee8fd" + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + }, + "dateafter": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 19, + "d": 1705251600 + } + } + ], + "s": { + "v": { + "s": "19 trim" + }, + "i": "83e580ce" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1c12e0cc" + }, + "datebefore": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 18, + "d": 1705255200 + } + } + ], + "s": { + "v": { + "s": "18 trim" + }, + "i": "34614b07" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "26d4f328" + }, + "numberequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 10, + "d": 3 + } + } + ], + "s": { + "v": { + "s": "10 trim" + }, + "i": "6a8c0a08" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "7b8e49b9" + }, + "numbergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 14, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "14 trim" + }, + "i": "2037a7a4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "902f9bd9" + }, + "numbergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 15, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "15 trim" + }, + "i": "527c49d2" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2280c961" + }, + "numberless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 12, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "12 trim" + }, + "i": "c454f775" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ec935943" + }, + "numberlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 13, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "13 trim" + }, + "i": "1e31aed8" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1d53c679" + }, + "numbernotequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 11, + "d": 6 + } + } + ], + "s": { + "v": { + "s": "11 trim" + }, + "i": "e8d7cf05" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "21c749a7" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "2 trim" + }, + "i": "c3ab37cf" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "3 trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "f91ecf16" + } + } +} \ No newline at end of file diff --git a/Tests/ConfigCatTests/Resources/json/comparison_value_trimming.json b/Tests/ConfigCatTests/Resources/json/comparison_value_trimming.json new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/json/comparison_value_trimming.json @@ -0,0 +1,777 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "zsVN1DQ9Oa2FjFc96MvPfMM5Vs+KKV00NyybJZipyf4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + " 028fdb841bf3b2cc27fce407da08f87acd3a58a08c67d819cdb9351857b14237 " + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + " 60b747c290642863f9a6c68773ed309a9fb02c6c1ae65c77037046918f4c1d3c " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "2 trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "c3ab37cf" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + " 5_a6ce5e2838d4e0c27cd705c90f39e60d79056062983c39951668cf947ec406c2 " + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": " a2868640b1fe24c98e50b168756d83fd03779dd4349d6ddab5d7d6ef8dad13bd " + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + " 55ce90920d20fc0bf8078471062a85f82cc5ea2226012a901a5045775bace0f4 " + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "3 trim" + }, + "i": "f91ecf16" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + " 5_c517fc957907e30b6a790540a20172a3a5d3a7458a85e340a7b1a1ac982be278 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": " 31ceae14b865b0842e93fdc3a42a7e45780ccc41772ca9355db50e09d81e13ef " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + " 5_3643bbdd1bce4021fe4dbd55e6cc2f4902e4f50e592597d1a2d0e944fb7dfb42 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + " 1.0.1 " + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + " 1.0.0 " + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + " 5_3e052709552ca9d5bd6c459cb7ab0389f3210f6aafc3d006a2481635e9614a7c " + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + } + } +} \ No newline at end of file diff --git a/Tests/ConfigCatTests/Resources/json/test_circulardependency_v6.json b/Tests/ConfigCatTests/Resources/json/test_circulardependency_v6.json new file mode 100644 index 0000000..a8a9e17 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/json/test_circulardependency_v6.json @@ -0,0 +1,80 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 1, + "v": { "s": "key1-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key1", + "c": 0, + "v": { "s": "key1-prereq" } + } + } + ], + "s": { "v": { "s": "key1-prereq" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "key2-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key2-prereq" } } + } + ] + }, + "key3": { + "t": 1, + "v": { "s": "key3-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key2", + "c": 0, + "v": { "s": "key2-prereq" } + } + } + ], + "s": { "v": { "s": "key3-prereq" } } + } + ] + }, + "key4": { + "t": 1, + "v": { "s": "key4-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key4-prereq" } } + } + ] + } + } +} diff --git a/Tests/ConfigCatTests/Resources/json/test_override_flagdependency_v6.json b/Tests/ConfigCatTests/Resources/json/test_override_flagdependency_v6.json new file mode 100644 index 0000000..62e159e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/json/test_override_flagdependency_v6.json @@ -0,0 +1,44 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" + }, + "f": { + "mainStringFlag": { + "t": 1, + "v": { + "s": "private" + }, + "i": "24c96275" + }, + "stringDependsOnInt": { + "t": 1, + "r": [ + { + "c": [ + { + "p": { + "f": "mainIntFlag", + "c": 0, + "v": { + "i": 42 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "12531eec" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "e227d926" + } + } +} diff --git a/Tests/ConfigCatTests/Resources/testmatrix.csv b/Tests/ConfigCatTests/Resources/testmatrix.csv index 7f5d406..6de7454 100644 --- a/Tests/ConfigCatTests/Resources/testmatrix.csv +++ b/Tests/ConfigCatTests/Resources/testmatrix.csv @@ -1,5 +1,5 @@ Identifier;Email;Country;Custom1;bool30TrueAdvancedRules;boolDefaultFalse;boolDefaultTrue;double25Pi25E25Gr25Zero;doubleDefaultPi;integer25One25Two25Three25FourAdvancedRules;integerDefaultOne;string25Cat25Dog25Falcon25Horse;string25Cat25Dog25Falcon25HorseAdvancedRules;string75Cat0Dog25Falcon0Horse;stringContainsDogDefaultCat;stringDefaultCat;stringIsInDogDefaultCat;stringIsNotInDogDefaultCat;stringNotContainsDogDefaultCat -##null##;;;;True;False;True;-1.0;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat +##null##;;;;True;False;True;-1;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat ;;;;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat a@configcat.com;a@configcat.com;Hungary;admin;False;False;True;5.561;3.1415;5;1;Cat;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat b@configcat.com;b@configcat.com;Hungary;;False;False;True;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat @@ -12,9 +12,9 @@ h@configcat.com;h@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;C i@configcat.com;i@configcat.com;;admin;True;False;True;5.561;3.1415;5;1;Cat;Lion;Falcon;Dog;Cat;Dog;Dog;Cat j@configcat.com;j@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat stern@msn.com;stern@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;True;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog luebke@hotmail.com;luebke@hotmail.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -padme@icloud.com;padme@icloud.com;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +padme@icloud.com;padme@icloud.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog claypool@aol.com;claypool@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog bogjobber@verizon.net;bogjobber@verizon.net;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog cliffordj@aol.com;cliffordj@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog @@ -29,57 +29,57 @@ josem@icloud.com;josem@icloud.com;##null##;##null##;False;False;True;2.7182;3.14 hedwig@outlook.com;hedwig@outlook.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog camenisch@yahoo.com;camenisch@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ccohen@comcast.net;ccohen@comcast.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -techie@att.net;techie@att.net;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +techie@att.net;techie@att.net;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog damian@gmail.com;damian@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ebassi@me.com;ebassi@me.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog curly@aol.com;curly@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog rddesign@optonline.net;rddesign@optonline.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog boftx@gmail.com;boftx@gmail.com;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog eegsa@yahoo.ca;eegsa@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog ganter@gmail.com;ganter@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -mleary@att.net;mleary@att.net;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +mleary@att.net;mleary@att.net;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog kassiesa@icloud.com;kassiesa@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog mhanoh@yahoo.ca;mhanoh@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog +henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog barjam@yahoo.com;barjam@yahoo.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog mirod@msn.com;mirod@msn.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog niknejad@optonline.net;niknejad@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog bwcarty@sbcglobal.net;bwcarty@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog mcast@aol.com;mcast@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -portscan@msn.com;portscan@msn.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +portscan@msn.com;portscan@msn.com;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog pereinar@yahoo.ca;pereinar@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog floxy@verizon.net;floxy@verizon.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog mgemmons@optonline.net;mgemmons@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -luvirini@mac.com;luvirini@mac.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +luvirini@mac.com;luvirini@mac.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog gslondon@gmail.com;gslondon@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog lamky@comcast.net;lamky@comcast.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog keiji@mac.com;keiji@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog gumpish@verizon.net;gumpish@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog tromey@hotmail.com;tromey@hotmail.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog miyop@aol.com;miyop@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog natepuri@me.com;natepuri@me.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog sbmrjbr@outlook.com;sbmrjbr@outlook.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;True;False;True;0.0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -munson@gmail.com;munson@gmail.com;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +munson@gmail.com;munson@gmail.com;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog lushe@yahoo.ca;lushe@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog skythe@gmail.com;skythe@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog jigsaw@me.com;jigsaw@me.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog schwaang@gmail.com;schwaang@gmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog eurohack@verizon.net;eurohack@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog janneh@icloud.com;janneh@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -frederic@me.com;frederic@me.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +frederic@me.com;frederic@me.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog facet@optonline.net;facet@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -uncle@aol.com;uncle@aol.com;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +uncle@aol.com;uncle@aol.com;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog wilsonpm@comcast.net;wilsonpm@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog garland@optonline.net;garland@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog srour@yahoo.com;srour@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog @@ -91,9 +91,9 @@ bester@mac.com;bester@mac.com;##null##;##null##;True;False;True;1.61803;3.1415;1 kildjean@verizon.net;kildjean@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Dog;Dog arandal@comcast.net;arandal@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog bartlett@yahoo.com;bartlett@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog valdez@mac.com;valdez@mac.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -scato@yahoo.com;scato@yahoo.com;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog +scato@yahoo.com;scato@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog sinkou@live.com;sinkou@live.com;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog evilopie@comcast.net;evilopie@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog ducasse@gmail.com;ducasse@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog @@ -101,30 +101,30 @@ sthomas@sbcglobal.net;sthomas@sbcglobal.net;##null##;##null##;False;False;True;1 plover@msn.com;plover@msn.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog mavilar@yahoo.com;mavilar@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog josephw@msn.com;josephw@msn.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog munson@mac.com;munson@mac.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog paulv@mac.com;paulv@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog dogdude@hotmail.com;dogdude@hotmail.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog carcus@yahoo.com;carcus@yahoo.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog sblack@me.com;sblack@me.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog richard@gmail.com;richard@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog tbusch@yahoo.ca;tbusch@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog gtaylor@aol.com;gtaylor@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog burniske@att.net;burniske@att.net;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -bebing@me.com;bebing@me.com;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +bebing@me.com;bebing@me.com;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog joglo@gmail.com;joglo@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog chrwin@sbcglobal.net;chrwin@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog chaikin@yahoo.com;chaikin@yahoo.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog wbarker@yahoo.ca;wbarker@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog ganter@verizon.net;ganter@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -eegsa@att.net;eegsa@att.net;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +eegsa@att.net;eegsa@att.net;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog sethbrown@hotmail.com;sethbrown@hotmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog solomon@me.com;solomon@me.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog tellis@yahoo.ca;tellis@yahoo.ca;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog jshirley@optonline.net;jshirley@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog bescoto@yahoo.com;bescoto@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog hstiles@comcast.net;hstiles@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog gumpish@optonline.net;gumpish@optonline.net;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog @@ -139,12 +139,12 @@ shrapnull@att.net;shrapnull@att.net;##null##;##null##;True;False;True;2.7182;3.1 lcheng@comcast.net;lcheng@comcast.net;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog cyrus@msn.com;cyrus@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog suresh@yahoo.ca;suresh@yahoo.ca;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -elflord@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sassen@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +elflord@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +sassen@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dbindel@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -morain@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +morain@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat timtroyr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -esbeck@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +esbeck@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat ilyaz@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat grinder@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat trieuvan@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -154,7 +154,7 @@ nichoj@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1 sopwith@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat odlyzko@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat warrior@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -budinger@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +budinger@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lstein@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kmiller@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat british@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -162,19 +162,19 @@ webinc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1; kohlis@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat matthijs@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat mmccool@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@hotmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wildfire@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +ribet@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wildfire@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat makarow@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat garland@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -kjohnson@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +kjohnson@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat oneiros@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jaxweb@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -raides@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jaxweb@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +raides@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat cantu@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat msherr@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat comdig@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -esokullu@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +esokullu@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat kjetilk@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat boomzilla@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat cvrcek@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -186,76 +186,76 @@ weidai@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2; dpitts@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat bebing@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat wikinerd@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat niknejad@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat aukjan@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat dleconte@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -noahb@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +noahb@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bdbrown@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -adillon@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +adillon@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat eegsa@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat chunzi@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat privcan@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat mglee@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat leocharre@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dwendlan@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lpalmer@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lpalmer@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat emcleod@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mwandel@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -stewwy@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +stewwy@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat drolsky@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -lukka@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lukka@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat epeeist@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat papathan@verizon.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat singh@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat willg@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jimmichie@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat frosal@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat dunstan@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat parasite@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat konit@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat amaranth@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcsporran@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gommix@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dprice@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gommix@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dprice@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat lcheng@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dwendlan@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miami@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dwendlan@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miami@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat ajlitt@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat grdschl@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat andersbr@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat hoangle@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jbuchana@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -knorr@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -saridder@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -scotfl@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skoch@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +knorr@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +saridder@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +scotfl@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skoch@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat luebke@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bsikdar@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bsikdar@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat ryanvm@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat dburrows@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -seebs@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +seebs@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mgemmons@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat kobayasi@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat marcs@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -martink@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +netsfr@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +martink@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bflong@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat bhima@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat oster@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -teverett@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +teverett@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat kannan@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat yzheng@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat msusa@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat falcao@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -uraeus@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +uraeus@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dunstan@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat oracle@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat tbeck@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -265,25 +265,25 @@ yenya@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog; bjoern@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat benanov@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat preneel@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -punkis@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rwelty@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +punkis@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rwelty@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat penna@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat baveja@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat louise@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat arachne@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat hahiss@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat wayward@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pajas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -intlprog@comcast.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +pajas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +intlprog@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat hermanab@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat presoff@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat trygstad@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -denton@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skythe@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +denton@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skythe@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lushe@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ullman@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +magusnet@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ullman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jyoliver@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat flavell@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat ianbuck@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -292,11 +292,11 @@ gommix@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4 rnelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat crusader@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rddesign@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nanop@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +nanop@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat ngedmond@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat munjal@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat unreal@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jemarch@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jemarch@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat shawnce@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat nweaver@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat british@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -310,10 +310,10 @@ mrobshaw@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415 denton@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat konst@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat louise@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wetter@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wetter@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat kohlis@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat monkeydo@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -melnik@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +melnik@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat munge@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat stefano@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat giafly@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -324,8 +324,8 @@ firstpr@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1; nichoj@comcast.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat aibrahim@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat inico@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ajlitt@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ribet@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ajlitt@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat errxn@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat lstein@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mgemmons@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -340,17 +340,17 @@ dhwon@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Do mstrout@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat manuals@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat andrewik@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hahsler@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +hahsler@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat miami@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat facet@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -zeitlin@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -lamprecht@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +zeitlin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +lamprecht@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat johnh@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mrsam@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrsam@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lipeng@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat dsowsy@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat philen@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kjohnson@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kjohnson@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat nelson@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat syncnine@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat @@ -360,16 +360,16 @@ mrdvt@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1; cfhsoft@outlook.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kodeman@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat mbrown@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jaxweb@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jaxweb@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat symbolic@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat hager@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -zavadsky@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +zavadsky@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat mugwump@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dprice@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -petersko@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +dprice@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +petersko@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +netsfr@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat tbmaddux@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat meder@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat benits@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -395,12 +395,12 @@ wayward@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1; amaranth@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat garland@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat rfisher@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat mavilar@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rfisher@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat tarreau@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -koudas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bwcarty@mac.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +koudas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bwcarty@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jeteve@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat pmint@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat barlow@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -415,24 +415,24 @@ clkao@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1; noahb@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat ducasse@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jrkorson@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat corrada@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat tmaek@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat richard@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat pkplex@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kwilliams@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kwilliams@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcrawfor@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat webteam@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat leakin@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat konst@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat godeke@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -godeke@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +godeke@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat glenz@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fallorn@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat nacho@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +dkeeler@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat adhere@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gfody@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat codex@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -444,37 +444,37 @@ miami@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat; kewley@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dkeeler@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat galbra@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mastinfo@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kempsonc@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mastinfo@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kempsonc@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat andale@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat airship@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat xtang@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jhardin@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat frederic@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -matsn@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +matsn@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pereinar@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat salesgeek@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat tezbo@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat keijser@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat chaki@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wetter@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wetter@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -thassine@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +thassine@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat hoangle@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat bester@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat jdhedden@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat killmenow@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat retoh@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -goresky@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -microfab@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +goresky@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +microfab@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat onestab@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat aracne@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat sherzodr@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat feamster@verizon.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat hyper@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jmgomez@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -fwitness@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +fwitness@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat storerm@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat improv@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat arnold@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -484,11 +484,11 @@ rfisher@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4; dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat alastair@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat multiplx@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -caidaperl@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +caidaperl@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat keutzer@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lbaxter@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hachi@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hachi@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mfburgo@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat gfody@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat jaxweb@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -500,25 +500,25 @@ lstein@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3; conteb@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat wildixon@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat oechslin@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -metzzo@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +metzzo@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat kosact@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kodeman@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kodeman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat ebassi@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat pgolle@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jdhildeb@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat garyjb@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat gslondon@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -maratb@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marnanel@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +maratb@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marnanel@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat rgiersig@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat gozer@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat valdez@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat mnemonic@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat paina@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syncnine@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +syncnine@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat melnik@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jaesenj@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sekiya@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sekiya@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbusch@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat whimsy@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat firstpr@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -529,22 +529,22 @@ bcevc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;C sethbrown@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat mcmillan@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat raines@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -psharpe@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bachmann@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +psharpe@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bachmann@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat leslie@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hager@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ismail@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +ismail@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat nacho@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat kohlis@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat lahvak@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat willg@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat zavadsky@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -steve@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +steve@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ccohen@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat msusa@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat jsmith@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jshearer@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jshearer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat majordick@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat sjava@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -556,32 +556,32 @@ portscan@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.141 morain@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat akoblin@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mhassel@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mhassel@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat spadkins@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat rohitm@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat mwandel@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat warrior@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jipsen@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jipsen@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat bancboy@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat corrada@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat wojciech@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat marcs@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -atmarks@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +atmarks@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat quinn@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pizza@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dkeeler@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pizza@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat empathy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dmouse@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -dinther@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dinther@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat pappp@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat dougj@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat cfhsoft@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat maratb@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat stewwy@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -sravani@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +sravani@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat tmaek@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat juliano@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mcsporran@optonline.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mcsporran@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat mgemmons@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat seasweb@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat lushe@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -592,34 +592,34 @@ eegsa@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat; rhavyn@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat cremonini@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat boftx@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -smartfart@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -uncled@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +smartfart@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +uncled@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat quantaman@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat heidrich@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat moinefou@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ilial@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ilial@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat fraser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -csilvers@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -csilvers@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kalpol@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +csilvers@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +csilvers@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kalpol@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat punkis@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -nacho@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nacho@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mcsporran@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jaarnial@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gboss@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat henkp@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat philb@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hllam@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hllam@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat roamer@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat cantu@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat firstpr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat hmbrand@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat arandal@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jaarnial@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +jaarnial@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat hoyer@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat mmccool@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -smcnabb@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +smcnabb@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat pakaste@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat panolex@mac.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hikoza@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -633,64 +633,64 @@ hstiles@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1 lushe@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat devphil@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat dowdy@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -arachne@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +arachne@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat donev@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bowmanbs@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat roesch@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat lridener@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hmbrand@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat sopwith@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -vsprintf@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +vsprintf@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -symbolic@aol.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +symbolic@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat gbacon@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hillct@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -earmstro@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hillct@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +earmstro@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat studyabr@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat shawnce@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat boser@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gknauss@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat marcs@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bruck@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +bruck@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat comdig@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat floxy@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat tmccarth@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat darin@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mcraigw@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fhirsch@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mcraigw@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fhirsch@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat unreal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat crypt@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pakaste@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat staffelb@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jonas@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jonas@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat staikos@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat mfburgo@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -psichel@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +psichel@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat brainless@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat jmgomez@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat wsnyder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -carcus@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +carcus@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat dmouse@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat graham@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat murdocj@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat reziac@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@sbcglobal.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +caronni@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat hoyer@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -amcuri@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +amcuri@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mstrout@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -osrin@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +osrin@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat geeber@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat konit@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat mxiao@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ryanshaw@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ryanshaw@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat bowmanbs@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -yamla@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +yamla@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat ardagna@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -darin@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jmorris@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -valdez@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -haddawy@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +darin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jmorris@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +valdez@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +haddawy@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat biglou@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pplinux@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat afeldspar@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -698,10 +698,10 @@ campbell@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415 akoblin@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat nwiger@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat stinson@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -daveed@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +daveed@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat arachne@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat augusto@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -xtang@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +xtang@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat itstatus@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kspiteri@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -714,69 +714,69 @@ sisyphus@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4; jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat jamuir@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat dobey@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rande@live.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +rande@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat dkasak@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat greear@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat thaljef@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sjmuir@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +sjmuir@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat joehall@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat kronvold@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jmorris@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat ajohnson@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat russotto@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat pgolle@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -mrdvt@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -starstuff@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mrdvt@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jesse@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat luebke@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat emmanuel@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -imightb@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +imightb@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat wbarker@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat luvirini@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat ylchang@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -elflord@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +elflord@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat scottzed@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mcraigw@yahoo.ca;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mcraigw@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat nacho@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat rwelty@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat subir@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbusch@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -rupak@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rupak@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat giafly@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -british@outlook.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +british@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hllam@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fatelk@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat parsimony@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat tbeck@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat citizenl@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jimxugle@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -starstuff@sbcglobal.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bader@icloud.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bader@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat starstuff@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -satishr@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +satishr@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat ilikered@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -bader@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -yruan@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bader@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +yruan@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat skoch@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat bader@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat smallpaul@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat feamster@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat mcnihil@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -bmorrow@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +bmorrow@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat laird@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -ingolfke@msn.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -irving@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ingolfke@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +irving@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat monopole@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -keiji@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wortmanj@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +keiji@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wortmanj@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat keijser@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat aschmitz@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -tangsh@mac.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +tangsh@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jdray@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kewley@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kewley@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat wayward@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat errxn@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat fglock@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -789,14 +789,14 @@ dmiller@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2; choset@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat dbrobins@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat pizza@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noahb@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +noahb@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jespley@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jfriedl@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat arebenti@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat eidac@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jipsen@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat malin@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jramio@optonline.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jramio@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat jsnover@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat martyloo@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat danny@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -816,37 +816,37 @@ knorr@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1; kawasaki@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat ducasse@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat satishr@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -drewf@comcast.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +drewf@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat martyloo@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat lstein@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat nighthawk@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat debest@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cyrus@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +cyrus@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat yruan@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat bmidd@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat policies@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -treit@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +treit@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat animats@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kawasaki@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jramio@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +jramio@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat josephw@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rgarcia@me.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +rgarcia@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat ryanvm@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat rnewman@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat yangyan@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat tubesteak@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat munjal@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pgolle@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -milton@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +milton@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat harryh@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -howler@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +howler@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat drewf@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat cantu@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat leslie@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mfleming@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat nelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -valdez@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +valdez@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat jsmith@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat djpig@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat bader@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -855,24 +855,24 @@ dawnsong@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1; mcrawfor@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat pthomsen@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat raides@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ahuillet@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kostas@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lridener@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ahuillet@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kostas@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lridener@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat maneesh@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat sartak@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rohitm@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rohitm@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat heidrich@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat koudas@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat okroeger@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cgcra@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +cgcra@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat janusfury@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat seurat@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat dhrakar@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat bebing@msn.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat keiji@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -okroeger@hotmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gward@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +okroeger@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gward@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat jusdisgi@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat sakusha@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat rande@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -882,14 +882,14 @@ marcs@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Hor juerd@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat sethbrown@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kdawson@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mfburgo@aol.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mfburgo@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat crandall@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat schwaang@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mrsam@icloud.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrsam@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat szymansk@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat epeeist@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat miyop@sbcglobal.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wainwrig@me.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wainwrig@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat pereinar@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat killmenow@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat dsowsy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat @@ -899,8 +899,8 @@ mkearl@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1; jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat openldap@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat world@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -adamk@att.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pdbaby@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +adamk@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pdbaby@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat hellfire@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat firstpr@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kenja@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -908,14 +908,14 @@ leslie@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;F bogjobber@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat hauma@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat hoangle@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nimaclea@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +nimaclea@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat fraterk@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat ninenine@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -campware@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +campware@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat amimojo@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -karasik@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -yenya@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +karasik@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +yenya@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat stevelim@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat dvdotnet@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat bonmots@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat @@ -925,8 +925,8 @@ alfred@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1; multiplx@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat tjensen@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat dmath@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kostas@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -carmena@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kostas@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +carmena@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat terjesa@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat tjensen@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat schwaang@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat @@ -943,14 +943,14 @@ lpalmer@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Ca dgatwood@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat jtorkbob@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat rfoley@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -andale@comcast.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +andale@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat mlewan@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ianbuck@yahoo.ca;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syrinx@live.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ianbuck@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +syrinx@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat imightb@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat gozer@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gozer@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -shawnce@gmail.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +gozer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +shawnce@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat lauronen@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat raines@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat jfriedl@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat @@ -960,25 +960,25 @@ thurston@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415; flaviog@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat mnemonic@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat privcan@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mobileip@verizon.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mobileip@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat gbacon@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@optonline.net;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +caronni@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat tbeck@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat keijser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat scotfl@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat ryanshaw@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -eimear@att.net;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +eimear@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat noticias@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -leocharre@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +leocharre@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat killmenow@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat petersen@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat bdthomas@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat mavilar@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat isaacson@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miyop@msn.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miyop@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat bwcarty@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat warrior@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@hotmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +magusnet@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat crowemojo@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat crypt@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat kempsonc@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat @@ -987,27 +987,27 @@ noahb@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Fal symbolic@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat wsnyder@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat pdbaby@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jfriedl@yahoo.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat jwarren@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat dsugal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat kayvonf@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nasarius@mac.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nasarius@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat bolow@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat tbmaddux@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat maradine@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat breegster@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jshirley@gmail.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jshirley@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat tfinniga@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat delpino@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat stecoop@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat jnolan@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jhardin@yahoo.com;##null##;##null##;##null##;True;False;True;0.0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jhardin@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat teverett@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat wsnyder@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat treeves@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -garland@outlook.com;##null##;##null##;##null##;False;False;True;0.0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +garland@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat ullman@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat sumdumass@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat diff --git a/Tests/ConfigCatTests/Resources/testmatrix_and_or.csv b/Tests/ConfigCatTests/Resources/testmatrix_and_or.csv new file mode 100644 index 0000000..5a149f4 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_and_or.csv @@ -0,0 +1,15 @@ +Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr +##null##;;;;public;Chicken;Cat;Cat +;;;;public;Chicken;Cat;Cat +jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane +john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John +a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat +mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark +nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat +stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane +anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane +jane;jane;##null##;##null##;public;Chicken;Cat;Cat +@sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat +jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat diff --git a/Tests/ConfigCatTests/Resources/testmatrix_comparators_v6.csv b/Tests/ConfigCatTests/Resources/testmatrix_comparators_v6.csv new file mode 100644 index 0000000..d53efb5 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_comparators_v6.csv @@ -0,0 +1,24 @@ +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat +##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat diff --git a/Tests/ConfigCatTests/Resources/testmatrix_prerequisite_flag.csv b/Tests/ConfigCatTests/Resources/testmatrix_prerequisite_flag.csv new file mode 100644 index 0000000..dcf68f4 --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_prerequisite_flag.csv @@ -0,0 +1,5 @@ +Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse +##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +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 +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 diff --git a/Tests/ConfigCatTests/Resources/testmatrix_segments.csv b/Tests/ConfigCatTests/Resources/testmatrix_segments.csv new file mode 100644 index 0000000..b59ba3a --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_segments.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment +##null##;;;;False;False;False;False +;;;;False;False;False;False +john@example.com;john@example.com;##null##;##null##;False;False;False;False +jane@example.com;jane@example.com;##null##;##null##;False;False;False;False +kate@example.com;kate@example.com;##null##;##null##;True;True;True;True diff --git a/Tests/ConfigCatTests/Resources/testmatrix_segments_old.csv b/Tests/ConfigCatTests/Resources/testmatrix_segments_old.csv new file mode 100644 index 0000000..9fc605e --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_segments_old.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext +##null##;;;;False;False;False;False;False;False;False;False +;;;;False;False;False;False;False;False;False;False +john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True +jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True +kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False diff --git a/Tests/ConfigCatTests/Resources/testmatrix_unicode.csv b/Tests/ConfigCatTests/Resources/testmatrix_unicode.csv new file mode 100644 index 0000000..e5b01de --- /dev/null +++ b/Tests/ConfigCatTests/Resources/testmatrix_unicode.csv @@ -0,0 +1,14 @@ +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 +1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +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 +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 +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 +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 +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 +;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False +;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False +;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +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 +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 +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 diff --git a/Tests/ConfigCatTests/RolloutIntegrationTests.swift b/Tests/ConfigCatTests/RolloutIntegrationV1Tests.swift similarity index 57% rename from Tests/ConfigCatTests/RolloutIntegrationTests.swift rename to Tests/ConfigCatTests/RolloutIntegrationV1Tests.swift index c005211..b5c4af5 100644 --- a/Tests/ConfigCatTests/RolloutIntegrationTests.swift +++ b/Tests/ConfigCatTests/RolloutIntegrationV1Tests.swift @@ -1,7 +1,7 @@ import XCTest @testable import ConfigCat -class RolloutIntegrationTests: XCTestCase { +class RolloutIntegrationV1Tests: XCTestCase { enum TestType { case value case variation @@ -15,67 +15,71 @@ class RolloutIntegrationTests: XCTestCase { #endif }() - func testRolloutMatrixText() throws { - if let url = testBundle.url(forResource: "testmatrix", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", type: .value) + func testRolloutMatrixText() { + if let content = loadResource(bundle: testBundle, path: "testmatrix.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", type: .value) + } else { + XCTFail() + } + } + + func testRolloutMatrixSegments() throws { + if let content = loadResource(bundle: testBundle, path: "testmatrix_segments_old.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", type: .value) } else { XCTFail() } } func testRolloutMatrixSemantic() throws { - if let url = testBundle.url(forResource: "testmatrix_semantic", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", type: .value) + if let content = loadResource(bundle: testBundle, path: "testmatrix_semantic.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", type: .value) } else { XCTFail() } } func testRolloutMatrixSemantic2() throws { - if let url = testBundle.url(forResource: "testmatrix_semantic_2", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", type: .value) + if let content = loadResource(bundle: testBundle, path: "testmatrix_semantic_2.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", type: .value) } else { XCTFail() } } func testRolloutMatrixNumber() throws { - if let url = testBundle.url(forResource: "testmatrix_number", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", type: .value) + if let content = loadResource(bundle: testBundle, path: "testmatrix_number.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", type: .value) } else { XCTFail() } } func testRolloutMatrixSensitive() throws { - if let url = testBundle.url(forResource: "testmatrix_sensitive", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", type: .value) + if let content = loadResource(bundle: testBundle, path: "testmatrix_sensitive.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", type: .value) } else { XCTFail() } } func testRolloutMatrixVariationId() throws { - if let url = testBundle.url(forResource: "testmatrix_variationId", withExtension: "csv") { - try testRolloutMatrix(url: url, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", type: .variation) + if let content = loadResource(bundle: testBundle, path: "testmatrix_variationid.csv") { + testRolloutMatrix(content: content, sdkKey: "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", type: .variation) } else { XCTFail() } } - func testRolloutMatrix(url: URL, sdkKey: String, type: TestType) throws { + func testRolloutMatrix(content: String, sdkKey: String, type: TestType) { let client: ConfigCatClient = ConfigCatClient.get(sdkKey: sdkKey) { options in options.pollingMode = PollingModes.lazyLoad() + options.logLevel = .nolog } defer { ConfigCatClient.closeAll() } - guard let matrixData = try? Data(contentsOf: url), let content = String(bytes: matrixData, encoding: .utf8) else { - XCTFail() - return - } - let rows = content.components(separatedBy: "\n") .map { row in row.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) @@ -105,8 +109,8 @@ class RolloutIntegrationTests: XCTestCase { var user: ConfigCatUser? = nil if testObjects[0] != "##null##" { - var email = "" - var country = "" + var email: String? = nil + var country: String? = nil let identifier = testObjects[0] @@ -134,44 +138,31 @@ class RolloutIntegrationTests: XCTestCase { if let boolValue = anyValue as? Bool, let expectedValue = Bool(testObjects[i + 4].lowercased()) { if boolValue != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, boolValue)) + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(boolValue)") } - expectation.fulfill() - return - } - - if let intValue = anyValue as? Int, + } else if let intValue = anyValue as? Int, let expectedValue = Int(testObjects[i + 4]) { if intValue != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, intValue)) + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(intValue)") } - expectation.fulfill() - return - } - - if let doubleValue = anyValue as? Double, + } else if let doubleValue = anyValue as? Double, let expectedValue = Double(testObjects[i + 4]) { if doubleValue != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, doubleValue)) + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(doubleValue)") } - expectation.fulfill() - return - } - - if let stringValue = anyValue as? String { + } else if let stringValue = anyValue as? String { let expectedValue = testObjects[i + 4] if stringValue != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, stringValue)) + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(stringValue)") } - expectation.fulfill() - return } + expectation.fulfill() } } else { client.getValueDetails(for: settingKey, defaultValue: nil, user: user) { (details: TypedEvaluationDetails) in let expectedValue = testObjects[i + 4] if details.variationId != expectedValue { - errors.append(String(format: "Identifier: %@, Key: %@. Expected: %@, Result: %@", testObjects[0], settingKey, expectedValue, details.variationId ?? "")) + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(details.variationId ?? "")") } expectation.fulfill() } @@ -180,13 +171,13 @@ class RolloutIntegrationTests: XCTestCase { i += 1 } } - + + if !errors.isEmpty { + for err in errors { + print(err) + } + } + XCTAssertEqual(0, errors.count) } } - -extension Array { - func skip(count: Int) -> [Element] { - [Element](self[count..) in + let expectedValue = testObjects[i + 4] + if details.variationId != expectedValue { + errors.append("Identifier: \(testObjects[0]), Key: \(settingKey). Expected: \(expectedValue), Result: \(details.variationId ?? "")") + } + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 20) + i += 1 + } + } + + if !errors.isEmpty { + for err in errors { + print(err) + } + } + + XCTAssertEqual(0, errors.count) + } +} diff --git a/Tests/ConfigCatTests/SnapshotTests.swift b/Tests/ConfigCatTests/SnapshotTests.swift index 5d7f165..889b78d 100644 --- a/Tests/ConfigCatTests/SnapshotTests.swift +++ b/Tests/ConfigCatTests/SnapshotTests.swift @@ -2,14 +2,14 @@ import XCTest @testable import ConfigCat class SnapshotTests: XCTestCase { - let testJsonMultiple = #"{ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}] } } }"# + let testJsonMultiple = #"{"f":{"key1":{"t":0,"v":{"b":true},"i":"fakeId1"},"key2":{"t":0,"r":[{"c":[{"u":{"a":"Email","c":2,"l":["@example.com"]}}],"s":{"v":{"b":true},"i":"9f21c24c"}}],"v":{"b":false},"i":"fakeId2"}}}"# let user = ConfigCatUser(identifier: "id", email: "test@example.com") func testGetValue() { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for ready") client.hooks.addOnReady { _ in expectation.fulfill() @@ -27,7 +27,7 @@ class SnapshotTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for ready") client.hooks.addOnReady { _ in expectation.fulfill() @@ -44,7 +44,7 @@ class SnapshotTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) let expectation = self.expectation(description: "wait for ready") client.hooks.addOnReady { _ in expectation.fulfill() @@ -64,7 +64,7 @@ class SnapshotTests: XCTestCase { let engine = MockEngine() engine.enqueueResponse(response: Response(body: testJsonMultiple, statusCode: 200)) - let client = ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.autoPoll(), httpEngine: engine) + let client = ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.autoPoll(), logger: NoLogger(), httpEngine: engine) await client.waitForReady() diff --git a/Tests/ConfigCatTests/VariationIdTests.swift b/Tests/ConfigCatTests/VariationIdTests.swift index 1cd3eed..79a3fb1 100755 --- a/Tests/ConfigCatTests/VariationIdTests.swift +++ b/Tests/ConfigCatTests/VariationIdTests.swift @@ -5,42 +5,74 @@ class VariationIdTests: XCTestCase { let testJson = #""" {"f":{ "key1":{ - "v":true, + "v": { + "b": true + }, "i":"fakeId1", + "t": 0, "p":[ - { - "v":true, - "p":50, - "i":"percentageId1" - }, - { - "v":false, - "p":50, - "i":"percentageId2" - } + { + "p":50, + "v":{ + "b":true + }, + "i":"percentageId1" + }, + { + "p":50, + "v":{ + "b":false + }, + "i":"percentageId2" + } ], "r":[ - { - "a":"Email", - "t":2, - "c":"@configcat.com", - "v":true, - "i":"rolloutId1" - }, - { - "a":"Email", - "t":2, - "c":"@test.com", - "v":false, - "i":"rolloutId2" - } + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@configcat.com" + ] + } + } + ], + "s":{ + "v":{ + "b":true + }, + "i":"rolloutId1" + } + }, + { + "c":[ + { + "u":{ + "a":"Email", + "c":2, + "l":[ + "@test.com" + ] + } + } + ], + "s":{ + "v":{ + "b":false + }, + "i":"rolloutId2" + } + } ] }, "key2":{ - "v":false, + "v": { + "b": false + }, "i":"fakeId2", - "p":[], - "r":[] + "t": 0 } }} """# @@ -129,6 +161,6 @@ class VariationIdTests: XCTestCase { } private func createClient(httpEngine: HttpEngine) -> ConfigCatClient { - ConfigCatClient(sdkKey: "test", pollingMode: PollingModes.manualPoll(), httpEngine: httpEngine) + ConfigCatClient(sdkKey: randomSdkKey(), pollingMode: PollingModes.manualPoll(), logger: NoLogger(), httpEngine: httpEngine) } } diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..9d0b61b --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,16 @@ +sonar.host.url=https://sonarcloud.io +sonar.organization=configcat +sonar.projectKey=configcat_swift-sdk +sonar.language=swift + +sonar.swift.project=ConfigCat.xcodeproj +sonar.sources=Sources + +sonar.exclusion=Tests,Version + +sonar.coverageReportPaths=reports/sonarqube-generic-coverage.xml +sonar.swift.coverage.reportPaths=reports/sonarqube-generic-coverage.xml + +sonar.c.file.suffixes=- +sonar.cpp.file.suffixes=- +sonar.objc.file.suffixes=- \ No newline at end of file