First basic ready-to-use implementation

This commit is contained in:
Anthony Axenov 2023-08-03 12:48:59 +08:00
parent 5cc681de63
commit 3b94911895
Signed by: anthony
GPG Key ID: EA9EC32FF7CCD4EC
23 changed files with 1177 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.idea
/.vscode
/vendor

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Антон Аксенов (aka Anthony Axenov)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

114
README.md Normal file
View File

@ -0,0 +1,114 @@
# Postman collection converter
Convert your Postman collections into different formats.
Very fast.
Offline.
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.
## Supported features
* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json);
* 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);
* `json` body (forces header `Content-Type` to `application/json`);
* `formdata` body (including disabled fields for `http`-format; forces header `Content-Type` to `multipart/form-data`)
## Planned features
- support as many as possible/necessary of authentication kinds (_currently no ones_);
- support as many as possible/necessary of body formats (_currently json and formdata_);
- documentation generation support (markdown) with responce examples (if present);
- maybe some another convert formats (like httpie or something...);
- replace `{{vars}}` from folder;
- replace `{{vars}}` from environment;
- performance measurement;
- better logging;
- tests, phpcs, psalm, etc.;
- web version.
## Installation
```
composer global r axenov/pm-convert
```
Make sure your `~/.config/composer/vendor/bin` is in `$PATH` env:
```
echo $PATH | grep --color=auto 'composer'
# if not then execute this command and add it into ~/.profile:
export PATH="$PATH:~/.config/composer/vendor/bin"
```
## Usage
```
$ pm-convert --help
Postman collection converter
Usage:
./pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]
php pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]
composer pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]
./vendor/bin/pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]
Possible ARGUMENTS:
-f, --file - a PATH to single collection located in PATH to convert from
-d, --dir - a directory with collections located in COLLECTION_FILEPATH to convert from
-o, --output - a directory OUTPUT_PATH to put results in
-p, --preserve - do not delete OUTPUT_PATH (if exists)
-h, --help - show this help message and exit
-v, --version - show version info and exit
If both -c and -d are specified then only unique set of files will be converted.
-f or -d are required to be specified at least once, but each may be specified multiple times.
PATH must be a valid path to readable json-file or directory.
OUTPUT_PATH must be a valid path to writeable directory.
If -o is specified several times then only last one will be applied.
Possible FORMATS:
--http - generate raw *.http files (default)
--curl - generate shell scripts with curl command
--wget - generate shell scripts with wget command
If no FORMATS specified then --http implied.
Any of FORMATS can be specified at the same time.
Example:
./pm-convert \
-f ~/dir1/first.postman_collection.json \
--directory ~/team \
--file ~/dir2/second.postman_collection.json \
-d ~/personal \
-o ~/postman_export
```
### Notice
Make sure every (I mean _every_) collection (not collection file), its folders and/or requests has unique names.
If not, you can rename them in Postman or convert collections with similar names into different directories.
Otherwise converted files may be overwritten by each other.
## License
You can use, share and develop this project according to [MIT License](LICENSE).
Postman is [protected legal trademark](https://www.postman.com/legal/trademark-policy/) of Postman, Inc.
-----
## Disclaimer
I'm **not** affiliated with Postman, Inc. in any way.
I'm just a backend developer who is forced to use this javascripted gigachad-shitmonster.
So the goal of this project is to:
* take the data and its synchronization under own transparent control;
* easily migrate to something more RAM tolerant and productive, easier and free to use;
* get off the needle of the vendor lock, strict restrictions for teams and not to pay incredible $$$ for heavy useless WYSIWYGs;
* give YOU these opportunities.

34
composer.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "axenov/pm-convert",
"type": "library",
"description": "Postman collection coverter",
"license": "MIT",
"homepage": "https://axenov.dev/",
"authors": [
{
"name": "Anthony Axenov",
"homepage": "https://axenov.dev"
}
],
"keywords": ["postman", "collection", "converter", "http", "wget", "curl", "api", "convert"],
"require": {
"php": "^8.1",
"ext-json": "*"
},
"bin": [
"pm-convert"
],
"autoload": {
"psr-4": {
"PmConverter\\": "src\\"
}
},
"scripts": {
"pm-convert": "@php ./pm-convert"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
}
}

