From a8687e4bef8a6cda538c879b1e80081e00a9ffcc Mon Sep 17 00:00:00 2001 From: George Steel Date: Wed, 1 Sep 2021 17:12:50 +0100 Subject: [PATCH 01/18] Initial rev of tools and commands to upload and download types to the custom type api --- composer.json | 12 +- composer.lock | 187 ++++++++++++------ example/upload-download.php | 80 ++++++++ phpcs.xml | 4 + src/Assert.php | 23 +++ .../Container/DownloadCommandFactory.php | 21 ++ .../Container/UploadCommandFactory.php | 21 ++ src/Console/DownloadCommand.php | 82 ++++++++ src/Console/UploadCommand.php | 90 +++++++++ src/Container/CustomTypeClientFactory.php | 33 ++++ src/Container/HttpComponentDiscovery.php | 52 +++++ src/Container/PrismicApiClientFactory.php | 11 +- src/Container/TypePersisterFactory.php | 21 ++ src/CustomTypeApiConfigProvider.php | 57 ++++++ src/Exception/AssertionFailed.php | 9 + src/Type/Spec.php | 5 + src/TypePersister.php | 122 ++++++++++++ .../Container/CustomTypeClientFactoryTest.php | 81 ++++++++ 18 files changed, 844 insertions(+), 67 deletions(-) create mode 100644 example/upload-download.php create mode 100644 src/Assert.php create mode 100644 src/Console/Container/DownloadCommandFactory.php create mode 100644 src/Console/Container/UploadCommandFactory.php create mode 100644 src/Console/DownloadCommand.php create mode 100644 src/Console/UploadCommand.php create mode 100644 src/Container/CustomTypeClientFactory.php create mode 100644 src/Container/HttpComponentDiscovery.php create mode 100644 src/Container/TypePersisterFactory.php create mode 100644 src/CustomTypeApiConfigProvider.php create mode 100644 src/Exception/AssertionFailed.php create mode 100644 src/TypePersister.php create mode 100644 test/Unit/Container/CustomTypeClientFactoryTest.php diff --git a/composer.json b/composer.json index eaa21d9..824b209 100644 --- a/composer.json +++ b/composer.json @@ -29,8 +29,15 @@ "php": "^7.3||~8.0", "ext-json": "*", "netglue/prismic-client": "^0", + "netglue/prismic-doctype-client": "^0.1.0", + "php-http/discovery": "^1.14", "psr/container": "^1.0||^2.0", - "symfony/console": "^5.1" + "psr/http-client": "^1.0", + "psr/http-client-implementation": "*", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "symfony/console": "^5.1", + "webmozart/assert": "^1.10" }, "require-dev": { "ext-curl": "*", @@ -45,7 +52,8 @@ "laminas": { "config-provider": [ "Primo\\Cli\\ConfigProvider", - "Primo\\Cli\\ApiToolsConfigProvider" + "Primo\\Cli\\ApiToolsConfigProvider", + "Primo\\Cli\\CustomTypeApiConfigProvider" ] } }, diff --git a/composer.lock b/composer.lock index d83b5d7..707a17c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "219e77d1b04df56536cabfae70f716ec", + "content-hash": "86f4457cc13589ef49644bed4d709372", "packages": [ { "name": "clue/stream-filter", @@ -270,6 +270,75 @@ }, "time": "2021-07-05T08:55:01+00:00" }, + { + "name": "netglue/prismic-doctype-client", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/netglue/prismic-doctype-client.git", + "reference": "f40c924e312556565c809c8bba42a11b12410e13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/netglue/prismic-doctype-client/zipball/f40c924e312556565c809c8bba42a11b12410e13", + "reference": "f40c924e312556565c809c8bba42a11b12410e13", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ~8.0", + "php-http/discovery": "^1.11", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "*", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "webmozart/assert": "^1.10" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "ergebnis/composer-normalize": "^2.15", + "ext-curl": "*", + "infection/infection": "^0", + "laminas/laminas-diactoros": "^2.6", + "php-http/curl-client": "^2.2", + "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.16.1", + "react/child-process": "^0.6.3", + "react/http": "^1.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6", + "vimeo/psalm": "^4.8" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 4, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Prismic\\DocumentType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "George Steel", + "email": "george@net-glue.co.uk" + } + ], + "description": "Prismic Custom Type API Client", + "homepage": "https://github.com/netglue/prismic-doctype-client", + "support": { + "issues": "https://github.com/netglue/prismic-doctype-client/issues", + "source": "https://github.com/netglue/prismic-doctype-client/tree/0.1.0" + }, + "time": "2021-09-01T10:50:33+00:00" + }, { "name": "php-http/curl-client", "version": "2.2.0", @@ -1780,6 +1849,64 @@ } ], "time": "2021-08-26T08:00:08+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, + "time": "2021-03-09T10:59:23+00:00" } ], "packages-dev": [ @@ -4518,64 +4645,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], diff --git a/example/upload-download.php b/example/upload-download.php new file mode 100644 index 0000000..b8ce0bb --- /dev/null +++ b/example/upload-download.php @@ -0,0 +1,80 @@ + 'page', + 'name' => 'A Web Page', + 'repeatable' => true, + ], + [ + 'id' => 'error', + 'name' => 'An Error Page', + 'repeatable' => true, + ], + [ + 'id' => 'article', + 'name' => 'Blog Post', + 'repeatable' => true, + ], +]; + +/** + * Build config will throw exceptions if there are any configuration problems. + */ +$config = BuildConfig::withArraySpecs($source, $dist, []); +$persister = new TypePersister($config, $client); + +$application = new Application('Primo Upload and Download Example'); +$application->add(new DownloadCommand($persister, $config)); +$application->add(new UploadCommand($persister, $config)); + +return $application->run(); diff --git a/phpcs.xml b/phpcs.xml index bd970d6..64da030 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -26,4 +26,8 @@ 0 + + + 0 + diff --git a/src/Assert.php b/src/Assert.php new file mode 100644 index 0000000..4fc5beb --- /dev/null +++ b/src/Assert.php @@ -0,0 +1,23 @@ +get(TypePersister::class), + $container->get(BuildConfig::class) + ); + } +} diff --git a/src/Console/Container/UploadCommandFactory.php b/src/Console/Container/UploadCommandFactory.php new file mode 100644 index 0000000..f959687 --- /dev/null +++ b/src/Console/Container/UploadCommandFactory.php @@ -0,0 +1,21 @@ +get(TypePersister::class), + $container->get(BuildConfig::class), + ); + } +} diff --git a/src/Console/DownloadCommand.php b/src/Console/DownloadCommand.php new file mode 100644 index 0000000..b352428 --- /dev/null +++ b/src/Console/DownloadCommand.php @@ -0,0 +1,82 @@ +persister = $uploader; + $this->config = $config; + parent::__construct($name); + } + + protected function configure(): void + { + $this->setDescription('Download one or all document type definitions'); + $this->setHelp(sprintf( + 'This command iterates over all your configured Prismic types and downloads them to the ' . PHP_EOL . + 'configured `dist` directory (%s).' . PHP_EOL . + 'You can optionally provide a single type identifier to download just one of the configured types.', + $this->config->distDirectory() + )); + + $this->addArgument('type', InputArgument::OPTIONAL, 'An individual type identifier to download', null); + $this->addOption('no-index', null, InputOption::VALUE_NONE, 'Disable index.json generation'); + $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Include disabled types'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $single = $input->getArgument('type'); + if ($single !== null) { + return $this->downloadOne($single, $style); + } + + $writeIndex = $input->getOption('no-index') === false; + $skipDisabled = $input->getOption('all') !== false; + + $this->persister->download($writeIndex, $skipDisabled); + + $style->success('Types definitions saved'); + + return self::SUCCESS; + } + + private function downloadOne(string $single, SymfonyStyle $style): int + { + try { + $this->persister->downloadType($single); + } catch (DefinitionNotFound $error) { + $style->error(sprintf('The type "%s" does not exist', $single)); + + return self::FAILURE; + } + + return self::SUCCESS; + } +} diff --git a/src/Console/UploadCommand.php b/src/Console/UploadCommand.php new file mode 100644 index 0000000..e20cbe8 --- /dev/null +++ b/src/Console/UploadCommand.php @@ -0,0 +1,90 @@ +uploader = $uploader; + $this->config = $config; + parent::__construct($name); + } + + protected function configure(): void + { + $this->setDescription('Upload one or all configured document type definitions'); + $this->setHelp( + 'This command iterates over all your configured Prismic types and uploads them to the remote ' . + 'custom types api endpoint, making them immediately usable in your Prismic repository.' . PHP_EOL . + 'You can optionally provide a single type identifier to upload just one of the configured types.' + ); + + $this->addArgument('type', InputArgument::OPTIONAL, 'An individual type identifier to upload', null); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $specs = $this->resolveTypes($input->getArgument('type')); + + $style->progressStart(count($specs)); + foreach ($specs as $spec) { + $this->uploader->uploadType($spec); + } + + $style->progressFinish(); + + $style->success(sprintf( + '%d document types uploaded', + count($specs) + )); + + return self::SUCCESS; + } + + /** + * @return array + */ + private function resolveTypes(?string $type): array + { + $specs = $this->config->types(); + if ($type === null) { + return $specs; + } + + foreach ($specs as $spec) { + if ($spec->id() !== $type) { + continue; + } + + return [$spec]; + } + + throw new InvalidArgument(sprintf('The type "%s" is not a known type', $type)); + } +} diff --git a/src/Container/CustomTypeClientFactory.php b/src/Container/CustomTypeClientFactory.php new file mode 100644 index 0000000..db825dd --- /dev/null +++ b/src/Container/CustomTypeClientFactory.php @@ -0,0 +1,33 @@ +get('config'); + Assert::isArrayAccessible($config); + $token = $config['primo']['custom-type-api']['token'] ?? null; + $repository = $config['primo']['custom-type-api']['repository'] ?? null; + Assert::string($token, 'An access token must be provided in config.primo.custom-type-api.token in order to access the Prismic custom type API.'); + Assert::string($repository, 'The repository name must be provided in config.primo.custom-type-api.repository in order to access the Prismic custom type API.'); + + return new BaseClient( + $token, + $repository, + $this->httpClient($container), + $this->requestFactory($container), + $this->uriFactory($container), + $this->streamFactory($container) + ); + } +} diff --git a/src/Container/HttpComponentDiscovery.php b/src/Container/HttpComponentDiscovery.php new file mode 100644 index 0000000..5c1c769 --- /dev/null +++ b/src/Container/HttpComponentDiscovery.php @@ -0,0 +1,52 @@ +has(ClientInterface::class)) { + return $container->get(ClientInterface::class); + } + + return Psr18ClientDiscovery::find(); + } + + private function requestFactory(ContainerInterface $container): RequestFactoryInterface + { + if ($container->has(RequestFactoryInterface::class)) { + return $container->get(RequestFactoryInterface::class); + } + + return Psr17FactoryDiscovery::findRequestFactory(); + } + + private function uriFactory(ContainerInterface $container): UriFactoryInterface + { + if ($container->has(UriFactoryInterface::class)) { + return $container->get(UriFactoryInterface::class); + } + + return Psr17FactoryDiscovery::findUriFactory(); + } + + private function streamFactory(ContainerInterface $container): StreamFactoryInterface + { + if ($container->has(StreamFactoryInterface::class)) { + return $container->get(StreamFactoryInterface::class); + } + + return Psr17FactoryDiscovery::findStreamFactory(); + } +} diff --git a/src/Container/PrismicApiClientFactory.php b/src/Container/PrismicApiClientFactory.php index a1e9d42..d061680 100644 --- a/src/Container/PrismicApiClientFactory.php +++ b/src/Container/PrismicApiClientFactory.php @@ -8,12 +8,11 @@ use Prismic\Api; use Prismic\ApiClient; use Psr\Container\ContainerInterface; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\UriFactoryInterface; final class PrismicApiClientFactory { + use HttpComponentDiscovery; + public function __invoke(ContainerInterface $container): ApiClient { $config = $container->has('config') ? $container->get('config') : []; @@ -28,9 +27,9 @@ public function __invoke(ContainerInterface $container): ApiClient return Api::get( $apiUrl, $config['prismic']['token'] ?? null, - $container->has(ClientInterface::class) ? $container->get(ClientInterface::class) : null, - $container->has(RequestFactoryInterface::class) ? $container->get(RequestFactoryInterface::class) : null, - $container->has(UriFactoryInterface::class) ? $container->get(UriFactoryInterface::class) : null + $this->httpClient($container), + $this->requestFactory($container), + $this->uriFactory($container) ); } } diff --git a/src/Container/TypePersisterFactory.php b/src/Container/TypePersisterFactory.php new file mode 100644 index 0000000..94ad1e1 --- /dev/null +++ b/src/Container/TypePersisterFactory.php @@ -0,0 +1,21 @@ +get(BuildConfig::class), + $container->get(Client::class) + ); + } +} diff --git a/src/CustomTypeApiConfigProvider.php b/src/CustomTypeApiConfigProvider.php new file mode 100644 index 0000000..ecb51b9 --- /dev/null +++ b/src/CustomTypeApiConfigProvider.php @@ -0,0 +1,57 @@ + */ + public function __invoke(): array + { + return [ + 'primo' => [ + 'custom-type-api' => [ + 'token' => null, // A permanent access token for the custom types api. + 'repository' => null, // The name of the repository - not a URL, just the name. + ], + ], + 'dependencies' => $this->dependencies(), + 'console' => [ + 'commands' => $this->commands(), + ], + 'laminas-cli' => [ + 'commands' => $this->commands(), + ], + ]; + } + + /** @return array> */ + private function dependencies(): array + { + return [ + 'factories' => [ + Prismic\DocumentType\BaseClient::class => Container\CustomTypeClientFactory::class, + + Console\DownloadCommand::class => Console\Container\DownloadCommandFactory::class, + Console\UploadCommand::class => Console\Container\UploadCommandFactory::class, + + TypePersister::class => Container\TypePersisterFactory::class, + ], + 'aliases' => [ + Prismic\DocumentType\Client::class => Prismic\DocumentType\BaseClient::class, + ], + ]; + } + + /** @return array */ + private function commands(): array + { + return [ + Console\DownloadCommand::DEFAULT_NAME => Console\DownloadCommand::class, + Console\UploadCommand::DEFAULT_NAME => Console\UploadCommand::class, + ]; + } +} diff --git a/src/Exception/AssertionFailed.php b/src/Exception/AssertionFailed.php new file mode 100644 index 0000000..044431a --- /dev/null +++ b/src/Exception/AssertionFailed.php @@ -0,0 +1,9 @@ +name; } + + public function repeatable(): bool + { + return $this->repeatable; + } } diff --git a/src/TypePersister.php b/src/TypePersister.php new file mode 100644 index 0000000..a04370a --- /dev/null +++ b/src/TypePersister.php @@ -0,0 +1,122 @@ +config = $config; + $this->client = $client; + } + + /** + * Iterates over all of the configured types and posts each to the remote API + */ + public function upload(): void + { + $types = $this->config->types(); + + foreach ($types as $type) { + $this->uploadType($type); + } + } + + /** + * Reads the content of the json file on disk for the given spec and POSTs it to the remote API + */ + public function uploadType(Spec $type): void + { + $directory = $this->config->distDirectory(); + $path = sprintf('%s%s%s', $directory, DIRECTORY_SEPARATOR, $type->source()); + Assert::fileExists($path); + Assert::readable($path); + $fileContent = file_get_contents($path); + + // A round trip encode(decode) ensures consistent whitespace for equality checks: + $json = Json::encodeArray(Json::decodeToArray($fileContent)); + + $definition = Definition::new( + $type->id(), + $type->name(), + $type->repeatable(), + true, + $json + ); + + $this->client->saveDefinition($definition); + } + + public function download(bool $writeIndex = true, bool $skipDisabled = true): void + { + $types = $this->client->fetchAllDefinitions(); + $specs = []; + + foreach ($types as $type) { + if ($skipDisabled && ! $type->isActive()) { + continue; + } + + $this->writeDefinition($type); + $specs[] = Spec::new( + $type->id(), + $type->label(), + $type->isRepeatable() + ); + } + + if (! $writeIndex) { + return; + } + + $indexPath = sprintf('%s%sindex.json', $this->config->distDirectory(), DIRECTORY_SEPARATOR); + file_put_contents($indexPath, json_encode($specs, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + } + + /** + * @throws DefinitionNotFound if the requested id does not exist. + */ + public function downloadType(string $id): void + { + $definition = $this->client->getDefinition($id); + $this->writeDefinition($definition); + } + + private function writeDefinition(Definition $definition): void + { + $destination = sprintf( + '%s%s%s.json', + $this->config->distDirectory(), + DIRECTORY_SEPARATOR, + $definition->id() + ); + + $jsonString = json_encode(Json::decodeToArray($definition->json()), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); + + file_put_contents($destination, $jsonString); + } +} diff --git a/test/Unit/Container/CustomTypeClientFactoryTest.php b/test/Unit/Container/CustomTypeClientFactoryTest.php new file mode 100644 index 0000000..fd03af8 --- /dev/null +++ b/test/Unit/Container/CustomTypeClientFactoryTest.php @@ -0,0 +1,81 @@ +container = $this->createMock(ContainerInterface::class); + $this->factory = new CustomTypeClientFactory(); + } + + public function testThatAnExceptionIsThrownWhenConfigurationDoesNotContainAToken(): void + { + $this->container->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn([]); + + $this->expectException(AssertionFailed::class); + $this->expectExceptionMessage('config.primo.custom-type-api.token'); + + ($this->factory)($this->container); + } + + public function testThatAnExceptionIsThrownWhenConfigurationDoesNotContainARepositoryName(): void + { + $this->container->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn([ + 'primo' => [ + 'custom-type-api' => [ + 'token' => 'Foo', + ], + ], + ]); + + $this->expectException(AssertionFailed::class); + $this->expectExceptionMessage('config.primo.custom-type-api.repository'); + + ($this->factory)($this->container); + } + + public function testThatAClientCanBeReturnedWhenTheContainerDoesNotHaveAnyHttpComponents(): void + { + $this->container->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn([ + 'primo' => [ + 'custom-type-api' => [ + 'token' => 'Foo', + 'repository' => 'Bar', + ], + ], + ]); + + $this->container->expects(self::exactly(4)) + ->method('has') + ->willReturn(false); + + $client = ($this->factory)($this->container); + self::assertInstanceOf(Client::class, $client); + } +} From de0433572db54fc9e1fbbdc9c474ceca74029489 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 08:32:48 +0100 Subject: [PATCH 02/18] Add a diff formatter for console commands, borrowed from various sources --- src/Console/ConsoleColourDiffFormatter.php | 98 +++++++++++++++++++ .../ConsoleColourDiffFormatterTest.php | 53 ++++++++++ 2 files changed, 151 insertions(+) create mode 100644 src/Console/ConsoleColourDiffFormatter.php create mode 100644 test/Unit/Console/ConsoleColourDiffFormatterTest.php diff --git a/src/Console/ConsoleColourDiffFormatter.php b/src/Console/ConsoleColourDiffFormatter.php new file mode 100644 index 0000000..d96bd60 --- /dev/null +++ b/src/Console/ConsoleColourDiffFormatter.php @@ -0,0 +1,98 @@ +template = << ---------- begin diff ---------- + %s + ----------- end diff ----------- + + TEXT; + } + + public function format(string $diff): string + { + return $this->formatWithTemplate($diff, $this->template); + } + + private function formatWithTemplate(string $diff, string $template): string + { + $escapedDiff = OutputFormatter::escape(rtrim($diff)); + + $escapedDiffLines = preg_split(self::NEWLINES_REGEX, $escapedDiff); + + // remove description of added + remove; obvious on diffs + foreach ($escapedDiffLines as $key => $escapedDiffLine) { + if ($escapedDiffLine === '--- Original') { + unset($escapedDiffLines[$key]); + } + if ($escapedDiffLine === '+++ New') { + unset($escapedDiffLines[$key]); + } + } + + $coloredLines = array_map(function (string $string): string { + $string = $this->makePlusLinesGreen($string); + $string = $this->makeMinusLinesRed($string); + $string = $this->makeAtNoteCyan($string); + + if ($string === ' ') { + return ''; + } + + return $string; + }, $escapedDiffLines); + + return sprintf($template, implode(PHP_EOL, $coloredLines)); + } + + private function makePlusLinesGreen(string $string): string + { + return (string) preg_replace( + self::PLUS_START_REGEX, + '$1', + $string + ); + } + + private function makeMinusLinesRed(string $string): string + { + return (string) preg_replace( + self::MINUS_START_REGEX, + '$1', + $string + ); + } + + private function makeAtNoteCyan(string $string): string + { + return (string) preg_replace( + self::AT_START_REGEX, + '$1', + $string + ); + } +} diff --git a/test/Unit/Console/ConsoleColourDiffFormatterTest.php b/test/Unit/Console/ConsoleColourDiffFormatterTest.php new file mode 100644 index 0000000..bf4ad11 --- /dev/null +++ b/test/Unit/Console/ConsoleColourDiffFormatterTest.php @@ -0,0 +1,53 @@ +diff($left, $right); + + $formatter = new ConsoleColourDiffFormatter(); + + $expect = << ---------- begin diff ---------- + @@ @@ + Mary had + a little + -lamb, it's + +goat, it's + fleece was white + -as snow + +as flour. + ----------- end diff ----------- + + DIFF; + + + self::assertEquals($expect, $formatter->format($diff)); + } +} From 67c9fb9b5ca517a9d345b889b9c44943710627e2 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 08:34:39 +0100 Subject: [PATCH 03/18] Adds a tool to diff the json payload of two type definitions --- composer.json | 1 + src/Container/DiffToolFactory.php | 19 ++++++++++++ src/DiffTool.php | 44 +++++++++++++++++++++++++++ test/Unit/DiffToolTest.php | 50 +++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 src/Container/DiffToolFactory.php create mode 100644 src/DiffTool.php create mode 100644 test/Unit/DiffToolTest.php diff --git a/composer.json b/composer.json index 824b209..5b7d562 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "psr/http-client-implementation": "*", "psr/http-factory": "^1.0", "psr/http-message": "^1.0", + "sebastian/diff": "^4.0", "symfony/console": "^5.1", "webmozart/assert": "^1.10" }, diff --git a/src/Container/DiffToolFactory.php b/src/Container/DiffToolFactory.php new file mode 100644 index 0000000..e5b06ae --- /dev/null +++ b/src/Container/DiffToolFactory.php @@ -0,0 +1,19 @@ +differ = $differ; + } + + public function diff(Definition $left, Definition $right): ?string + { + $left = $this->prettyPrint($left); + $right = $this->prettyPrint($right); + + if ($left === $right) { + return null; + } + + return $this->differ->diff($left, $right); + } + + private function prettyPrint(Definition $definition): string + { + return json_encode( + json_decode($definition->json(), true, 512, JSON_THROW_ON_ERROR), + JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT + ); + } +} diff --git a/test/Unit/DiffToolTest.php b/test/Unit/DiffToolTest.php new file mode 100644 index 0000000..6bcc1d4 --- /dev/null +++ b/test/Unit/DiffToolTest.php @@ -0,0 +1,50 @@ +tool = new DiffTool(new Differ()); + } + + public function testThatTheDifferencesCanBeDiffed(): void + { + $left = Definition::new('foo', 'bar', true, true, '{"foo":["a","b","c"]}'); + $right = Definition::new('foo', 'bar', true, true, '{"foo":["a","b","d"]}'); + + $diff = $this->tool->diff($left, $right); + + $expect = <<tool->diff($left, $left); + self::assertNull($diff); + } +} From deab79e8818da72824ff37eb8e0b61c90fcf9705 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 08:42:53 +0100 Subject: [PATCH 04/18] Adds a persistence abstraction for Definitions and implementations for local and remote --- src/Type/LocalPersistence.php | 111 +++++++++++++++++++++++++++++++++ src/Type/RemotePersistence.php | 46 ++++++++++++++ src/Type/TypePersistence.php | 32 ++++++++++ 3 files changed, 189 insertions(+) create mode 100644 src/Type/LocalPersistence.php create mode 100644 src/Type/RemotePersistence.php create mode 100644 src/Type/TypePersistence.php diff --git a/src/Type/LocalPersistence.php b/src/Type/LocalPersistence.php new file mode 100644 index 0000000..0ffd157 --- /dev/null +++ b/src/Type/LocalPersistence.php @@ -0,0 +1,111 @@ +config = $config; + } + + public function has(string $id): bool + { + try { + $this->getSpec($id); + + return true; + } catch (InvalidArgument $e) { + return false; + } + } + + public function read(string $id): Definition + { + $spec = $this->getSpec($id); + $path = $this->path($spec); + Assert::fileExists($path); + Assert::readable($path); + + return Definition::new( + $spec->id(), + $spec->name(), + $spec->repeatable(), + true, + file_get_contents($path) + ); + } + + public function write(Definition $definition): void + { + $spec = $this->has($definition->id()) + ? $this->getSpec($definition->id()) + : $this->specFromDefinition($definition); + + $path = $this->path($spec); + file_put_contents($path, $definition->json()); + } + + private function path(Spec $spec): string + { + return sprintf( + '%s%s%s', + $this->config->distDirectory(), + DIRECTORY_SEPARATOR, + $spec->filename() + ); + } + + private function getSpec(string $id): Spec + { + foreach ($this->config->types() as $type) { + if ($type->id() !== $id) { + continue; + } + + return $type; + } + + throw new InvalidArgument(sprintf('The type "%s" is not locally defined', $id)); + } + + private function specFromDefinition(Definition $definition): Spec + { + return Spec::new( + $definition->id(), + $definition->label(), + $definition->isRepeatable() + ); + } + + /** @inheritDoc */ + public function all(): iterable + { + $types = $this->config->types(); + $types = $types instanceof Traversable ? iterator_to_array($types): $types; + + return array_map(function (Spec $spec): Definition { + return $this->read($spec->id()); + }, $types); + } +} diff --git a/src/Type/RemotePersistence.php b/src/Type/RemotePersistence.php new file mode 100644 index 0000000..59bb82d --- /dev/null +++ b/src/Type/RemotePersistence.php @@ -0,0 +1,46 @@ +client = $client; + } + + public function has(string $id): bool + { + try { + $this->client->getDefinition($id); + + return true; + } catch (DefinitionNotFound $error) { + return false; + } + } + + public function read(string $id): Definition + { + return $this->client->getDefinition($id); + } + + public function write(Definition $definition): void + { + $this->client->saveDefinition($definition); + } + + public function all(): iterable + { + return $this->client->fetchAllDefinitions(); + } +} diff --git a/src/Type/TypePersistence.php b/src/Type/TypePersistence.php new file mode 100644 index 0000000..4ef7564 --- /dev/null +++ b/src/Type/TypePersistence.php @@ -0,0 +1,32 @@ + + */ + public function all(): iterable; +} From dd8d8fb66c08ae0d414e9b0b25a28293e33e7813 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:01:57 +0100 Subject: [PATCH 05/18] Add factories for persistence implementations --- src/CustomTypeApiConfigProvider.php | 4 ++++ src/Type/Container/LocalPersistenceFactory.php | 17 +++++++++++++++++ src/Type/Container/RemotePersistenceFactory.php | 17 +++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/Type/Container/LocalPersistenceFactory.php create mode 100644 src/Type/Container/RemotePersistenceFactory.php diff --git a/src/CustomTypeApiConfigProvider.php b/src/CustomTypeApiConfigProvider.php index ecb51b9..84ddcfa 100644 --- a/src/CustomTypeApiConfigProvider.php +++ b/src/CustomTypeApiConfigProvider.php @@ -38,6 +38,10 @@ private function dependencies(): array Console\DownloadCommand::class => Console\Container\DownloadCommandFactory::class, Console\UploadCommand::class => Console\Container\UploadCommandFactory::class, + Type\LocalPersistence::class => Type\Container\LocalPersistenceFactory::class, + Type\RemotePersistence::class => Type\Container\RemotePersistenceFactory::class, + + DiffTool::class => Container\DiffToolFactory::class, TypePersister::class => Container\TypePersisterFactory::class, ], 'aliases' => [ diff --git a/src/Type/Container/LocalPersistenceFactory.php b/src/Type/Container/LocalPersistenceFactory.php new file mode 100644 index 0000000..9e83198 --- /dev/null +++ b/src/Type/Container/LocalPersistenceFactory.php @@ -0,0 +1,17 @@ +get(BuildConfig::class)); + } +} diff --git a/src/Type/Container/RemotePersistenceFactory.php b/src/Type/Container/RemotePersistenceFactory.php new file mode 100644 index 0000000..f5d013e --- /dev/null +++ b/src/Type/Container/RemotePersistenceFactory.php @@ -0,0 +1,17 @@ +get(Client::class)); + } +} From e0502c36b3297af96de0078af3a61293f15def65 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:02:40 +0100 Subject: [PATCH 06/18] Initial rev of a diff command for the console --- src/Console/Container/DiffCommandFactory.php | 25 ++++ src/Console/DiffCommand.php | 121 +++++++++++++++++++ src/CustomTypeApiConfigProvider.php | 2 + 3 files changed, 148 insertions(+) create mode 100644 src/Console/Container/DiffCommandFactory.php create mode 100644 src/Console/DiffCommand.php diff --git a/src/Console/Container/DiffCommandFactory.php b/src/Console/Container/DiffCommandFactory.php new file mode 100644 index 0000000..3524daa --- /dev/null +++ b/src/Console/Container/DiffCommandFactory.php @@ -0,0 +1,25 @@ +get(DiffTool::class), + new ConsoleColourDiffFormatter(), + $container->get(LocalPersistence::class), + $container->get(RemotePersistence::class), + ); + } +} diff --git a/src/Console/DiffCommand.php b/src/Console/DiffCommand.php new file mode 100644 index 0000000..ea0e93d --- /dev/null +++ b/src/Console/DiffCommand.php @@ -0,0 +1,121 @@ +diffTool = $diffTool; + $this->formatter = $formatter; + $this->local = $localStorage; + $this->remote = $remoteStorage; + parent::__construct($name); + } + + protected function configure(): void + { + $this->setDescription('Diff local changes to document models against the remote versions'); + $this->setHelp( + 'This command iterates over all your configured Prismic types and displays a unified diff ' . PHP_EOL . + 'between the local version and the version on the remote.' . PHP_EOL . + 'You can optionally provide a single type identifier to diff just one of the configured types.' . PHP_EOL . + 'The command returns 0 if there are no changes and 1 if there are differences between local and remote.' + ); + + $this->addArgument('type', InputArgument::OPTIONAL, 'An individual type identifier to diff', null); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $type = $input->getArgument('type'); + $types = is_string($type) + ? [$this->local->read($type)] + : $this->local->all(); + + $returnValue = self::SUCCESS; + + foreach ($types as $local) { + $style->section(sprintf('Changes in %s.json', $local->id())); + + if (! $this->remote->has($local->id())) { + $this->newLocalType($local, $style); + $returnValue = self::FAILURE; + + continue; + } + + $remote = $this->remote->read($local->id()); + + + $diff = $this->diffTool->diff($local, $remote); + if ($diff === null) { + $this->identical($local, $style); + + continue; + } + + $this->formatDiff($diff, $style); + $returnValue = self::FAILURE; + } + + return $returnValue; + } + + private function newLocalType(Definition $type, SymfonyStyle $style): void + { + $style->info(sprintf( + "%s.json is not present in the remote API", + $type->id() + )); + } + + private function identical(Definition $local, SymfonyStyle $style): void + { + $style->info(sprintf( + "%s.json is unchanged", + $local->id() + )); + } + + private function formatDiff(string $diff, SymfonyStyle $style): void + { + + $style->write( + $this->formatter->format($diff) + ); + } +} diff --git a/src/CustomTypeApiConfigProvider.php b/src/CustomTypeApiConfigProvider.php index 84ddcfa..dd91841 100644 --- a/src/CustomTypeApiConfigProvider.php +++ b/src/CustomTypeApiConfigProvider.php @@ -35,6 +35,7 @@ private function dependencies(): array 'factories' => [ Prismic\DocumentType\BaseClient::class => Container\CustomTypeClientFactory::class, + Console\DiffCommand::class => Console\Container\DiffCommandFactory::class, Console\DownloadCommand::class => Console\Container\DownloadCommandFactory::class, Console\UploadCommand::class => Console\Container\UploadCommandFactory::class, @@ -54,6 +55,7 @@ private function dependencies(): array private function commands(): array { return [ + Console\DiffCommand::DEFAULT_NAME => Console\DiffCommand::class, Console\DownloadCommand::DEFAULT_NAME => Console\DownloadCommand::class, Console\UploadCommand::DEFAULT_NAME => Console\UploadCommand::class, ]; From ec51963714594e1e81dc394917cea845fe6427d8 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:13:48 +0100 Subject: [PATCH 07/18] Simplify the upload and download commands to use the persistence implementations --- example/upload-download.php | 23 +++++-- .../Container/DownloadCommandFactory.php | 8 +-- .../Container/UploadCommandFactory.php | 8 +-- src/Console/DownloadCommand.php | 62 +++++++------------ src/Console/UploadCommand.php | 61 +++++------------- 5 files changed, 64 insertions(+), 98 deletions(-) diff --git a/example/upload-download.php b/example/upload-download.php index b8ce0bb..aa97208 100644 --- a/example/upload-download.php +++ b/example/upload-download.php @@ -11,10 +11,15 @@ use Http\Discovery\Psr17FactoryDiscovery; use Http\Discovery\Psr18ClientDiscovery; use Primo\Cli\BuildConfig; +use Primo\Cli\Console\ConsoleColourDiffFormatter; +use Primo\Cli\Console\DiffCommand; use Primo\Cli\Console\DownloadCommand; use Primo\Cli\Console\UploadCommand; -use Primo\Cli\TypePersister; +use Primo\Cli\DiffTool; +use Primo\Cli\Type\LocalPersistence; +use Primo\Cli\Type\RemotePersistence; use Prismic\DocumentType\BaseClient; +use SebastianBergmann\Diff\Differ; use Symfony\Component\Console\Application; $repo = getenv('PRISMIC_REPOSITORY'); @@ -70,11 +75,19 @@ /** * Build config will throw exceptions if there are any configuration problems. */ -$config = BuildConfig::withArraySpecs($source, $dist, []); -$persister = new TypePersister($config, $client); +$config = BuildConfig::withArraySpecs($source, $dist, $types); + +$localStorage = new LocalPersistence($config); +$remoteStorage = new RemotePersistence($client); $application = new Application('Primo Upload and Download Example'); -$application->add(new DownloadCommand($persister, $config)); -$application->add(new UploadCommand($persister, $config)); +$application->add(new DownloadCommand($localStorage, $remoteStorage)); +$application->add(new UploadCommand($localStorage, $remoteStorage)); +$application->add(new DiffCommand( + new DiffTool(new Differ()), + new ConsoleColourDiffFormatter(), + $localStorage, + $remoteStorage +)); return $application->run(); diff --git a/src/Console/Container/DownloadCommandFactory.php b/src/Console/Container/DownloadCommandFactory.php index 6cabb1a..2f15b78 100644 --- a/src/Console/Container/DownloadCommandFactory.php +++ b/src/Console/Container/DownloadCommandFactory.php @@ -4,9 +4,9 @@ namespace Primo\Cli\Console\Container; -use Primo\Cli\BuildConfig; use Primo\Cli\Console\DownloadCommand; -use Primo\Cli\TypePersister; +use Primo\Cli\Type\LocalPersistence; +use Primo\Cli\Type\RemotePersistence; use Psr\Container\ContainerInterface; final class DownloadCommandFactory @@ -14,8 +14,8 @@ final class DownloadCommandFactory public function __invoke(ContainerInterface $container): DownloadCommand { return new DownloadCommand( - $container->get(TypePersister::class), - $container->get(BuildConfig::class) + $container->get(LocalPersistence::class), + $container->get(RemotePersistence::class) ); } } diff --git a/src/Console/Container/UploadCommandFactory.php b/src/Console/Container/UploadCommandFactory.php index f959687..8d5f52c 100644 --- a/src/Console/Container/UploadCommandFactory.php +++ b/src/Console/Container/UploadCommandFactory.php @@ -4,9 +4,9 @@ namespace Primo\Cli\Console\Container; -use Primo\Cli\BuildConfig; use Primo\Cli\Console\UploadCommand; -use Primo\Cli\TypePersister; +use Primo\Cli\Type\LocalPersistence; +use Primo\Cli\Type\RemotePersistence; use Psr\Container\ContainerInterface; final class UploadCommandFactory @@ -14,8 +14,8 @@ final class UploadCommandFactory public function __invoke(ContainerInterface $container): UploadCommand { return new UploadCommand( - $container->get(TypePersister::class), - $container->get(BuildConfig::class), + $container->get(LocalPersistence::class), + $container->get(RemotePersistence::class), ); } } diff --git a/src/Console/DownloadCommand.php b/src/Console/DownloadCommand.php index b352428..3fbbcc8 100644 --- a/src/Console/DownloadCommand.php +++ b/src/Console/DownloadCommand.php @@ -4,16 +4,14 @@ namespace Primo\Cli\Console; -use Primo\Cli\BuildConfig; -use Primo\Cli\TypePersister; -use Prismic\DocumentType\Exception\DefinitionNotFound; +use Primo\Cli\Type\TypePersistence; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use function is_string; use function sprintf; use const PHP_EOL; @@ -22,59 +20,41 @@ final class DownloadCommand extends Command { public const DEFAULT_NAME = 'primo:download'; - /** @var TypePersister */ - private $persister; - /** @var BuildConfig */ - private $config; + /** @var TypePersistence */ + private $local; + /** @var TypePersistence */ + private $remote; - public function __construct(TypePersister $uploader, BuildConfig $config, string $name = self::DEFAULT_NAME) + public function __construct(TypePersistence $local, TypePersistence $remote, string $name = self::DEFAULT_NAME) { - $this->persister = $uploader; - $this->config = $config; + $this->local = $local; + $this->remote = $remote; parent::__construct($name); } protected function configure(): void { $this->setDescription('Download one or all document type definitions'); - $this->setHelp(sprintf( + $this->setHelp( 'This command iterates over all your configured Prismic types and downloads them to the ' . PHP_EOL . - 'configured `dist` directory (%s).' . PHP_EOL . - 'You can optionally provide a single type identifier to download just one of the configured types.', - $this->config->distDirectory() - )); + 'configured `dist` directory.' . PHP_EOL . + 'You can optionally provide a single type identifier to download just one of the configured types.' + ); $this->addArgument('type', InputArgument::OPTIONAL, 'An individual type identifier to download', null); - $this->addOption('no-index', null, InputOption::VALUE_NONE, 'Disable index.json generation'); - $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Include disabled types'); } protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); - $single = $input->getArgument('type'); - if ($single !== null) { - return $this->downloadOne($single, $style); - } - - $writeIndex = $input->getOption('no-index') === false; - $skipDisabled = $input->getOption('all') !== false; - - $this->persister->download($writeIndex, $skipDisabled); - - $style->success('Types definitions saved'); - - return self::SUCCESS; - } - - private function downloadOne(string $single, SymfonyStyle $style): int - { - try { - $this->persister->downloadType($single); - } catch (DefinitionNotFound $error) { - $style->error(sprintf('The type "%s" does not exist', $single)); - - return self::FAILURE; + $type = $input->getArgument('type'); + $types = is_string($type) + ? [$this->remote->read($type)] + : $this->remote->all(); + + foreach ($types as $type) { + $style->comment(sprintf('Writing "%s"', $type->label())); + $this->local->write($type); } return self::SUCCESS; diff --git a/src/Console/UploadCommand.php b/src/Console/UploadCommand.php index e20cbe8..6911160 100644 --- a/src/Console/UploadCommand.php +++ b/src/Console/UploadCommand.php @@ -4,17 +4,14 @@ namespace Primo\Cli\Console; -use Primo\Cli\BuildConfig; -use Primo\Cli\Exception\InvalidArgument; -use Primo\Cli\Type\Spec; -use Primo\Cli\TypePersister; +use Primo\Cli\Type\TypePersistence; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use function count; +use function is_string; use function sprintf; use const PHP_EOL; @@ -23,15 +20,15 @@ final class UploadCommand extends Command { public const DEFAULT_NAME = 'primo:upload'; - /** @var TypePersister */ - private $uploader; - /** @var BuildConfig */ - private $config; + /** @var TypePersistence */ + private $local; + /** @var TypePersistence */ + private $remote; - public function __construct(TypePersister $uploader, BuildConfig $config, string $name = self::DEFAULT_NAME) + public function __construct(TypePersistence $local, TypePersistence $remote, string $name = self::DEFAULT_NAME) { - $this->uploader = $uploader; - $this->config = $config; + $this->local = $local; + $this->remote = $remote; parent::__construct($name); } @@ -50,41 +47,17 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); - $specs = $this->resolveTypes($input->getArgument('type')); - $style->progressStart(count($specs)); - foreach ($specs as $spec) { - $this->uploader->uploadType($spec); - } - - $style->progressFinish(); - - $style->success(sprintf( - '%d document types uploaded', - count($specs) - )); - - return self::SUCCESS; - } + $type = $input->getArgument('type'); + $types = is_string($type) + ? [$this->local->read($type)] + : $this->local->all(); - /** - * @return array - */ - private function resolveTypes(?string $type): array - { - $specs = $this->config->types(); - if ($type === null) { - return $specs; + foreach ($types as $type) { + $style->comment(sprintf('Uploading "%s"', $type->label())); + $this->remote->write($type); } - foreach ($specs as $spec) { - if ($spec->id() !== $type) { - continue; - } - - return [$spec]; - } - - throw new InvalidArgument(sprintf('The type "%s" is not a known type', $type)); + return self::SUCCESS; } } From c7cd32578ee3a0200bfcb960f826f8d6b5ad680c Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:15:33 +0100 Subject: [PATCH 08/18] Delete and its factory --- src/Container/TypePersisterFactory.php | 21 ----- src/CustomTypeApiConfigProvider.php | 1 - src/TypePersister.php | 122 ------------------------- 3 files changed, 144 deletions(-) delete mode 100644 src/Container/TypePersisterFactory.php delete mode 100644 src/TypePersister.php diff --git a/src/Container/TypePersisterFactory.php b/src/Container/TypePersisterFactory.php deleted file mode 100644 index 94ad1e1..0000000 --- a/src/Container/TypePersisterFactory.php +++ /dev/null @@ -1,21 +0,0 @@ -get(BuildConfig::class), - $container->get(Client::class) - ); - } -} diff --git a/src/CustomTypeApiConfigProvider.php b/src/CustomTypeApiConfigProvider.php index dd91841..b41a31b 100644 --- a/src/CustomTypeApiConfigProvider.php +++ b/src/CustomTypeApiConfigProvider.php @@ -43,7 +43,6 @@ private function dependencies(): array Type\RemotePersistence::class => Type\Container\RemotePersistenceFactory::class, DiffTool::class => Container\DiffToolFactory::class, - TypePersister::class => Container\TypePersisterFactory::class, ], 'aliases' => [ Prismic\DocumentType\Client::class => Prismic\DocumentType\BaseClient::class, diff --git a/src/TypePersister.php b/src/TypePersister.php deleted file mode 100644 index a04370a..0000000 --- a/src/TypePersister.php +++ /dev/null @@ -1,122 +0,0 @@ -config = $config; - $this->client = $client; - } - - /** - * Iterates over all of the configured types and posts each to the remote API - */ - public function upload(): void - { - $types = $this->config->types(); - - foreach ($types as $type) { - $this->uploadType($type); - } - } - - /** - * Reads the content of the json file on disk for the given spec and POSTs it to the remote API - */ - public function uploadType(Spec $type): void - { - $directory = $this->config->distDirectory(); - $path = sprintf('%s%s%s', $directory, DIRECTORY_SEPARATOR, $type->source()); - Assert::fileExists($path); - Assert::readable($path); - $fileContent = file_get_contents($path); - - // A round trip encode(decode) ensures consistent whitespace for equality checks: - $json = Json::encodeArray(Json::decodeToArray($fileContent)); - - $definition = Definition::new( - $type->id(), - $type->name(), - $type->repeatable(), - true, - $json - ); - - $this->client->saveDefinition($definition); - } - - public function download(bool $writeIndex = true, bool $skipDisabled = true): void - { - $types = $this->client->fetchAllDefinitions(); - $specs = []; - - foreach ($types as $type) { - if ($skipDisabled && ! $type->isActive()) { - continue; - } - - $this->writeDefinition($type); - $specs[] = Spec::new( - $type->id(), - $type->label(), - $type->isRepeatable() - ); - } - - if (! $writeIndex) { - return; - } - - $indexPath = sprintf('%s%sindex.json', $this->config->distDirectory(), DIRECTORY_SEPARATOR); - file_put_contents($indexPath, json_encode($specs, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); - } - - /** - * @throws DefinitionNotFound if the requested id does not exist. - */ - public function downloadType(string $id): void - { - $definition = $this->client->getDefinition($id); - $this->writeDefinition($definition); - } - - private function writeDefinition(Definition $definition): void - { - $destination = sprintf( - '%s%s%s.json', - $this->config->distDirectory(), - DIRECTORY_SEPARATOR, - $definition->id() - ); - - $jsonString = json_encode(Json::decodeToArray($definition->json()), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); - - file_put_contents($destination, $jsonString); - } -} From a2eca0bc3085d47806af1b0cddf24d93838ad0d5 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:15:54 +0100 Subject: [PATCH 09/18] CS Fix --- src/BuildConfig.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BuildConfig.php b/src/BuildConfig.php index e18bc0c..6c97798 100644 --- a/src/BuildConfig.php +++ b/src/BuildConfig.php @@ -42,7 +42,7 @@ private function __construct(string $sourceDirectory, string $distDirectory, ite /** @param Spec[] $types */ public static function with(string $sourceDir, string $distDir, iterable $types): self { - return new static($sourceDir, $distDir, $types); + return new self($sourceDir, $distDir, $types); } /** @@ -68,7 +68,7 @@ public function distDirectory(): string return $this->distDirectory; } - /** @return Spec[] */ + /** @return iterable */ public function types(): iterable { return $this->types; From 6bc57ad4a92863ceefc9423292ca89c29356b29b Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 09:18:04 +0100 Subject: [PATCH 10/18] Update locked deps --- composer.lock | 204 +++++++++++++++++++++++++++----------------------- 1 file changed, 111 insertions(+), 93 deletions(-) diff --git a/composer.lock b/composer.lock index 707a17c..bd0abd0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "86f4457cc13589ef49644bed4d709372", + "content-hash": "aee57d48951184bbc93f99e25a27813a", "packages": [ { "name": "clue/stream-filter", @@ -774,27 +774,22 @@ }, { "name": "psr/container", - "version": "2.0.1", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/2ae37329ee82f91efadc282cc2d527fd6065a5ef", - "reference": "2ae37329ee82f91efadc282cc2d527fd6065a5ef", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -821,9 +816,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.1" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2021-03-24T13:40:57+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/http-client", @@ -985,6 +980,72 @@ }, "time": "2016-08-06T14:39:51+00:00" }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, { "name": "symfony/console", "version": "v5.3.7", @@ -1708,29 +1769,33 @@ }, { "name": "symfony/service-contracts", - "version": "v1.1.2", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0", - "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.5", + "psr/container": "^1.1" }, "suggest": { - "psr/container": "", "symfony/service-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1763,9 +1828,23 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v1.1.2" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, - "time": "2019-05-28T07:50:59+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/string", @@ -3132,12 +3211,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "94cca8d2520d4626036c799109c278548572bdbe" + "reference": "bc10788e8d1fc3c962d92faa2442bd1852c08c03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/94cca8d2520d4626036c799109c278548572bdbe", - "reference": "94cca8d2520d4626036c799109c278548572bdbe", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bc10788e8d1fc3c962d92faa2442bd1852c08c03", + "reference": "bc10788e8d1fc3c962d92faa2442bd1852c08c03", "shasum": "" }, "conflict": { @@ -3167,7 +3246,9 @@ "cartalyst/sentry": "<=2.1.6", "centreon/centreon": "<20.10.7", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", + "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<=3.0.6", + "codiad/codiad": "<=2.8.4", "composer/composer": "<1.10.22|>=2-alpha.1,<2.0.13", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/core": ">=2,<3.5.39", @@ -3245,6 +3326,7 @@ "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", "illuminate/view": ">=7,<7.1.2", "impresscms/impresscms": "<=1.4.2", + "in2code/femanager": "<5.5.1|>=6,<6.3.1", "intelliants/subrion": "<=4.2.1", "ivankristianto/phpwhois": "<=4.3", "james-heinrich/getid3": "<1.9.9", @@ -3258,6 +3340,7 @@ "la-haute-societe/tcpdf": "<6.2.22", "laminas/laminas-http": "<2.14.2", "laravel/framework": "<6.20.26|>=7,<8.40", + "laravel/laravel": "<8.4.4", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", "lavalite/cms": "<=5.8", "league/commonmark": "<0.18.3", @@ -3347,6 +3430,7 @@ "shopware/platform": "<=6.4.3", "shopware/production": "<=6.3.5.2", "shopware/shopware": "<=5.6.9", + "showdoc/showdoc": "<=2.9.8", "silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1", "silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2", "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4", @@ -3446,12 +3530,12 @@ "yiisoft/yii": ">=1.1.14,<1.1.15", "yiisoft/yii2": "<2.0.38", "yiisoft/yii2-bootstrap": "<2.0.4", - "yiisoft/yii2-dev": "<2.0.15", + "yiisoft/yii2-dev": "<2.0.43", "yiisoft/yii2-elasticsearch": "<2.0.5", "yiisoft/yii2-gii": "<2.0.4", "yiisoft/yii2-jui": "<2.0.4", "yiisoft/yii2-redis": "<2.0.8", - "yoast-seo-for-typo3/yoast_seo": "<7.2.1", + "yoast-seo-for-typo3/yoast_seo": "<7.2.3", "yourls/yourls": "<=1.8.1", "zendesk/zendesk_api_client_php": "<2.2.11", "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", @@ -3513,7 +3597,7 @@ "type": "tidelift" } ], - "time": "2021-09-01T09:02:34+00:00" + "time": "2021-09-01T19:02:25+00:00" }, { "name": "sebastian/cli-parser", @@ -3813,72 +3897,6 @@ ], "time": "2020-10-26T15:52:27+00:00" }, - { - "name": "sebastian/diff", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:10:38+00:00" - }, { "name": "sebastian/environment", "version": "5.1.3", From 2c5bae3554f9d315aacecd96998728cac8c403dd Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 10:07:18 +0100 Subject: [PATCH 11/18] Implement a persistence error for read and write failures in persistence and improve error handling in the various commands --- src/Console/DiffCommand.php | 26 ++++++++--- src/Console/DownloadCommand.php | 44 +++++++++++++++--- src/Console/UploadCommand.php | 21 +++++++-- src/Exception/PersistenceError.php | 21 +++++++++ src/Type/LocalPersistence.php | 71 ++++++++++++++++++++++-------- src/Type/RemotePersistence.php | 57 ++++++++++++++++++++++-- src/Type/TypePersistence.php | 27 ++++++++++++ 7 files changed, 229 insertions(+), 38 deletions(-) create mode 100644 src/Exception/PersistenceError.php diff --git a/src/Console/DiffCommand.php b/src/Console/DiffCommand.php index ea0e93d..4a9f70a 100644 --- a/src/Console/DiffCommand.php +++ b/src/Console/DiffCommand.php @@ -5,6 +5,7 @@ namespace Primo\Cli\Console; use Primo\Cli\DiffTool; +use Primo\Cli\Exception\PersistenceError; use Primo\Cli\Type\TypePersistence; use Prismic\DocumentType\Definition; use Symfony\Component\Console\Command\Command; @@ -62,10 +63,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); $type = $input->getArgument('type'); - $types = is_string($type) - ? [$this->local->read($type)] - : $this->local->all(); + try { + $types = is_string($type) + ? [$this->local->read($type)] + : $this->local->all(); + + return $this->showDiff($types, $style); + } catch (PersistenceError $error) { + $style->error('An error occurred reading definitions. Check local types have been built and that credentials are correct for the remote API'); + + return self::FAILURE; + } + } + + /** @param iterable $types */ + private function showDiff(iterable $types, SymfonyStyle $style): int + { $returnValue = self::SUCCESS; foreach ($types as $local) { @@ -80,7 +94,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $remote = $this->remote->read($local->id()); - $diff = $this->diffTool->diff($local, $remote); if ($diff === null) { $this->identical($local, $style); @@ -98,7 +111,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function newLocalType(Definition $type, SymfonyStyle $style): void { $style->info(sprintf( - "%s.json is not present in the remote API", + '%s.json is not present in the remote API', $type->id() )); } @@ -106,14 +119,13 @@ private function newLocalType(Definition $type, SymfonyStyle $style): void private function identical(Definition $local, SymfonyStyle $style): void { $style->info(sprintf( - "%s.json is unchanged", + '%s.json is unchanged', $local->id() )); } private function formatDiff(string $diff, SymfonyStyle $style): void { - $style->write( $this->formatter->format($diff) ); diff --git a/src/Console/DownloadCommand.php b/src/Console/DownloadCommand.php index 3fbbcc8..daa0151 100644 --- a/src/Console/DownloadCommand.php +++ b/src/Console/DownloadCommand.php @@ -4,14 +4,15 @@ namespace Primo\Cli\Console; +use Primo\Cli\Exception\PersistenceError; use Primo\Cli\Type\TypePersistence; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use function is_string; use function sprintf; use const PHP_EOL; @@ -42,19 +43,52 @@ protected function configure(): void ); $this->addArgument('type', InputArgument::OPTIONAL, 'An individual type identifier to download', null); + $this->addOption('no-index', null, InputOption::VALUE_NONE, 'Disable index.json generation'); + $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Include disabled types'); } protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); $type = $input->getArgument('type'); - $types = is_string($type) - ? [$this->remote->read($type)] - : $this->remote->all(); + + try { + $types = $type !== null + ? [$this->remote->read($type)] + : $this->remote->all(); + } catch (PersistenceError $error) { + $style->error($error->getMessage()); + + return self::FAILURE; + } + + $writeIndex = $input->getOption('no-index') === false; + $skipDisabled = $input->getOption('all') !== false; foreach ($types as $type) { + if ($skipDisabled && ! $type->isActive()) { + continue; + } + $style->comment(sprintf('Writing "%s"', $type->label())); - $this->local->write($type); + try { + $this->local->write($type); + } catch (PersistenceError $error) { + $style->error(sprintf('Failed to write "%s" to local storage', $type->label())); + + return self::FAILURE; + } + } + + if ($writeIndex) { + try { + $style->comment('Writing Index'); + $this->local->writeIndex($this->remote->indexSpecs()); + } catch (PersistenceError $error) { + $style->error('Failed to write the index to local storage'); + + return self::FAILURE; + } } return self::SUCCESS; diff --git a/src/Console/UploadCommand.php b/src/Console/UploadCommand.php index 6911160..491416f 100644 --- a/src/Console/UploadCommand.php +++ b/src/Console/UploadCommand.php @@ -4,6 +4,7 @@ namespace Primo\Cli\Console; +use Primo\Cli\Exception\PersistenceError; use Primo\Cli\Type\TypePersistence; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -49,13 +50,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int $style = new SymfonyStyle($input, $output); $type = $input->getArgument('type'); - $types = is_string($type) - ? [$this->local->read($type)] - : $this->local->all(); + try { + $types = is_string($type) + ? [$this->local->read($type)] + : $this->local->all(); + } catch (PersistenceError $error) { + $style->error('Failed to read local type definitions - make sure they have been built first'); + + return self::FAILURE; + } foreach ($types as $type) { $style->comment(sprintf('Uploading "%s"', $type->label())); - $this->remote->write($type); + try { + $this->remote->write($type); + } catch (PersistenceError $error) { + $style->error(sprintf('Upload of "%s" failed', $type->label())); + + return self::FAILURE; + } } return self::SUCCESS; diff --git a/src/Exception/PersistenceError.php b/src/Exception/PersistenceError.php new file mode 100644 index 0000000..2b6604b --- /dev/null +++ b/src/Exception/PersistenceError.php @@ -0,0 +1,21 @@ +getSpec($id); - $path = $this->path($spec); - Assert::fileExists($path); - Assert::readable($path); - - return Definition::new( - $spec->id(), - $spec->name(), - $spec->repeatable(), - true, - file_get_contents($path) - ); + try { + $spec = $this->getSpec($id); + $path = $this->path($spec); + Assert::fileExists($path); + Assert::readable($path); + + return Definition::new( + $spec->id(), + $spec->name(), + $spec->repeatable(), + true, + file_get_contents($path) + ); + } catch (Throwable $error) { + throw PersistenceError::readFailure($error); + } } public function write(Definition $definition): void { - $spec = $this->has($definition->id()) - ? $this->getSpec($definition->id()) - : $this->specFromDefinition($definition); - - $path = $this->path($spec); - file_put_contents($path, $definition->json()); + try { + $spec = $this->has($definition->id()) + ? $this->getSpec($definition->id()) + : $this->specFromDefinition($definition); + + $path = $this->path($spec); + file_put_contents($path, $definition->json()); + } catch (Throwable $error) { + throw PersistenceError::writeFailure($error); + } } private function path(Spec $spec): string @@ -102,10 +115,30 @@ private function specFromDefinition(Definition $definition): Spec public function all(): iterable { $types = $this->config->types(); - $types = $types instanceof Traversable ? iterator_to_array($types): $types; + $types = $types instanceof Traversable ? iterator_to_array($types) : $types; return array_map(function (Spec $spec): Definition { return $this->read($spec->id()); }, $types); } + + /** @inheritDoc */ + public function indexSpecs(): iterable + { + return $this->config->types(); + } + + /** @inheritDoc */ + public function writeIndex(iterable $specs): void + { + $dest = sprintf('%s%s%s', $this->config->distDirectory(), DIRECTORY_SEPARATOR, 'index.json'); + try { + file_put_contents( + $dest, + json_encode($specs, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) + ); + } catch (Throwable $error) { + throw PersistenceError::writeFailure($error); + } + } } diff --git a/src/Type/RemotePersistence.php b/src/Type/RemotePersistence.php index 59bb82d..21ab81c 100644 --- a/src/Type/RemotePersistence.php +++ b/src/Type/RemotePersistence.php @@ -4,9 +4,16 @@ namespace Primo\Cli\Type; +use Primo\Cli\Exception\PersistenceError; use Prismic\DocumentType\Client; use Prismic\DocumentType\Definition; use Prismic\DocumentType\Exception\DefinitionNotFound; +use Throwable; +use Traversable; + +use function array_filter; +use function array_map; +use function iterator_to_array; final class RemotePersistence implements TypePersistence { @@ -26,21 +33,65 @@ public function has(string $id): bool return true; } catch (DefinitionNotFound $error) { return false; + } catch (Throwable $error) { + throw PersistenceError::readFailure($error); } } public function read(string $id): Definition { - return $this->client->getDefinition($id); + try { + return $this->client->getDefinition($id); + } catch (Throwable $error) { + throw PersistenceError::readFailure($error); + } } public function write(Definition $definition): void { - $this->client->saveDefinition($definition); + try { + $this->client->saveDefinition($definition); + } catch (Throwable $error) { + throw PersistenceError::writeFailure($error); + } } + /** @inheritDoc */ public function all(): iterable { - return $this->client->fetchAllDefinitions(); + try { + return $this->client->fetchAllDefinitions(); + } catch (Throwable $error) { + throw PersistenceError::writeFailure($error); + } + } + + /** @inheritDoc */ + public function indexSpecs(): iterable + { + try { + $types = $this->client->fetchAllDefinitions(); + } catch (Throwable $error) { + throw PersistenceError::readFailure($error); + } + + $types = $types instanceof Traversable ? iterator_to_array($types) : $types; + $types = array_filter($types, static function (Definition $definition): bool { + return $definition->isActive(); + }); + + return array_map(static function (Definition $definition): Spec { + return Spec::new( + $definition->id(), + $definition->label(), + $definition->isRepeatable() + ); + }, $types); + } + + /** @inheritDoc */ + public function writeIndex(iterable $specs): void + { + // It is not possible to write an index to remote storage } } diff --git a/src/Type/TypePersistence.php b/src/Type/TypePersistence.php index 4ef7564..2bfbb4c 100644 --- a/src/Type/TypePersistence.php +++ b/src/Type/TypePersistence.php @@ -4,22 +4,29 @@ namespace Primo\Cli\Type; +use Primo\Cli\Exception\PersistenceError; use Prismic\DocumentType\Definition; interface TypePersistence { /** * Whether the type is currently persisted + * + * @throws PersistenceError if a problem occurs querying the underlying storage. */ public function has(string $id): bool; /** * Retrieve the type definition by its id + * + * @throws PersistenceError if a problem occurs reading from the underlying storage. */ public function read(string $id): Definition; /** * Write the definition to storage + * + * @throws PersistenceError if a problem occurs writing to the underlying storage. */ public function write(Definition $definition): void; @@ -27,6 +34,26 @@ public function write(Definition $definition): void; * Retrieve a list of known types * * @return iterable + * + * @throws PersistenceError if a problem occurs reading from the underlying storage. */ public function all(): iterable; + + /** + * Return a list of type specs for creating indexes + * + * @return iterable + * + * @throws PersistenceError if a problem occurs reading from the underlying storage. + */ + public function indexSpecs(): iterable; + + /** + * Write an index file to storage + * + * @param iterable $specs + * + * @throws PersistenceError if a problem occurs writing to the underlying storage. + */ + public function writeIndex(iterable $specs): void; } From 6844a5ce15519d1f81d335d3ce0166335e540be9 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 10:12:21 +0100 Subject: [PATCH 12/18] CS Fixes --- src/Console/ConsoleColourDiffFormatter.php | 17 +++++++++++------ src/DiffTool.php | 1 + .../Console/ConsoleColourDiffFormatterTest.php | 3 +-- test/Unit/DiffToolTest.php | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Console/ConsoleColourDiffFormatter.php b/src/Console/ConsoleColourDiffFormatter.php index d96bd60..7d7e542 100644 --- a/src/Console/ConsoleColourDiffFormatter.php +++ b/src/Console/ConsoleColourDiffFormatter.php @@ -6,11 +6,18 @@ use Symfony\Component\Console\Formatter\OutputFormatter; +use function array_map; +use function implode; use function preg_replace; use function preg_split; +use function rtrim; +use function sprintf; + +use const PHP_EOL; /** * Copy/pasted from: + * * @link https://github.com/symplify/console-color-diff/blob/main/src/Console/Formatter/ColorConsoleDiffFormatter.php */ final class ConsoleColourDiffFormatter @@ -41,17 +48,15 @@ public function format(string $diff): string private function formatWithTemplate(string $diff, string $template): string { $escapedDiff = OutputFormatter::escape(rtrim($diff)); - $escapedDiffLines = preg_split(self::NEWLINES_REGEX, $escapedDiff); // remove description of added + remove; obvious on diffs foreach ($escapedDiffLines as $key => $escapedDiffLine) { - if ($escapedDiffLine === '--- Original') { - unset($escapedDiffLines[$key]); - } - if ($escapedDiffLine === '+++ New') { - unset($escapedDiffLines[$key]); + if ($escapedDiffLine !== '--- Original' && $escapedDiffLine !== '+++ New') { + continue; } + + unset($escapedDiffLines[$key]); } $coloredLines = array_map(function (string $string): string { diff --git a/src/DiffTool.php b/src/DiffTool.php index 9ddcc9c..3bc3285 100644 --- a/src/DiffTool.php +++ b/src/DiffTool.php @@ -10,6 +10,7 @@ use function json_decode; use function json_encode; +use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; final class DiffTool diff --git a/test/Unit/Console/ConsoleColourDiffFormatterTest.php b/test/Unit/Console/ConsoleColourDiffFormatterTest.php index bf4ad11..35835c1 100644 --- a/test/Unit/Console/ConsoleColourDiffFormatterTest.php +++ b/test/Unit/Console/ConsoleColourDiffFormatterTest.php @@ -4,8 +4,8 @@ namespace PrimoTest\Cli\Unit\Console; -use Primo\Cli\Console\ConsoleColourDiffFormatter; use PHPUnit\Framework\TestCase; +use Primo\Cli\Console\ConsoleColourDiffFormatter; use SebastianBergmann\Diff\Differ; class ConsoleColourDiffFormatterTest extends TestCase @@ -47,7 +47,6 @@ public function testSimpleDiff(): void DIFF; - self::assertEquals($expect, $formatter->format($diff)); } } diff --git a/test/Unit/DiffToolTest.php b/test/Unit/DiffToolTest.php index 6bcc1d4..e5d0d69 100644 --- a/test/Unit/DiffToolTest.php +++ b/test/Unit/DiffToolTest.php @@ -4,8 +4,8 @@ namespace PrimoTest\Cli\Unit; -use Primo\Cli\DiffTool; use PHPUnit\Framework\TestCase; +use Primo\Cli\DiffTool; use Prismic\DocumentType\Definition; use SebastianBergmann\Diff\Differ; From 71018a7a7dc5ba656ca6004ada32375a9c52ece9 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 10:22:42 +0100 Subject: [PATCH 13/18] Namespace upload/download/diff default command names --- src/Console/DiffCommand.php | 2 +- src/Console/DownloadCommand.php | 2 +- src/Console/UploadCommand.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Console/DiffCommand.php b/src/Console/DiffCommand.php index 4a9f70a..7165df3 100644 --- a/src/Console/DiffCommand.php +++ b/src/Console/DiffCommand.php @@ -21,7 +21,7 @@ final class DiffCommand extends Command { - public const DEFAULT_NAME = 'primo:diff'; + public const DEFAULT_NAME = 'primo:types:diff'; /** @var ConsoleColourDiffFormatter */ private $formatter; diff --git a/src/Console/DownloadCommand.php b/src/Console/DownloadCommand.php index daa0151..71e6f24 100644 --- a/src/Console/DownloadCommand.php +++ b/src/Console/DownloadCommand.php @@ -19,7 +19,7 @@ final class DownloadCommand extends Command { - public const DEFAULT_NAME = 'primo:download'; + public const DEFAULT_NAME = 'primo:types:download'; /** @var TypePersistence */ private $local; diff --git a/src/Console/UploadCommand.php b/src/Console/UploadCommand.php index 491416f..f668308 100644 --- a/src/Console/UploadCommand.php +++ b/src/Console/UploadCommand.php @@ -19,7 +19,7 @@ final class UploadCommand extends Command { - public const DEFAULT_NAME = 'primo:upload'; + public const DEFAULT_NAME = 'primo:types:upload'; /** @var TypePersistence */ private $local; From f681d7b03a70d26763b2a48108904af921f82d83 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 10:46:06 +0100 Subject: [PATCH 14/18] Update the build command to use local persistence to reduce some code duplication --- example/example.php | 3 +- src/Console/BuildCommand.php | 37 +++++++++---------- src/Console/Container/BuildCommandFactory.php | 6 ++- test/Integration/BuildExamplesTest.php | 3 +- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/example/example.php b/example/example.php index 82ce0c8..b281c26 100644 --- a/example/example.php +++ b/example/example.php @@ -4,6 +4,7 @@ use Primo\Cli\BuildConfig; use Primo\Cli\Console\BuildCommand; +use Primo\Cli\Type\LocalPersistence; use Symfony\Component\Console\Application; require __DIR__ . '/../vendor/autoload.php'; @@ -74,6 +75,6 @@ $config = BuildConfig::withArraySpecs($source, $dist, $types); $application = new Application('Primo Builder Example'); -$application->add(new BuildCommand($config)); +$application->add(new BuildCommand($config, new LocalPersistence($config))); return $application->run(); diff --git a/src/Console/BuildCommand.php b/src/Console/BuildCommand.php index addbdee..4e7d4e8 100644 --- a/src/Console/BuildCommand.php +++ b/src/Console/BuildCommand.php @@ -7,6 +7,8 @@ use Primo\Cli\BuildConfig; use Primo\Cli\Exception\BuildError; use Primo\Cli\Type\Spec; +use Primo\Cli\Type\TypePersistence; +use Prismic\DocumentType\Definition; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,7 +16,6 @@ use Throwable; use function count; -use function file_put_contents; use function is_array; use function json_encode; use function sprintf; @@ -30,16 +31,20 @@ final class BuildCommand extends Command /** @var BuildConfig */ private $config; + /** @var TypePersistence */ + private $localStorage; - public function __construct(BuildConfig $config, string $name = self::DEFAULT_NAME) + public function __construct(BuildConfig $config, TypePersistence $localStorage, string $name = self::DEFAULT_NAME) { $this->config = $config; + $this->localStorage = $localStorage; parent::__construct($name); } protected function configure(): void { - $this->setDescription( + $this->setDescription('Build JSON document models from local PHP Sources'); + $this->setHelp( 'This command iterates over all your configured Prismic types and renders the json into a file ' . 'for each type in the configured output directory.' . PHP_EOL . 'There are no arguments or parameters.' @@ -56,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $style->progressAdvance(1); } - $this->buildIndex(); + $this->localStorage->writeIndex($types); $style->progressAdvance(1); $style->progressFinish(); @@ -65,26 +70,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int count($types) )); - return 0; - } - - private function buildIndex(): void - { - $dest = sprintf('%s%s%s', $this->config->distDirectory(), DIRECTORY_SEPARATOR, 'index.json'); - try { - file_put_contents( - $dest, - json_encode($this->config->types(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) - ); - } catch (Throwable $error) { - throw BuildError::unknown($error); - } + return self::SUCCESS; } private function buildType(Spec $type): void { $source = sprintf('%s%s%s', $this->config->sourceDirectory(), DIRECTORY_SEPARATOR, $type->source()); - $dest = sprintf('%s%s%s', $this->config->distDirectory(), DIRECTORY_SEPARATOR, $type->filename()); try { $data = require $source; @@ -102,6 +93,12 @@ private function buildType(Spec $type): void throw BuildError::unknown($error); } - file_put_contents($dest, $content); + $this->localStorage->write(Definition::new( + $type->id(), + $type->name(), + $type->repeatable(), + true, + $content + )); } } diff --git a/src/Console/Container/BuildCommandFactory.php b/src/Console/Container/BuildCommandFactory.php index a3ec11b..660b874 100644 --- a/src/Console/Container/BuildCommandFactory.php +++ b/src/Console/Container/BuildCommandFactory.php @@ -6,12 +6,16 @@ use Primo\Cli\BuildConfig; use Primo\Cli\Console\BuildCommand; +use Primo\Cli\Type\LocalPersistence; use Psr\Container\ContainerInterface; final class BuildCommandFactory { public function __invoke(ContainerInterface $container): BuildCommand { - return new BuildCommand($container->get(BuildConfig::class)); + return new BuildCommand( + $container->get(BuildConfig::class), + $container->get(LocalPersistence::class) + ); } } diff --git a/test/Integration/BuildExamplesTest.php b/test/Integration/BuildExamplesTest.php index d07fcc9..007b9d7 100644 --- a/test/Integration/BuildExamplesTest.php +++ b/test/Integration/BuildExamplesTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Primo\Cli\BuildConfig; use Primo\Cli\Console\BuildCommand; +use Primo\Cli\Type\LocalPersistence; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArgvInput; @@ -40,7 +41,7 @@ public function testThatNoErrorsOccurWhenProcessingTheExampleConfiguration(): vo ); $application = new Application('Type Builder Example'); - $application->add(new BuildCommand($config)); + $application->add(new BuildCommand($config, new LocalPersistence($config))); $application->setAutoExit(false); $application->setDefaultCommand(BuildCommand::DEFAULT_NAME, true); $application->run(new ArgvInput(['', '-qn'])); From 609aadd6d1eed56587dc7b226e968e934cef2230 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 12:03:38 +0100 Subject: [PATCH 15/18] Improve tests, adding integration tests with the Laminas config aggregator and service manager components --- composer.json | 2 + composer.lock | 358 +++++++++++++++++- src/Type/RemotePersistence.php | 2 +- .../ServiceManagerIntegrationTest.php | 93 +++++ test/Unit/Type/LocalPersistenceTest.php | 143 +++++++ test/Unit/Type/RemotePersistenceTest.php | 153 ++++++++ test/Unit/Type/SpecTest.php | 5 + test/Unit/build-specs/dist/.gitkeep | 0 test/Unit/build-specs/src/example.php | 5 + 9 files changed, 759 insertions(+), 2 deletions(-) create mode 100644 test/Integration/ServiceManagerIntegrationTest.php create mode 100644 test/Unit/Type/LocalPersistenceTest.php create mode 100644 test/Unit/Type/RemotePersistenceTest.php create mode 100644 test/Unit/build-specs/dist/.gitkeep create mode 100644 test/Unit/build-specs/src/example.php diff --git a/composer.json b/composer.json index 5b7d562..169bd7d 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,9 @@ "require-dev": { "ext-curl": "*", "doctrine/coding-standard": "^9.0.0", + "laminas/laminas-config-aggregator": "^1.5", "laminas/laminas-diactoros": "^2.5", + "laminas/laminas-servicemanager": "^3.7", "php-http/curl-client": "^2.2", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index bd0abd0..8c5f702 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aee57d48951184bbc93f99e25a27813a", + "content-hash": "91f4a2eda48cfdd957bcecb9e7a5ee64", "packages": [ { "name": "clue/stream-filter", @@ -1989,6 +1989,85 @@ } ], "packages-dev": [ + { + "name": "brick/varexporter", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/05241f28dfcba2b51b11e2d750e296316ebbe518", + "reference": "05241f28dfcba2b51b11e2d750e296316ebbe518", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5 || ^9.0", + "vimeo/psalm": "4.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.3.5" + }, + "time": "2021-02-10T13:53:07+00:00" + }, + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "support": { + "issues": "https://github.com/container-interop/container-interop/issues", + "source": "https://github.com/container-interop/container-interop/tree/master" + }, + "abandoned": "psr/container", + "time": "2017-02-14T19:40:03+00:00" + }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "v0.7.1", @@ -2183,6 +2262,76 @@ ], "time": "2020-11-10T18:47:58+00:00" }, + { + "name": "laminas/laminas-config-aggregator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config-aggregator.git", + "reference": "c5908c265ada01c8952baf84f102a073de30947f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/c5908c265ada01c8952baf84f102a073de30947f", + "reference": "c5908c265ada01c8952baf84f102a073de30947f", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.3.2", + "laminas/laminas-stdlib": "^2.7.7 || ^3.1.0", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0", + "webimpress/safe-writer": "^1.0.2 || ^2.0.1" + }, + "replace": { + "zendframework/zend-config-aggregator": "^1.2.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "laminas/laminas-config": "^2.6 || ^3.0", + "laminas/laminas-servicemanager": "^2.7.7 || ^3.1.1", + "malukenho/docheader": "^0.1.5", + "phpunit/phpunit": "^9.3", + "psalm/plugin-phpunit": "^0.15.1", + "vimeo/psalm": "^4.6" + }, + "suggest": { + "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", + "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", + "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ConfigAggregator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Lightweight library for collecting and merging configuration from different sources", + "homepage": "https://laminas.dev", + "keywords": [ + "config-aggregator", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config-aggregator/issues", + "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", + "source": "https://github.com/laminas/laminas-config-aggregator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-03-16T14:31:55+00:00" + }, { "name": "laminas/laminas-diactoros", "version": "2.6.0", @@ -2285,6 +2434,154 @@ ], "time": "2021-05-18T14:41:54+00:00" }, + { + "name": "laminas/laminas-servicemanager", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-servicemanager.git", + "reference": "2b0aee477fdbd3191af7c302b93dbc5fda0626f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/2b0aee477fdbd3191af7c302b93dbc5fda0626f4", + "reference": "2b0aee477fdbd3191af7c302b93dbc5fda0626f4", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "laminas/laminas-stdlib": "^3.2.1", + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.3 || ~8.0.0", + "psr/container": "^1.0" + }, + "conflict": { + "laminas/laminas-code": "<3.3.1", + "zendframework/zend-code": "<3.3.1" + }, + "provide": { + "container-interop/container-interop-implementation": "^1.2", + "psr/container-implementation": "^1.0" + }, + "replace": { + "zendframework/zend-servicemanager": "^3.4.0" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.0", + "laminas/laminas-coding-standard": "~2.2.0", + "laminas/laminas-container-config-test": "^0.3", + "laminas/laminas-dependency-plugin": "^2.1.2", + "mikey179/vfsstream": "^1.6.8", + "ocramius/proxy-manager": "^2.2.3", + "phpbench/phpbench": "^1.0.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.4", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.8" + }, + "suggest": { + "ocramius/proxy-manager": "ProxyManager ^2.1.1 to handle lazy initialization of services" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "homepage": "https://laminas.dev", + "keywords": [ + "PSR-11", + "dependency-injection", + "di", + "dic", + "laminas", + "service-manager", + "servicemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-servicemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-servicemanager/issues", + "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", + "source": "https://github.com/laminas/laminas-servicemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-07-24T19:33:07+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "c8ac6a76a133e682acfabc821d4a2ec646934b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/c8ac6a76a133e682acfabc821d4a2ec646934b12", + "reference": "c8ac6a76a133e682acfabc821d4a2ec646934b12", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "conflict": { + "zendframework/zend-stdlib": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.3.0", + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "~9.3.7", + "psalm/plugin-phpunit": "^0.16.0", + "vimeo/psalm": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-08-03T13:40:40+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.10.2", @@ -4663,6 +4960,65 @@ } ], "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" } ], "aliases": [], diff --git a/src/Type/RemotePersistence.php b/src/Type/RemotePersistence.php index 21ab81c..4068049 100644 --- a/src/Type/RemotePersistence.php +++ b/src/Type/RemotePersistence.php @@ -62,7 +62,7 @@ public function all(): iterable try { return $this->client->fetchAllDefinitions(); } catch (Throwable $error) { - throw PersistenceError::writeFailure($error); + throw PersistenceError::readFailure($error); } } diff --git a/test/Integration/ServiceManagerIntegrationTest.php b/test/Integration/ServiceManagerIntegrationTest.php new file mode 100644 index 0000000..81003d5 --- /dev/null +++ b/test/Integration/ServiceManagerIntegrationTest.php @@ -0,0 +1,93 @@ + */ + private function validConfig(): array + { + return [ + 'prismic' => [ + 'api' => 'https://your-repo.cdn.prismic.io/api/v2', + 'token' => null, + ], + 'primo' => [ + 'custom-type-api' => [ + 'token' => 'whatever', + 'repository' => 'Something', + ], + 'cli' => [ + 'builder' => [ + 'source' => __DIR__ . '/../Unit/build-specs/src', + 'dist' => __DIR__ . '/../Unit/build-specs/dist', + ], + ], + 'types' => [ + [ + 'id' => 'example', + 'name' => 'Example', + 'repeatable' => true, + ], + ], + ], + ]; + } + + /** @return array */ + private function mergedConfig(): array + { + $aggregator = new ConfigAggregator([ + ConfigProvider::class, + ApiToolsConfigProvider::class, + CustomTypeApiConfigProvider::class, + new ArrayProvider($this->validConfig()), + ]); + + return $aggregator->getMergedConfig(); + } + + private function serviceManager(): ServiceManager + { + $config = $this->mergedConfig(); + $dependencies = $config['dependencies']; + $dependencies['services']['config'] = $config; + + return new ServiceManager($dependencies); + } + + /** @return Generator */ + public function serviceDataProvider(): Generator + { + $container = $this->serviceManager(); + self::assertTrue($container->has('config')); + $config = $container->get('config'); + $factories = $config['dependencies']['factories'] ?? null; + self::assertIsArray($factories); + $services = array_keys($factories); + + foreach ($services as $serviceId) { + yield $serviceId => [$serviceId, $container]; + } + } + + /** @dataProvider serviceDataProvider */ + public function testThatConfigProvidersCanProduceAllRequiredDependenciesGivenValidConfig(string $serviceId, ServiceManager $container): void + { + self::assertTrue($container->has($serviceId)); + self::assertIsObject($container->get($serviceId)); + } +} diff --git a/test/Unit/Type/LocalPersistenceTest.php b/test/Unit/Type/LocalPersistenceTest.php new file mode 100644 index 0000000..8da7523 --- /dev/null +++ b/test/Unit/Type/LocalPersistenceTest.php @@ -0,0 +1,143 @@ +sourceDir = __DIR__ . '/../build-specs/src'; + $this->distDir = __DIR__ . '/../build-specs/dist'; + $this->config = BuildConfig::with( + $this->sourceDir, + $this->distDir, + [ + Spec::new( + 'example', + 'Example', + true + ), + ] + ); + $this->storage = new LocalPersistence($this->config); + } + + protected function tearDown(): void + { + foreach (glob(sprintf('%s/*.json', realpath($this->distDir))) as $file) { + unlink($file); + } + } + + public function testHasReturnsTrueForAKnownSpec(): void + { + self::assertTrue($this->storage->has('example')); + } + + public function testHasReturnsFalseForUnknownSpec(): void + { + self::assertFalse($this->storage->has('unknown')); + } + + public function testIndexSpecReturnsExpectedList(): void + { + $value = $this->storage->indexSpecs(); + self::assertCount(1, $value); + self::assertContainsOnlyInstancesOf(Spec::class, $value); + } + + public function testThatTheIndexCanBeWritten(): void + { + $target = sprintf('%s/index.json', $this->distDir); + self::assertFileDoesNotExist($target); + $this->storage->writeIndex($this->storage->indexSpecs()); + self::assertFileExists($target); + } + + public function testReadIsExceptionalIfDistIsNotBuilt(): void + { + $this->expectException(PersistenceError::class); + $this->storage->read('example'); + } + + public function testAllIsExceptionalIfDistIsNotBuilt(): void + { + $this->expectException(PersistenceError::class); + $this->storage->all(); + } + + public function testWriteWillCreateAFile(): void + { + $definition = Definition::new( + 'example', + 'Example', + true, + true, + 'Some Content' + ); + + $target = sprintf('%s/example.json', $this->distDir); + self::assertFileDoesNotExist($target); + $this->storage->write($definition); + self::assertFileExists($target); + } + + public function testThatDefinitionsNotFoundInConfigurationCanBeWritten(): void + { + $definition = Definition::new( + 'unknown', + 'Example', + true, + true, + 'Some Content' + ); + + self::assertFalse($this->storage->has('unknown')); + + $target = sprintf('%s/unknown.json', $this->distDir); + self::assertFileDoesNotExist($target); + $this->storage->write($definition); + self::assertFileExists($target); + } + + public function testThatOnceWrittenDefinitionsCanBeRead(): void + { + $input = Definition::new( + 'example', + 'Example', + true, + true, + 'Some Content' + ); + + $this->storage->write($input); + $output = $this->storage->read('example'); + + self::assertNotSame($input, $output); + self::assertTrue($input->equals($output)); + } +} diff --git a/test/Unit/Type/RemotePersistenceTest.php b/test/Unit/Type/RemotePersistenceTest.php new file mode 100644 index 0000000..45fc944 --- /dev/null +++ b/test/Unit/Type/RemotePersistenceTest.php @@ -0,0 +1,153 @@ +client = $this->createMock(Client::class); + $this->storage = new RemotePersistence($this->client); + } + + public function testThatHasReturnsTrueWhenTheTypeExists(): void + { + $this->client->expects(self::once()) + ->method('getDefinition') + ->with('example') + ->willReturn(Definition::new('foo', 'foo', true, true, 'foo')); + + self::assertTrue($this->storage->has('example')); + } + + public function testThatHasReturnsFalseWhenTheTypeDoesNotExist(): void + { + $this->client->expects(self::once()) + ->method('getDefinition') + ->with('example') + ->willThrowException(new DefinitionNotFound('Whut?', 0)); + + self::assertFalse($this->storage->has('example')); + } + + public function testThatHasThrowsPersistenceErrorWhenAnyOtherErrorOccurs(): void + { + $this->expectException(PersistenceError::class); + $this->client->expects(self::once()) + ->method('getDefinition') + ->with('example') + ->willThrowException(new AuthenticationFailed('Whut?', 0)); + + $this->storage->has('example'); + } + + public function testReadThrowsPersistenceErrorWhenTypeIsNotFound(): void + { + $this->expectException(PersistenceError::class); + $this->client->expects(self::once()) + ->method('getDefinition') + ->with('example') + ->willThrowException(new DefinitionNotFound('Whut?', 0)); + $this->storage->read('example'); + } + + public function testThatReadReturnsTheDefinitionWhenTheTypeExists(): void + { + $definition = Definition::new('foo', 'foo', true, true, 'foo'); + $this->client->expects(self::once()) + ->method('getDefinition') + ->with('example') + ->willReturn($definition); + + self::assertSame($definition, $this->storage->read('example')); + } + + public function testThatWriteThrowsPersistenceErrorForAnyFailure(): void + { + $definition = Definition::new('foo', 'foo', true, true, 'foo'); + $this->client->expects(self::once()) + ->method('saveDefinition') + ->with($definition) + ->willThrowException(new AuthenticationFailed('Whut?', 0)); + + $this->expectException(PersistenceError::class); + $this->storage->write($definition); + } + + public function testThatAllThrowsPersistenceErrorForAnyFailure(): void + { + $this->client->expects(self::once()) + ->method('fetchAllDefinitions') + ->willThrowException(new AuthenticationFailed('Whut?', 0)); + + $this->expectException(PersistenceError::class); + $this->storage->all(); + } + + public function testThatIndexSpecsWillReturnAnIterableWithTheExpectedSpec(): void + { + $definition = Definition::new('id', 'label', true, true, 'foo'); + $this->client->expects(self::once()) + ->method('fetchAllDefinitions') + ->willReturn([$definition]); + + $result = $this->storage->indexSpecs(); + self::assertCount(1, $result); + self::assertContainsOnlyInstancesOf(Spec::class, $result); + assert(is_array($result)); + $spec = reset($result); + self::assertEquals('id', $spec->id()); + self::assertEquals('label', $spec->name()); + self::assertTrue($spec->repeatable()); + } + + public function testThatDisabledTypesWillNotBeListedInTheIndex(): void + { + $definitions = [ + Definition::new('id', 'label', true, true, 'foo'), + Definition::new('id2', 'label2', true, false, 'foo'), + ]; + + $this->client->expects(self::once()) + ->method('fetchAllDefinitions') + ->willReturn($definitions); + $result = $this->storage->indexSpecs(); + self::assertCount(1, $result); + assert(is_array($result)); + $spec = reset($result); + self::assertEquals('id', $spec->id()); + } + + public function testThatIndexThrowsPersistenceErrorForAnyFailure(): void + { + $this->client->expects(self::once()) + ->method('fetchAllDefinitions') + ->willThrowException(new AuthenticationFailed('Whut?', 0)); + + $this->expectException(PersistenceError::class); + $this->storage->indexSpecs(); + } +} diff --git a/test/Unit/Type/SpecTest.php b/test/Unit/Type/SpecTest.php index 19ea7f8..8ba5d6e 100644 --- a/test/Unit/Type/SpecTest.php +++ b/test/Unit/Type/SpecTest.php @@ -45,4 +45,9 @@ public function testSerialize(): void $expect = '{"id":"page","name":"Web Page","repeatable":true,"value":"page.json"}'; $this->assertJsonStringEqualsJsonString($expect, json_encode($this->spec)); } + + public function testRepeatable(): void + { + self::assertTrue($this->spec->repeatable()); + } } diff --git a/test/Unit/build-specs/dist/.gitkeep b/test/Unit/build-specs/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/Unit/build-specs/src/example.php b/test/Unit/build-specs/src/example.php new file mode 100644 index 0000000..3f3998c --- /dev/null +++ b/test/Unit/build-specs/src/example.php @@ -0,0 +1,5 @@ + [], +]; From 91e2daf9bc2140db566dd6b2ac8fd5b578a772de Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 12:43:08 +0100 Subject: [PATCH 16/18] Update readme and changelog with information on new functionality --- CHANGELOG.md | 25 +++++++++++++++++++++++++ README.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e513e8..02698df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 0.4.0 - TBD + +### Added + +- `primo:types:download` - A console command that can download all document type definitions to local storage using the Prismic Custom Types API. +- `primo:types:upload` - A console command that can upload local document type definitions to the Prismic Custom Types API. +- `primo:types:diff` - A console command that can show a colour diff between local and remote document type definitions. +- To facilitate the commands above, a new persistence abstraction has been introduced for local and remote storage, including a dependency on [`netglue/prismic-doctype-client`](https://github.com/netglue/prismic-doctype-client). + +### Changed + +- Factories that produce some kind of HTTP API Client, now make use of [`php-http/discovery`](https://github.com/php-http/discovery) for finding dependencies, but still prefer those that have been configured in the container. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + ## 0.3.0 - 2021-03-03 ### Added diff --git a/README.md b/README.md index 8987605..d25dfce 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,36 @@ return [ The lib currently lacks documentation and a decent test suite but there is an annotated example in `./example`. When it comes to configuring as part of a Mezzio app, please examine `./src/ConfigProvider.php` for more information. +### Upload, Download and Diff Document Models Against the Remote Repository + +If you have setup the "Custom Types API" and have a valid access token to use it, adding the following to your configuration along with the contents of `CustomTypeApiConfigProvider` will configure 3 additional commands that will enable you to upload, download and diff changes between your local and remote definitions: + +```php + [ + + // ... + + 'custom-type-api' => [ + 'token' => 'an access token retrieved from repository settings', + 'repository' => 'my-repo', // The repo name such as "my-repo" as opposed to the full url or "my-repo.prismic.io" + ], + + // ... + ], +]; +``` + +Once configured, you can issue + +- `primo:types:download` to download all JSON definitions to your local dist directory, or add a `type` argument to download just one of them. +- `primo:types:upload` to upload locally defined definitions to the remote types api make them immediately available in your repository. Again, a `type` argument will process a single definition. +- `primo:types:diff` will produce colourised diffs in your console showing the changes between local and remote. + +These tools make use of [`netglue/prismic-doctype-client`](https://github.com/netglue/prismic-doctype-client), so check that out if you'd like some more information, also [link to the Prismic Custom Types API Docs](https://prismic.io/docs/technologies/custom-types-api). + ### Commands that Query a Repository Theres also some commands for getting information from a repository. These commands are opt-in. During installation there's a config From 3324ed6dc15f757014aba145a25fb153f242540a Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 12:49:19 +0100 Subject: [PATCH 17/18] CS Fix --- test/Unit/build-specs/src/example.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Unit/build-specs/src/example.php b/test/Unit/build-specs/src/example.php index 3f3998c..89a5960 100644 --- a/test/Unit/build-specs/src/example.php +++ b/test/Unit/build-specs/src/example.php @@ -1,5 +1,7 @@ [], ]; From 9f029c109cf3e26ad38be5c8ba066190137507b5 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 2 Sep 2021 12:52:15 +0100 Subject: [PATCH 18/18] Minimum 0.7 (latest) for the api client - we must have the interface added in 0.2 at least. --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 169bd7d..ccc1f62 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require": { "php": "^7.3||~8.0", "ext-json": "*", - "netglue/prismic-client": "^0", + "netglue/prismic-client": "^0.7", "netglue/prismic-doctype-client": "^0.1.0", "php-http/discovery": "^1.14", "psr/container": "^1.0||^2.0", diff --git a/composer.lock b/composer.lock index 8c5f702..f624647 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "91f4a2eda48cfdd957bcecb9e7a5ee64", + "content-hash": "1265d342c6f8ace1f8e4e25cd44bce41", "packages": [ { "name": "clue/stream-filter",