From ded16e607c31cd8ff930c19652d374ae4810d939 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Sat, 11 Oct 2025 11:44:00 +0100 Subject: [PATCH] Validate schema for PIE settings file --- phpstan-baseline.neon | 8 +-- resources/pie-settings-schema.json | 18 +++++++ src/Settings.php | 42 +++++++++++---- test/unit/SettingsTest.php | 84 ++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 resources/pie-settings-schema.json create mode 100644 test/unit/SettingsTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3374d5f..cef582e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -432,12 +432,6 @@ parameters: count: 1 path: src/Platform/TargetPlatform.php - - - message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(mixed\)\: mixed\)\|null, Closure\(array\)\: non\-empty\-array given\.$#' - identifier: argument.type - count: 1 - path: src/SelfManage/Update/FetchPieReleaseFromGitHub.php - - message: '#^Dead catch \- Php\\Pie\\SelfManage\\Verify\\GithubCliNotAvailable is never thrown in the try block\.$#' identifier: catch.neverThrown @@ -675,7 +669,7 @@ parameters: - message: '#^Parameter \#4 \$body of class Composer\\Util\\Http\\Response constructor expects string\|null, string\|false given\.$#' identifier: argument.type - count: 2 + count: 1 path: test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php - diff --git a/resources/pie-settings-schema.json b/resources/pie-settings-schema.json new file mode 100644 index 0000000..8e2fbd0 --- /dev/null +++ b/resources/pie-settings-schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/php/pie/main/resources/pie-config-schema.json", + "title": "PIE configuration file schema", + "description": "Schema for PIE tool configuration file", + "type": "object", + "properties": { + "channel": { + "type": "string", + "description": "Which update channel to use when running self-update", + "enum": [ + "nightly", + "preview", + "stable" + ] + } + } +} diff --git a/src/Settings.php b/src/Settings.php index c7339f5..5b78fdb 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -4,15 +4,17 @@ declare(strict_types=1); namespace Php\Pie; +use Composer\Json\JsonFile; use Php\Pie\SelfManage\Update\Channel; use function array_key_exists; use function file_exists; use function file_get_contents; use function file_put_contents; -use function is_array; use function json_decode; use function json_encode; +use function mkdir; +use function rtrim; use const DIRECTORY_SEPARATOR; use const JSON_PRETTY_PRINT; @@ -27,16 +29,39 @@ use const JSON_THROW_ON_ERROR; */ class Settings { - private const PIE_SETTINGS_FILE_NAME = 'pie-settings.json'; + private const PIE_SETTINGS_SCHEMA_FILE_NAME = __DIR__ . '/../resources/pie-settings-schema.json'; + private const PIE_SETTINGS_FILE_NAME = 'pie-settings.json'; public function __construct(private readonly string $pieWorkingDirectory) { } + private function pieSettingsFullPath(): string + { + $workDir = rtrim($this->pieWorkingDirectory, DIRECTORY_SEPARATOR); + + if (! file_exists($workDir)) { + mkdir($workDir, recursive: true); + } + + return $workDir . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + } + + /** @phpstan-assert PieSettings $settingsBlob */ + private function validateSchema(mixed $settingsBlob): void + { + JsonFile::validateJsonSchema( + self::PIE_SETTINGS_FILE_NAME, + $settingsBlob, + JsonFile::STRICT_SCHEMA, + self::PIE_SETTINGS_SCHEMA_FILE_NAME, + ); + } + /** @phpstan-return PieSettings */ private function read(): array { - $pieSettingsFileName = $this->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; + $pieSettingsFileName = $this->pieSettingsFullPath(); if (! file_exists($pieSettingsFileName)) { return []; } @@ -48,18 +73,17 @@ class Settings $config = json_decode($content, true, flags: JSON_THROW_ON_ERROR); - // @todo schema validation + $this->validateSchema($config); - return is_array($config) ? $config : []; + return $config; } - /** @param PieSettings $config */ + /** @param array $config */ private function write(array $config): void { - // @todo schema validation + $this->validateSchema($config); - $pieSettingsFileName = $this->pieWorkingDirectory . DIRECTORY_SEPARATOR . self::PIE_SETTINGS_FILE_NAME; - file_put_contents($pieSettingsFileName, json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + file_put_contents($this->pieSettingsFullPath(), json_encode($config, flags: JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } public function updateChannel(): Channel diff --git a/test/unit/SettingsTest.php b/test/unit/SettingsTest.php new file mode 100644 index 0000000..282931e --- /dev/null +++ b/test/unit/SettingsTest.php @@ -0,0 +1,84 @@ +expectException(JsonValidationException::class); + $settings->updateChannel(); + } + + public function testNewSettingsJsonCanBeCreated(): void + { + $workingDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_settings_test', true) . DIRECTORY_SEPARATOR; + + $settings = new Settings($workingDir); + self::assertSame(Channel::Stable, $settings->updateChannel()); + + $settings->changeUpdateChannel(Channel::Preview); + self::assertSame(Channel::Preview, $settings->updateChannel()); + + self::assertSame( + trim(<<<'JSON' +{ + "channel": "preview" +} +JSON), + str_replace("\r\n", "\n", (string) file_get_contents($workingDir . 'pie-settings.json')), + ); + + (new Filesystem())->remove($workingDir); + } + + public function testExistingSettingsCanBeUpdated(): void + { + $workingDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_settings_test', true) . DIRECTORY_SEPARATOR; + mkdir($workingDir, recursive: true); + file_put_contents($workingDir . 'pie-settings.json', '{"channel": "stable"}'); + + $settings = new Settings($workingDir); + self::assertSame(Channel::Stable, $settings->updateChannel()); + + $settings->changeUpdateChannel(Channel::Preview); + self::assertSame(Channel::Preview, $settings->updateChannel()); + + self::assertSame( + trim(<<<'JSON' +{ + "channel": "preview" +} +JSON), + str_replace("\r\n", "\n", (string) file_get_contents($workingDir . 'pie-settings.json')), + ); + + (new Filesystem())->remove($workingDir); + } +}