21
composer.lock generated Normal file
View File

@ -0,0 +1,21 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b49bb03a97ad612632a24ebc59dfafcb",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.1",
"ext-json": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

28
pm-convert Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env php
<?php
declare(strict_types = 1);
use PmConverter\Processor;
$paths = [
__DIR__ . '/../../autoload.php',
__DIR__ . '/../autoload.php',
__DIR__ . '/vendor/autoload.php'
];
$file = null;
foreach ($paths as $path) {
if (file_exists($path)) {
require_once $file = $path;
break;
}
}
is_null($file) && throw new RuntimeException('Unable to locate autoload.php file.');
try {
(new Processor($argv))->start();
} catch (Exception $e) {
fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL));
die(1);
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exceptions;
use Exception;
class CannotCreateDirectoryException extends Exception
{
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exceptions;
use Exception;
class DirectoryIsNotReadableException extends Exception
{
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exceptions;
use Exception;
class DirectoryIsNotWriteableException extends Exception
{
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exceptions;
use Exception;
class DirectoryNotExistsException extends Exception
{
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Abstract;
use Exception;
use PmConverter\Exporters\{
RequestContract};
use PmConverter\FileSystem;
/**
*
*/
abstract class AbstractConverter
{
/**
* @var object|null
*/
protected ?object $collection = null;
/**
* @var string
*/
protected string $outputPath;
/**
* @throws Exception
*/
public function convert(object $collection, string $outputPath): void
{
$outputPath = sprintf('%s%s%s', $outputPath, DIRECTORY_SEPARATOR, static::OUTPUT_DIR);
$this->outputPath = FileSystem::makeDir($outputPath);
$this->collection = $collection;
foreach ($collection->item as $item) {
$this->convertItem($item);
}
}
/**
* @return string
*/
public function getOutputPath(): string
{
return $this->outputPath;
}
/**
* @param object $item
* @return bool
*/
protected function isItemFolder(object $item): bool
{
return !empty($item->item) && empty($item->request);
}
/**
* @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(DIRECTORY_SEPARATOR, $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));
}
}
/**
* @param object $item
* @return RequestContract
*/
protected function initRequest(object $item): RequestContract
{
$request_class = static::REQUEST;
$result = new $request_class();
$result->setName($item->name);
$result->setDescription($item->request?->description ?? null);
$result->setVerb($item->request->method);
$result->setUrl($item->request->url->raw);
$result->setHeaders($item->request->header);
if ($item->request->method !== 'GET' && !empty($item->request->body)) {
$result->setBody($item->request->body);
}
return $result;
}
/**
* @param RequestContract $request
* @param string|null $subpath
* @return bool
* @throws Exception
*/
protected function writeRequest(RequestContract $request, string $subpath = null): bool
{
$filedir = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $subpath);
$filedir = FileSystem::makeDir($filedir);
$filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $request->getName(), static::FILE_EXT);
return file_put_contents($filepath, (string)$request) > 0;
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Abstract;
use PmConverter\Exporters\Http\HttpRequest;
use PmConverter\Exporters\RequestContract;
/**
*
*/
abstract class AbstractRequest implements RequestContract
{
/**
* @var string
*/
protected string $http = 'HTTP/1.1'; //TODO verb
/**
* @var string
*/
protected string $name;
/**
* @var string|null
*/
protected ?string $description = null;
/**
* @var array
*/
protected array $headers = [];
/**
* @var mixed
*/
protected mixed $body = null;
/**
* @var string
*/
protected string $bodymode = 'raw';
/**
* @var string
*/
protected string $verb;
/**
* @var string
*/
protected string $url;
/**
* @param string $name
* @return HttpRequest
*/
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return str_replace(DIRECTORY_SEPARATOR, '_', $this->name);
}
/**
* @param string|null $description
* @return HttpRequest
*/
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
/**
* @param string $verb
* @return HttpRequest
*/
public function setVerb(string $verb): static
{
$this->verb = $verb;
return $this;
}
/**
* @param string $url
* @return HttpRequest
*/
public function setUrl(string $url): static
{
$this->url = $url;
return $this;
}
/**
* @param object[]|null $headers
* @return $this
*/
public function setHeaders(?array $headers): static
{
foreach ($headers as $header) {
$this->headers[$header->key] = [
'value' => $header->value,
'disabled' => $header?->disabled ?? false,
];
}
return $this;
}
/**
* @param string $bodymode
* @return HttpRequest
*/
public function setBodymode(string $bodymode): static
{
$this->bodymode = $bodymode;
return $this;
}
/**
* @param string $body
* @return HttpRequest
*/
public function setBody(object $body): static
{
$this->setBodymode($body->mode);
if (!empty($body->options) && $body->options->{$this->bodymode}->language === 'json') {
empty($this->headers['Content-Type']) && $this->setHeaders([
(object)[
'key' => 'Content-Type',
'value' => 'application/json',
'disabled' => false,
],
]);
}
$body->mode === 'formdata' && $this->setHeaders([
(object)[
'key' => 'Content-Type',
'value' => 'multipart/form-data',
'disabled' => false,
],
]);
$this->body = $body->{$body->mode};
return $this;
}
/**
* @return string
*/
abstract protected function prepareBody(): ?string;
/**
* @return string
*/
abstract public function __toString(): string;
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters;
use PmConverter\Exporters\Curl\CurlConverter;
use PmConverter\Exporters\Http\HttpConverter;
use PmConverter\Exporters\Wget\WgetConverter;
enum ConvertFormat: string
{
case Http = HttpConverter::class;
case Curl = CurlConverter::class;
case Wget = WgetConverter::class;
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters;
interface ConverterContract
{
public function convert(object $collection, string $outputPath): void;
public function getOutputPath(): string;
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Curl;
use PmConverter\Exporters\{
Abstract\AbstractConverter,
ConverterContract};
class CurlConverter extends AbstractConverter implements ConverterContract
{
protected const FILE_EXT = 'sh';
protected const OUTPUT_DIR = 'curl';
protected const REQUEST = CurlRequest::class;
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Curl;
use PmConverter\Exporters\Abstract\AbstractRequest;
/**
*
*/
class CurlRequest extends AbstractRequest
{
/**
* @return string
*/
protected function prepareBody(): ?string
{
switch ($this->bodymode) {
case 'formdata':
$body = [];
foreach ($this->body as $data) {
$body[] = sprintf(
"%s\t--form '%s=%s' \ ",
isset($data->disabled) ? '# ' : '',
$data->key,
$data->type === 'file' ? "@$data->src" : $data->value
);
}
return implode(PHP_EOL, $body);
default:
return $this->body;
}
}
/**
* @return string
*/
public function __toString(): string
{
$output[] = '#!/bin/sh';
if ($this->description) {
$output[] = '# ' . str_replace("\n", "\n# ", $this->description);
$output[] = '';
}
$output[] = "curl \ ";
$output[] = "\t--http1.1 \ "; //TODO verb
$output[] = "\t--request $this->verb \ ";
$output[] = "\t--location $this->url \ ";
foreach ($this->headers as $header_key => $header) {
if ($header['disabled']) {
continue;
}
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
}
if (!is_null($body = $this->prepareBody())) {
$output[] = match ($this->bodymode) {
'formdata' => $body,
default => "\t--data '$body'",
};
}
$output[] = rtrim(array_pop($output), '\ ');
return implode(PHP_EOL, $output);
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Http;
use PmConverter\Exporters\{
Abstract\AbstractConverter,
ConverterContract};
class HttpConverter extends AbstractConverter implements ConverterContract
{
protected const FILE_EXT = 'http';
protected const OUTPUT_DIR = 'http';
protected const REQUEST = HttpRequest::class;
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Http;
use PmConverter\Exporters\Abstract\AbstractRequest;
/**
*
*/
class HttpRequest extends AbstractRequest
{
/**
* @return string
*/
protected function prepareBody(): ?string
{
switch ($this->bodymode) {
case 'formdata':
$body = [];
foreach ($this->body as $data) {
$body[] = sprintf(
'%s%s=%s',
empty($data->disabled) ? '' : '# ',
$data->key,
$data->type === 'file' ? "$data->src" : $data->value
);
}
return implode(PHP_EOL, $body);
default:
return $this->body;
}
}
/**
* @return string
*/
public function __toString(): string
{
if ($this->description) {
$output[] = '# ' . str_replace("\n", "\n# ", $this->description);
$output[] = '';
}
$output[] = "$this->verb $this->url $this->http";
foreach ($this->headers as $header_key => $header) {
$output[] = sprintf('%s%s: %s', $header['disabled'] ? '# ' : '', $header_key, $header['value']);
}
$output[] = '';
$output[] = (string)$this->prepareBody();
return implode(PHP_EOL, $output);
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters;
interface RequestContract
{
public function setName(string $name): static;
public function getName(): string;
public function setDescription(?string $description): static;
public function setVerb(string $verb): static;
public function setUrl(string $url): static;
public function setHeaders(?array $headers): static;
public function setBodymode(string $bodymode): static;
public function setBody(object $body): static;
public function __toString(): string;
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Wget;
use PmConverter\Exporters\{
Abstract\AbstractConverter,
ConverterContract};
class WgetConverter extends AbstractConverter implements ConverterContract
{
protected const FILE_EXT = 'sh';
protected const OUTPUT_DIR = 'wget';
protected const REQUEST = WgetRequest::class;
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace PmConverter\Exporters\Wget;
use PmConverter\Exporters\Abstract\AbstractRequest;
/**
*
*/
class WgetRequest extends AbstractRequest
{
/**
* @return string
*/
protected function prepareBody(): ?string
{
switch ($this->bodymode) {
case 'formdata':
$lines = [];
foreach ($this->body as &$data) {
if ($data->type === 'file') {
continue;
}
$lines[$data->key] = $data->value;
}
$body[] = http_build_query($lines);
return implode(PHP_EOL, $body);
default:
return $this->body;
}
}
/**
* @return string
*/
public function __toString(): string
{
$output[] = '#!/bin/sh';
if ($this->description) {
$output[] = '# ' . str_replace("\n", "\n# ", $this->description);
$output[] = '';
}
$output[] = 'wget \ ';
$output[] = "\t--no-check-certificate \ ";
$output[] = "\t--quiet \ ";
$output[] = "\t--timeout=0 \ ";
$output[] = "\t--method $this->verb \ ";
foreach ($this->headers as $header_key => $header) {
if ($header['disabled']) {
continue;
}
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
}
if (!is_null($body = $this->prepareBody())) {
$output[] = "\t--body-data '$body' \ ";
}
$output[] = rtrim(array_pop($output), '\ ');
$output[] = "\t'$this->url'";
return implode(PHP_EOL, $output);
}
}

95
src/FileSystem.php Normal file
View File

@ -0,0 +1,95 @@
<?php
declare(strict_types = 1);
namespace PmConverter;
use PmConverter\Exceptions\{
CannotCreateDirectoryException,
DirectoryIsNotReadableException,
DirectoryIsNotWriteableException,
DirectoryNotExistsException};
class FileSystem
{
public static function normalizePath(string $path): string
{
$path = str_replace('~', $_SERVER['HOME'], $path);
return rtrim($path, DIRECTORY_SEPARATOR);
}
/**
* @param string $path
* @return string
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
public static function makeDir(string $path): string
{
$path = static::normalizePath($path);
if (!file_exists($path)) {
mkdir($path, recursive: true)
|| throw new CannotCreateDirectoryException("cannot create output directory: $path");
}
if (!is_writable($path)) {
throw new DirectoryIsNotWriteableException("output directory permissions are not valid: $path");
}
return $path;
}
/**
* @param string $path
* @return void
* @throws DirectoryIsNotReadableException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
*/
public static function removeDir(string $path): void
{
$path = static::normalizePath($path);
$dir_contents = static::dirContents($path);
foreach ($dir_contents as $record) {
is_dir($record) ? static::removeDir($record) : @unlink($record);
}
file_exists($path) && @rmdir($path);
}
/**
* @param string $path
* @return bool
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
*/
public static function checkDir(string $path): bool
{
$path = static::normalizePath($path);
if (!file_exists($path)) {
throw new DirectoryNotExistsException("output directory is not exist: $path");
}
if (!is_readable($path)) {
throw new DirectoryIsNotReadableException("output directory permissions are not valid: $path");
}
if (!is_writable($path)) {
throw new DirectoryIsNotWriteableException("output directory permissions are not valid: $path");
}
return true;
}
/**
* @param string $path
* @return array
* @throws DirectoryIsNotReadableException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
*/
public static function dirContents(string $path): array
{
$path = static::normalizePath($path);
$records = array_diff(@scandir($path) ?: [], ['.', '..']);
foreach ($records as &$record) {
$record = sprintf('%s%s%s', $path, DIRECTORY_SEPARATOR, $record);
}
return $records;
}
}

264
src/Processor.php Normal file
View File

@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace PmConverter;
use Exception;
use InvalidArgumentException;
use JsonException;
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotReadableException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\Exceptions\DirectoryNotExistsException;
use PmConverter\Exporters\ConverterContract;
use PmConverter\Exporters\ConvertFormat;
/**
*
*/
class Processor
{
/**
* Converter version
*/
public const VERSION = '1.0.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 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 object[] Collections that will be converted into choosen formats
*/
protected array $collections;
/**
* Constructor
*
* @param array $argv Arguments came from cli
*/
public function __construct(protected array $argv)
{
}
/**
* Parses an array of arguments came from cli
*
* @return void
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
*/
protected function parseArgs(): void
{
if (count($this->argv) < 2) {
die(implode(PHP_EOL, $this->usage()) . PHP_EOL);
}
foreach ($this->argv as $idx => $arg) {
switch ($arg) {
case '-f':
case '--file':
$path = $this->argv[$idx + 1];
if (empty($path) || !str_ends_with($path, '.json') || !file_exists($path) || !is_readable($path)) {
throw new InvalidArgumentException('a valid json-file path is expected for -f (--file)');
}
$this->collectionPaths[] = $this->argv[$idx + 1];
break;
case '-o':
case '--output':
if (empty($this->argv[$idx + 1])) {
throw new InvalidArgumentException('-o expected');
}
$this->outputPath = $this->argv[$idx + 1];
break;
case '-d':
case '--dir':
if (empty($this->argv[$idx + 1])) {
throw new InvalidArgumentException('a directory path is expected for -d (--dir)');
}
$path = $this->argv[$idx + 1];
if (FileSystem::checkDir($path)) {
$files = array_filter(
FileSystem::dirContents($path),
static fn($filename) => str_ends_with($filename, '.json')
);
$this->collectionPaths = array_unique(array_merge($this?->collectionPaths ?? [], $files));
}
break;
case '-p':
case '--preserve':
$this->preserveOutput = true;
break;
case '--http':
$this->formats[ConvertFormat::Http->name] = ConvertFormat::Http;
break;
case '--curl':
$this->formats[ConvertFormat::Curl->name] = ConvertFormat::Curl;
break;
case '--wget':
$this->formats[ConvertFormat::Wget->name] = ConvertFormat::Wget;
break;
case '-v':
case '--version':
die(implode(PHP_EOL, $this->version()) . PHP_EOL);
case '-h':
case '--help':
die(implode(PHP_EOL, $this->usage()) . PHP_EOL);
}
}
if (empty($this->formats)) {
$this->formats = [ConvertFormat::Http->name => ConvertFormat::Http];
}
}
/**
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
*/
protected function initOutputDirectory(): void
{
if (isset($this?->outputPath) && !$this->preserveOutput) {
FileSystem::removeDir($this->outputPath);
}
FileSystem::makeDir($this->outputPath);
}
/**
* Initializes converters according to choosen formats
*
* @return void
*/
protected function initConverters(): void
{
foreach ($this->formats as $type) {
$this->converters[$type->name] = new $type->value($this->preserveOutput);
}
}
/**
* @throws JsonException
*/
protected function initCollections(): void
{
foreach ($this->collectionPaths as $collectionPath) {
$content = file_get_contents(FileSystem::normalizePath($collectionPath));
$content = json_decode($content, flags: JSON_THROW_ON_ERROR);
if (!property_exists($content, 'collection') || empty($content?->collection)) {
throw new JsonException("not a valid collection: $collectionPath");
}
$this->collections[$content->collection->info->name] = $content->collection;
}
}
/**
* Begins a conversion
*
* @throws Exception
*/
public function start(): void
{
$this->parseArgs();
$this->initOutputDirectory();
$this->initConverters();
$this->initCollections();
print(implode(PHP_EOL, array_merge($this->version(), $this->copyright())) . PHP_EOL . PHP_EOL);
foreach ($this->collections as $collectionName => $collection) {
print("Converting '$collectionName':" . PHP_EOL);
foreach ($this->converters as $type => $exporter) {
print("\t-> " . strtolower($type));
$outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName);
$exporter->convert($collection, $outputPath);
printf("\t- OK: %s%s", $exporter->getOutputPath(), PHP_EOL);
}
}
}
/**
* @return string[]
*/
protected function version(): array
{
return ["Postman collection converter v" . self::VERSION];
}
/**
* @return string[]
*/
protected function copyright(): array
{
return [
'Anthony Axenov (c) ' . date('Y') . ", MIT license",
'https://git.axenov.dev/anthony/pm-convert'
];
}
/**
* @return array
*/
protected function usage(): array
{
return array_merge($this->version(), [
'Usage:',
"\t./pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]",
"\tphp pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]",
"\tcomposer pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]",
"\t./vendor/bin/pm-convert -f|-d PATH -o OUTPUT_PATH [ARGUMENTS] [FORMATS]",
'',
'Possible ARGUMENTS:',
"\t-f, --file - a PATH to single collection located in PATH to convert from",
"\t-d, --dir - a directory with collections located in COLLECTION_FILEPATH to convert from",
"\t-o, --output - a directory OUTPUT_PATH to put results in",
"\t-p, --preserve - do not delete OUTPUT_PATH (if exists)",
"\t-h, --help - show this help message and exit",
"\t-v, --version - show version info and exit",
'',
'If no ARGUMENTS passed then --help implied.',
'If both -c and -d are specified then only unique set of files will be converted.',
'-f or -d are required to be specified at least once, but each may be specified multiple times.',
'PATH must be a valid path to readable json-file or directory.',
'OUTPUT_PATH must be a valid path to writeable directory.',
'If -o is specified several times then only last one will be used.',
'',
'Possible FORMATS:',
"\t--http - generate raw *.http files (default)",
"\t--curl - generate shell scripts with curl command",
"\t--wget - generate shell scripts with wget command",
'If no FORMATS specified then --http implied.',
'Any of FORMATS can be specified at the same time.',
'',
'Example:',
" ./pm-convert \ ",
" -f ~/dir1/first.postman_collection.json \ ",
" --directory ~/team \ ",
" --file ~/dir2/second.postman_collection.json \ ",
" -d ~/personal \ ",
" -o ~/postman_export ",
"",
], $this->copyright());
}
}