From 5666702ccce35d15eb329093f2463f628da51508 Mon Sep 17 00:00:00 2001 From: Anthony Axenov Date: Tue, 3 Oct 2023 09:01:37 +0800 Subject: [PATCH 1/2] Error stacktrace with --dev enabled while conversion is in progress Also shorter constants. --- pm-convert | 8 ++-- src/Converters/Abstract/AbstractConverter.php | 8 ++-- src/Converters/Abstract/AbstractRequest.php | 2 +- src/Converters/Curl/CurlRequest.php | 2 +- src/Converters/Http/HttpRequest.php | 2 +- .../Postman20/Postman20Converter.php | 2 +- .../Postman21/Postman21Converter.php | 2 +- src/Converters/Wget/WgetRequest.php | 2 +- src/FileSystem.php | 4 +- src/Processor.php | 41 ++++++++++++------- 10 files changed, 44 insertions(+), 29 deletions(-) diff --git a/pm-convert b/pm-convert index 6a9ae06..cb64cd5 100755 --- a/pm-convert +++ b/pm-convert @@ -2,6 +2,8 @@ convert(); } catch (InvalidArgumentException $e) { - fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL)); - print(implode(PHP_EOL, $processor->usage())); + fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), EOL)); + print(implode(EOL, $processor->usage())); die(1); } catch (Exception $e) { - fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL)); + fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), EOL)); die(1); } diff --git a/src/Converters/Abstract/AbstractConverter.php b/src/Converters/Abstract/AbstractConverter.php index a2672ee..669be45 100644 --- a/src/Converters/Abstract/AbstractConverter.php +++ b/src/Converters/Abstract/AbstractConverter.php @@ -58,7 +58,7 @@ abstract class AbstractConverter implements ConverterContract */ protected function prepareOutputDir(string $outputPath): void { - $outputPath = sprintf('%s%s%s', $outputPath, DIRECTORY_SEPARATOR, static::OUTPUT_DIR); + $outputPath = sprintf('%s%s%s', $outputPath, DS, static::OUTPUT_DIR); $this->outputPath = FileSystem::makeDir($outputPath); } @@ -132,7 +132,7 @@ abstract class AbstractConverter implements ConverterContract static $dir_tree; foreach ($item->item as $subitem) { $dir_tree[] = $item->name; - $path = implode(DIRECTORY_SEPARATOR, $dir_tree); + $path = implode(DS, $dir_tree); if ($this->isItemFolder($subitem)) { $this->convertItem($subitem); } else { @@ -181,9 +181,9 @@ abstract class AbstractConverter implements ConverterContract */ protected function writeRequest(RequestContract $request, string $subpath = null): bool { - $filedir = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $subpath); + $filedir = sprintf('%s%s%s', $this->outputPath, DS, $subpath); $filedir = FileSystem::makeDir($filedir); - $filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $request->getName(), static::FILE_EXT); + $filepath = sprintf('%s%s%s.%s', $filedir, DS, $request->getName(), static::FILE_EXT); $content = $this->interpolate((string)$request); return file_put_contents($filepath, $content) > 0; } diff --git a/src/Converters/Abstract/AbstractRequest.php b/src/Converters/Abstract/AbstractRequest.php index ace779b..7af094e 100644 --- a/src/Converters/Abstract/AbstractRequest.php +++ b/src/Converters/Abstract/AbstractRequest.php @@ -92,7 +92,7 @@ abstract class AbstractRequest implements Stringable, RequestContract */ public function getName(): string { - return str_replace(DIRECTORY_SEPARATOR, '_', $this->name); + return str_replace(DS, '_', $this->name); } /** diff --git a/src/Converters/Curl/CurlRequest.php b/src/Converters/Curl/CurlRequest.php index 3dd06a8..259d807 100644 --- a/src/Converters/Curl/CurlRequest.php +++ b/src/Converters/Curl/CurlRequest.php @@ -78,6 +78,6 @@ class CurlRequest extends AbstractRequest $this->prepareBody() ); $output[] = rtrim(array_pop($output), '\ '); - return implode(PHP_EOL, array_merge($output, [''])); + return implode(EOL, array_merge($output, [''])); } } diff --git a/src/Converters/Http/HttpRequest.php b/src/Converters/Http/HttpRequest.php index 6297429..8a304be 100644 --- a/src/Converters/Http/HttpRequest.php +++ b/src/Converters/Http/HttpRequest.php @@ -69,6 +69,6 @@ class HttpRequest extends AbstractRequest $this->prepareHeaders(), $this->prepareBody() ); - return implode(PHP_EOL, $output); + return implode(EOL, $output); } } diff --git a/src/Converters/Postman20/Postman20Converter.php b/src/Converters/Postman20/Postman20Converter.php index 76adfc4..c068baa 100644 --- a/src/Converters/Postman20/Postman20Converter.php +++ b/src/Converters/Postman20/Postman20Converter.php @@ -57,7 +57,7 @@ class Postman20Converter extends AbstractConverter implements ConverterContract protected function writeCollection(): bool { $filedir = FileSystem::makeDir($this->outputPath); - $filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $this->collection->name(), static::FILE_EXT); + $filepath = sprintf('%s%s%s.%s', $filedir, DS, $this->collection->name(), static::FILE_EXT); return file_put_contents($filepath, $this->collection) > 0; } diff --git a/src/Converters/Postman21/Postman21Converter.php b/src/Converters/Postman21/Postman21Converter.php index 948de30..73f1b5d 100644 --- a/src/Converters/Postman21/Postman21Converter.php +++ b/src/Converters/Postman21/Postman21Converter.php @@ -57,7 +57,7 @@ class Postman21Converter extends AbstractConverter implements ConverterContract protected function writeCollection(): bool { $filedir = FileSystem::makeDir($this->outputPath); - $filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $this->collection->name(), static::FILE_EXT); + $filepath = sprintf('%s%s%s.%s', $filedir, DS, $this->collection->name(), static::FILE_EXT); return file_put_contents($filepath, $this->collection) > 0; } diff --git a/src/Converters/Wget/WgetRequest.php b/src/Converters/Wget/WgetRequest.php index 9613ab1..5d9f5c7 100644 --- a/src/Converters/Wget/WgetRequest.php +++ b/src/Converters/Wget/WgetRequest.php @@ -89,6 +89,6 @@ class WgetRequest extends AbstractRequest $output[] = sprintf("\t%s", $this->getUrl()); } } - return implode(PHP_EOL, array_merge($output, [''])); + return implode(EOL, array_merge($output, [''])); } } diff --git a/src/FileSystem.php b/src/FileSystem.php index 7cd4219..45c8f1a 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -25,7 +25,7 @@ class FileSystem public static function normalizePath(string $path): string { $path = str_replace('~', $_SERVER['HOME'], $path); - return rtrim($path, DIRECTORY_SEPARATOR); + return rtrim($path, DS); } /** @@ -101,7 +101,7 @@ class FileSystem $path = static::normalizePath($path); $records = array_diff(@scandir($path) ?: [], ['.', '..']); foreach ($records as &$record) { - $record = sprintf('%s%s%s', $path, DIRECTORY_SEPARATOR, $record); + $record = sprintf('%s%s%s', $path, DS, $record); } return $records; } diff --git a/src/Processor.php b/src/Processor.php index acd9a29..a8bab6b 100644 --- a/src/Processor.php +++ b/src/Processor.php @@ -78,6 +78,11 @@ class Processor */ protected Environment $env; + /** + * @var bool Flag to output some debug-specific messages + */ + protected bool $devMode = false; + /** * Constructor * @@ -98,7 +103,7 @@ class Processor protected function parseArgs(): void { if (count($this->argv) < 2) { - die(implode(PHP_EOL, $this->usage()) . PHP_EOL); + die(implode(EOL, $this->usage()) . EOL); } foreach ($this->argv as $idx => $arg) { switch ($arg) { @@ -108,7 +113,7 @@ class Processor $normpath = FileSystem::normalizePath($rawpath); if (!FileSystem::isCollectionFile($normpath)) { throw new InvalidArgumentException( - sprintf("not a valid collection:%s\t%s %s", PHP_EOL, $arg, $rawpath) + sprintf("not a valid collection:%s\t%s %s", EOL, $arg, $rawpath) ); } $this->collectionPaths[] = $this->argv[$idx + 1]; @@ -179,11 +184,15 @@ class Processor case '-v': case '--version': - die(implode(PHP_EOL, $this->version()) . PHP_EOL); + die(implode(EOL, $this->version()) . EOL); case '-h': case '--help': - die(implode(PHP_EOL, $this->usage()) . PHP_EOL); + die(implode(EOL, $this->usage()) . EOL); + + case '--dev': + $this->devMode = true; + break; } } if (empty($this->collectionPaths)) { @@ -277,22 +286,26 @@ class Processor $this->initCollections(); $this->initEnv(); $count = count($this->collections); - $current = 0; - $success = 0; - print(implode(PHP_EOL, array_merge($this->version(), $this->copyright())) . PHP_EOL . PHP_EOL); + $current = $success = 0; + print(implode(EOL, array_merge($this->version(), $this->copyright())) . EOL . EOL); foreach ($this->collections as $collectionName => $collection) { ++$current; - printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, PHP_EOL); + printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, EOL); foreach ($this->converters as $type => $exporter) { - printf('> %s%s', strtolower($type), PHP_EOL); - $outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName); + printf('> %s%s', strtolower($type), EOL); + $outputPath = sprintf('%s%s%s', $this->outputPath, DS, $collectionName); if (!empty($this->env)) { $exporter->withEnv($this->env); } - $exporter->convert($collection, $outputPath); - printf(' OK: %s%s', $exporter->getOutputPath(), PHP_EOL); + try { + $exporter->convert($collection, $outputPath); + printf(' OK: %s%s', $exporter->getOutputPath(), EOL); + } catch (Exception $e) { + printf(' ERROR %s: %s%s', $e->getCode(), $e->getMessage(), EOL); + $this->devMode && array_map(static fn ($line) => printf(" %s%s", $line, EOL), $e->getTrace()); + } } - print(PHP_EOL); + print(EOL); ++$success; } unset($this->converters, $type, $exporter, $outputPath, $this->collections, $collectionName, $collection); @@ -315,7 +328,7 @@ class Processor $timeFmt = 'sec'; } $ram = (memory_get_peak_usage(true) - $this->initRam) / 1024 / 1024; - printf("Converted %d/%d in %.2f $timeFmt using up to %.2f MiB RAM%s", $success, $count, $time, $ram, PHP_EOL); + printf("Converted %d/%d in %.2f $timeFmt using up to %.2f MiB RAM%s", $success, $count, $time, $ram, EOL); } /** From 47930b010f9174aa98b525ce3c753da8a7e136e6 Mon Sep 17 00:00:00 2001 From: Anthony Axenov Date: Sun, 7 Jul 2024 23:43:15 +0800 Subject: [PATCH 2/2] Introducing settings file, some refactorings and stabilization --- README.md | 72 +++- composer.json | 3 +- pm-convert | 4 +- src/Collection.php | 77 ++++- src/Converters/Abstract/AbstractConverter.php | 138 ++++---- src/Converters/Abstract/AbstractRequest.php | 31 +- src/Converters/ConvertFormat.php | 33 +- src/Converters/Curl/CurlConverter.php | 6 +- src/Converters/Curl/CurlRequest.php | 2 +- src/Converters/Http/HttpConverter.php | 6 +- src/Converters/Http/HttpRequest.php | 5 +- .../Postman20/Postman20Converter.php | 24 +- .../Postman21/Postman21Converter.php | 16 +- src/Converters/RequestContract.php | 4 +- src/Converters/Wget/WgetConverter.php | 6 +- src/Converters/Wget/WgetRequest.php | 6 +- src/Environment.php | 161 ++++++++- .../IncorrectSettingsFileException.php | 11 + src/FileSystem.php | 17 +- src/Processor.php | 294 ++++++++-------- src/Settings.php | 313 ++++++++++++++++++ tests/AbstractRequestTest.php | 9 +- 22 files changed, 910 insertions(+), 328 deletions(-) create mode 100644 src/Exceptions/IncorrectSettingsFileException.php create mode 100644 src/Settings.php diff --git a/README.md b/README.md index 638e5aa..e80223e 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,17 @@ Without 3rd-party dependencies. These formats are supported for now: `http`, `curl`, `wget`. -> This project was quickly written in my spare time to solve one exact problem in one NDA-project, so it may -> contain stupid errors and (for sure) doesn't cover all possible cases according to collection schema. -> So feel free to propose your improvements. +> This project has been started and quickly written in my spare time to solve one exact problem in one NDA-project, +> so it may contain stupid errors and (for sure) doesn't cover all possible cases according to collection schema. +> Feel free to propose your improvements. + +Versions older than the latest are not supported, only current one is. +If you found an error in old version please ensure if an error you found has been fixed in latest version. +So please always use the latest version of `pm-convert`. ## Supported features -* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json); -* `Bearer` auth; +* collection schemas [**v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json) and [**v2.0**](https://schema.postman.com/json/collection/v2.0.0/collection.json); * replace vars in requests by stored in collection and environment file; * export one or several collections (or even whole directories) into one or all of formats supported at the same time; * all headers (including disabled for `http`-format); @@ -25,7 +28,7 @@ These formats are supported for now: `http`, `curl`, `wget`. ## Planned features - support as many as possible/necessary of authentication kinds (_currently only `Bearer` supported_); -- support as many as possible/necessary of body formats (_currently only `json` and `formdata`_); +- support as many as possible/necessary of body formats (_currently only `json` and `formdata` supported_); - documentation generation support (markdown) with response examples (if present) (#6); - maybe some another convert formats (like httpie or something...); - better logging; @@ -65,6 +68,7 @@ Possible ARGUMENTS: -e, --env - use environment file with variables to replace in requests --var "NAME=VALUE" - force replace specified env variable called NAME with custom VALUE -p, --preserve - do not delete OUTPUT_PATH (if exists) + --dump - convert provided arguments into settings file in `pwd` -h, --help - show this help message and exit -v, --version - show version info and exit @@ -134,6 +138,62 @@ In such case collection itself places in single root object called `collection` So, pm-convert will just raise actual data up on top level and write into disk. +## Settings file + +You may want to specify parameters once and just use them everytime without explicit defining arguments to `pm-convert`. + +This might be done in several ways. + +1. Save this file as `pm-convert-settings.json` in your project directory: + + ```json + { + "directories": [], + "files": [], + "environment": "", + "output": "", + "preserveOutput": false, + "formats": [], + "vars": {} + } + ``` + + Fill it with values you need. + +2. Add `--dump` at the end of your command and all arguments you provided will be converted and saved as + `pm-convert-settings.json` in your curent working directory. For example in `--help` file will contain this: + + ```json + { + "directories": [ + "~/team", + "~/personal" + ], + "files": [ + "~/dir1/first.postman_collection.json", + "~/dir2/second.postman_collection.json" + ], + "environment": "~/localhost.postman_environment.json", + "output": "~/postman_export", + "preserveOutput": false, + "formats": [ + "http", + "curl", + "wget", + "v2.0", + "v2.1" + ], + "vars": { + "myvar": "some value" + } + } + ``` + + If settings file already exists then you will be asked what to do: overwrite it, back it up or exit. + +Once settings file saved in current you can just run `pm-convert`. +Settings will be applied like if you pass them explicitly via arguments. + ## How to implement a new format 1. Create new namespace in `./src/Converters` and name it according to format of your choice. diff --git a/composer.json b/composer.json index fe966c0..dbd39ee 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "require": { "php": "^8.1", "ext-json": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-readline": "*" }, "bin": ["pm-convert"], "autoload": { diff --git a/pm-convert b/pm-convert index cb64cd5..1546575 100755 --- a/pm-convert +++ b/pm-convert @@ -24,10 +24,10 @@ is_null($file) && throw new RuntimeException('Unable to locate autoload.php file $processor = new Processor($argv); try { - $processor->convert(); + $processor->handle(); } catch (InvalidArgumentException $e) { fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), EOL)); - print(implode(EOL, $processor->usage())); + print(implode(EOL, Processor::usage())); die(1); } catch (Exception $e) { fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), EOL)); diff --git a/src/Collection.php b/src/Collection.php index 891419b..1428715 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -4,6 +4,8 @@ declare(strict_types = 1); namespace PmConverter; +use Exception; +use Generator; use JsonException; use Stringable; @@ -16,6 +18,8 @@ use Stringable; */ class Collection implements Stringable { + public readonly CollectionVersion $version; + /** * Closed constructor so that we could use factory methods * @@ -24,10 +28,11 @@ class Collection implements Stringable private function __construct(protected object $json) { // specific case when collection has been exported via postman api - if (isset($json->collection)) { + if (property_exists($json, 'collection')) { $json = $json->collection; } $this->json = $json; + $this->version = $this->detectVersion(); } /** @@ -88,7 +93,7 @@ class Collection implements Stringable * * @return CollectionVersion */ - public function version(): CollectionVersion + protected function detectVersion(): CollectionVersion { return match (true) { str_contains($this->json->info->schema, '/v2.0.') => CollectionVersion::Version20, @@ -96,4 +101,72 @@ class Collection implements Stringable default => CollectionVersion::Unknown }; } + + /** + * Returns the collection version from raw file + * + * @param string $filepath + * @return CollectionVersion + * @throws Exception + */ + public static function detectFileVersion(string $filepath): CollectionVersion + { + $handle = fopen($filepath, 'r'); + if ($handle === false) { + throw new Exception("Cannot open file for reading: $filepath"); + } + $content = ''; + // Postman collection files may be HUGE and I don't need to parse + // them here to find value .info.schema field because normally it + // is stored at the beginning of a file, so if it's not then this + // is a user problem, not mine. + while (\mb_strlen($content) <= 2048) { + $content .= fgets($handle, 50); + if (str_contains($content, 'https://schema.getpostman.com/json/collection')) { + if (str_contains($content, '/v2.0.')) { + return CollectionVersion::Version20; + } + if (str_contains($content, '/v2.1.')) { + return CollectionVersion::Version21; + } + } + } + return CollectionVersion::Unknown; + } + + /** + * Iterates over collection request items and returns item associated by its path in folder + * + * @param mixed|null $item + * @return Generator + */ + public function iterate(mixed $item = null): Generator + { + $is_recursive = !is_null($item); + $folder = $is_recursive ? $item : $this->json; + static $dir_tree; + $path = DS . ($is_recursive ? implode(DS, $dir_tree ?? []) : ''); + foreach ($folder->item as $subitem) { + if ($this->isItemFolder($subitem)) { + $dir_tree[] = $subitem->name; + yield from $this->iterate($subitem); + continue; + } + yield $path => $subitem; + } + $is_recursive && array_pop($dir_tree); + } + + /** + * Checks whether item contains another items or not + * + * @param object $item + * @return bool + */ + protected function isItemFolder(object $item): bool + { + return !empty($item->item) + && is_array($item->item) + && empty($item->request); + } } diff --git a/src/Converters/Abstract/AbstractConverter.php b/src/Converters/Abstract/AbstractConverter.php index 669be45..74018f7 100644 --- a/src/Converters/Abstract/AbstractConverter.php +++ b/src/Converters/Abstract/AbstractConverter.php @@ -5,21 +5,19 @@ declare(strict_types=1); namespace PmConverter\Converters\Abstract; use Exception; +use Iterator; use PmConverter\Collection; -use PmConverter\Converters\{ - ConverterContract, - RequestContract}; +use PmConverter\Converters\RequestContract; use PmConverter\Environment; -use PmConverter\Exceptions\{ - CannotCreateDirectoryException, - DirectoryIsNotWriteableException, - InvalidHttpVersionException}; +use PmConverter\Exceptions\CannotCreateDirectoryException; +use PmConverter\Exceptions\DirectoryIsNotWriteableException; +use PmConverter\Exceptions\InvalidHttpVersionException; use PmConverter\FileSystem; /** * */ -abstract class AbstractConverter implements ConverterContract +abstract class AbstractConverter { /** * @var Collection|null @@ -32,53 +30,66 @@ abstract class AbstractConverter implements ConverterContract protected string $outputPath; /** - * @var Environment|null + * @var RequestContract[] Converted requests */ - protected ?Environment $env = null; + protected array $requests = []; /** - * Sets an environment with vars + * Sets output path * - * @param Environment $env + * @param string $outputPath * @return $this */ - public function withEnv(Environment $env): static + public function to(string $outputPath): self { - $this->env = $env; + $this->outputPath = sprintf('%s%s%s', $outputPath, DS, static::OUTPUT_DIR); return $this; } /** - * Creates a new directory to save a converted collection into - * - * @param string $outputPath - * @return void - * @throws CannotCreateDirectoryException - * @throws DirectoryIsNotWriteableException - */ - protected function prepareOutputDir(string $outputPath): void - { - $outputPath = sprintf('%s%s%s', $outputPath, DS, static::OUTPUT_DIR); - $this->outputPath = FileSystem::makeDir($outputPath); - } - - /** - * Converts collection requests + * Converts requests from collection * * @param Collection $collection - * @param string $outputPath - * @return void + * @return static * @throws CannotCreateDirectoryException * @throws DirectoryIsNotWriteableException * @throws Exception */ - public function convert(Collection $collection, string $outputPath): void + public function convert(Collection $collection): static { - $this->prepareOutputDir($outputPath); $this->collection = $collection; - $this->setVariables(); - foreach ($collection->item as $item) { - $this->convertItem($item); + $this->outputPath = FileSystem::makeDir($this->outputPath); + $this->setCollectionVars(); + foreach ($collection->iterate() as $path => $item) { + // $this->requests[$path][] = $this->makeRequest($item); + $this->writeRequest($this->makeRequest($item), $path); + } + return $this; + } + + /** + * Returns converted requests + * + * @return Iterator + */ + public function converted(): Iterator + { + foreach ($this->requests as $path => $requests) { + foreach ($requests as $request) { + yield $path => $request; + } + } + } + + /** + * Writes requests on disk + * + * @throws Exception + */ + public function flush(): void + { + foreach ($this->converted() as $path => $request) { + $this->writeRequest($request, $path); } } @@ -87,13 +98,10 @@ abstract class AbstractConverter implements ConverterContract * * @return $this */ - protected function setVariables(): static + protected function setCollectionVars(): static { - empty($this->env) && $this->env = new Environment($this->collection?->variable); - if (!empty($this->collection?->variable)) { - foreach ($this->collection->variable as $var) { - $this->env[$var->key] = $var->value; - } + foreach ($this->collection?->variable ?? [] as $var) { + Environment::instance()->setCustomVar($var->key, $var->value); } return $this; } @@ -121,30 +129,6 @@ abstract class AbstractConverter implements ConverterContract && empty($item->request); } - /** - * Converts an item to request object and writes it into file - * - * @throws Exception - */ - protected function convertItem(mixed $item): void - { - if ($this->isItemFolder($item)) { - static $dir_tree; - foreach ($item->item as $subitem) { - $dir_tree[] = $item->name; - $path = implode(DS, $dir_tree); - if ($this->isItemFolder($subitem)) { - $this->convertItem($subitem); - } else { - $this->writeRequest($this->initRequest($subitem), $path); - } - array_pop($dir_tree); - } - } else { - $this->writeRequest($this->initRequest($item)); - } - } - /** * Initialiazes request object to be written in file * @@ -152,17 +136,18 @@ abstract class AbstractConverter implements ConverterContract * @return RequestContract * @throws InvalidHttpVersionException */ - protected function initRequest(object $item): RequestContract + protected function makeRequest(object $item): RequestContract { $request_class = static::REQUEST_CLASS; /** @var RequestContract $request */ $request = new $request_class(); $request->setName($item->name); + $request->setVersion($this->collection->version); $request->setHttpVersion(1.1); //TODO http version? $request->setDescription($item->request?->description ?? null); $request->setVerb($item->request->method); - $request->setUrl($item->request->url->raw); + $request->setUrl($item->request->url); $request->setHeaders($item->request->header); $request->setAuth($item->request?->auth ?? $this->collection?->auth ?? null); if ($item->request->method !== 'GET' && !empty($item->request->body)) { @@ -196,18 +181,9 @@ abstract class AbstractConverter implements ConverterContract */ protected function interpolate(string $content): string { - if (!$this->env?->hasVars()) { - return $content; - } - $matches = []; - if (preg_match_all('/\{\{.*}}/m', $content, $matches, PREG_PATTERN_ORDER) > 0) { - foreach ($matches[0] as $key => $var) { - if (str_contains($content, $var)) { - $content = str_replace($var, $this->env[$var] ?: $var, $content); - unset($matches[0][$key]); - } - } - } - return $content; + $replace = static fn ($a) => Environment::instance()->var($var = $a[0]) ?: $var; + return Environment::instance()->hasVars() + ? preg_replace_callback('/\{\{.*}}/m', $replace, $content) + : $content; } } diff --git a/src/Converters/Abstract/AbstractRequest.php b/src/Converters/Abstract/AbstractRequest.php index 7af094e..ea177cb 100644 --- a/src/Converters/Abstract/AbstractRequest.php +++ b/src/Converters/Abstract/AbstractRequest.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace PmConverter\Converters\Abstract; +use PmConverter\CollectionVersion; use PmConverter\Converters\RequestContract; -use PmConverter\Exceptions\{ - EmptyHttpVerbException, - InvalidHttpVersionException}; +use PmConverter\Exceptions\EmptyHttpVerbException; +use PmConverter\Exceptions\InvalidHttpVersionException; use PmConverter\HttpVersion; use Stringable; @@ -22,9 +22,9 @@ abstract class AbstractRequest implements Stringable, RequestContract protected string $verb; /** - * @var string URL where to send a request + * @var object|string URL where to send a request */ - protected string $url; + protected object|string $url; /** * @var float HTTP protocol version @@ -56,6 +56,15 @@ abstract class AbstractRequest implements Stringable, RequestContract */ protected string $bodymode = 'raw'; + + protected CollectionVersion $version; + + public function setVersion(CollectionVersion $version): static + { + $this->version = $version; + return $this; + } + /** * @inheritDoc */ @@ -133,7 +142,7 @@ abstract class AbstractRequest implements Stringable, RequestContract /** * @inheritDoc */ - public function setUrl(string $url): static + public function setUrl(object|string $url): static { $this->url = $url; return $this; @@ -142,9 +151,9 @@ abstract class AbstractRequest implements Stringable, RequestContract /** * @inheritDoc */ - public function getUrl(): string + public function getRawUrl(): string { - return $this->url ?: ''; + return is_object($this->url) ? $this->url->raw : $this->url; } /** @@ -186,7 +195,11 @@ abstract class AbstractRequest implements Stringable, RequestContract if (!empty($auth)) { switch ($auth->type) { case 'bearer': - $this->setHeader('Authorization', 'Bearer ' . $auth->{$auth->type}[0]->value); + $this->setHeader('Authorization', 'Bearer ' . match ($this->version) { + CollectionVersion::Version20 => $auth->{$auth->type}->token, + CollectionVersion::Version21 => $auth->{$auth->type}[0]->value, + default => null + }); break; default: break; diff --git a/src/Converters/ConvertFormat.php b/src/Converters/ConvertFormat.php index 3bebb54..9aa9eca 100644 --- a/src/Converters/ConvertFormat.php +++ b/src/Converters/ConvertFormat.php @@ -5,12 +5,11 @@ declare(strict_types=1); namespace PmConverter\Converters; -use PmConverter\Converters\{ - Curl\CurlConverter, - Http\HttpConverter, - Postman20\Postman20Converter, - Postman21\Postman21Converter, - Wget\WgetConverter}; +use PmConverter\Converters\Curl\CurlConverter; +use PmConverter\Converters\Http\HttpConverter; +use PmConverter\Converters\Postman20\Postman20Converter; +use PmConverter\Converters\Postman21\Postman21Converter; +use PmConverter\Converters\Wget\WgetConverter; enum ConvertFormat: string { @@ -19,4 +18,26 @@ enum ConvertFormat: string case Wget = WgetConverter::class; case Postman20 = Postman20Converter::class; case Postman21 = Postman21Converter::class; + + public static function fromArg(string $arg): self + { + return match ($arg) { + 'http' => ConvertFormat::Http, + 'curl' => ConvertFormat::Curl, + 'wget' => ConvertFormat::Wget, + 'v2.0' => ConvertFormat::Postman20, + 'v2.1' => ConvertFormat::Postman21, + }; + } + + public function toArg(): string + { + return match ($this) { + ConvertFormat::Http => 'http', + ConvertFormat::Curl => 'curl', + ConvertFormat::Wget => 'wget', + ConvertFormat::Postman20 => 'v2.0', + ConvertFormat::Postman21 => 'v2.1', + }; + } } diff --git a/src/Converters/Curl/CurlConverter.php b/src/Converters/Curl/CurlConverter.php index 5b11944..22a4c88 100644 --- a/src/Converters/Curl/CurlConverter.php +++ b/src/Converters/Curl/CurlConverter.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace PmConverter\Converters\Curl; -use PmConverter\Converters\{ - Abstract\AbstractConverter, - ConverterContract}; +use PmConverter\Converters\Abstract\AbstractConverter; -class CurlConverter extends AbstractConverter implements ConverterContract +class CurlConverter extends AbstractConverter { protected const FILE_EXT = 'sh'; diff --git a/src/Converters/Curl/CurlRequest.php b/src/Converters/Curl/CurlRequest.php index 259d807..ec2c404 100644 --- a/src/Converters/Curl/CurlRequest.php +++ b/src/Converters/Curl/CurlRequest.php @@ -72,7 +72,7 @@ class CurlRequest extends AbstractRequest "curl \ ", "\t--http1.1 \ ", //TODO proto "\t--request $this->verb \ ", - "\t--location $this->url \ ", + "\t--location {$this->getRawUrl()} \ ", ], $this->prepareHeaders(), $this->prepareBody() diff --git a/src/Converters/Http/HttpConverter.php b/src/Converters/Http/HttpConverter.php index e344172..741ddfc 100644 --- a/src/Converters/Http/HttpConverter.php +++ b/src/Converters/Http/HttpConverter.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace PmConverter\Converters\Http; -use PmConverter\Converters\{ - Abstract\AbstractConverter, - ConverterContract}; +use PmConverter\Converters\Abstract\AbstractConverter; -class HttpConverter extends AbstractConverter implements ConverterContract +class HttpConverter extends AbstractConverter { protected const FILE_EXT = 'http'; diff --git a/src/Converters/Http/HttpRequest.php b/src/Converters/Http/HttpRequest.php index 8a304be..8da9ae3 100644 --- a/src/Converters/Http/HttpRequest.php +++ b/src/Converters/Http/HttpRequest.php @@ -5,8 +5,7 @@ declare(strict_types=1); namespace PmConverter\Converters\Http; use PmConverter\Converters\Abstract\AbstractRequest; -use PmConverter\Exceptions\{ - EmptyHttpVerbException}; +use PmConverter\Exceptions\EmptyHttpVerbException; /** * Class to determine file content with http request format @@ -29,7 +28,7 @@ class HttpRequest extends AbstractRequest */ protected function prepareHeaders(): array { - $output[] = sprintf('%s %s HTTP/%s', $this->getVerb(), $this->getUrl(), $this->getHttpVersion()); + $output[] = sprintf('%s %s HTTP/%s', $this->getVerb(), $this->getRawUrl(), $this->getHttpVersion()); foreach ($this->headers as $name => $data) { $output[] = sprintf('%s%s: %s', $data['disabled'] ? '# ' : '', $name, $data['value']); } diff --git a/src/Converters/Postman20/Postman20Converter.php b/src/Converters/Postman20/Postman20Converter.php index c068baa..4b34e48 100644 --- a/src/Converters/Postman20/Postman20Converter.php +++ b/src/Converters/Postman20/Postman20Converter.php @@ -6,9 +6,7 @@ namespace PmConverter\Converters\Postman20; use PmConverter\Collection; use PmConverter\CollectionVersion; -use PmConverter\Converters\{ - Abstract\AbstractConverter, - ConverterContract}; +use PmConverter\Converters\Abstract\AbstractConverter; use PmConverter\Exceptions\CannotCreateDirectoryException; use PmConverter\Exceptions\DirectoryIsNotWriteableException; use PmConverter\FileSystem; @@ -16,7 +14,7 @@ use PmConverter\FileSystem; /** * Converts Postman Collection v2.1 to v2.0 */ -class Postman20Converter extends AbstractConverter implements ConverterContract +class Postman20Converter extends AbstractConverter { protected const FILE_EXT = 'v20.postman_collection.json'; @@ -26,25 +24,25 @@ class Postman20Converter extends AbstractConverter implements ConverterContract * Converts collection requests * * @param Collection $collection - * @param string $outputPath - * @return void + * @return static * @throws CannotCreateDirectoryException * @throws DirectoryIsNotWriteableException */ - public function convert(Collection $collection, string $outputPath): void + public function convert(Collection $collection): static { $this->collection = $collection; // if data was exported from API, here is already valid json to // just flush it in file, otherwise we need to convert it deeper - if ($this->collection->version() === CollectionVersion::Version21) { + if ($this->collection->version === CollectionVersion::Version21) { $this->collection->info->schema = str_replace('/v2.1.', '/v2.0.', $this->collection->info->schema); $this->convertAuth($this->collection->raw()); foreach ($this->collection->item as $item) { $this->convertItem($item); } } - $this->prepareOutputDir($outputPath); + $this->outputPath = FileSystem::makeDir($this->outputPath); $this->writeCollection(); + return $this; } /** @@ -97,11 +95,11 @@ class Postman20Converter extends AbstractConverter implements ConverterContract if (empty($request->auth)) { return; } - $type = $request->auth->type; - if ($type !== 'noauth' && is_array($request->auth->$type)) { - $auth = []; + $auth = ['type' => 'noauth']; + $type = strtolower($request->auth->type); + if ($type !== 'noauth') { foreach ($request->auth->$type as $param) { - $auth[$param->key] = $param->value; + $auth[$param->key] = $param->value ?? ''; } $request->auth->$type = (object)$auth; } diff --git a/src/Converters/Postman21/Postman21Converter.php b/src/Converters/Postman21/Postman21Converter.php index 73f1b5d..3ae59b9 100644 --- a/src/Converters/Postman21/Postman21Converter.php +++ b/src/Converters/Postman21/Postman21Converter.php @@ -6,9 +6,7 @@ namespace PmConverter\Converters\Postman21; use PmConverter\Collection; use PmConverter\CollectionVersion; -use PmConverter\Converters\{ - Abstract\AbstractConverter, - ConverterContract}; +use PmConverter\Converters\Abstract\AbstractConverter; use PmConverter\Exceptions\CannotCreateDirectoryException; use PmConverter\Exceptions\DirectoryIsNotWriteableException; use PmConverter\FileSystem; @@ -16,7 +14,7 @@ use PmConverter\FileSystem; /** * Converts Postman Collection v2.0 to v2.1 */ -class Postman21Converter extends AbstractConverter implements ConverterContract +class Postman21Converter extends AbstractConverter { protected const FILE_EXT = 'v21.postman_collection.json'; @@ -26,25 +24,25 @@ class Postman21Converter extends AbstractConverter implements ConverterContract * Converts collection requests * * @param Collection $collection - * @param string $outputPath - * @return void + * @return static * @throws CannotCreateDirectoryException * @throws DirectoryIsNotWriteableException */ - public function convert(Collection $collection, string $outputPath): void + public function convert(Collection $collection): static { $this->collection = $collection; // if data was exported from API, here is already valid json to // just flush it in file, otherwise we need to convert it deeper - if ($this->collection->version() === CollectionVersion::Version20) { + if ($this->collection->version === CollectionVersion::Version20) { $this->collection->info->schema = str_replace('/v2.0.', '/v2.1.', $this->collection->info->schema); $this->convertAuth($this->collection->raw()); foreach ($this->collection->item as $item) { $this->convertItem($item); } } - $this->prepareOutputDir($outputPath); + $this->outputPath = FileSystem::makeDir($this->outputPath); $this->writeCollection(); + return $this; } /** diff --git a/src/Converters/RequestContract.php b/src/Converters/RequestContract.php index 424f31e..77ea405 100644 --- a/src/Converters/RequestContract.php +++ b/src/Converters/RequestContract.php @@ -86,7 +86,7 @@ interface RequestContract * * @return string */ - public function getUrl(): string; + public function getRawUrl(): string; /** * Sets headers from collection item to request object @@ -116,7 +116,7 @@ interface RequestContract /** * Sets authorization headers * - * @param object|null $auth + * @param object $auth * @return $this */ public function setAuth(object $auth): static; diff --git a/src/Converters/Wget/WgetConverter.php b/src/Converters/Wget/WgetConverter.php index 63563bb..7a0d4fa 100644 --- a/src/Converters/Wget/WgetConverter.php +++ b/src/Converters/Wget/WgetConverter.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace PmConverter\Converters\Wget; -use PmConverter\Converters\{ - Abstract\AbstractConverter, - ConverterContract}; +use PmConverter\Converters\Abstract\AbstractConverter; -class WgetConverter extends AbstractConverter implements ConverterContract +class WgetConverter extends AbstractConverter { protected const FILE_EXT = 'sh'; diff --git a/src/Converters/Wget/WgetRequest.php b/src/Converters/Wget/WgetRequest.php index 5d9f5c7..d0f53cf 100644 --- a/src/Converters/Wget/WgetRequest.php +++ b/src/Converters/Wget/WgetRequest.php @@ -77,16 +77,16 @@ class WgetRequest extends AbstractRequest if ($this->getBodymode() === 'formdata') { if ($this->getBody()) { if ($this->getVerb() === 'GET') { - $output[] = sprintf("\t%s?%s", $this->getUrl(), http_build_query($this->prepareBody())); + $output[] = sprintf("\t%s?%s", $this->getRawUrl(), http_build_query($this->prepareBody())); } else { $output[] = sprintf("\t--body-data '%s' \ ", http_build_query($this->prepareBody())); - $output[] = sprintf("\t%s", $this->getUrl()); + $output[] = sprintf("\t%s", $this->getRawUrl()); } } } else { if ($this->getVerb() !== 'GET') { $output[] = sprintf("\t--body-data '%s' \ ", implode("\n", $this->prepareBody())); - $output[] = sprintf("\t%s", $this->getUrl()); + $output[] = sprintf("\t%s", $this->getRawUrl()); } } return implode(EOL, array_merge($output, [''])); diff --git a/src/Environment.php b/src/Environment.php index ab3bcaf..3fe8bf8 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -4,23 +4,138 @@ declare(strict_types=1); namespace PmConverter; -class Environment implements \ArrayAccess +use ArrayAccess; +use JsonException; + +/** + * + */ +class Environment implements ArrayAccess { + /** + * @var string Path to env file + */ + protected static string $filepath = ''; + + /** + * @var Environment + */ + protected static Environment $instance; + /** * @var array */ - protected array $vars = []; + protected array $ownVars = []; /** - * @param object|null $env + * @var array */ - public function __construct(protected ?object $env) + protected array $customVars = []; + + public static function instance(): static { - if (!empty($env->values)) { - foreach ($env->values as $var) { - $this->vars[static::formatKey($var->key)] = $var->value; - } + return static::$instance ??= new static(); + } + + /** + * @param string $filepath + * @return $this + * @throws JsonException + */ + public function readFromFile(string $filepath): static + { + $content = file_get_contents(static::$filepath = $filepath); + $content = json_decode($content, flags: JSON_THROW_ON_ERROR); //TODO try-catch + $content || throw new JsonException("not a valid environment: $filepath"); + property_exists($content, 'environment') && $content = $content->environment; + if (!property_exists($content, 'id') && !property_exists($content, 'name')) { + throw new JsonException("not a valid environment: $filepath"); } + return $this->setOwnVars($content->values); + } + + /** + * @param array $vars + * @return $this + */ + protected function setOwnVars(array $vars): static + { + foreach ($vars as $key => $value) { + is_object($value) && [$key, $value] = [$value->key, $value->value]; + $this->setOwnVar($key, $value); + } + return $this; + } + + /** + * Sets value to some environment own variable + * + * @param string $name + * @param string $value + * @return $this + */ + protected function setOwnVar(string $name, string $value): static + { + $this->ownVars[static::formatKey($name)] = $value; + return $this; + } + + /** + * @param array $vars + * @return $this + */ + public function setCustomVars(array $vars): static + { + foreach ($vars as $key => $value) { + is_object($value) && [$key, $value] = [$value->key, $value->value]; + $this->setCustomVar($key, $value); + } + return $this; + } + + /** + * Sets value to some environment own variable + * + * @param string $name + * @param string $value + * @return $this + */ + public function setCustomVar(string $name, string $value): static + { + $this->customVars[static::formatKey($name)] = $value; + return $this; + } + + /** + * Returns value of specific variable + * + * @param string $name + * @return mixed + */ + public function var(string $name): mixed + { + $format_key = static::formatKey($name); + return $this->ownVars[$format_key] ?? $this->customVars[$format_key] ?? null; + } + + /** + * Returns array of own and custom variables + * + * @return string[] + */ + public function vars(): array + { + return array_merge($this->ownVars, $this->customVars); + } + + /** + * Returns array of custom variables + * + * @return string[] + */ + public function customVars(): array + { + return $this->customVars; } /** @@ -30,15 +145,35 @@ class Environment implements \ArrayAccess */ public function hasVars(): bool { - return !empty($this->vars); + return !empty($this->ownVars) && !empty($this->customVars); } + + + + + + + + + + + + + /** + * Closed constructor + */ + protected function __construct() + { + } + + /** * @inheritDoc */ public function offsetExists(mixed $offset): bool { - return array_key_exists(static::formatKey($offset), $this->vars); + return array_key_exists(static::formatKey($offset), $this->vars()); } /** @@ -46,7 +181,7 @@ class Environment implements \ArrayAccess */ public function offsetGet(mixed $offset): mixed { - return $this->vars[static::formatKey($offset)] ?? null; + return $this->var($offset); } /** @@ -54,7 +189,7 @@ class Environment implements \ArrayAccess */ public function offsetSet(mixed $offset, mixed $value): void { - $this->vars[static::formatKey($offset)] = $value; + $this->customVars[static::formatKey($offset)] = $value; } /** @@ -62,7 +197,7 @@ class Environment implements \ArrayAccess */ public function offsetUnset(mixed $offset): void { - unset($this->vars[static::formatKey($offset)]); + unset($this->customVars[static::formatKey($offset)]); } /** diff --git a/src/Exceptions/IncorrectSettingsFileException.php b/src/Exceptions/IncorrectSettingsFileException.php new file mode 100644 index 0000000..dfcf034 --- /dev/null +++ b/src/Exceptions/IncorrectSettingsFileException.php @@ -0,0 +1,11 @@ +version() !== CollectionVersion::Unknown; + && Collection::detectFileVersion($path) !== CollectionVersion::Unknown; } } diff --git a/src/Processor.php b/src/Processor.php index a8bab6b..73d57d7 100644 --- a/src/Processor.php +++ b/src/Processor.php @@ -5,93 +5,74 @@ declare(strict_types=1); namespace PmConverter; use Exception; +use Generator; use InvalidArgumentException; +use JetBrains\PhpStorm\NoReturn; use JsonException; -use PmConverter\Converters\{ - ConverterContract, - ConvertFormat}; -use PmConverter\Exceptions\{ - CannotCreateDirectoryException, - DirectoryIsNotReadableException, - DirectoryIsNotWriteableException, - DirectoryNotExistsException}; +use PmConverter\Converters\Abstract\AbstractConverter; +use PmConverter\Converters\ConverterContract; +use PmConverter\Converters\ConvertFormat; +use PmConverter\Exceptions\CannotCreateDirectoryException; +use PmConverter\Exceptions\DirectoryIsNotReadableException; +use PmConverter\Exceptions\DirectoryIsNotWriteableException; +use PmConverter\Exceptions\DirectoryNotExistsException; +use PmConverter\Exceptions\IncorrectSettingsFileException; +/** + * Main class + */ class Processor { /** * Converter version */ - public const VERSION = '1.5.0'; - - /** - * @var string[] Paths to collection files - */ - protected array $collectionPaths = []; - - /** - * @var string Output path where to put results in - */ - protected string $outputPath; - - /** - * @var bool Flag to remove output directories or not before conversion started - */ - protected bool $preserveOutput = false; - - /** - * @var string[] Additional variables - */ - protected array $vars; - - /** - * @var ConvertFormat[] Formats to convert a collections into - */ - protected array $formats; - - /** - * @var ConverterContract[] Converters will be used for conversion according to choosen formats - */ - protected array $converters = []; - - /** - * @var Collection[] Collections that will be converted into choosen formats - */ - protected array $collections = []; + public const VERSION = '1.6.0'; /** * @var int Initial timestamp */ - protected int $initTime; + protected readonly int $initTime; /** * @var int Initial RAM usage */ - protected int $initRam; + protected readonly int $initRam; /** - * @var string Path to environment file + * @var Settings Settings (lol) */ - protected string $envFile; + protected Settings $settings; + + /** + * @var ConverterContract[] Converters will be used for conversion according to chosen formats + */ + protected array $converters = []; + + /** + * @var bool Do we need to save settings file and exit or not? + */ + protected bool $needDumpSettings = false; /** * @var Environment */ - protected Environment $env; - - /** - * @var bool Flag to output some debug-specific messages - */ - protected bool $devMode = false; + public Environment $env; /** * Constructor * * @param array $argv Arguments came from cli */ - public function __construct(protected array $argv) + public function __construct(protected readonly array $argv) { $this->initTime = hrtime(true); $this->initRam = memory_get_usage(true); + $this->settings = Settings::init(); + $this->env = Environment::instance() + ->readFromFile($this->settings->envFilepath()) + ->setCustomVars($this->settings->vars()); + $this->parseArgs(); + $this->needDumpSettings && $this->dumpSettingsFile(); } /** @@ -102,21 +83,11 @@ class Processor */ protected function parseArgs(): void { - if (count($this->argv) < 2) { - die(implode(EOL, $this->usage()) . EOL); - } foreach ($this->argv as $idx => $arg) { switch ($arg) { case '-f': case '--file': - $rawpath = $this->argv[$idx + 1]; - $normpath = FileSystem::normalizePath($rawpath); - if (!FileSystem::isCollectionFile($normpath)) { - throw new InvalidArgumentException( - sprintf("not a valid collection:%s\t%s %s", EOL, $arg, $rawpath) - ); - } - $this->collectionPaths[] = $this->argv[$idx + 1]; + $this->settings->addFilePath($this->argv[$idx + 1]); break; case '-o': @@ -124,7 +95,7 @@ class Processor if (empty($this->argv[$idx + 1])) { throw new InvalidArgumentException('-o is required'); } - $this->outputPath = $this->argv[$idx + 1]; + $this->settings->setOutputPath($this->argv[$idx + 1]); break; case '-d': @@ -132,54 +103,57 @@ class Processor if (empty($this->argv[$idx + 1])) { throw new InvalidArgumentException('a directory path is expected for -d (--dir)'); } - $rawpath = $this->argv[$idx + 1]; - $files = array_filter( - FileSystem::dirContents($rawpath), - static fn($filename) => FileSystem::isCollectionFile($filename) - ); - $this->collectionPaths = array_unique(array_merge($this?->collectionPaths ?? [], $files)); + $this->settings->addDirPath($this->argv[$idx + 1]); break; case '-e': case '--env': - $this->envFile = FileSystem::normalizePath($this->argv[$idx + 1]); + $this->settings->setEnvFilepath($this->argv[$idx + 1]); break; case '-p': case '--preserve': - $this->preserveOutput = true; + $this->settings->setPreserveOutput(true); break; case '--http': - $this->formats[ConvertFormat::Http->name] = ConvertFormat::Http; + $this->settings->addFormat(ConvertFormat::Http); break; case '--curl': - $this->formats[ConvertFormat::Curl->name] = ConvertFormat::Curl; + $this->settings->addFormat(ConvertFormat::Curl); break; case '--wget': - $this->formats[ConvertFormat::Wget->name] = ConvertFormat::Wget; + $this->settings->addFormat(ConvertFormat::Wget); break; case '--v2.0': - $this->formats[ConvertFormat::Postman20->name] = ConvertFormat::Postman20; + $this->settings->addFormat(ConvertFormat::Postman20); break; case '--v2.1': - $this->formats[ConvertFormat::Postman21->name] = ConvertFormat::Postman21; + $this->settings->addFormat(ConvertFormat::Postman21); break; case '-a': case '--all': foreach (ConvertFormat::cases() as $format) { - $this->formats[$format->name] = $format; + $this->settings->addFormat($format); } break; case '--var': - [$var, $value] = explode('=', trim($this->argv[$idx + 1])); - $this->vars[$var] = $value; + //TODO split by first equal sign + $this->env->setCustomVar(...explode('=', trim($this->argv[$idx + 1]))); + break; + + case '--dev': + $this->settings->setDevMode(true); + break; + + case '--dump': + $this->needDumpSettings = true; break; case '-v': @@ -189,23 +163,63 @@ class Processor case '-h': case '--help': die(implode(EOL, $this->usage()) . EOL); - - case '--dev': - $this->devMode = true; - break; } } - if (empty($this->collectionPaths)) { + if (empty($this->settings->collectionPaths())) { throw new InvalidArgumentException('there are no collections to convert'); } - if (empty($this->outputPath)) { + if (empty($this->settings->outputPath())) { throw new InvalidArgumentException('-o is required'); } - if (empty($this->formats)) { - $this->formats = [ConvertFormat::Http->name => ConvertFormat::Http]; + if (empty($this->settings->formats())) { + $this->settings->addFormat(ConvertFormat::Http); } } + /** + * Handles input command + * + * @return void + * @throws CannotCreateDirectoryException + * @throws DirectoryIsNotReadableException + * @throws DirectoryIsNotWriteableException + * @throws DirectoryNotExistsException + * @throws JsonException + * @throws IncorrectSettingsFileException + */ + public function handle(): void + { + $this->prepareOutputDirectory(); + $this->initConverters(); + $this->convert(); + } + + /** + * Writes all settings into file if --dump provided + * + * @return never + */ + #[NoReturn] + protected function dumpSettingsFile(): never + { + $answer = 'o'; + if ($this->settings::fileExists()) { + echo 'Settings file already exists: ' . $this->settings::filepath() . EOL; + echo 'Do you want to (o)verwrite it, (b)ackup it and create new one or (c)ancel (default)?' . EOL; + $answer = strtolower(trim(readline('> '))); + } + if (!in_array($answer, ['o', 'b'])) { + die('Current settings file has not been changed' . EOL); + } + if ($answer === 'b') { + $filepath = $this->settings->backup(); + printf("Settings file has been backed up to file:%s\t%s%s", EOL, $filepath, EOL); + } + $this->settings->dump($this->env->customVars()); + printf("Arguments has been converted into settings file:%s\t%s%s", EOL, $this->settings::filepath(), EOL); + die('Review and edit it if needed.' . EOL); + } + /** * Initializes output directory * @@ -215,100 +229,74 @@ class Processor * @throws DirectoryNotExistsException * @throws DirectoryIsNotReadableException */ - protected function initOutputDirectory(): void + protected function prepareOutputDirectory(): void { - if (isset($this?->outputPath) && !$this->preserveOutput) { - FileSystem::removeDir($this->outputPath); + if (!$this->settings->isPreserveOutput()) { + FileSystem::removeDir($this->settings->outputPath()); } - FileSystem::makeDir($this->outputPath); + FileSystem::makeDir($this->settings->outputPath()); } /** - * Initializes converters according to choosen formats + * Initializes converters according to chosen formats * * @return void */ protected function initConverters(): void { - foreach ($this->formats as $type) { - $this->converters[$type->name] = new $type->value($this->preserveOutput); + foreach ($this->settings->formats() as $type) { + $this->converters[$type->name] = new $type->value($this->settings->isPreserveOutput()); } unset($this->formats); } /** - * Initializes collection objects + * Generates collections from settings * + * @return Generator * @throws JsonException */ - protected function initCollections(): void + protected function newCollection(): Generator { - foreach ($this->collectionPaths as $collectionPath) { - $collection = Collection::fromFile($collectionPath); - $this->collections[$collection->name()] = $collection; + foreach ($this->settings->collectionPaths() as $collectionPath) { + yield Collection::fromFile($collectionPath); } - unset($this->collectionPaths, $content); - } - - /** - * Initializes environment object - * - * @return void - * @throws JsonException - */ - protected function initEnv(): void - { - if (!isset($this->envFile)) { - return; - } - $content = file_get_contents(FileSystem::normalizePath($this->envFile)); - $content = json_decode($content, flags: JSON_THROW_ON_ERROR); - if (!property_exists($content, 'environment') || empty($content?->environment)) { - throw new JsonException("not a valid environment: $this->envFile"); - } - $this->env = new Environment($content->environment); - foreach ($this->vars as $var => $value) { - $this->env[$var] = $value; - } - unset($this->vars, $this->envFile, $content, $var, $value); } /** * Begins a conversion * - * @throws Exception + * @throws JsonException */ public function convert(): void { - $this->parseArgs(); - $this->initOutputDirectory(); - $this->initConverters(); - $this->initCollections(); - $this->initEnv(); - $count = count($this->collections); + $count = count($this->settings->collectionPaths()); $current = $success = 0; + $collection = null; print(implode(EOL, array_merge($this->version(), $this->copyright())) . EOL . EOL); - foreach ($this->collections as $collectionName => $collection) { + foreach ($this->newCollection() as $collection) { ++$current; - printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, EOL); - foreach ($this->converters as $type => $exporter) { + printf("Converting '%s' (%d/%d):%s", $collection->name(), $current, $count, EOL); + foreach ($this->converters as $type => $converter) { + /** @var AbstractConverter $converter */ printf('> %s%s', strtolower($type), EOL); - $outputPath = sprintf('%s%s%s', $this->outputPath, DS, $collectionName); - if (!empty($this->env)) { - $exporter->withEnv($this->env); - } + $outputPath = sprintf('%s%s%s', $this->settings->outputPath(), DS, $collection->name()); try { - $exporter->convert($collection, $outputPath); - printf(' OK: %s%s', $exporter->getOutputPath(), EOL); + $converter = $converter->to($outputPath); + $converter = $converter->convert($collection); + $converter->flush(); + printf(' OK: %s%s', $converter->getOutputPath(), EOL); } catch (Exception $e) { printf(' ERROR %s: %s%s', $e->getCode(), $e->getMessage(), EOL); - $this->devMode && array_map(static fn ($line) => printf(" %s%s", $line, EOL), $e->getTrace()); + if ($this->settings->isDevMode()) { + array_map(static fn ($line) => printf(' %s%s', $line, EOL), $e->getTrace()); + } } } print(EOL); ++$success; } - unset($this->converters, $type, $exporter, $outputPath, $this->collections, $collectionName, $collection); + unset($this->converters, $type, $converter, $outputPath, $this->collections, $collectionName, $collection); $this->printStats($success, $current); } @@ -334,18 +322,19 @@ class Processor /** * @return string[] */ - public function version(): array + public static function version(): array { - return ["Postman collection converter v" . self::VERSION]; + return ['Postman collection converter v' . self::VERSION]; } /** * @return string[] */ - public function copyright(): array + public static function copyright(): array { + $years = ($year = (int)date('Y')) > 2023 ? "2023 - $year" : $year; return [ - 'Anthony Axenov (c) ' . date('Y') . ", MIT license", + "Anthony Axenov (c) $years, MIT license", 'https://git.axenov.dev/anthony/pm-convert' ]; } @@ -353,9 +342,9 @@ class Processor /** * @return array */ - public function usage(): array + public static function usage(): array { - return array_merge($this->version(), [ + return array_merge(static::version(), [ 'Usage:', "\t./pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]", "\tphp pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]", @@ -369,6 +358,7 @@ class Processor "\t-e, --env - use environment file with variables to replace in requests", "\t--var \"NAME=VALUE\" - force replace specified env variable called NAME with custom VALUE", "\t-p, --preserve - do not delete OUTPUT_PATH (if exists)", + "\t --dump - convert provided arguments into settings file in `pwd", "\t-h, --help - show this help message and exit", "\t-v, --version - show version info and exit", '', @@ -401,6 +391,6 @@ class Processor " -o ~/postman_export \ ", " --all", "", - ], $this->copyright()); + ], static::copyright()); } } diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 0000000..4b65aff --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,313 @@ +getMessage(), $e->getCode()); + } + return new self($settings); + } + + /** + * Returns full path to settings file + * + * @return string + */ + public static function filepath(): string + { + return self::$filepath; + } + + /** + * @param object $settings + * @throws JsonException + */ + protected function __construct(object $settings) + { + foreach ($settings->directories ?? [] as $path) { + $this->addDirPath($path); + } + foreach ($settings->files ?? [] as $path) { + $this->addFilePath($path); + } + $this->setDevMode(!empty($settings->devMode)); + $this->setPreserveOutput(!empty($settings->preserveOutput)); + isset($settings->environment) && $this->setEnvFilepath($settings->environment); + isset($settings->output) && $this->setOutputPath($settings->output); + foreach ($settings->formats ?? [] as $format) { + $this->addFormat(ConvertFormat::fromArg($format)); + } + foreach ($settings->vars ?? [] as $name => $value) { + $this->vars[$name] = $value; + } + } + + /** + * @param string $path + * @return void + * @throws JsonException + */ + public function addDirPath(string $path): void + { + $this->directories = array_unique(array_merge( + $this->directories ?? [], + [FileSystem::normalizePath($path)] + )); + $files = array_filter( + FileSystem::dirContents($path), + static fn ($filename) => FileSystem::isCollectionFile($filename) + ); + $this->collectionPaths = array_unique(array_merge($this->collectionPaths ?? [], $files)); + } + + /** + * @param string $path + * @return void + * @throws JsonException + */ + public function addFilePath(string $path): void + { + $normpath = FileSystem::normalizePath($path); + if (!FileSystem::isCollectionFile($normpath)) { + throw new InvalidArgumentException("not a valid collection: $path"); + } + in_array($path, $this->collectionPaths) || $this->collectionPaths[] = $path; + } + + /** + * @param string $outputPath + * @return void + */ + public function setOutputPath(string $outputPath): void + { + $this->outputPath = $outputPath; + } + + /** + * @param bool $devMode + * @return void + */ + public function setDevMode(bool $devMode): void + { + $this->devMode = $devMode; + } + + /** + * @param ConvertFormat $format + * @return void + */ + public function addFormat(ConvertFormat $format): void + { + $this->formats[$format->name] = $format; + } + + /** + * Returns array of variables + * + * @return string[] + */ + public function vars(): array + { + return $this->vars; + } + + /** + * @param bool $preserveOutput + * @return void + */ + public function setPreserveOutput(bool $preserveOutput): void + { + $this->preserveOutput = $preserveOutput; + } + + /** + * @param string $filepath + * @return void + */ + public function setEnvFilepath(string $filepath): void + { + $this->envFilepath = FileSystem::normalizePath($filepath); + } + + /** + * @return bool + */ + public function isDevMode(): bool + { + return $this->devMode; + } + + /** + * @return string[] + */ + public function collectionPaths(): array + { + return $this->collectionPaths; + } + + /** + * @return string + */ + public function outputPath(): string + { + return $this->outputPath; + } + + /** + * @return bool + */ + public function isPreserveOutput(): bool + { + return $this->preserveOutput; + } + + /** + * @return ConvertFormat[] + */ + public function formats(): array + { + return $this->formats; + } + + /** + * @return string + */ + public function envFilepath(): string + { + return $this->envFilepath; + } + + /** + * Determines fieldset of settings JSON + * + * @return array + */ + public function __serialize(): array + { + return [ + 'dev' => $this->isDevMode(), + 'directories' => $this->directories, + 'files' => $this->collectionPaths(), + 'environment' => $this->envFilepath(), + 'output' => $this->outputPath(), + 'preserve-output' => $this->isPreserveOutput(), + 'formats' => array_values(array_map( + static fn (ConvertFormat $format) => $format->toArg(), + $this->formats(), + )), + 'vars' => $this->vars, + ]; + } + + /** + * Converts settings into JSON format + * + * @return string + */ + public function __toString(): string + { + return json_encode($this->__serialize(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + /** + * Writes settings in JSON format into settings file + * + * @param array $vars + * @return bool + */ + public function dump(array $vars = []): bool + { + count($vars) > 0 && $this->vars = $vars; + return file_put_contents(self::$filepath, (string)$this) > 0; + } + + /** + * Makes a backup file of current settings file + * + * @return string + */ + public function backup(): string + { + copy(self::$filepath, $newfilepath = self::$filepath . '.bak.' . time()); + return $newfilepath; + } +} diff --git a/tests/AbstractRequestTest.php b/tests/AbstractRequestTest.php index 54c9c2d..7afd877 100644 --- a/tests/AbstractRequestTest.php +++ b/tests/AbstractRequestTest.php @@ -3,9 +3,8 @@ declare(strict_types=1); use PHPUnit\Framework\TestCase; -use PmConverter\Exceptions\{ - EmptyHttpVerbException, - InvalidHttpVersionException}; +use PmConverter\Exceptions\EmptyHttpVerbException; +use PmConverter\Exceptions\InvalidHttpVersionException; class AbstractRequestTest extends TestCase { @@ -71,7 +70,7 @@ class AbstractRequestTest extends TestCase /** * @covers PmConverter\Converters\Abstract\AbstractRequest * @covers PmConverter\Converters\Abstract\AbstractRequest::setUrl() - * @covers PmConverter\Converters\Abstract\AbstractRequest::getUrl() + * @covers PmConverter\Converters\Abstract\AbstractRequest::getRawUrl() * @return void */ public function testUrl(): void @@ -79,7 +78,7 @@ class AbstractRequestTest extends TestCase $request = new \PmConverter\Converters\Http\HttpRequest(); $request->setUrl('http://localhost'); - $this->assertSame('http://localhost', $request->getUrl()); + $this->assertSame('http://localhost', $request->getRawUrl()); } /**