diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 475f03689..7298975c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,7 +13,6 @@ /packages/cli/ @eldadfux @lohanidamodar /packages/client/ @loks0n /packages/compression/ @loks0n @ChiragAgg5k -/packages/config/ @Meldiron @eldadfux /packages/console/ @eldadfux @lohanidamodar /packages/di/ @eldadfux @ChiragAgg5k /packages/domains/ @ChiragAgg5k @eldadfux @lohanidamodar diff --git a/README.md b/README.md index 79111938f..b9a899a79 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The source of truth for the [utopia-php](https://github.com/utopia-php) libraries. Each `packages/` is an independent Composer library; development happens here, and every push is mirrored to its read-only repository (e.g. `utopia-php/http`), so Composer/Packagist distribution is unchanged — mechanics in [docs/distribution.md](docs/distribution.md). -[`utopia-php/audit`](https://github.com/appwrite/appwrite/tree/main/packages/audit), [`utopia-php/auth`](https://github.com/appwrite/appwrite/tree/main/packages/auth), [`utopia-php/cache`](https://github.com/appwrite/appwrite/tree/main/packages/cache), [`utopia-php/cdn`](https://github.com/appwrite/appwrite/tree/main/packages/cdn), [`utopia-php/circuit-breaker`](https://github.com/appwrite/appwrite/tree/main/packages/circuit-breaker), [`utopia-php/cli`](https://github.com/appwrite/appwrite/tree/main/packages/cli), [`utopia-php/client`](https://github.com/appwrite/appwrite/tree/main/packages/client), [`utopia-php/compression`](https://github.com/appwrite/appwrite/tree/main/packages/compression), [`utopia-php/console`](https://github.com/appwrite/appwrite/tree/main/packages/console), [`utopia-php/di`](https://github.com/appwrite/appwrite/tree/main/packages/di), [`utopia-php/dns`](https://github.com/appwrite/appwrite/tree/main/packages/dns), [`utopia-php/domains`](https://github.com/appwrite/appwrite/tree/main/packages/domains), [`utopia-php/http`](https://github.com/appwrite/appwrite/tree/main/packages/http), [`utopia-php/image`](https://github.com/appwrite/appwrite/tree/main/packages/image), [`utopia-php/messaging`](https://github.com/appwrite/appwrite/tree/main/packages/messaging), [`utopia-php/nats`](https://github.com/appwrite/appwrite/tree/main/packages/nats), [`utopia-php/openapi`](https://github.com/appwrite/appwrite/tree/main/packages/openapi), [`utopia-php/platform`](https://github.com/appwrite/appwrite/tree/main/packages/platform), [`utopia-php/pools`](https://github.com/appwrite/appwrite/tree/main/packages/pools), [`utopia-php/queue`](https://github.com/appwrite/appwrite/tree/main/packages/queue), [`utopia-php/schedule`](https://github.com/appwrite/appwrite/tree/main/packages/schedule), [`utopia-php/servers`](https://github.com/appwrite/appwrite/tree/main/packages/servers), [`utopia-php/smtp`](https://github.com/appwrite/appwrite/tree/main/packages/smtp), [`utopia-php/span`](https://github.com/appwrite/appwrite/tree/main/packages/span), [`utopia-php/storage`](https://github.com/appwrite/appwrite/tree/main/packages/storage), [`utopia-php/system`](https://github.com/appwrite/appwrite/tree/main/packages/system), [`utopia-php/telemetry`](https://github.com/appwrite/appwrite/tree/main/packages/telemetry), [`utopia-php/user-agent`](https://github.com/appwrite/appwrite/tree/main/packages/user-agent), [`utopia-php/validators`](https://github.com/appwrite/appwrite/tree/main/packages/validators), [`utopia-php/vcs`](https://github.com/appwrite/appwrite/tree/main/packages/vcs) and [`utopia-php/websocket`](https://github.com/appwrite/appwrite/tree/main/packages/websocket) are maintained in Appwrite. Their distribution mirrors and Packagist packages remain available. [`utopia-php/fastly`](https://github.com/utopia-php/fastly) is maintained in Appwrite's Edge repository, its only consumer. +[`utopia-php/audit`](https://github.com/appwrite/appwrite/tree/main/packages/audit), [`utopia-php/auth`](https://github.com/appwrite/appwrite/tree/main/packages/auth), [`utopia-php/cache`](https://github.com/appwrite/appwrite/tree/main/packages/cache), [`utopia-php/cdn`](https://github.com/appwrite/appwrite/tree/main/packages/cdn), [`utopia-php/circuit-breaker`](https://github.com/appwrite/appwrite/tree/main/packages/circuit-breaker), [`utopia-php/cli`](https://github.com/appwrite/appwrite/tree/main/packages/cli), [`utopia-php/client`](https://github.com/appwrite/appwrite/tree/main/packages/client), [`utopia-php/compression`](https://github.com/appwrite/appwrite/tree/main/packages/compression), [`utopia-php/config`](https://github.com/appwrite/appwrite/tree/main/packages/config), [`utopia-php/console`](https://github.com/appwrite/appwrite/tree/main/packages/console), [`utopia-php/di`](https://github.com/appwrite/appwrite/tree/main/packages/di), [`utopia-php/dns`](https://github.com/appwrite/appwrite/tree/main/packages/dns), [`utopia-php/domains`](https://github.com/appwrite/appwrite/tree/main/packages/domains), [`utopia-php/http`](https://github.com/appwrite/appwrite/tree/main/packages/http), [`utopia-php/image`](https://github.com/appwrite/appwrite/tree/main/packages/image), [`utopia-php/messaging`](https://github.com/appwrite/appwrite/tree/main/packages/messaging), [`utopia-php/nats`](https://github.com/appwrite/appwrite/tree/main/packages/nats), [`utopia-php/openapi`](https://github.com/appwrite/appwrite/tree/main/packages/openapi), [`utopia-php/platform`](https://github.com/appwrite/appwrite/tree/main/packages/platform), [`utopia-php/pools`](https://github.com/appwrite/appwrite/tree/main/packages/pools), [`utopia-php/queue`](https://github.com/appwrite/appwrite/tree/main/packages/queue), [`utopia-php/schedule`](https://github.com/appwrite/appwrite/tree/main/packages/schedule), [`utopia-php/servers`](https://github.com/appwrite/appwrite/tree/main/packages/servers), [`utopia-php/smtp`](https://github.com/appwrite/appwrite/tree/main/packages/smtp), [`utopia-php/span`](https://github.com/appwrite/appwrite/tree/main/packages/span), [`utopia-php/storage`](https://github.com/appwrite/appwrite/tree/main/packages/storage), [`utopia-php/system`](https://github.com/appwrite/appwrite/tree/main/packages/system), [`utopia-php/telemetry`](https://github.com/appwrite/appwrite/tree/main/packages/telemetry), [`utopia-php/user-agent`](https://github.com/appwrite/appwrite/tree/main/packages/user-agent), [`utopia-php/validators`](https://github.com/appwrite/appwrite/tree/main/packages/validators), [`utopia-php/vcs`](https://github.com/appwrite/appwrite/tree/main/packages/vcs) and [`utopia-php/websocket`](https://github.com/appwrite/appwrite/tree/main/packages/websocket) are maintained in Appwrite. Their distribution mirrors and Packagist packages remain available. [`utopia-php/fastly`](https://github.com/utopia-php/fastly) is maintained in Appwrite's Edge repository, its only consumer. ## Quickstart @@ -51,7 +51,6 @@ Local siblings only: arrows point at dependencies between the packages in this c ```mermaid graph TD - config replication reputation ``` diff --git a/packages/config/.github/workflows/mirror.yml b/packages/config/.github/workflows/mirror.yml deleted file mode 100644 index 975b1153b..000000000 --- a/packages/config/.github/workflows/mirror.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Mirror - -on: - pull_request_target: - types: [opened] - issues: - types: [opened] - -permissions: - issues: write - pull-requests: write - -jobs: - redirect: - uses: utopia-php/monorepo/.github/workflows/mirror-redirect.yml@main - with: - package: config diff --git a/packages/config/.gitignore b/packages/config/.gitignore deleted file mode 100755 index ec4788ce5..000000000 --- a/packages/config/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -composer.lock -/vendor/ -/.idea/ -.phpunit.cache/ -.phpunit.result.cache \ No newline at end of file diff --git a/packages/config/LICENSE.md b/packages/config/LICENSE.md deleted file mode 100755 index 27feb0854..000000000 --- a/packages/config/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Eldad Fux - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/config/README.md b/packages/config/README.md deleted file mode 100644 index d8036aecc..000000000 --- a/packages/config/README.md +++ /dev/null @@ -1,235 +0,0 @@ -# Utopia Config - -> [!IMPORTANT] -> This repository is a read-only mirror of the [utopia-php monorepo](https://github.com/utopia-php/monorepo). Development happens in [`packages/config`](https://github.com/utopia-php/monorepo/tree/main/packages/config) — please open issues and pull requests there. - -[![Build Status](https://travis-ci.org/utopia-php/config.svg?branch=master)](https://travis-ci.com/utopia-php/config) -![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/config.svg) -[![Discord](https://img.shields.io/discord/564160730845151244?label=discord)](https://appwrite.io/discord) - -Utopia Config library is simple and lite library for managing application configuration. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). - -Although this library is part of the [Utopia Framework](https://github.com/utopia-php/framework) project it is dependency free and can be used as standalone with any other PHP project or framework. - -## Getting started - -Install using Composer: -```bash -composer require utopia-php/config -``` - -```php - $config - */ - #[Key('db.config', new Nullable(new JSONValidator), required: true)] - public ?array $config; - - /** - * @var array $whitelistIps - */ - #[Key('db.whitelistIps', new ArrayList(new Text(length: 100), length: 100), required: true)] - public array $whitelistIps; -} - -$source = new File(__DIR__.'/config.json'); -$parser = new JSON(); - -try { - $config = Config::load($source, $parser, DatabaseConfig::class); -} catch (Load $err) { - exit('Config could not be loaded from a file: ' . $err->getMessage()); -} catch (Parse $err) { - exit('Config could not be parsed as JSON: ' . $err->getMessage()); -} - -\var_dump($config); -// $config->host -// $config->port -// $config->username -// ... -``` - -For above example to work, make sure to setup `config.json` file too: - -```json -{ - "db": { - "host": "127.0.0.1", - "port": 3306, - "username": "root", - "password": "password", - "name": "utopia", - "config": { - "timeout": 3000, - "handshakeTimeout": 5000 - }, - "whitelistIps": [ - "127.0.0.1", - "172.17.0.0/16" - ] - }, -} -``` - -> Notice dot notation is supported, and automatically finds nested objects. - -Alternatively, you can load configs directly from a variable: - -```php - - 'high', - ]), - parser: new None(), - FirewallConfig::class -); -\var_dump($config); -// $config->securityLevel -``` - -Below is example how to combine multiple configs into one: -```php - $allowIps - */ - #[Key('ALLOW_IPS', new ArrayList(new Text(length: 100), length: 100), required: true)] - public array $allowIps; - - #[Key('CAPTCHA', new Whitelist(['enabled', 'disabled']), required: true)] - public string $captcha; -} - -class CredentialsConfig -{ - #[Key('DATABASE_PASSWORD', new Text(length: 1024), required: true)] - public string $dbPass; - - #[Key('CACHE_PASSWORD', new Text(length: 1024), required: true)] - public string $cachePass; -} - -class EnvironmentConfig -{ - #[Key('RATE_LIMIT_HITS', new Integer(loose: true), required: true)] - public int $abuseHits; - - #[Key('RATE_LIMIT_SECONDS', new Integer(loose: true), required: true)] - public int $abuseTime; -} - -class AppConfig -{ - #[ConfigKey] - public FirewallConfig $firewall; - - #[ConfigKey] - public CredentialsConfig $credentials; - - #[ConfigKey] - public EnvironmentConfig $environment; -} - -$config = Config::load( - new Variable([ - 'firewall' => Config::load(new File('firewall.json'), new JSON(), FirewallConfig::class), - 'credentials' => Config::load(new File('credentials.yml'), new YAML(), CredentialsConfig::class), - 'environment' => Config::load(new File('.env'), new Dotenv(), EnvironmentConfig::class), - ]), - new None(), - AppConfig::class -); - -\var_dump($config); -// $config->firewall->allowIps -// $config->credentials->dbPass -// $config->environment->abuseHits -``` - -You can also load environment variables instead of reading dotenv file: - -```php -dbPass -// $config->$cachePass -``` - -## System requirements - -Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP version whenever possible. - -When using YAML adapter, or running tests with it, you need to install the YAML extension for PHP. - -## Copyright and license - -The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php) diff --git a/packages/config/composer.json b/packages/config/composer.json deleted file mode 100755 index 95e4ac2bd..000000000 --- a/packages/config/composer.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "utopia-php/config", - "description": "A simple Config library to managing application config variables", - "type": "library", - "keywords": [ - "php", - "framework", - "upf", - "utopia", - "config" - ], - "license": "MIT", - "autoload": { - "psr-4": { - "Utopia\\Config\\": "src/Config" - } - }, - "scripts": { - "test": "phpunit" - }, - "require": { - "php": ">=8.5", - "utopia-php/validators": "^1.0" - }, - "suggest": { - "ext-yaml": "Required only for the YAML parser" - }, - "minimum-stability": "stable", - "prefer-stable": true -} diff --git a/packages/config/phpunit.xml b/packages/config/phpunit.xml deleted file mode 100755 index ed809f688..000000000 --- a/packages/config/phpunit.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - tests - - - - - src - - - - - - - diff --git a/packages/config/src/Config/Attribute/ConfigKey.php b/packages/config/src/Config/Attribute/ConfigKey.php deleted file mode 100644 index 3cea1e27b..000000000 --- a/packages/config/src/Config/Attribute/ConfigKey.php +++ /dev/null @@ -1,16 +0,0 @@ - $className - * @return T - */ - public static function load(Source $source, Parser $parser, string $className): mixed - { - $contents = $source->getContents(); - if ($contents === null) { - throw new Load('Loader returned null contents.'); - } - - $reflection = new \ReflectionClass($className); - - // Reject methods (including a constructor) before instantiating, so a - // schema with a required constructor argument or a side-effecting - // constructor raises Load instead of running or throwing on `new`. - if (\count($reflection->getMethods()) > 0) { - throw new Load("Class {$className} cannot have any functions."); - } - - $data = $parser->parse($contents, $reflection); - - $instance = new $className(); - - foreach ($reflection->getProperties() as $property) { - $attributeFound = false; - - foreach ($property->getAttributes(Key::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attributeFound = true; - $key = $attribute->newInstance(); - - $value = self::resolveValue($data, $key->name); - - // A present null is kept; only a genuinely absent key is skipped - // or, when required, reported missing. - if ($value === self::missing()) { - if ($key->required) { - throw new Load("Missing required key: {$key->name}"); - } - continue; - } - - $expectedType = $property->getType(); - if ($expectedType === null) { - throw new Load("Property {$property->name} is missing a type."); - } - - if (! $key->validator->isValid($value)) { - throw new Load("Invalid value for {$key->name}: {$key->validator->getDescription()}"); - } - - $propertyName = $property->name; - try { - $instance->$propertyName = $value; - } catch (\TypeError $e) { - throw new Load("Invalid value for {$key->name}: does not match the type of property {$property->name}.", 0, $e); - } - } - - foreach ($property->getAttributes(ConfigKey::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $attributeFound = true; - - $key = $attribute->newInstance(); - $keyName = $key->name; - if (empty($keyName)) { - $keyName = $property->name; - } - - $value = self::resolveValue($data, $keyName); - - if ($value === self::missing()) { - if ($key->required) { - throw new Load("Missing required key: {$keyName}"); - } - continue; - } - - $expectedType = $property->getType(); - if ($expectedType === null || !method_exists($expectedType, 'getName')) { - throw new Load("Property {$property->name} is missing a type."); - } - - $expectedClass = $expectedType->getName(); - - if (!($value instanceof $expectedClass)) { - throw new Load("Invalid value for {$keyName}: Must be instance of {$expectedClass}."); - } - - $propertyName = $property->name; - $instance->$propertyName = $value; - } - - if (!$attributeFound) { - throw new Load("Property {$property->name} is missing attribute syntax."); - } - } - - return $instance; - } - - /** - * Sentinel returned by value resolution when a key is genuinely absent, so - * an explicit null in the config can be told apart from a missing key. - */ - protected static function missing(): object - { - static $missing; - - return $missing ??= new \stdClass(); - } - - /** - * @param array $data The data array to search in - * @return mixed The value if found, the missing() sentinel otherwise - */ - protected static function resolveValue(array $data, string $key): mixed - { - // Exact match - if (\array_key_exists($key, $data)) { - return $data[$key]; - } - - // Dot notation - $parts = explode('.', $key); - return self::resolveValueRecursive($data, $parts, 0); - } - - /** - * @param array $data Current data context - * @param array $parts Remaining parts of the key - * @return mixed The value if found, the missing() sentinel otherwise - */ - protected static function resolveValueRecursive(array $data, array $parts, int $index): mixed - { - if ($index >= \count($parts)) { - return self::missing(); - } - - if ($index === \count($parts) - 1) { - return \array_key_exists($parts[$index], $data) ? $data[$parts[$index]] : self::missing(); - } - - for ($length = 1; $length <= \count($parts) - $index; $length++) { - $keyParts = \array_slice($parts, $index, $length); - $key = implode('.', $keyParts); - - if (\array_key_exists($key, $data)) { - $value = $data[$key]; - - if ($index + $length === \count($parts)) { - return $value; - } - - if (\is_array($value)) { - $result = self::resolveValueRecursive($value, $parts, $index + $length); - if ($result !== self::missing()) { - return $result; - } - } - } - } - - return self::missing(); - } -} diff --git a/packages/config/src/Config/Exception/Load.php b/packages/config/src/Config/Exception/Load.php deleted file mode 100644 index c6d105fcb..000000000 --- a/packages/config/src/Config/Exception/Load.php +++ /dev/null @@ -1,7 +0,0 @@ -|null $reflection Useful for loosely typed parsers (like Dotenv) to cast smartly to correct types - * @return array - * @throws Parse - */ - abstract public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array; - - /** - * A config must be a key/value map. Valid JSON/YAML may also be a scalar - * (e.g. `123`) or a sequence (e.g. `[a, b]`); reject those so callers never - * receive a non-map. An empty array is allowed (an empty object and an empty - * list are indistinguishable once decoded, and both are harmless). - * - * Limitation: a top-level map whose keys are all sequential integers from 0 - * (e.g. `{"0": ...}`) is rejected, because it decodes to a list-shaped PHP - * array that is indistinguishable from a real sequence — yaml_parse() in - * particular gives identical output for `0: x` and `- x`. Config keys are - * names, so this degenerate shape is not supported. - * - * @return array - * @throws Parse - */ - protected function requireMap(mixed $config, string $message): array - { - if (!\is_array($config) || (\count($config) > 0 && array_is_list($config))) { - throw new Parse($message); - } - - return $config; - } -} diff --git a/packages/config/src/Config/Parser/Dotenv.php b/packages/config/src/Config/Parser/Dotenv.php deleted file mode 100644 index f8cf5ae8e..000000000 --- a/packages/config/src/Config/Parser/Dotenv.php +++ /dev/null @@ -1,176 +0,0 @@ - $truthyValues - */ - protected array $truthyValues = ['1', 'true', 'yes', 'on', 'enabled']; - - /** - * @var array $falsyValues - */ - protected array $falsyValues = ['0', 'false', 'no', 'off', 'disabled']; - - /** - * @return string|bool|null - */ - protected function convertValue(string $value, string $type): mixed - { - if ($type === 'bool') { - if (\in_array(strtolower($value), $this->truthyValues)) { - return true; - } - if (\in_array(strtolower($value), $this->falsyValues)) { - return false; - } - } - - return $value; - } - - /** - * Resolve the raw right-hand side of a dotenv line into its value. - * - * A quoted value keeps a `#` inside its quotes instead of having it treated - * as a comment; an unquoted value has any inline comment (from the first - * `#`) stripped. - */ - protected function parseValue(string $raw): string - { - $raw = trim($raw); - - if ($raw !== '' && ($raw[0] === '"' || $raw[0] === "'")) { - return $this->parseQuoted($raw, $raw[0]); - } - - $hash = strpos($raw, '#'); - if ($hash !== false) { - $raw = substr($raw, 0, $hash); - } - - return trim($raw); - } - - /** - * Read a quoted value, scanning to the matching closing quote. Double quotes - * honour backslash escapes (so `\"` stays in the value); single quotes are - * literal. Anything other than an inline comment after the closing quote, or - * a missing closing quote, is malformed. - * - * @throws Parse - */ - protected function parseQuoted(string $raw, string $quote): string - { - $value = ''; - $length = \strlen($raw); - - for ($i = 1; $i < $length; $i++) { - $char = $raw[$i]; - - // In double quotes, only \" and \\ are escapes; every other - // backslash is literal (e.g. a Windows path like C:\tmp). - if ($char === '\\' && $quote === '"' && $i + 1 < $length - && ($raw[$i + 1] === '"' || $raw[$i + 1] === '\\')) { - $value .= $raw[$i + 1]; - $i++; - continue; - } - - if ($char === $quote) { - // Only whitespace or an inline comment may follow the close - $rest = trim(substr($raw, $i + 1)); - if ($rest !== '' && $rest[0] !== '#') { - throw new Parse('Config file is not a valid dotenv file.'); - } - - return $value; - } - - $value .= $char; - } - - throw new Parse('Config file is not a valid dotenv file.'); - } - - /** - * @param \ReflectionClass|null $reflection - * @return array - */ - public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array - { - if (!\is_string($contents)) { - throw new Parse('Contents must be a string.'); - } - - if ($contents === '' || $contents === '0') { - return []; - } - - $config = []; - - $lines = explode("\n", $contents); - foreach ($lines as $line) { - $line = trim($line); - // Blank line or whole-line comment - if ($line === '') { - continue; - } - if ($line[0] === '#') { - continue; - } - - // Split into KEY=value - $parts = explode('=', $line, 2); - - // A line without '=' is malformed; fail fast rather than load it - // as a key with an empty value. - if (\count($parts) < 2) { - throw new Parse('Config file is not a valid dotenv file.'); - } - - $name = trim($parts[0]); - $value = $this->parseValue($parts[1]); - - // Missing name likely means bad syntax - if ($name === '' || $name === '0') { - throw new Parse('Config file is not a valid dotenv file.'); - } - - // Smart type-casting - if ($reflection instanceof \ReflectionClass) { - $reflectionProperty = null; - foreach ($reflection->getProperties() as $property) { - foreach ($property->getAttributes(Key::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - $key = $attribute->newInstance(); - if ($key->name === $name) { - $reflectionProperty = $property; - break 2; - } - } - } - if ($reflectionProperty !== null) { - $reflectionType = $reflectionProperty->getType(); - if ($reflectionType !== null && method_exists($reflectionType, 'getName')) { - $value = $this->convertValue($value, $reflectionType->getName()); - } - } - } - - if (\is_string($value) && strtolower($value) === 'null') { - $value = null; - } - - $config[$name] = $value; - } - - return $config; - } -} diff --git a/packages/config/src/Config/Parser/JSON.php b/packages/config/src/Config/Parser/JSON.php deleted file mode 100644 index 5b677ba65..000000000 --- a/packages/config/src/Config/Parser/JSON.php +++ /dev/null @@ -1,34 +0,0 @@ -|null $reflection - * @return array - */ - public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array - { - if (!\is_string($contents)) { - throw new Parse('Contents must be a string.'); - } - - if ($contents === '' || $contents === '0') { - return []; - } - - $config = json_decode($contents, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Parse('Config file is not a valid JSON file.'); - } - - return $this->requireMap($config, 'Config file must decode to a JSON object.'); - } -} diff --git a/packages/config/src/Config/Parser/None.php b/packages/config/src/Config/Parser/None.php deleted file mode 100644 index 1f7a6b7b5..000000000 --- a/packages/config/src/Config/Parser/None.php +++ /dev/null @@ -1,24 +0,0 @@ -|null $reflection - * @return array - */ - public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array - { - if (!\is_array($contents)) { - throw new Parse('Contents must be an array'); - } - - return $contents; - } -} diff --git a/packages/config/src/Config/Parser/PHP.php b/packages/config/src/Config/Parser/PHP.php deleted file mode 100644 index e3142ce45..000000000 --- a/packages/config/src/Config/Parser/PHP.php +++ /dev/null @@ -1,45 +0,0 @@ -|null $reflection - * @return array - */ - public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array - { - if (!\is_string($contents)) { - throw new Parse('Contents must be a string.'); - } - - $tempPath = tempnam(sys_get_temp_dir(), 'utopia_config_'); - if ($tempPath === false) { - throw new Parse('Failed to create temporary file for PHP config.'); - } - - if (file_put_contents($tempPath, $contents) === false) { - throw new Parse('Failed to write PHP config to temporary file.'); - } - - try { - $contents = include $tempPath; - } catch (\Throwable $e) { - throw new Parse('Failed to parse PHP config file: ' . $e->getMessage(), $e->getCode(), $e); - } finally { - unlink($tempPath); - } - - if (!\is_array($contents)) { - throw new Parse('PHP config file must return an array.'); - } - - return $contents; - } -} diff --git a/packages/config/src/Config/Parser/YAML.php b/packages/config/src/Config/Parser/YAML.php deleted file mode 100644 index 2f4cc4049..000000000 --- a/packages/config/src/Config/Parser/YAML.php +++ /dev/null @@ -1,42 +0,0 @@ -|null $reflection - * @return array - */ - public function parse(mixed $contents, ?\ReflectionClass $reflection = null): array - { - if (!\is_string($contents)) { - throw new Parse('Contents must be a string.'); - } - - if ($contents === '' || $contents === '0') { - return []; - } - - $config = null; - try { - $config = yaml_parse($contents); - } catch (\Throwable $e) { - throw new Parse('Failed to parse YAML config file: ' . $e->getMessage(), $e->getCode(), $e); - } - - if (\in_array($config, [false, null, $contents], true)) { - throw new Parse('Config file is not a valid YAML file.'); - } - - // TODO: Consider to cast depending on reflection, similar to Dotenv - // Source: https://softwareengineering.stackexchange.com/questions/387827/is-it-wrong-to-parse-yaml-true-as-a-string - - return $this->requireMap($config, 'Config file must decode to a YAML mapping.'); - } -} diff --git a/packages/config/src/Config/Source.php b/packages/config/src/Config/Source.php deleted file mode 100644 index e53002c49..000000000 --- a/packages/config/src/Config/Source.php +++ /dev/null @@ -1,15 +0,0 @@ -contents; - } -} diff --git a/packages/config/src/Config/Source/Environment.php b/packages/config/src/Config/Source/Environment.php deleted file mode 100644 index 8b9b4ee90..000000000 --- a/packages/config/src/Config/Source/Environment.php +++ /dev/null @@ -1,15 +0,0 @@ -contents = getenv(); - } -} diff --git a/packages/config/src/Config/Source/File.php b/packages/config/src/Config/Source/File.php deleted file mode 100644 index f2f3d238a..000000000 --- a/packages/config/src/Config/Source/File.php +++ /dev/null @@ -1,23 +0,0 @@ -contents = null; - return; - } - - $this->contents = file_get_contents($path); - if ($this->contents === false) { - $this->contents = null; - } - } -} diff --git a/packages/config/src/Config/Source/Variable.php b/packages/config/src/Config/Source/Variable.php deleted file mode 100644 index 97aa9232e..000000000 --- a/packages/config/src/Config/Source/Variable.php +++ /dev/null @@ -1,15 +0,0 @@ -contents = $contents; - } -} diff --git a/packages/config/tests/Config/ConfigTest.php b/packages/config/tests/Config/ConfigTest.php deleted file mode 100644 index b3b11d896..000000000 --- a/packages/config/tests/Config/ConfigTest.php +++ /dev/null @@ -1,343 +0,0 @@ -assertSame('customValue', $config->jsonKey); - } - - public function testFileSourceException(): void - { - $this->expectException(Load::class); - Config::load(new File(__DIR__ . '/../resources/non-existing.json'), new JSON(), TestConfig::class); - } - - public function testVariableSource(): void - { - $config = Config::load(new Variable([ - 'phpKey' => 'aValue', - 'ENV_KEY' => 'aValue', - ]), new None(), TestConfig::class); - $this->assertSame('aValue', $config->phpKey); - $this->assertSame('aValue', $config->envKey); - - $config = Config::load(new Variable('ENV_KEY=aValue'), new Dotenv(), TestConfig::class); - $this->assertSame('aValue', $config->envKey); - - $config = Config::load(new Environment(), new None(), TestEnvConfig::class); - $this->assertSame('hello', $config->key1); - $this->assertSame('world', $config->key2); - } - - /** - * @return \Iterator<(int | string), mixed> - */ - public static function provideAdapterScenarios(): \Iterator - { - yield [ - 'adapter' => PHP::class, - 'extension' => 'php', - 'key' => 'phpKey', - ]; - yield [ - 'adapter' => JSON::class, - 'extension' => 'json', - 'key' => 'jsonKey', - ]; - yield [ - 'adapter' => YAML::class, - 'extension' => 'yaml', - 'key' => 'yamlKey', - ]; - yield [ - 'adapter' => YAML::class, - 'extension' => 'yml', - 'key' => 'ymlKey', - ]; - yield [ - 'adapter' => Dotenv::class, - 'extension' => 'env', - 'key' => 'envKey', - ]; - } - - /** - * @param class-string $adapter - */ - #[DataProvider('provideAdapterScenarios')] - public function testAdapters(string $adapter, string $extension, string $key): void - { - $adapter = new $adapter(); - if (! ($adapter instanceof Parser)) { - throw new \Exception('Test scenario includes invalid adapter.'); - } - - $config = Config::load(new File(__DIR__ . '/../resources/config.' . $extension), $adapter, TestConfig::class); - - $this->assertSame('customValue', $config->$key); - } - - public function testSubConfigs(): void - { - $config1 = Config::load(new Variable('ENV_KEY=envValue'), new Dotenv(), TestConfig::class); - $config2 = Config::load(new Variable('yml_key: ymlValue'), new YAML(), TestConfig::class); - - $config = Config::load(new Variable([ - 'config1' => $config1, - 'config2' => $config2, - 'rootKey' => 'rootValue', - ]), new None(), TestGroupConfig::class); - - $this->assertSame('rootValue', $config->rootKey); - $this->assertSame('envValue', $config->config1->envKey); - $this->assertSame('ymlValue', $config->config2->ymlKey); - } - - public function testExceptionWithMethod(): void - { - $this->expectException(Load::class); - Config::load(new Variable('KEY=value'), new Dotenv(), TestConfigWithMethod::class); - } - - public function testExceptionWithExtraProperties(): void - { - $this->expectException(Load::class); - Config::load(new Variable('KEY=value'), new Dotenv(), TestConfigWithExtraProperties::class); - } - - public function testExceptionValidator(): void - { - $this->expectException(Load::class); - Config::load(new Variable('KEY=too_long_value_that_will_not_get_accepted'), new Dotenv(), TestConfigRequired::class); - } - - public function testExceptionRequired(): void - { - $this->expectException(Load::class); - Config::load(new Variable('SOME_OTHER_KEY=value'), new Dotenv(), TestConfigRequired::class); - } - - public function testExceptionWithoutType(): void - { - $this->expectException(Load::class); - Config::load(new Variable('KEY=value'), new Dotenv(), TestConfigWithoutType::class); - } - - public function testNestedValues(): void - { - $jsons = [ - <<assertSame('docker.internal', $config->dbHost); - $this->assertTrue($config->tls); - } - } - - public function testRequiredNullablePresentNull(): void - { - $config = Config::load(new Variable('{"name": null}'), new JSON(), TestNullableConfig::class); - $this->assertNull($config->name); - } - - public function testRequiredNullableNestedPresentNull(): void - { - $config = Config::load(new Variable('{"db": {"name": null}}'), new JSON(), TestNestedNullableConfig::class); - $this->assertNull($config->dbName); - } - - public function testOptionalNullablePresentNullIsAssigned(): void - { - $config = Config::load(new Variable('{"name": null}'), new JSON(), TestOptionalNullableConfig::class); - $this->assertNull($config->name); - } - - public function testValueTypeMismatchThrowsLoad(): void - { - // A loose validator accepts the string, but assigning it to the int - // property would raise TypeError — the loader must surface Load instead. - $this->expectException(Load::class); - Config::load(new Variable('PORT=abc'), new Dotenv(), TestTypeMismatchConfig::class); - } - - public function testConstructorArgumentThrowsLoad(): void - { - // A required constructor argument must not blow up as ArgumentCountError. - $this->expectException(Load::class); - Config::load(new Variable('KEY=value'), new Dotenv(), TestConstructorConfig::class); - } -} - -// Schemas used for configs in test scenarios -class TestConfig -{ - #[Key('phpKey', new Text(1024, 0), required: false)] - public string $phpKey; - - #[Key('jsonKey', new Text(1024, 0), required: false)] - public string $jsonKey; - - #[Key('yaml-key', new Text(1024, 0), required: false)] - public string $yamlKey; - - #[Key('yml_key', new Text(1024, 0), required: false)] - public string $ymlKey; - - #[Key('ENV_KEY', new Text(1024, 0), required: false)] - public string $envKey; -} - -class TestGroupConfig -{ - #[ConfigKey] - public TestConfig $config1; - - #[ConfigKey] - public TestConfig $config2; - - #[Key('rootKey', new Text(1024, 0), required: true)] - public string $rootKey; -} - -class TestEnvConfig -{ - #[Key('_UTOPIA_KEY1', new Text(8, 0), required: true)] - public string $key1; - - #[Key('_UTOPIA_KEY2', new Text(8, 0), required: true)] - public string $key2; -} - -class TestConfigRequired -{ - #[Key('key', new Text(8, 0), required: true)] - public string $key; -} - -class TestConfigWithMethod -{ - #[Key('key', new Text(1024, 0))] - public string $key; - - public function convertKey(): string - { - return strtoupper($this->key); - } -} - -class TestConfigWithoutType -{ - // Intentionally untyped: at runtime we ensure the type is required - #[Key('key', new Text(1024, 0))] - public $key; -} - -class TestConfigWithExtraProperties -{ - #[Key('KEY', new Text(1024, 0))] - public string $key; - - public string $key2; -} - -class TestNestedValueConfig -{ - #[Key('db.host', new Text(1024), required: true)] - public string $dbHost; - - #[Key('db.config.tls', new Boolean(), required: true)] - public bool $tls; -} - -class TestNullableConfig -{ - #[Key('name', new Nullable(new Text(1024)), required: true)] - public ?string $name = null; -} - -class TestNestedNullableConfig -{ - #[Key('db.name', new Nullable(new Text(1024)), required: true)] - public ?string $dbName = null; -} - -class TestOptionalNullableConfig -{ - #[Key('name', new Nullable(new Text(1024)), required: false)] - public ?string $name = 'unset'; -} - -class TestTypeMismatchConfig -{ - #[Key('PORT', new Text(1024, 0), required: true)] - public int $port; -} - -class TestConstructorConfig -{ - public function __construct(public string $required) {} -} diff --git a/packages/config/tests/Config/Parser/DotenvTest.php b/packages/config/tests/Config/Parser/DotenvTest.php deleted file mode 100644 index 189022259..000000000 --- a/packages/config/tests/Config/Parser/DotenvTest.php +++ /dev/null @@ -1,320 +0,0 @@ -parser = new Dotenv(); - } - - protected function tearDown(): void {} - - public function testDotenvBasicTypes(): void - { - $dotenv = <<parser->parse($dotenv); - - $this->assertSame('hello world', $data['STRING']); - $this->assertSame('ä你こحب🌍', $data['UNICODE_STRING']); - $this->assertSame('42', $data['INTEGER']); - $this->assertSame('3.14159', $data['FLOAT']); - $this->assertSame('-50', $data['NEGATIVE']); - - $this->assertSame('true', $data['BOOLEAN_TRUE']); - $this->assertSame('false', $data['BOOLEAN_FALSE']); - $this->assertNull($data['NULL_VALUE']); - } - - public function testDotenvParseExceptionInvalid(): void - { - $this->expectException(Parse::class); - $this->parser->parse('=b'); - } - - public function testDotenvParseExceptionNumber(): void - { - $this->expectException(Parse::class); - $this->parser->parse(12); - } - - public function testDotenvParseExceptionBoolean(): void - { - $this->expectException(Parse::class); - $this->parser->parse(false); - } - - public function testDotenvParseExceptionNull(): void - { - $this->expectException(Parse::class); - $this->parser->parse(null); - } - - public function testDotenvEdgeCases(): void - { - $data = $this->parser->parse(''); - $this->assertCount(0, $data); - $data = $this->parser->parse('KEY='); - $this->assertCount(1, $data); - $this->assertSame('', $data['KEY']); - } - - public function testDotenvComment(): void - { - $data = $this->parser->parse( - <<assertSame('127.0.0.1', $data['HOST']); - $this->assertSame('3306', $data['PORT']); - $this->assertSame('secret', $data['PASSWORD']); - $this->assertCount(3, array_keys($data)); - $this->assertArrayNotHasKey('PASSWORD2', $data); - } - - public function testDotenvLineWithoutEqualsThrows(): void - { - $this->expectException(Parse::class); - $this->parser->parse("HOST=127.0.0.1\nDATABASE_PASSWORD"); - } - - public function testDotenvHashInsideQuotesPreserved(): void - { - $data = $this->parser->parse( - <<assertSame('abc#123', $data['PASSWORD']); - $this->assertSame('x#y#z', $data['TOKEN']); - $this->assertSame('https://example.com/path#fragment', $data['URL']); - $this->assertSame('value', $data['PLAIN']); - } - - public function testDotenvEscapedQuoteInsideValue(): void - { - $data = $this->parser->parse('KEY="abc\"#def"'); - $this->assertSame('abc"#def', $data['KEY']); - } - - public function testDotenvLiteralBackslashPreserved(): void - { - // \t is not a supported escape, so the backslash is kept verbatim - // (e.g. a Windows path); \\ collapses to a single backslash. - $data = $this->parser->parse("PATH=\"C:\\tmp\"\nESCAPED=\"a\\\\b\""); - $this->assertSame('C:\tmp', $data['PATH']); - $this->assertSame('a\b', $data['ESCAPED']); - } - - public function testDotenvTrailingGarbageAfterQuoteThrows(): void - { - $this->expectException(Parse::class); - $this->parser->parse('KEY="prod"oops'); - } - - public function testDotenvUnterminatedQuoteThrows(): void - { - $this->expectException(Parse::class); - $this->parser->parse('KEY="abc'); - } - - public function testValueConvertor(): void - { - $data = $this->parser->parse( - << true, - 'KEY2' => true, - 'KEY3' => true, - 'KEY4' => true, - 'KEY5' => true, - 'KEY6' => true, - 'KEY7' => true, - - 'KEY8' => false, - 'KEY9' => false, - 'KEY10' => false, - 'KEY11' => false, - 'KEY12' => false, - 'KEY13' => false, - 'KEY14' => false, - - 'KEY15' => '11', - 'KEY16' => '20', - 'KEY17' => 'online', - 'KEY18' => 'offline', - 'KEY19' => 'notenabled', - 'KEY20' => 'notdisabled', - 'KEY21' => 'yesterday', - 'KEY22' => 'november', - 'KEY23' => 'agree', - 'KEY24' => 'disagree', - - 'KEY25' => '1', - 'KEY26' => '0', - 'KEY27' => null, - ]; - - $this->assertCount(\count($expectedValues), array_keys($data)); - - foreach ($expectedValues as $key => $value) { - $this->assertSame($value, $data[$key], "Failed for key: $key"); - } - } -} - -class TestValueConvertorConfig -{ - #[Key('KEY1', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key1; - - #[Key('KEY2', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key2; - - #[Key('KEY3', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key3; - - #[Key('KEY4', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key4; - - #[Key('KEY5', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key5; - - #[Key('KEY6', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key6; - - #[Key('KEY7', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key7; - - #[Key('KEY8', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key8; - - #[Key('KEY9', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key9; - - #[Key('KEY10', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key10; - - #[Key('KEY11', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key11; - - #[Key('KEY12', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key12; - - #[Key('KEY13', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key13; - - #[Key('KEY14', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key14; - - #[Key('KEY15', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key15; - - #[Key('KEY16', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key16; - - #[Key('KEY17', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key17; - - #[Key('KEY18', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key18; - - #[Key('KEY19', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key19; - - #[Key('KEY20', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key20; - - #[Key('KEY21', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key21; - - #[Key('KEY22', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key22; - - #[Key('KEY23', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key23; - - #[Key('KEY24', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public bool $key24; - - #[Key('KEY25', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public string $key25; - - #[Key('KEY26', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public string $key26; - - #[Key('KEY27', new AnyOf([new WhiteList([true, false]), new Text(1024)]), required: true)] - public string $key27; -} diff --git a/packages/config/tests/Config/Parser/JSONTest.php b/packages/config/tests/Config/Parser/JSONTest.php deleted file mode 100644 index bea6effe4..000000000 --- a/packages/config/tests/Config/Parser/JSONTest.php +++ /dev/null @@ -1,187 +0,0 @@ -parser = new JSON(); - } - - public function testJSONBasicTypes(): void - { - $json = <<parser->parse($json); - - $this->assertSame('hello world', $data['string']); - $this->assertSame('ä你こحب🌍', $data['unicode_string']); - $this->assertSame(42, $data['integer']); - $this->assertEqualsWithDelta(3.14159, $data['float'], PHP_FLOAT_EPSILON); - $this->assertSame(-50, $data['negative']); - $this->assertTrue($data['boolean_true']); - $this->assertFalse($data['boolean_false']); - $this->assertNull($data['null_value']); - } - - public function testJSONArray(): void - { - $json = <<parser->parse($json); - - $this->assertCount(5, $data['simple_array']); - $this->assertSame(1, $data['simple_array'][0]); - $this->assertSame(5, $data['simple_array'][4]); - - $this->assertCount(5, $data['mixed_array']); - $this->assertSame('string', $data['mixed_array'][0]); - $this->assertSame(42, $data['mixed_array'][1]); - $this->assertTrue($data['mixed_array'][2]); - $this->assertNull($data['mixed_array'][3]); - $this->assertEqualsWithDelta(3.14, $data['mixed_array'][4], PHP_FLOAT_EPSILON); - - $this->assertCount(3, $data['nested_array']); - - $this->assertCount(3, $data['nested_array'][0]); - $this->assertCount(4, $data['nested_array'][1]); - $this->assertCount(2, $data['nested_array'][2]); - - $this->assertSame(2, $data['nested_array'][0][1]); - $this->assertSame('b', $data['nested_array'][1][1]); - $this->assertFalse($data['nested_array'][2][1]); - - $this->assertIsArray($data['empty_array']); - $this->assertCount(0, $data['empty_array']); - } - - public function testJSONObject(): void - { - $json = <<parser->parse($json); - - $this->assertSame('John Doe', $data['simple_object']['name']); - $this->assertSame(30, $data['simple_object']['age']); - $this->assertTrue($data['simple_object']['active']); - - $this->assertArrayHasKey('user', $data['nested_object']); - $this->assertArrayHasKey('profile', $data['nested_object']['user']); - $this->assertArrayHasKey( - 'settings', - $data['nested_object']['user']['profile'], - ); - - $this->assertSame( - 'Jane', - $data['nested_object']['user']['profile']['name'], - ); - $this->assertSame( - 'dark', - $data['nested_object']['user']['profile']['settings']['theme'], - ); - - $this->assertIsArray($data['empty_object']); - $this->assertCount(0, $data['empty_object']); - } - - public function testJSONParseExceptionInvalid(): void - { - $this->expectException(Parse::class); - $this->parser->parse('{"invalid": json}'); - } - - public function testJSONParseExceptionNumber(): void - { - $this->expectException(Parse::class); - $this->parser->parse(12); - } - - public function testJSONParseExceptionBoolean(): void - { - $this->expectException(Parse::class); - $this->parser->parse(false); - } - - public function testJSONParseExceptionNull(): void - { - $this->expectException(Parse::class); - $this->parser->parse(null); - } - - public function testJSONScalarTopLevelThrows(): void - { - // Valid JSON, but not a config map — must raise Parse, not a TypeError. - foreach (['"foo"', '123', 'true', 'null'] as $scalar) { - try { - $this->parser->parse($scalar); - $this->fail("Expected Parse for scalar JSON input: {$scalar}"); - } catch (Parse) { - $this->addToAssertionCount(1); - } - } - } - - public function testJSONTopLevelListThrows(): void - { - // A non-empty list is not a config map (its keys would be 0, 1, 2, …). - $this->expectException(Parse::class); - $this->parser->parse('["secret", "other"]'); - } - - public function testJSONEdgeCases(): void - { - $data = $this->parser->parse(''); - $this->assertCount(0, $data); - $data = $this->parser->parse('{}'); - $this->assertCount(0, $data); - $data = $this->parser->parse('[]'); - $this->assertCount(0, $data); - } -} diff --git a/packages/config/tests/Config/Parser/NoneTest.php b/packages/config/tests/Config/Parser/NoneTest.php deleted file mode 100644 index de1dcce45..000000000 --- a/packages/config/tests/Config/Parser/NoneTest.php +++ /dev/null @@ -1,75 +0,0 @@ -parser = new None(); - } - - public function testNoneBasicTypes(): void - { - $variable = [ - 'string' => 'hello world', - 'unicode_string' => 'ä你こحب🌍', - 'integer' => 42, - 'float' => 3.14159, - 'negative' => -50, - 'boolean_true' => true, - 'boolean_false' => false, - 'null' => null, - ]; - - $data = $this->parser->parse($variable); - - $this->assertSame('hello world', $data['string']); - $this->assertSame('ä你こحب🌍', $data['unicode_string']); - $this->assertSame(42, $data['integer']); - $this->assertEqualsWithDelta(3.14159, $data['float'], PHP_FLOAT_EPSILON); - $this->assertSame(-50, $data['negative']); - $this->assertTrue($data['boolean_true']); - $this->assertFalse($data['boolean_false']); - $this->assertNull($data['null']); - } - - public function testNoneParseExceptionNull(): void - { - $this->expectException(Parse::class); - $this->parser->parse(null); - } - - public function testNoneParseExceptionString(): void - { - $this->expectException(Parse::class); - $this->parser->parse('hello'); - } - - public function testNoneParseExceptionBoolean(): void - { - $this->expectException(Parse::class); - $this->parser->parse(false); - } - - public function testNoneParseExceptionNumber(): void - { - $this->expectException(Parse::class); - $this->parser->parse(12); - } - - public function testNoneEdgeCases(): void - { - $value = []; - $data = $this->parser->parse($value); - $this->assertSame($value, $data); - } -} diff --git a/packages/config/tests/Config/Parser/PHPTest.php b/packages/config/tests/Config/Parser/PHPTest.php deleted file mode 100644 index 0e4b5a01a..000000000 --- a/packages/config/tests/Config/Parser/PHPTest.php +++ /dev/null @@ -1,193 +0,0 @@ -parser = new PHP(); - } - - - public function testPHPBasicTypes(): void - { - $php = << "hello world", - "unicode_string" => "ä你こحب🌍", - "integer" => 42, - "float" => 3.14159, - "negative" => -50, - "boolean_true" => true, - "boolean_false" => false, - "null_value" => null, - ]; - PHP; - - $data = $this->parser->parse($php); - - $this->assertSame('hello world', $data['string']); - $this->assertSame('ä你こحب🌍', $data['unicode_string']); - $this->assertSame(42, $data['integer']); - $this->assertEqualsWithDelta(3.14159, $data['float'], PHP_FLOAT_EPSILON); - $this->assertSame(-50, $data['negative']); - $this->assertTrue($data['boolean_true']); - $this->assertFalse($data['boolean_false']); - $this->assertNull($data['null_value']); - } - - public function testPHPArray(): void - { - $php = << [1, 2, 3, 4, 5], - "mixed_array" => ["string", 42, true, null, 3.14], - "nested_array" => [[1, 2, 3], ["a", "b", "c", "d"], [true, false]], - "empty_array" => [], - ]; - PHP; - - $data = $this->parser->parse($php); - - $this->assertCount(5, $data['simple_array']); - $this->assertSame(1, $data['simple_array'][0]); - $this->assertSame(5, $data['simple_array'][4]); - - $this->assertCount(5, $data['mixed_array']); - $this->assertSame('string', $data['mixed_array'][0]); - $this->assertSame(42, $data['mixed_array'][1]); - $this->assertTrue($data['mixed_array'][2]); - $this->assertNull($data['mixed_array'][3]); - $this->assertEqualsWithDelta(3.14, $data['mixed_array'][4], PHP_FLOAT_EPSILON); - - $this->assertCount(3, $data['nested_array']); - - $this->assertCount(3, $data['nested_array'][0]); - $this->assertCount(4, $data['nested_array'][1]); - $this->assertCount(2, $data['nested_array'][2]); - - $this->assertSame(2, $data['nested_array'][0][1]); - $this->assertSame('b', $data['nested_array'][1][1]); - $this->assertFalse($data['nested_array'][2][1]); - - $this->assertIsArray($data['empty_array']); - $this->assertCount(0, $data['empty_array']); - } - - public function testPHPObject(): void - { - $php = << [ - "name" => "John Doe", - "age" => 30, - "active" => true, - ], - "nested_object" => [ - "user" => [ - "profile" => [ - "name" => "Jane", - "settings" => [ - "theme" => "dark", - ], - ], - ], - ], - "empty_object" => [], - ]; - PHP; - - $data = $this->parser->parse($php); - - $this->assertSame('John Doe', $data['simple_object']['name']); - $this->assertSame(30, $data['simple_object']['age']); - $this->assertTrue($data['simple_object']['active']); - - $this->assertArrayHasKey('user', $data['nested_object']); - $this->assertArrayHasKey('profile', $data['nested_object']['user']); - $this->assertArrayHasKey( - 'settings', - $data['nested_object']['user']['profile'], - ); - - $this->assertSame( - 'Jane', - $data['nested_object']['user']['profile']['name'], - ); - $this->assertSame( - 'dark', - $data['nested_object']['user']['profile']['settings']['theme'], - ); - - $this->assertIsArray($data['empty_object']); - $this->assertCount(0, $data['empty_object']); - } - - public function testPHPParseExceptionMissingStart(): void - { - $this->expectException(Parse::class); - $this->parser->parse('return [];'); - } - - public function testPHPParseExceptionMissingCode(): void - { - $this->expectException(Parse::class); - $this->parser->parse('expectException(Parse::class); - $this->parser->parse('expectException(Parse::class); - $this->parser->parse(''); - } - - public function testPHPParseExceptionNumber(): void - { - $this->expectException(Parse::class); - $this->parser->parse(12); - } - - - public function testPHPParseExceptionBoolean(): void - { - $this->expectException(Parse::class); - $this->parser->parse(false); - } - - public function testPHPParseExceptionNull(): void - { - $this->expectException(Parse::class); - $this->parser->parse(null); - } - - public function testPHPEdgeCases(): void - { - $data = $this->parser->parse('assertCount(0, $data); - - $data = $this->parser->parse(" 5 + 3 ];"); - $this->assertCount(1, $data); - $this->assertSame(8, $data['key']); - } -} diff --git a/packages/config/tests/Config/Parser/YAMLTest.php b/packages/config/tests/Config/Parser/YAMLTest.php deleted file mode 100644 index 347064c2f..000000000 --- a/packages/config/tests/Config/Parser/YAMLTest.php +++ /dev/null @@ -1,201 +0,0 @@ -parser = new YAML(); - } - - public function testYAMLBasicTypes(): void - { - $yaml = <<parser->parse($yaml); - - $this->assertSame('hello world', $data['string']); - $this->assertSame('ä你こحب🌍', $data['unicode_string']); - $this->assertSame(42, $data['integer']); - $this->assertEqualsWithDelta(3.14159, $data['float'], PHP_FLOAT_EPSILON); - $this->assertSame(-50, $data['negative']); - $this->assertTrue($data['boolean_true']); - $this->assertFalse($data['boolean_false']); - $this->assertNull($data['null_value']); - } - - public function testYAMLArray(): void - { - $yaml = <<parser->parse($yaml); - - $this->assertCount(5, $data['simple_array']); - $this->assertSame(1, $data['simple_array'][0]); - $this->assertSame(5, $data['simple_array'][4]); - - $this->assertCount(5, $data['mixed_array']); - $this->assertSame('string', $data['mixed_array'][0]); - $this->assertSame(42, $data['mixed_array'][1]); - $this->assertTrue($data['mixed_array'][2]); - $this->assertNull($data['mixed_array'][3]); - $this->assertEqualsWithDelta(3.14, $data['mixed_array'][4], PHP_FLOAT_EPSILON); - - $this->assertCount(3, $data['nested_array']); - - $this->assertCount(3, $data['nested_array'][0]); - $this->assertCount(4, $data['nested_array'][1]); - $this->assertCount(2, $data['nested_array'][2]); - - $this->assertSame(2, $data['nested_array'][0][1]); - $this->assertSame('b', $data['nested_array'][1][1]); - $this->assertFalse($data['nested_array'][2][1]); - - $this->assertIsArray($data['empty_array']); - $this->assertCount(0, $data['empty_array']); - } - - public function testYAMLObject(): void - { - $yaml = <<parser->parse($yaml); - - $this->assertSame('John Doe', $data['simple_object']['name']); - $this->assertSame(30, $data['simple_object']['age']); - $this->assertTrue($data['simple_object']['active']); - - $this->assertArrayHasKey('user', $data['nested_object']); - $this->assertArrayHasKey('profile', $data['nested_object']['user']); - $this->assertArrayHasKey( - 'settings', - $data['nested_object']['user']['profile'], - ); - - $this->assertSame( - 'Jane', - $data['nested_object']['user']['profile']['name'], - ); - $this->assertSame( - 'dark', - $data['nested_object']['user']['profile']['settings']['theme'], - ); - - $this->assertIsArray($data['empty_object']); - $this->assertCount(0, $data['empty_object']); - } - - public function testYAMLParseExceptionInvalid(): void - { - $this->expectException(Parse::class); - $this->parser->parse(':value'); - } - - public function testYAMLParseExceptionNumber(): void - { - $this->expectException(Parse::class); - $this->parser->parse(12); - } - - public function testYAMLParseExceptionBoolean(): void - { - $this->expectException(Parse::class); - $this->parser->parse(false); - } - - public function testYAMLParseExceptionNull(): void - { - $this->expectException(Parse::class); - $this->parser->parse(null); - } - - public function testYAMLScalarTopLevelThrows(): void - { - // Valid YAML, but not a mapping — must raise Parse, not a TypeError. - foreach (['123', '3.14', 'true', 'plain string'] as $scalar) { - try { - $this->parser->parse($scalar); - $this->fail("Expected Parse for scalar YAML input: {$scalar}"); - } catch (Parse) { - $this->addToAssertionCount(1); - } - } - } - - public function testYAMLTopLevelSequenceThrows(): void - { - // A non-empty sequence is not a config map. - $this->expectException(Parse::class); - $this->parser->parse("- a\n- b"); - } - - public function testYAMLEdgeCases(): void - { - $data = $this->parser->parse(''); - $this->assertCount(0, $data); - } - - public function testYAMLComment(): void - { - $yaml = <<parser->parse($yaml); - - $this->assertSame('value', $data['keyWithComment']); - $this->assertCount(1, array_keys($data)); - } -} diff --git a/packages/config/tests/resources/config.env b/packages/config/tests/resources/config.env deleted file mode 100644 index 03fdc7d02..000000000 --- a/packages/config/tests/resources/config.env +++ /dev/null @@ -1 +0,0 @@ -ENV_KEY=customValue \ No newline at end of file diff --git a/packages/config/tests/resources/config.json b/packages/config/tests/resources/config.json deleted file mode 100644 index 2c1df7fdc..000000000 --- a/packages/config/tests/resources/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "jsonKey": "customValue" -} diff --git a/packages/config/tests/resources/config.php b/packages/config/tests/resources/config.php deleted file mode 100644 index a909c428d..000000000 --- a/packages/config/tests/resources/config.php +++ /dev/null @@ -1,7 +0,0 @@ - 'customValue', -]; diff --git a/packages/config/tests/resources/config.yaml b/packages/config/tests/resources/config.yaml deleted file mode 100644 index d89e963d9..000000000 --- a/packages/config/tests/resources/config.yaml +++ /dev/null @@ -1 +0,0 @@ -yaml-key: customValue \ No newline at end of file diff --git a/packages/config/tests/resources/config.yml b/packages/config/tests/resources/config.yml deleted file mode 100644 index a622123e9..000000000 --- a/packages/config/tests/resources/config.yml +++ /dev/null @@ -1 +0,0 @@ -yml_key: customValue \ No newline at end of file