From 351bd5392362eab103142dfdf92525e95b6ef93e Mon Sep 17 00:00:00 2001 From: Adam Simon Date: Thu, 25 Jan 2024 18:29:19 +0100 Subject: [PATCH] Update config JSON model to v6 + fix inconsistent error 1000 reporting + improve config json deserialization error reporting --- src/Attributes/Config.php | 14 -- src/Attributes/PercentageAttributes.php | 15 -- src/Attributes/Preferences.php | 14 -- src/Attributes/RolloutAttributes.php | 17 --- src/Attributes/SettingAttributes.php | 17 --- src/Cache/ConfigEntry.php | 6 +- src/ConfigCatClient.php | 61 +++++---- src/ConfigFetcher.php | 28 ++-- src/ConfigJson/Condition.php | 15 ++ src/ConfigJson/Config.php | 68 +++++++++ src/ConfigJson/PercentageOption.php | 31 +++++ src/ConfigJson/Preferences.php | 26 ++++ src/ConfigJson/PrerequisiteFlagComparator.php | 17 +++ src/ConfigJson/PrerequisiteFlagCondition.php | 15 ++ src/ConfigJson/RedirectMode.php | 15 ++ src/ConfigJson/Segment.php | 14 ++ src/ConfigJson/SegmentComparator.php | 17 +++ src/ConfigJson/SegmentCondition.php | 14 ++ src/ConfigJson/Setting.php | 115 ++++++++++++++++ src/ConfigJson/SettingType.php | 23 ++++ src/ConfigJson/SettingValue.php | 62 +++++++++ src/ConfigJson/SettingValueContainer.php | 14 ++ src/ConfigJson/TargetingRule.php | 72 ++++++++++ src/ConfigJson/UserComparator.php | 119 ++++++++++++++++ src/ConfigJson/UserCondition.php | 17 +++ src/Override/ArrayDataSource.php | 6 +- src/Override/LocalFileDataSource.php | 50 ++++--- src/RolloutEvaluator.php | 129 ++++++++++-------- src/Utils.php | 13 -- tests/CacheTest.php | 10 +- tests/ConfigCatClientTest.php | 16 ++- tests/DataGovernanceTest.php | 23 ++-- tests/LocalSourceTest.php | 2 +- tests/Utils.php | 67 ++++++--- tests/test-rules.json | 28 +++- tests/test.json | 27 +++- 36 files changed, 933 insertions(+), 264 deletions(-) delete mode 100644 src/Attributes/Config.php delete mode 100644 src/Attributes/PercentageAttributes.php delete mode 100644 src/Attributes/Preferences.php delete mode 100644 src/Attributes/RolloutAttributes.php delete mode 100644 src/Attributes/SettingAttributes.php create mode 100644 src/ConfigJson/Condition.php create mode 100644 src/ConfigJson/Config.php create mode 100644 src/ConfigJson/PercentageOption.php create mode 100644 src/ConfigJson/Preferences.php create mode 100644 src/ConfigJson/PrerequisiteFlagComparator.php create mode 100644 src/ConfigJson/PrerequisiteFlagCondition.php create mode 100644 src/ConfigJson/RedirectMode.php create mode 100644 src/ConfigJson/Segment.php create mode 100644 src/ConfigJson/SegmentComparator.php create mode 100644 src/ConfigJson/SegmentCondition.php create mode 100644 src/ConfigJson/Setting.php create mode 100644 src/ConfigJson/SettingType.php create mode 100644 src/ConfigJson/SettingValue.php create mode 100644 src/ConfigJson/SettingValueContainer.php create mode 100644 src/ConfigJson/TargetingRule.php create mode 100644 src/ConfigJson/UserComparator.php create mode 100644 src/ConfigJson/UserCondition.php diff --git a/src/Attributes/Config.php b/src/Attributes/Config.php deleted file mode 100644 index fc6c4c1..0000000 --- a/src/Attributes/Config.php +++ /dev/null @@ -1,14 +0,0 @@ -settings)) { + if (!$settingsResult->hasConfigJson) { $this->logger->error('Config JSON is not present. Returning '.$defaultReturnValue.'.', [ 'event_id' => 1000, ]); @@ -513,27 +515,38 @@ private function evaluate(string $key, array $setting, ?User $user, float $fetch } /** - * @param array $json + * @param array $settings */ - private function parseKeyAndValue(array $json, string $variationId): ?Pair + private function parseKeyAndValue(array $settings, string $variationId): ?Pair { - foreach ($json as $key => $value) { - if ($variationId == $value[SettingAttributes::VARIATION_ID]) { - return new Pair($key, $value[SettingAttributes::VALUE]); - } + foreach ($settings as $key => $setting) { + $settingType = Setting::getType(Setting::ensure($setting)); - $rolloutRules = $value[SettingAttributes::ROLLOUT_RULES]; - $percentageItems = $value[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]; + if ($variationId === ($setting[Setting::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($setting[Setting::VALUE], $settingType)); + } - foreach ($rolloutRules as $rolloutValue) { - if ($variationId == $rolloutValue[RolloutAttributes::VARIATION_ID]) { - return new Pair($key, $rolloutValue[RolloutAttributes::VALUE]); + $targetingRules = TargetingRule::ensureList($setting[Setting::TARGETING_RULES] ?? []); + foreach ($targetingRules as $targetingRule) { + if (TargetingRule::hasPercentageOptions(TargetingRule::ensure($targetingRule))) { + $percentageOptions = $targetingRule[TargetingRule::PERCENTAGE_OPTIONS]; + foreach ($percentageOptions as $percentageOption) { + if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE], $settingType)); + } + } + } else { + $simpleValue = $targetingRule[TargetingRule::SIMPLE_VALUE]; + if ($variationId === ($simpleValue[SettingValueContainer::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($simpleValue[SettingValueContainer::VALUE], $settingType)); + } } } - foreach ($percentageItems as $percentageValue) { - if ($variationId == $percentageValue[PercentageAttributes::VARIATION_ID]) { - return new Pair($key, $percentageValue[PercentageAttributes::VALUE]); + $percentageOptions = PercentageOption::ensureList($setting[Setting::PERCENTAGE_OPTIONS] ?? []); + foreach ($percentageOptions as $percentageOption) { + if ($variationId === ($percentageOption[PercentageOption::VARIATION_ID] ?? null)) { + return new Pair($key, SettingValue::get($percentageOption[PercentageOption::VALUE], $settingType)); } } } @@ -556,13 +569,13 @@ private function getSettingsResult(): SettingsResult $local = $this->overrides->getDataSource()->getOverrides(); $remote = $this->getRemoteSettingsResult(); - return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, $remote->hasConfigJson); + return new SettingsResult(array_merge($remote->settings, $local), $remote->fetchTime, true); default: // remote over local $local = $this->overrides->getDataSource()->getOverrides(); $remote = $this->getRemoteSettingsResult(); - return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, $remote->hasConfigJson); + return new SettingsResult(array_merge($local, $remote->settings), $remote->fetchTime, true); } } @@ -581,13 +594,15 @@ private function getRemoteSettingsResult(): SettingsResult return new SettingsResult([], 0, false); } - return new SettingsResult($cacheEntry->getConfig()[Config::ENTRIES], $cacheEntry->getFetchTime(), true); + $settings = Setting::ensureMap($cacheEntry->getConfig()[Config::SETTINGS] ?? []); + + return new SettingsResult($settings, $cacheEntry->getFetchTime(), true); } private function handleResponse(FetchResponse $response, ConfigEntry $cacheEntry): ConfigEntry { if ($response->isFetched()) { - $this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::ENTRIES]); + $this->hooks->fireOnConfigChanged($response->getConfigEntry()->getConfig()[Config::SETTINGS] ?? []); $this->cache->store($this->cacheKey, $response->getConfigEntry()); return $response->getConfigEntry(); diff --git a/src/ConfigFetcher.php b/src/ConfigFetcher.php index 128d367..2cdddd0 100644 --- a/src/ConfigFetcher.php +++ b/src/ConfigFetcher.php @@ -4,14 +4,16 @@ namespace ConfigCat; -use ConfigCat\Attributes\Config; -use ConfigCat\Attributes\Preferences; use ConfigCat\Cache\ConfigEntry; +use ConfigCat\ConfigJson\Config; +use ConfigCat\ConfigJson\Preferences; +use ConfigCat\ConfigJson\RedirectMode; use ConfigCat\Http\FetchClientInterface; use ConfigCat\Http\GuzzleFetchClient; use ConfigCat\Log\InternalLogger; use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; +use UnexpectedValueException; /** * Class ConfigFetcher This class is used to fetch the latest configuration. @@ -21,15 +23,11 @@ final class ConfigFetcher { public const ETAG_HEADER = 'ETag'; - public const CONFIG_JSON_NAME = 'config_v5.json'; + public const CONFIG_JSON_NAME = 'config_v6.json'; public const GLOBAL_URL = 'https://cdn-global.configcat.com'; public const EU_ONLY_URL = 'https://cdn-eu.configcat.com'; - public const NO_REDIRECT = 0; - public const SHOULD_REDIRECT = 1; - public const FORCE_REDIRECT = 2; - private InternalLogger $logger; private string $urlPath; private string $baseUrl; @@ -105,16 +103,16 @@ private function executeFetch(?string $etag, string $url, int $executionCount): } $preferences = $response->getConfigEntry()->getConfig()[Config::PREFERENCES]; - $redirect = $preferences[Preferences::REDIRECT]; - if ($this->urlIsCustom && self::FORCE_REDIRECT != $redirect) { + $redirect = RedirectMode::from($preferences[Preferences::REDIRECT] ?? RedirectMode::NO->value); + if ($this->urlIsCustom && RedirectMode::FORCE != $redirect) { return $response; } - if (self::NO_REDIRECT == $redirect) { + if (RedirectMode::NO == $redirect) { return $response; } - if (self::SHOULD_REDIRECT == $redirect) { + if (RedirectMode::SHOULD == $redirect) { $this->logger->warning( 'The `dataGovernance` parameter specified at the client initialization is '. 'not in sync with the preferences on the ConfigCat Dashboard. '. @@ -159,11 +157,13 @@ private function sendConfigFetchRequest(?string $etag, string $url): FetchRespon if ($statusCode >= 200 && $statusCode < 300) { $this->logger->debug('Fetch was successful: new config fetched.'); - $entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds()); - if (JSON_ERROR_NONE !== json_last_error()) { - $message = 'Fetching config JSON was successful but the HTTP response content was invalid. JSON error: '.json_last_error_msg(); + try { + $entry = ConfigEntry::fromConfigJson((string) $response->getBody(), $etag ?? '', Utils::getUnixMilliseconds()); + } catch (UnexpectedValueException $ex) { + $message = 'Fetching config JSON was successful but the HTTP response content was invalid.'; $messageCtx = [ 'event_id' => 1105, + 'exception' => $ex, ]; $this->logger->error($message, $messageCtx); diff --git a/src/ConfigJson/Condition.php b/src/ConfigJson/Condition.php new file mode 100644 index 0000000..b5a5508 --- /dev/null +++ b/src/ConfigJson/Condition.php @@ -0,0 +1,15 @@ + $config + * + * @internal + */ + public static function fixup(array &$config): void + { + $settings = &$config[self::SETTINGS] ?? []; + if (is_array($settings) && !empty($settings)) { + $salt = $config[self::PREFERENCES][Preferences::SALT] ?? null; + $segments = $config[self::SEGMENTS] ?? []; + + foreach ($settings as &$setting) { + $setting[Setting::CONFIG_JSON_SALT] = $salt; + $setting[Setting::CONFIG_SEGMENTS] = $segments; + } + } + } + + /** + * @return array + * + * @throws UnexpectedValueException + */ + public static function deserialize(string $json): array + { + $config = json_decode($json, true); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new UnexpectedValueException('JSON error: '.json_last_error_msg()); + } + + if (!is_array($config)) { + throw new UnexpectedValueException('Invalid config JSON content: '.$json); + } + + self::fixup($config); + + return $config; + } +} diff --git a/src/ConfigJson/PercentageOption.php b/src/ConfigJson/PercentageOption.php new file mode 100644 index 0000000..80aed2c --- /dev/null +++ b/src/ConfigJson/PercentageOption.php @@ -0,0 +1,31 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $percentageOptions): array + { + if (!is_array($percentageOptions) || !array_is_list($percentageOptions)) { + throw new UnexpectedValueException('Percentage option list is invalid.'); + } + + return $percentageOptions; + } +} diff --git a/src/ConfigJson/Preferences.php b/src/ConfigJson/Preferences.php new file mode 100644 index 0000000..0612b40 --- /dev/null +++ b/src/ConfigJson/Preferences.php @@ -0,0 +1,26 @@ + + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureMap(mixed $settings): array + { + if (!is_array($settings)) { + throw new UnexpectedValueException('Setting map is invalid.'); + } + + return $settings; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $setting): array + { + if (!is_array($setting)) { + throw new UnexpectedValueException('Setting is missing or invalid.'); + } + + return $setting; + } + + /** + * @param array $setting + * + * @internal + */ + public static function getType(array $setting): object + { + $settingType = $setting[self::TYPE]; + if ($settingType === self::unsupportedTypeToken()) { + return $settingType; + } + + return SettingType::from($settingType); + } + + /** + * @return array + * + * @internal + */ + public static function fromValue(mixed $value): array + { + if (is_bool($value)) { + $settingType = SettingType::BOOLEAN->value; + $value = [SettingValue::BOOLEAN => $value]; + } elseif (is_string($value)) { + $settingType = SettingType::STRING->value; + $value = [SettingValue::STRING => $value]; + } elseif (is_int($value)) { + $settingType = SettingType::INT->value; + $value = [SettingValue::INT => $value]; + } elseif (is_double($value)) { + $settingType = SettingType::DOUBLE->value; + $value = [SettingValue::DOUBLE => $value]; + } else { + $settingType = self::unsupportedTypeToken(); + } + + return [ + self::TYPE => $settingType, + self::VALUE => $value, + ]; + } + + /** + * Returns a token object for indicating an unsupported value coming from flag overrides. + */ + private static function unsupportedTypeToken(): object + { + static $unsupportedTypeToken = null; + + return $unsupportedTypeToken ??= new stdClass(); + } +} diff --git a/src/ConfigJson/SettingType.php b/src/ConfigJson/SettingType.php new file mode 100644 index 0000000..91f49f1 --- /dev/null +++ b/src/ConfigJson/SettingType.php @@ -0,0 +1,23 @@ +> + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensureList(mixed $targetingRules): array + { + if (!is_array($targetingRules) || !array_is_list($targetingRules)) { + throw new UnexpectedValueException('Targeting rule list is invalid.'); + } + + return $targetingRules; + } + + /** + * @return array + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function ensure(mixed $targetingRule): array + { + if (!is_array($targetingRule)) { + throw new UnexpectedValueException('Targeting rule is missing or invalid.'); + } + + return $targetingRule; + } + + /** + * @param array $targetingRule + * + * @throws UnexpectedValueException + * + * @internal + */ + public static function hasPercentageOptions(array $targetingRule): bool + { + $simpleValue = $targetingRule[self::SIMPLE_VALUE] ?? null; + $percentageOptions = $targetingRule[self::PERCENTAGE_OPTIONS] ?? null; + + if (isset($simpleValue)) { + if (!isset($percentageOptions) && is_array($simpleValue)) { + return false; + } + } elseif (is_array($percentageOptions) && array_is_list($percentageOptions)) { + return true; + } + + throw new UnexpectedValueException('Targeting rule THEN part is missing or invalid.'); + } +} diff --git a/src/ConfigJson/UserComparator.php b/src/ConfigJson/UserComparator.php new file mode 100644 index 0000000..cb5c6a0 --- /dev/null +++ b/src/ConfigJson/UserComparator.php @@ -0,0 +1,119 @@ +Unix Epoch is less than the comparison value. */ + case DATETIME_BEFORE = 18; + + /** AFTER (UTC datetime) - It matches when the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. */ + case DATETIME_AFTER = 19; + + /** EQUALS (hashed) - It matches when the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_EQUALS = 20; + + /** NOT EQUALS (hashed) - It matches when the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_NOT_EQUALS = 21; + + /** STARTS WITH ANY OF (hashed) - It matches when the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_STARTS_WITH_ANY_OF = 22; + + /** NOT STARTS WITH ANY OF (hashed) - It matches when 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 SENSITIVE_TEXT_NOT_STARTS_WITH_ANY_OF = 23; + + /** ENDS WITH ANY OF (hashed) - It matches when the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_TEXT_ENDS_WITH_ANY_OF = 24; + + /** NOT ENDS WITH ANY OF (hashed) - It matches when 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 SENSITIVE_TEXT_NOT_ENDS_WITH_ANY_OF = 25; + + /** ARRAY CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_ARRAY_CONTAINS_ANY_OF = 26; + + /** ARRAY NOT CONTAINS ANY OF (hashed) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + case SENSITIVE_ARRAY_NOT_CONTAINS_ANY_OF = 27; + + /** EQUALS (cleartext) - It matches when the comparison attribute is equal to the comparison value. */ + case TEXT_EQUALS = 28; + + /** NOT EQUALS (cleartext) - It matches when the comparison attribute is not equal to the comparison value. */ + case TEXT_NOT_EQUALS = 29; + + /** STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute starts with any of the comparison values. */ + case TEXT_STARTS_WITH_ANY_OF = 30; + + /** NOT STARTS WITH ANY OF (cleartext) - It matches when the comparison attribute does not start with any of the comparison values. */ + case TEXT_NOT_STARTS_WITH_ANY_OF = 31; + + /** ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute ends with any of the comparison values. */ + case TEXT_ENDS_WITH_ANY_OF = 32; + + /** NOT ENDS WITH ANY OF (cleartext) - It matches when the comparison attribute does not end with any of the comparison values. */ + case TEXT_NOT_ENDS_WITH_ANY_OF = 33; + + /** ARRAY CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */ + case ARRAY_CONTAINS_ANY_OF = 34; + + /** ARRAY NOT CONTAINS ANY OF (cleartext) - It matches when the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */ + case ARRAY_NOT_CONTAINS_ANY_OF = 35; +} diff --git a/src/ConfigJson/UserCondition.php b/src/ConfigJson/UserCondition.php new file mode 100644 index 0000000..43a4864 --- /dev/null +++ b/src/ConfigJson/UserCondition.php @@ -0,0 +1,17 @@ +overrides as $key => $value) { - $result[$key] = [ - SettingAttributes::VALUE => $value, - ]; + $result[$key] = Setting::fromValue($value); } return $result; diff --git a/src/Override/LocalFileDataSource.php b/src/Override/LocalFileDataSource.php index 311452d..9b5fa39 100644 --- a/src/Override/LocalFileDataSource.php +++ b/src/Override/LocalFileDataSource.php @@ -4,9 +4,10 @@ namespace ConfigCat\Override; -use ConfigCat\Attributes\Config; -use ConfigCat\Attributes\SettingAttributes; +use ConfigCat\ConfigJson\Config; +use ConfigCat\ConfigJson\Setting; use InvalidArgumentException; +use UnexpectedValueException; /** * Describes a local file override data source. @@ -34,8 +35,8 @@ public function getOverrides(): array { $content = file_get_contents($this->filePath); if (false === $content) { - $this->logger->error("Cannot find the local config file '".$this->filePath."'. ' . - 'This is a path that your application provided to the ConfigCat SDK by passing it to the `FlagOverrides.LocalFile()` method. ' . + $this->logger->error("Cannot find the local config file '".$this->filePath."'. ' . + 'This is a path that your application provided to the ConfigCat SDK by passing it to the `FlagOverrides.LocalFile()` method. ' . 'Read more: https://configcat.com/docs/sdk-reference/php/#json-file", [ 'event_id' => 1300, ]); @@ -44,26 +45,37 @@ public function getOverrides(): array } $json = json_decode($content, true); + if (JSON_ERROR_NONE !== json_last_error()) { + $ex = new UnexpectedValueException('JSON error: '.json_last_error_msg()); + } elseif (is_array($json)) { + if (!isset($json['flags'])) { + Config::fixup($json); - if (null == $json) { - $this->logger->error("Failed to decode JSON from the local config file '".$this->filePath."'. JSON error: ".json_last_error_msg(), [ - 'event_id' => 2302, - ]); + try { + return Setting::ensureMap($json[Config::SETTINGS] ?? []); + } catch (UnexpectedValueException $ex) { + // intentional no-op + } + } elseif (is_array($json['flags'])) { + $result = []; - return []; - } + foreach ($json['flags'] as $key => $value) { + $result[$key] = Setting::fromValue($value); + } - if (isset($json['flags'])) { - $result = []; - foreach ($json['flags'] as $key => $value) { - $result[$key] = [ - SettingAttributes::VALUE => $value, - ]; + return $result; + } else { + $ex = new UnexpectedValueException('Invalid config JSON content: '.$content); } - - return $result; + } else { + $ex = new UnexpectedValueException('Invalid config JSON content: '.$content); } - return $json[Config::ENTRIES]; + $this->logger->error("Failed to decode JSON from the local config file '".$this->filePath."'.", [ + 'event_id' => 2302, + 'exception' => $ex, + ]); + + return []; } } diff --git a/src/RolloutEvaluator.php b/src/RolloutEvaluator.php index 896836f..8d7e755 100644 --- a/src/RolloutEvaluator.php +++ b/src/RolloutEvaluator.php @@ -4,9 +4,13 @@ namespace ConfigCat; -use ConfigCat\Attributes\PercentageAttributes; -use ConfigCat\Attributes\RolloutAttributes; -use ConfigCat\Attributes\SettingAttributes; +use ConfigCat\ConfigJson\Condition; +use ConfigCat\ConfigJson\PercentageOption; +use ConfigCat\ConfigJson\Setting; +use ConfigCat\ConfigJson\SettingValue; +use ConfigCat\ConfigJson\SettingValueContainer; +use ConfigCat\ConfigJson\TargetingRule; +use ConfigCat\ConfigJson\UserCondition; use ConfigCat\Log\InternalLogger; use Exception; use z4kn4fein\SemVer\SemverException; @@ -66,11 +70,13 @@ public function evaluate( EvaluationLogCollector $logCollector, ?User $user = null ): EvaluationResult { + $settingType = Setting::getType($json); + if (null === $user) { - if (isset($json[SettingAttributes::ROLLOUT_RULES]) - && !empty($json[SettingAttributes::ROLLOUT_RULES]) - || isset($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]) - && !empty($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS])) { + if (isset($json[Setting::TARGETING_RULES]) + && !empty($json[Setting::TARGETING_RULES]) + || isset($json[Setting::PERCENTAGE_OPTIONS]) + && !empty($json[Setting::PERCENTAGE_OPTIONS])) { $this->logger->warning("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()` in order to make targeting work properly. '. 'Read more: https://configcat.com/docs/advanced/user-object/', [ @@ -78,29 +84,32 @@ public function evaluate( ]); } - $result = $json[SettingAttributes::VALUE]; - $variationId = $json[SettingAttributes::VARIATION_ID] ?? ''; + $result = SettingValue::get($json[Setting::VALUE], $settingType); + $variationId = $json[Setting::VARIATION_ID] ?? ''; $logCollector->add('Returning '.Utils::getStringRepresentation($result).'.'); return new EvaluationResult($result, $variationId, null, null); } $logCollector->add('User object: '.$user); - if (isset($json[SettingAttributes::ROLLOUT_RULES]) && !empty($json[SettingAttributes::ROLLOUT_RULES])) { - foreach ($json[SettingAttributes::ROLLOUT_RULES] as $rule) { - $comparisonAttribute = $rule[RolloutAttributes::COMPARISON_ATTRIBUTE]; - $comparisonValue = $rule[RolloutAttributes::COMPARISON_VALUE]; - $comparator = $rule[RolloutAttributes::COMPARATOR]; - $value = $rule[RolloutAttributes::VALUE]; - $variationId = $rule[RolloutAttributes::VARIATION_ID] ?? ''; + if (isset($json[Setting::TARGETING_RULES]) && !empty($json[Setting::TARGETING_RULES])) { + foreach ($json[Setting::TARGETING_RULES] as $targetingRule) { + $rule = $targetingRule[TargetingRule::CONDITIONS][0][Condition::USER_CONDITION]; + $simpleValue = $targetingRule[TargetingRule::SIMPLE_VALUE]; + + $comparisonAttribute = $rule[UserCondition::COMPARISON_ATTRIBUTE]; + $comparator = $rule[UserCondition::COMPARATOR]; + $value = SettingValue::get($simpleValue[SettingValueContainer::VALUE], $settingType); + $variationId = $simpleValue[SettingValueContainer::VARIATION_ID] ?? ''; $userValue = $user->getAttribute($comparisonAttribute); + $comparisonValue = $rule[UserCondition::STRING_COMPARISON_VALUE] ?? $rule[UserCondition::NUMBER_COMPARISON_VALUE] ?? $rule[UserCondition::STRINGLIST_COMPARISON_VALUE]; if (empty($comparisonValue) || (!is_numeric($userValue) && empty($userValue))) { $logCollector->add($this->logNoMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue + (string) json_encode($comparisonValue) )); continue; @@ -109,66 +118,66 @@ public function evaluate( switch ($comparator) { // IS ONE OF case 0: - $split = array_filter(Utils::splitTrim($comparisonValue)); + $split = $comparisonValue; if (in_array($userValue, $split, true)) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; // IS NOT ONE OF case 1: - $split = array_filter(Utils::splitTrim($comparisonValue)); + $split = $comparisonValue; if (!in_array($userValue, $split, true)) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; // CONTAINS case 2: - if (Utils::strContains($userValue, $comparisonValue)) { + if (Utils::strContains($userValue, $comparisonValue[0])) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; // DOES NOT CONTAIN case 3: - if (!Utils::strContains($userValue, $comparisonValue)) { + if (!Utils::strContains($userValue, $comparisonValue[0])) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; @@ -176,11 +185,15 @@ public function evaluate( // IS ONE OF, IS NOT ONE OF (SemVer) case 4: case 5: - $split = array_filter(Utils::splitTrim($comparisonValue)); + $split = $comparisonValue; try { $matched = false; foreach ($split as $semVer) { + if (empty($semVer)) { + continue; + } + $matched = Version::equal($userValue, $semVer) || $matched; } @@ -189,18 +202,18 @@ public function evaluate( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } } catch (SemverException) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); @@ -227,18 +240,18 @@ public function evaluate( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } } catch (SemverException $exception) { $logCollector->add($this->logFormatError( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $exception )); @@ -255,13 +268,13 @@ public function evaluate( case 14: case 15: $userDouble = str_replace(',', '.', $userValue); - $comparisonDouble = str_replace(',', '.', $comparisonValue); + $comparisonDouble = $comparisonValue; if (!is_numeric($userDouble)) { $logCollector->add($this->logFormatErrorWithMessage( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $userDouble.'is not a valid number.' )); @@ -273,7 +286,7 @@ public function evaluate( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $comparisonDouble.'is not a valid number.' )); @@ -293,66 +306,66 @@ public function evaluate( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; // IS ONE OF (Sensitive) case 16: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (in_array(sha1($userValue), $split, true)) { + $split = $comparisonValue; + if (in_array(hash('sha256', $userValue.$json[Setting::CONFIG_JSON_SALT].$key), $split, true)) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; // IS NOT ONE OF (Sensitive) case 17: - $split = array_filter(Utils::splitTrim($comparisonValue)); - if (!in_array(sha1($userValue), $split, true)) { + $split = $comparisonValue; + if (!in_array(hash('sha256', $userValue.$json[Setting::CONFIG_JSON_SALT].$key), $split, true)) { $logCollector->add($this->logMatch( $comparisonAttribute, $userValue, $comparator, - $comparisonValue, + (string) json_encode($comparisonValue), $value )); - return new EvaluationResult($value, $variationId, $rule, null); + return new EvaluationResult($value, $variationId, $targetingRule, null); } break; } - $logCollector->add($this->logNoMatch($comparisonAttribute, $userValue, $comparator, $comparisonValue)); + $logCollector->add($this->logNoMatch($comparisonAttribute, $userValue, $comparator, (string) json_encode($comparisonValue))); } } - if (isset($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS]) - && !empty($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS])) { + if (isset($json[Setting::PERCENTAGE_OPTIONS]) + && !empty($json[Setting::PERCENTAGE_OPTIONS])) { $hashCandidate = $key.$user->getIdentifier(); $stringHash = substr(sha1($hashCandidate), 0, 7); $intHash = intval($stringHash, 16); $scale = $intHash % 100; $bucket = 0; - foreach ($json[SettingAttributes::ROLLOUT_PERCENTAGE_ITEMS] as $rule) { - $bucket += $rule[PercentageAttributes::PERCENTAGE]; + foreach ($json[Setting::PERCENTAGE_OPTIONS] as $rule) { + $bucket += $rule[PercentageOption::PERCENTAGE]; if ($scale < $bucket) { - $result = $rule[PercentageAttributes::VALUE]; - $variationId = $rule[PercentageAttributes::VARIATION_ID]; + $result = SettingValue::get($rule[PercentageOption::VALUE], $settingType); + $variationId = $rule[PercentageOption::VARIATION_ID]; $logCollector->add( 'Evaluating % options. Returning '.Utils::getStringRepresentation($result).'.' ); @@ -362,8 +375,8 @@ public function evaluate( } } - $result = $json[SettingAttributes::VALUE]; - $variationId = $json[SettingAttributes::VARIATION_ID] ?? ''; + $result = SettingValue::get($json[Setting::VALUE], $settingType); + $variationId = $json[Setting::VARIATION_ID] ?? ''; $logCollector->add('Returning '.Utils::getStringRepresentation($result).'.'); return new EvaluationResult($result, $variationId, null, null); diff --git a/src/Utils.php b/src/Utils.php index c508854..792fd15 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -24,19 +24,6 @@ public static function strContains(string $haystack, string $needle): bool return str_contains($haystack, $needle); } - /** - * Splits a given string and trims the result items. - * - * @param string $text the text to split and trim - * @param non-empty-string $delimiter the delimiter - * - * @return string[] the array of split items - */ - public static function splitTrim(string $text, string $delimiter = ','): array - { - return array_map('trim', explode($delimiter, $text)); - } - /** * Returns the string representation of a value. * diff --git a/tests/CacheTest.php b/tests/CacheTest.php index cd2774d..99cf442 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -14,11 +14,11 @@ class CacheTest extends TestCase { - private const TEST_JSON = '{ "f" : { "first": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "second": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + private const TEST_JSON = '{"f":{"first":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"second":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testCachePayload() { - $testJson = '{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"testKey":{"v":"testValue","t":1,"p":[],"r":[]}}}'; + $testJson = '{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"testKey":{"t":1,"v":{"s":"testValue"}}}}'; $dateTime = new DateTime('2023-06-14T15:27:15.8440000Z'); @@ -76,8 +76,10 @@ public function testCacheKeyGeneration($sdkKey, $cacheKey) public function cacheKeyTestData(): array { return [ - ['test1', '147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6'], - ['test2', 'c09513b1756de9e4bc48815ec7a142b2441ed4d5'], + ['test1', '7f845c43ecc95e202b91e271435935e6d1391e5d'], + ['test2', 'a78b7e323ef543a272c74540387566a22415148a'], + ['configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012', 'f83ba5d45bceb4bb704410f51b704fb6dfa19942'], + ['configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012', 'da7bfd8662209c8ed3f9db96daed4f8d91ba5876'], ]; } } diff --git a/tests/ConfigCatClientTest.php b/tests/ConfigCatClientTest.php index a1207d8..5b941da 100644 --- a/tests/ConfigCatClientTest.php +++ b/tests/ConfigCatClientTest.php @@ -25,7 +25,7 @@ class ConfigCatClientTest extends TestCase { - private const TEST_JSON = '{ "f" : { "first": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "second": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + private const TEST_JSON = '{"f":{"first":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"second":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testConstructEmptySdkKey() { @@ -440,9 +440,10 @@ public function testEvalDetails() $this->assertNull($details->getError()); $this->assertEquals('key', $details->getKey()); $this->assertEquals('test@test1.com', $details->getUser()->getIdentifier()); - $this->assertEquals('Identifier', $details->getMatchedEvaluationRule()['a']); - $this->assertEquals('@test1.com', $details->getMatchedEvaluationRule()['c']); - $this->assertEquals(2, $details->getMatchedEvaluationRule()['t']); + $condition = $details->getMatchedEvaluationRule()['c'][0]['u']; + $this->assertEquals('Identifier', $condition['a']); + $this->assertEquals('@test1.com', $condition['l'][0]); + $this->assertEquals(2, $condition['c']); $this->assertNull($details->getMatchedEvaluationPercentageRule()); $this->assertTrue($details->getFetchTimeUnixMilliseconds() > 0); $this->assertFalse($details->isDefaultValue()); @@ -479,9 +480,10 @@ public function testEvalDetailsHook() $this->assertNull($details->getError()); $this->assertEquals('key', $details->getKey()); $this->assertEquals('test@test1.com', $details->getUser()->getIdentifier()); - $this->assertEquals('Identifier', $details->getMatchedEvaluationRule()['a']); - $this->assertEquals('@test1.com', $details->getMatchedEvaluationRule()['c']); - $this->assertEquals(2, $details->getMatchedEvaluationRule()['t']); + $condition = $details->getMatchedEvaluationRule()['c'][0]['u']; + $this->assertEquals('Identifier', $condition['a']); + $this->assertEquals('@test1.com', $condition['l'][0]); + $this->assertEquals(2, $condition['c']); $this->assertNull($details->getMatchedEvaluationPercentageRule()); $this->assertFalse($details->isDefaultValue()); $this->assertTrue($details->getFetchTimeUnixMilliseconds() > 0); diff --git a/tests/DataGovernanceTest.php b/tests/DataGovernanceTest.php index f6b3635..542a308 100644 --- a/tests/DataGovernanceTest.php +++ b/tests/DataGovernanceTest.php @@ -4,6 +4,7 @@ use ConfigCat\ClientOptions; use ConfigCat\ConfigFetcher; +use ConfigCat\ConfigJson\Config; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; @@ -12,7 +13,7 @@ class DataGovernanceTest extends TestCase { - const JSON_TEMPLATE = '{ "p": { "u": "%s", "r": %d }, "f": {} }'; + const JSON_TEMPLATE = '{"p":{"u":"%s","r":%d}}'; const CUSTOM_CDN_URL = 'https://custom-cdn.configcat.com'; public function testShouldStayOnServer() @@ -33,7 +34,7 @@ public function testShouldStayOnServer() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldStayOnSameUrlWithRedirect() @@ -54,7 +55,7 @@ public function testShouldStayOnSameUrlWithRedirect() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldStayOnSameUrlEvenWhenForced() @@ -75,7 +76,7 @@ public function testShouldStayOnSameUrlEvenWhenForced() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($body, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($body), $response->getConfigEntry()->getConfig()); } public function testShouldRedirectToAnotherServer() @@ -99,7 +100,7 @@ public function testShouldRedirectToAnotherServer() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } public function testShouldRedirectToAnotherServerWhenForced() @@ -123,7 +124,7 @@ public function testShouldRedirectToAnotherServerWhenForced() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } public function testShouldBreakRedirectLoop() @@ -149,7 +150,7 @@ public function testShouldBreakRedirectLoop() $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); $this->assertStringContainsString($requests[2]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldBreakRedirectLoopWhenForced() @@ -175,7 +176,7 @@ public function testShouldBreakRedirectLoopWhenForced() $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::EU_ONLY_URL); $this->assertStringContainsString($requests[2]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldRespectCustomUrlWhenNotForced() @@ -197,7 +198,7 @@ public function testShouldRespectCustomUrlWhenNotForced() // Assert $this->assertEquals(1, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); // Act $response = $fetcher->fetch(''); @@ -205,7 +206,7 @@ public function testShouldRespectCustomUrlWhenNotForced() // Assert $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); - $this->assertEquals(json_decode($firstBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($firstBody), $response->getConfigEntry()->getConfig()); } public function testShouldNotRespectCustomUrlWhenForced() @@ -229,7 +230,7 @@ public function testShouldNotRespectCustomUrlWhenForced() $this->assertEquals(2, count($requests)); $this->assertStringContainsString($requests[0]['request']->getUri()->getHost(), self::CUSTOM_CDN_URL); $this->assertStringContainsString($requests[1]['request']->getUri()->getHost(), ConfigFetcher::GLOBAL_URL); - $this->assertEquals(json_decode($secondBody, true), $response->getConfigEntry()->getConfig()); + $this->assertEquals(Config::deserialize($secondBody), $response->getConfigEntry()->getConfig()); } private function getHandlerStack(array $responses, array &$container = []) diff --git a/tests/LocalSourceTest.php b/tests/LocalSourceTest.php index d1efd03..858c028 100644 --- a/tests/LocalSourceTest.php +++ b/tests/LocalSourceTest.php @@ -15,7 +15,7 @@ class LocalSourceTest extends TestCase { - const TEST_JSON_BODY = '{ "f" : { "disabled": { "v": false, "p": [], "r": [], "i":"fakeIdFirst" }, "enabled": { "v": true, "p": [], "r": [], "i":"fakeIdSecond" }}}'; + const TEST_JSON_BODY = '{"f":{"disabled":{"t":0,"v":{"b":false},"i":"fakeIdFirst"},"enabled":{"t":0,"v":{"b":true},"i":"fakeIdSecond"}}}'; public function testWithNonExistingFile() { diff --git a/tests/Utils.php b/tests/Utils.php index a35a464..75d9ba5 100644 --- a/tests/Utils.php +++ b/tests/Utils.php @@ -22,21 +22,56 @@ public static function getNullLogger(): InternalLogger public static function formatConfigWithRules(): string { - return '{ "f": { "key": { "v": "def", "i": "defVar", "p": [], "r": [ - { - "v": "fake1", - "i": "id1", - "t": 2, - "a": "Identifier", - "c": "@test1.com" - }, - { - "v": "fake2", - "i": "id2", - "t": 2, - "a": "Identifier", - "c": "@test2.com" - } - ] }}}'; + return '{ + "f": { + "key": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@test1.com" + ] + } + } + ], + "s": { + "v": { + "s": "fake1" + }, + "i": "id1" + } + }, + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@test2.com" + ] + } + } + ], + "s": { + "v": { + "s": "fake2" + }, + "i": "id2" + } + } + ], + "v": { + "s": "def" + }, + "i": "defVar" + } + } + }'; } } diff --git a/tests/test-rules.json b/tests/test-rules.json index f39953b..7d59a2d 100644 --- a/tests/test-rules.json +++ b/tests/test-rules.json @@ -1,16 +1,30 @@ { "f": { "rolloutFeature": { - "v": false, + "t": 0, "r": [ { - "o": 0, - "a": "Identifier", - "t": 2, - "c": "@example.com", - "v": true + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@example.com" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } } - ] + ], + "v": { + "b": false + } } } } \ No newline at end of file diff --git a/tests/test.json b/tests/test.json index d547507..8f1db8a 100644 --- a/tests/test.json +++ b/tests/test.json @@ -1,19 +1,34 @@ { "f": { "disabledFeature": { - "v": false + "t": 0, + "v": { + "b": false + } }, "enabledFeature": { - "v": true + "t": 0, + "v": { + "b": true + } }, "intSetting": { - "v": 5 + "t": 2, + "v": { + "i": 5 + } }, "doubleSetting": { - "v": 3.14 + "t": 3, + "v": { + "d": 3.14 + } }, "stringSetting": { - "v": "test" + "t": 1, + "v": { + "s": "test" + } } } -} \ No newline at end of file +}