diff --git a/config.ini.example b/config.ini.example index 18f874d8319afa3ba56ec7d2c7c090f55350b871..25c10e8d87ecf6379c13386095c4e5027814ed43 100644 --- a/config.ini.example +++ b/config.ini.example @@ -1,6 +1,6 @@ [app] env = development -esbuild_metafile = %DM_ROOT%/esbuild-meta-app.json +esbuild_metafile = null scheme = https url = http://localhost:9501 @@ -79,8 +79,8 @@ sitemap = %DM_ROOT%/docs/build/sitemap.xml host = 127.0.0.1 port = 9501 log_level = SWOOLE_LOG_DEBUG -ssl_cert_file = %DM_ROOT%/ssl/origin.crt -ssl_key_file = %DM_ROOT%/ssl/origin.key +ssl_cert_file = null +ssl_key_file = null [translator] base_directory = %DM_APP_ROOT%/lang diff --git a/docs/pages/docs/changelog/index.md b/docs/pages/docs/changelog/index.md index 5bb6603cdf168187d571bf0cc2ae2e655f02549d..93dbe5c36aa5a5069904f9160449459098ad31d2 100644 --- a/docs/pages/docs/changelog/index.md +++ b/docs/pages/docs/changelog/index.md @@ -10,6 +10,10 @@ title: Changelog # Changelog +## v0.23.0 + +- Feature: filename constraint in {{docs/features/validation/constraints/index}} + ## v0.22.0 - Change: switch to absolute paths in {{docs/features/configuration/index}} diff --git a/docs/pages/docs/features/validation/constraints/index.md b/docs/pages/docs/features/validation/constraints/index.md index f80cb8401c55aa98835ecf7b024cbfa724d423ff..62eddd57821134b88b42e74f8887bd1028cd5540 100644 --- a/docs/pages/docs/features/validation/constraints/index.md +++ b/docs/pages/docs/features/validation/constraints/index.md @@ -78,7 +78,7 @@ new ConstConstraint(constValue: $constValue); } ``` -Accepts exactly the provided value +Accepts exactly the provided value. ### Enum @@ -95,6 +95,20 @@ new EnumConstraint(values: $values); } ``` +### Filename + +Checks if a given file exists and is readable. + +```php +new FilenameConstraint(); +``` +```json +{ + "type": "string", + "enum": [...] +} +``` + ### Integer ```php diff --git a/src/ApplicationConfiguration.php b/src/ApplicationConfiguration.php index aa9b8308ef943081bd08dd10f132372bd118b946..4eb016ab8cf8fff0b31b0e9c07226054bb4874f4 100644 --- a/src/ApplicationConfiguration.php +++ b/src/ApplicationConfiguration.php @@ -13,7 +13,7 @@ readonly class ApplicationConfiguration */ public function __construct( public Environment $environment, - public string $esbuildMetafile, + public ?string $esbuildMetafile, public string $scheme, public string $url, ) {} diff --git a/src/Command/Watch.php b/src/Command/Watch.php index ab17e8c91a55ad2163da387111cb426e2cdc497d..a31d1058d9af9bd2f42ff34f5b2893be4f9e4b76 100644 --- a/src/Command/Watch.php +++ b/src/Command/Watch.php @@ -43,16 +43,19 @@ final class Watch extends Command */ $childCommandName = $input->getArgument('name'); - $directories = [ + $files = [ DM_APP_ROOT, DM_APP_ROOT.'/../config.ini', - $this->applicationConfiguration->esbuildMetafile, DM_RESONANCE_ROOT, ]; + if (is_string($this->applicationConfiguration->esbuildMetafile)) { + $files[] = $this->applicationConfiguration->esbuildMetafile; + } + $this->restartChildCommand($childCommandName); - foreach (new InotifyIterator($directories) as $event) { + foreach (new InotifyIterator($files) as $event) { $this->logger->info(sprintf('watch_file_changed(%s)', $event['name'])); $this->restartChildCommand($childCommandName); diff --git a/src/Constraint/FilenameConstraint.php b/src/Constraint/FilenameConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..12e6937b48a2d8ef281d4ecdea831e3fb118c982 --- /dev/null +++ b/src/Constraint/FilenameConstraint.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\ConstraintDefaultValue; +use Distantmagic\Resonance\ConstraintPath; +use Distantmagic\Resonance\ConstraintReason; +use Distantmagic\Resonance\ConstraintResult; +use Distantmagic\Resonance\ConstraintResultStatus; + +final readonly class FilenameConstraint extends Constraint +{ + public function __construct( + ?ConstraintDefaultValue $defaultValue = null, + bool $isNullable = false, + bool $isRequired = true, + ) { + parent::__construct( + defaultValue: $defaultValue, + isNullable: $isNullable, + isRequired: $isRequired, + ); + } + + public function default(mixed $defaultValue): self + { + return new self( + defaultValue: new ConstraintDefaultValue($defaultValue), + isNullable: $this->isNullable, + isRequired: $this->isRequired, + ); + } + + public function nullable(): self + { + return new self( + defaultValue: $this->defaultValue ?? new ConstraintDefaultValue(null), + isNullable: true, + isRequired: $this->isRequired, + ); + } + + public function optional(): self + { + return new self( + defaultValue: $this->defaultValue, + isNullable: $this->isNullable, + isRequired: false, + ); + } + + protected function doConvertToJsonSchema(): array + { + return [ + 'type' => 'string', + 'minLength' => 1, + ]; + } + + protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult + { + if (!is_string($notValidatedData) || empty($notValidatedData)) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::InvalidDataType, + status: ConstraintResultStatus::Invalid, + ); + } + + if (!file_exists($notValidatedData)) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::FileNotFound, + status: ConstraintResultStatus::Invalid, + ); + } + + if (!is_readable($notValidatedData)) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::FileNotReadable, + status: ConstraintResultStatus::Invalid, + ); + } + + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::Ok, + status: ConstraintResultStatus::Valid, + ); + } +} diff --git a/src/Constraint/FilenameConstraintTest.php b/src/Constraint/FilenameConstraintTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f168c71469224ca5ba21d19a3afdc4e79b1ec7bc --- /dev/null +++ b/src/Constraint/FilenameConstraintTest.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + * + * @internal + */ +final class FilenameConstraintTest extends TestCase +{ + public function test_invalid(): void + { + $constraint = new FilenameConstraint(); + + self::assertFalse($constraint->validate(__FILE__.'.not_exists')->status->isValid()); + } + + public function test_is_converted_optionally_to_json_schema(): void + { + $constraint = new FilenameConstraint(); + + self::assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $constraint->optional()->toJsonSchema()); + } + + public function test_is_converted_to_json_schema(): void + { + $constraint = new FilenameConstraint(); + + self::assertEquals([ + 'type' => 'string', + 'minLength' => 1, + ], $constraint->toJsonSchema()); + } + + public function test_nullable_is_converted_to_json_schema(): void + { + $constraint = new FilenameConstraint(); + + self::assertEquals([ + 'type' => ['null', 'string'], + 'minLength' => 1, + 'default' => null, + ], $constraint->nullable()->toJsonSchema()); + } + + public function test_validates(): void + { + $constraint = new FilenameConstraint(); + + self::assertTrue($constraint->validate(__FILE__)->status->isValid()); + } +} diff --git a/src/ConstraintReason.php b/src/ConstraintReason.php index 0c990ff22a7fb8c8e81a84e64f72bb9eb4c7eeeb..ad77e8569b34352df4c73c347ee9c3f366605bd9 100644 --- a/src/ConstraintReason.php +++ b/src/ConstraintReason.php @@ -6,6 +6,8 @@ namespace Distantmagic\Resonance; enum ConstraintReason: string { + case FileNotFound = 'file_not_found'; + case FileNotReadable = 'file_not_readable'; case InvalidDataType = 'invalid_data_type'; case InvalidEnumValue = 'invalid_enum_value'; case InvalidFormat = 'invalid_format'; diff --git a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php index 62ef9accdfc222445a9ce1b1ead40d359caa9c10..7fd24e67c6656cf19a89158974ac6828c9364156 100644 --- a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php @@ -8,6 +8,7 @@ use Distantmagic\Resonance\ApplicationConfiguration; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; use Distantmagic\Resonance\Constraint\EnumConstraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\Environment; @@ -30,7 +31,7 @@ final readonly class ApplicationConfigurationProvider extends ConfigurationProvi return new ObjectConstraint( properties: [ 'env' => new EnumConstraint(Environment::values()), - 'esbuild_metafile' => (new StringConstraint())->default('esbuild-meta.json'), + 'esbuild_metafile' => (new FilenameConstraint())->nullable(), 'scheme' => (new EnumConstraint(['http', 'https']))->default('https'), 'url' => new StringConstraint(), ], diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php index dddd3e439adbb2d72db86b39b093cdfda5579dd6..1e8bae3d7dba59e668a25082aabe55776825f515 100644 --- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php @@ -8,6 +8,7 @@ use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; use Distantmagic\Resonance\Constraint\BooleanConstraint; use Distantmagic\Resonance\Constraint\EnumConstraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\IntegerConstraint; use Distantmagic\Resonance\Constraint\MapConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; @@ -49,7 +50,7 @@ final readonly class DatabaseConfigurationProvider extends ConfigurationProvider 'pool_prefill' => (new BooleanConstraint())->default(true), 'pool_size' => new IntegerConstraint(), 'port' => (new IntegerConstraint())->nullable()->default(3306), - 'unix_socket' => (new StringConstraint())->nullable(), + 'unix_socket' => (new FilenameConstraint())->nullable(), 'username' => new StringConstraint(), ] ); diff --git a/src/SingletonProvider/ConfigurationProvider/GrpcConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/GrpcConfigurationProvider.php index 940d9eebd5f08a0513ae4b82d62f361935b76267..3fd5eda7a637b5867d58e06550afea865e0a2a8f 100644 --- a/src/SingletonProvider/ConfigurationProvider/GrpcConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/GrpcConfigurationProvider.php @@ -6,8 +6,8 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; -use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\GrpcConfiguration; use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; @@ -26,7 +26,8 @@ final readonly class GrpcConfigurationProvider extends ConfigurationProvider { return new ObjectConstraint( properties: [ - 'protoc_bin' => new StringConstraint(), + 'grpc_php_plugin_bin' => new FilenameConstraint(), + 'protoc_bin' => new FilenameConstraint(), ], ); } diff --git a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php index 1aa00bf873ed0a492f47f38f113e5bae1b280720..6796e045793682d4da8fa7070221484fa0f67f2a 100644 --- a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php @@ -7,6 +7,7 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\GrantsFeature; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\MapConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; @@ -39,8 +40,8 @@ final readonly class MailerConfigurationProvider extends ConfigurationProvider 'dkim_domain_name' => (new StringConstraint())->nullable(), 'dkim_selector' => (new StringConstraint())->nullable(), 'dkim_signing_key_passphrase' => (new StringConstraint())->nullable(), - 'dkim_signing_key_private' => (new StringConstraint())->nullable(), - 'dkim_signing_key_public' => (new StringConstraint())->nullable(), + 'dkim_signing_key_private' => (new FilenameConstraint())->nullable(), + 'dkim_signing_key_public' => (new FilenameConstraint())->nullable(), 'transport_dsn' => new StringConstraint(), ] ); diff --git a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php index d0bbbe8cb5791a36de9ea67210dd90730de411fd..a1b0bc3121b75d5d1bfaa919bff60992db261e6a 100644 --- a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php @@ -8,6 +8,7 @@ use Defuse\Crypto\Key; use Distantmagic\Resonance\Attribute\GrantsFeature; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\Feature; @@ -36,10 +37,10 @@ final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider { return new ObjectConstraint( properties: [ - 'encryption_key' => new StringConstraint(), + 'encryption_key' => new FilenameConstraint(), 'jwt_signing_key_passphrase' => (new StringConstraint())->nullable(), - 'jwt_signing_key_private' => new StringConstraint(), - 'jwt_signing_key_public' => new StringConstraint(), + 'jwt_signing_key_private' => new FilenameConstraint(), + 'jwt_signing_key_public' => new FilenameConstraint(), 'session_key_authorization_request' => (new StringConstraint())->default('oauth2.authorization_request'), 'session_key_pkce' => (new StringConstraint())->default('oauth2.pkce'), 'session_key_state' => (new StringConstraint())->default('oauth2.state'), diff --git a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php index 478a48295ce9fbba140b571e5629ca2ff8a2d832..88fbe9231bc486b3ba7c7210f105d05dc4879c47 100644 --- a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php @@ -6,8 +6,8 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; -use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\SQLiteVSSConfiguration; @@ -24,8 +24,8 @@ final readonly class SQLiteVSSConfigurationProvider extends ConfigurationProvide { return new ObjectConstraint( properties: [ - 'extension_vector0' => new StringConstraint(), - 'extension_vss0' => new StringConstraint(), + 'extension_vector0' => new FilenameConstraint(), + 'extension_vss0' => new FilenameConstraint(), ], ); } diff --git a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php index c567231c4ba59c8b7f251c3118a43c83df25aafd..270fcec222da41b3ec12dc58e38479761e1d01d1 100644 --- a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php @@ -6,6 +6,7 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; @@ -28,9 +29,9 @@ final readonly class StaticPageConfigurationProvider extends ConfigurationProvid return new ObjectConstraint( properties: [ 'base_url' => new StringConstraint(), - 'esbuild_metafile' => new StringConstraint(), - 'input_directory' => new StringConstraint(), - 'output_directory' => new StringConstraint(), + 'esbuild_metafile' => new FilenameConstraint(), + 'input_directory' => new FilenameConstraint(), + 'output_directory' => new FilenameConstraint(), 'sitemap' => new StringConstraint(), ] ); diff --git a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php index 45c99e67d1ce8593a6d04d03fab419fe74af767c..8fe0e045ad65d224647f7b65a679da02b1fd41b7 100644 --- a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php @@ -7,6 +7,7 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; use Distantmagic\Resonance\Constraint\BooleanConstraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\IntegerConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; @@ -35,8 +36,8 @@ final readonly class SwooleConfigurationProvider extends ConfigurationProvider 'log_level' => new IntegerConstraint(), 'log_requests' => (new BooleanConstraint())->default(false), 'port' => new IntegerConstraint(), - 'ssl_cert_file' => (new StringConstraint())->default(null), - 'ssl_key_file' => (new StringConstraint())->default(null), + 'ssl_cert_file' => (new FilenameConstraint())->nullable(), + 'ssl_key_file' => (new FilenameConstraint())->nullable(), 'task_worker_num' => (new IntegerConstraint())->default(4), ], ); diff --git a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php index ebaed71318b98e698a0b4c32f915792745012d75..e5f4f9f05489d88b19b5d0fd46e2fbe68815a69f 100644 --- a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php @@ -6,6 +6,7 @@ namespace Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\FilenameConstraint; use Distantmagic\Resonance\Constraint\ObjectConstraint; use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; @@ -24,7 +25,7 @@ final readonly class TranslatorConfigurationProvider extends ConfigurationProvid { return new ObjectConstraint( properties: [ - 'base_directory' => new StringConstraint(), + 'base_directory' => new FilenameConstraint(), 'default_primary_language' => new StringConstraint(), ] ); diff --git a/src/TwigEsbuildContext.php b/src/TwigEsbuildContext.php index 812503a10bec4d726c45e22665af7d201dbdabc3..13c23d74c3acf5c35879c28446ea5ccb4144d51f 100644 --- a/src/TwigEsbuildContext.php +++ b/src/TwigEsbuildContext.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; use Distantmagic\Resonance\Attribute\Singleton; +use RuntimeException; use Swoole\Http\Request; use WeakMap; @@ -16,7 +17,7 @@ readonly class TwigEsbuildContext */ private WeakMap $entryPoints; - private EsbuildMeta $esbuildMeta; + private ?EsbuildMeta $esbuildMeta; public function __construct( ApplicationConfiguration $applicationConfiguration, @@ -26,11 +27,17 @@ readonly class TwigEsbuildContext * @var WeakMap<Request,EsbuildMetaEntryPoints> */ $this->entryPoints = new WeakMap(); - $this->esbuildMeta = $esbuildMetaBuilder->build($applicationConfiguration->esbuildMetafile); + $this->esbuildMeta = is_string($applicationConfiguration->esbuildMetafile) + ? $esbuildMetaBuilder->build($applicationConfiguration->esbuildMetafile) + : null; } public function getEntryPoints(Request $request): EsbuildMetaEntryPoints { + if (is_null($this->esbuildMeta)) { + throw new RuntimeException("You need to provide application's esbuild metafile to use esbuild in Twig"); + } + if (!$this->entryPoints->offsetExists($request)) { $this->entryPoints->offsetSet($request, new EsbuildMetaEntryPoints($this->esbuildMeta)); }