34 Commits

Author SHA1 Message Date
83794a7464 Merge pull request 'v1.4.0' (#12) from dev into master
Reviewed-on: #12
2023-09-10 07:32:27 +00:00
1dc5f7deaf Bump version 2023-09-10 15:31:41 +08:00
0b4317f56a Introducing --var and some improvements in variables interpolation
- now you can override any env variable, see README to find out how --var works
- improvements around env and vars storage
- some tiny ram optimizations in Processor (not sure if it useful nor necessary, actually)
2023-09-10 15:28:09 +08:00
8ab615c062 README misc 2023-09-10 09:17:12 +08:00
da763a5a2f Bump version 2023-09-10 09:14:03 +08:00
af9406be47 Merge pull request 'v1.3.0' (#11) from dev into master
Reviewed-on: #11
2023-09-10 01:12:37 +00:00
1345b7eddb Refactorings
- important curl and wget improvements
- initial test coverage
- setters and getters in Request classes
- namespace fixes
- some additions in README
- docblocks and code-style
2023-09-10 09:03:54 +08:00
3734327357 Merge pull request 'v1.2.4' (#9) from dev into master
Reviewed-on: #9
2023-08-13 15:44:53 +00:00
af9c360684 Bump version 2023-08-13 23:43:21 +08:00
44f437eaf3 Misc edits in texts and output 2023-08-13 23:42:24 +08:00
1ebdffe2a6 Fixed again non-existent env when interpolating vars 2023-08-13 23:41:54 +08:00
d854732143 Merge pull request 'dev' (#7) from dev into master
Reviewed-on: #7
2023-08-13 15:12:22 +00:00
9b021296eb Bump version 2023-08-13 23:11:52 +08:00
d581afa793 Fixed non-existent env when interpolating vars 2023-08-13 23:11:37 +08:00
95ca655eb0 Merge pull request 'v1.2.2' (#5) from dev into master
Reviewed-on: #5
2023-08-13 03:04:03 +00:00
25887a47d3 Bump version 2023-08-13 11:02:59 +08:00
1f7816c917 When env was no set then dont try to collect vars from it
(cherry picked from commit fa8ad15fdd691fb4ea5db431062e89b512a9d526)
2023-08-13 11:02:08 +08:00
fb7b4baa32 Fixed wget file format
(cherry picked from commit 0febfd38aa3047555d34f7e125adb3773ee8a2da)
2023-08-13 11:02:07 +08:00
2c4eedbf8b When collection has no vars then dont try to collect them
(cherry picked from commit ee7528186f69e811b1b4db8e69ea2e4ec0ea2613)
2023-08-13 11:02:07 +08:00
c790adf611 Merge pull request 'v1.2.1' (#4) from dev into master
Reviewed-on: #4
2023-08-13 02:51:31 +00:00
c6094566b4 Fixed error when -o is missied 2023-08-13 10:50:52 +08:00
0ac5e64c17 Merge pull request 'v1.2.0' (#3) from dev into master
Reviewed-on: #3
2023-08-13 02:28:33 +00:00
40cc852328 Bump version 2023-08-13 10:26:46 +08:00
c4961b1238 Several cool features
- replace vars from collection by default
- replace vars from environment file with -e
- bearer auth header
2023-08-13 10:26:22 +08:00
c630af8795 Little output edit to prepare to some in-progress messages 2023-08-10 15:44:21 +08:00
9947987c20 Some refactorings in content generation 2023-08-10 15:43:18 +08:00
c5f928dc47 Merge pull request 'v1.1.1' (#2) from dev into master
Reviewed-on: #2
2023-08-10 05:33:30 +00:00
2081aa935a Bump version 2023-08-10 13:31:23 +08:00
1b3dd8b677 Improved detection of collections (affected on -f and -d) 2023-08-10 13:30:20 +08:00
9613697ada Fixed count bug 2023-08-09 21:31:08 +08:00
9ed0ddf79b Merge pull request 'v1.1.0' (#1) from dev into master
Reviewed-on: #1
2023-08-04 01:28:43 +00:00
b9c804b0f6 Bump version 2023-08-04 09:23:48 +08:00
882fbe4713 Time and RAM stats after conversion 2023-08-04 09:23:17 +08:00
aa583e7d43 Fixed some mistakes while processing -d 2023-08-04 09:05:22 +08:00
32 changed files with 3514 additions and 475 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/.idea
/.vscode
/vendor
.phpunit.result.cache
.phpunit.cache

102
README.md
View File

@@ -15,6 +15,8 @@ These formats are supported for now: `http`, `curl`, `wget`.
## Supported features
* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json);
* `Bearer` auth;
* 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);
* `json` body (forces header `Content-Type` to `application/json`);
@@ -22,21 +24,20 @@ These formats are supported for now: `http`, `curl`, `wget`.
## 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);
- conversion between postman schema v2.1 <-> v2.0 (#11);
- 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`_);
- documentation generation support (markdown) with responce examples (if present) (#6);
- 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.;
- 90%+ test coverage, phpcs, psalm, etc.;
- web version.
## Installation
## Install and upgrade
```
composer global r axenov/pm-convert
composer global r axenov/pm-convert # install
composer global u axenov/pm-convert # upgrade
```
Make sure your `~/.config/composer/vendor/bin` is in `$PATH` env:
@@ -53,45 +54,74 @@ export PATH="$PATH:~/.config/composer/vendor/bin"
$ 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]
./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
-f, --file - a PATH to a single collection file to convert from
-d, --dir - a PATH to a directory with collections to convert from
-o, --output - a directory OUTPUT_PATH to put results in
-e, --env - use environment file with variables to replace in requests
--var "NAME=VALUE" - force replace specified env variable called NAME with custom VALUE
(see interpolation notes below)
-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.
If no ARGUMENTS passed then --help implied.
If both -f and -d are specified then only unique set of files from both arguments 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.
If -o or -e was specified several times then only last one will be used.
Possible FORMATS:
--http - generate raw *.http files (default)
--curl - generate shell scripts with curl command
--wget - generate shell scripts with wget command
--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
Notes about variable interpolation:
1. You can use -e to tell where to find variables to replace in requests.
2. You can use one or several --var to replace specific env variables to your own value.
3. Correct syntax is `--var "NAME=VALUE". NAME may be in curly braces like {{NAME}}.
4. Since -e is optional, a bunch of --var will emulate an environment. Also it does not
matter if there is --var in environment file you provided or not.
5. Even if you (not) provided -e and/or --var, any of variable may still be overridden
from collection (if any), so last ones has top priority.
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.
Example:
./pm-convert \
-f ~/dir1/first.postman_collection.json \
--directory ~/team \
--file ~/dir2/second.postman_collection.json \
--env ~/localhost.postman_environment.json \
-d ~/personal \
--var "myvar=some value" \
-o ~/postman_export
```
### Notices
1. Result of `pm-convert` execution is bunch of generated files.
Most likely they will contain errors such as not interpolated `{{variables}}` values (due to missed ones in collection),
wrong command format or `GET`s with bodies.
You must review any generated file before using.
2. 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 any generated file may be accidently overwritten by another one.
## How to implement a new format
1. Create new namespace in `./src/Converters` and name it according to format of your choice
2. Create two classes for converter and request object which extends `Converters\Abstract\Abstract{Converter, Request}` respectively
3. Change constants values in your new request class according to format you want to implement
4. Write your own logic in converter's `__toString()` method, write new methods and override abstract ones
## License

View File

@@ -1,7 +1,7 @@
{
"name": "axenov/pm-convert",
"type": "library",
"description": "Postman collection coverter",
"description": "Postman collection converter",
"license": "MIT",
"homepage": "https://axenov.dev/",
"authors": [
@@ -15,9 +15,7 @@
"php": "^8.1",
"ext-json": "*"
},
"bin": [
"pm-convert"
],
"bin": ["pm-convert"],
"autoload": {
"psr-4": {
"PmConverter\\": "src\\"
@@ -30,5 +28,8 @@
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"require-dev": {
"phpunit/phpunit": "^10.3"
}
}

1614
composer.lock generated

File diff suppressed because it is too large Load Diff

26
phpunit.xml Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Main">
<!-- <directory>tests</directory>-->
<file>tests/AbstractRequestTest.php</file>
<file>tests/HttpRequestTest.php</file>
<file>tests/WgetRequestTest.php</file>
</testsuite>
</testsuites>
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env php
<?php
declare(strict_types = 1);
declare(strict_types=1);
use PmConverter\Processor;
@@ -20,8 +20,13 @@ foreach ($paths as $path) {
is_null($file) && throw new RuntimeException('Unable to locate autoload.php file.');
$processor = new Processor($argv);
try {
(new Processor($argv))->start();
$processor->convert();
} catch (InvalidArgumentException $e) {
fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL));
print(implode(PHP_EOL, $processor->usage()));
die(1);
} catch (Exception $e) {
fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL));
die(1);

View File

@@ -2,17 +2,20 @@
declare(strict_types=1);
namespace PmConverter\Exporters\Abstract;
namespace PmConverter\Converters\Abstract;
use Exception;
use PmConverter\Exporters\{
use PmConverter\Converters\{
ConverterContract,
RequestContract};
use PmConverter\Environment;
use PmConverter\Exceptions\InvalidHttpVersionException;
use PmConverter\FileSystem;
/**
*
*/
abstract class AbstractConverter
abstract class AbstractConverter implements ConverterContract
{
/**
* @var object|null
@@ -25,6 +28,25 @@ abstract class AbstractConverter
protected string $outputPath;
/**
* @var Environment|null
*/
protected ?Environment $env = null;
/**
* Sets an environment with vars
*
* @param Environment $env
* @return $this
*/
public function withEnv(Environment $env): static
{
$this->env = $env;
return $this;
}
/**
* Converts collection requests
*
* @throws Exception
*/
public function convert(object $collection, string $outputPath): void
@@ -32,12 +54,31 @@ abstract class AbstractConverter
$outputPath = sprintf('%s%s%s', $outputPath, DIRECTORY_SEPARATOR, static::OUTPUT_DIR);
$this->outputPath = FileSystem::makeDir($outputPath);
$this->collection = $collection;
$this->setVariables();
foreach ($collection->item as $item) {
$this->convertItem($item);
}
}
/**
* Prepares collection variables
*
* @return $this
*/
protected function setVariables(): 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;
}
}
return $this;
}
/**
* Returns output path
*
* @return string
*/
public function getOutputPath(): string
@@ -46,6 +87,8 @@ abstract class AbstractConverter
}
/**
* Checks whether item contains another items or not
*
* @param object $item
* @return bool
*/
@@ -55,6 +98,8 @@ abstract class AbstractConverter
}
/**
* Converts an item to request object and writes it into file
*
* @throws Exception
*/
protected function convertItem(mixed $item): void
@@ -77,18 +122,25 @@ abstract class AbstractConverter
}
/**
* Initialiazes request object to be written in file
*
* @param object $item
* @return RequestContract
* @throws InvalidHttpVersionException
*/
protected function initRequest(object $item): RequestContract
{
$request_class = static::REQUEST;
$request_class = static::REQUEST_CLASS;
/** @var RequestContract $result */
$result = new $request_class();
$result->setName($item->name);
$result->setHttpVersion(1.1); //TODO http version?
$result->setDescription($item->request?->description ?? null);
$result->setVerb($item->request->method);
$result->setUrl($item->request->url->raw);
$result->setHeaders($item->request->header);
$result->setAuth($item->request?->auth ?? $this->collection?->auth ?? null);
if ($item->request->method !== 'GET' && !empty($item->request->body)) {
$result->setBody($item->request->body);
}
@@ -96,6 +148,8 @@ abstract class AbstractConverter
}
/**
* Writes converted request object to file
*
* @param RequestContract $request
* @param string|null $subpath
* @return bool
@@ -106,6 +160,30 @@ abstract class AbstractConverter
$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;
$content = $this->interpolate((string)$request);
return file_put_contents($filepath, $content) > 0;
}
/**
* Replaces variables in request with values from collection or environment
*
* @param string $content
* @return string
*/
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;
}
}

View File

@@ -0,0 +1,295 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Abstract;
use PmConverter\Converters\RequestContract;
use PmConverter\Exceptions\{
EmptyHttpVerbException,
InvalidHttpVersionException};
use PmConverter\HttpVersions;
use Stringable;
/**
* Class to determine file content with any request format
*/
abstract class AbstractRequest implements Stringable, RequestContract
{
/**
* @var string HTTP verb (GET, POST, etc.)
*/
protected string $verb;
/**
* @var string URL where to send a request
*/
protected string $url;
/**
* @var float HTTP protocol version
*/
protected float $httpVersion = 1.1;
/**
* @var string Request name
*/
protected string $name;
/**
* @var string|null Request description
*/
protected ?string $description = null;
/**
* @var array Request headers
*/
protected array $headers = [];
/**
* @var mixed Request body
*/
protected mixed $body = null;
/**
* @var string Request body type
*/
protected string $bodymode = 'raw';
/**
* @inheritDoc
*/
public function setHttpVersion(float $version): static
{
if (!in_array($version, HttpVersions::values())) {
throw new InvalidHttpVersionException(
'Only these HTTP versions are supported: ' . implode(', ', HttpVersions::values())
);
}
$this->httpVersion = $version;
return $this;
}
/**
* @inheritDoc
*/
public function getHttpVersion(): float
{
return $this->httpVersion;
}
/**
* @inheritDoc
*/
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return str_replace(DIRECTORY_SEPARATOR, '_', $this->name);
}
/**
* @inheritDoc
*/
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
/**
* @inheritDoc
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @inheritDoc
*/
public function setVerb(string $verb): static
{
$this->verb = $verb;
return $this;
}
/**
* @inheritDoc
*/
public function getVerb(): string
{
empty($this->verb) && throw new EmptyHttpVerbException('Request HTTP verb must be defined before conversion');
return $this->verb;
}
/**
* @inheritDoc
*/
public function setUrl(string $url): static
{
$this->url = $url;
return $this;
}
/**
* @inheritDoc
*/
public function getUrl(): string
{
return $this->url ?: '<empty url>';
}
/**
* @inheritDoc
*/
public function setHeaders(?array $headers): static
{
foreach ($headers as $header) {
$this->setHeader($header->key, $header->value, $header?->disabled ?? false);
}
return $this;
}
/**
* @inheritDoc
*/
public function setHeader(string $name, mixed $value, bool $disabled = false): static
{
$this->headers[$name] = [
'value' => $value,
'disabled' => $disabled,
];
return $this;
}
/**
* @inheritDoc
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* @inheritDoc
*/
public function setAuth(?object $auth): static
{
if (!empty($auth)) {
switch ($auth->type) {
case 'bearer':
$this->setHeader('Authorization', 'Bearer ' . $auth->{$auth->type}[0]->value);
break;
default:
break;
}
}
return $this;
}
/**
* @inheritDoc
*/
public function setBodymode(string $bodymode): static
{
$this->bodymode = $bodymode;
return $this;
}
/**
* @inheritDoc
*/
public function getBodymode(): string
{
return $this->bodymode;
}
/**
* @inheritDoc
*/
public function setBody(object $body): static
{
$this->setBodymode($body->mode);
if ($body->mode === 'formdata') {
$this->setHeader('Content-Type', 'multipart/form-data')
->setFormdataBody($body);
} elseif (!empty($body->options) && $body->options->{$this->bodymode}->language === 'json') {
$this->setHeader('Content-Type', 'application/json')
->setJsonBody($body);
}
return $this;
}
/**
* Sets body content from multipart/formdata
*
* @param object $body
* @return $this
*/
protected function setFormdataBody(object $body): static
{
foreach ($body->formdata as $field) {
$this->body[$field->key] = [
'value' => $field->type === 'file' ? $field->src : $field->value,
'type' => $field->type,
];
}
return $this;
}
/**
* Sets body content from application/json
*
* @param object $body
* @return $this
*/
protected function setJsonBody(object $body): static
{
$this->body = $body->{$this->getBodymode()};
return $this;
}
/**
* @inheritDoc
*/
public function getBody(): mixed
{
return $this->body;
}
/**
* Returns array of description lines
*
* @return array
*/
abstract protected function prepareDescription(): array;
/**
* Returns array of headers
*
* @return array
*/
abstract protected function prepareHeaders(): array;
/**
* Returns array of request body lines
*
* @return array
*/
abstract protected function prepareBody(): array;
/**
* Converts request object to string to be written in result file
*
* @return string
*/
abstract public function __toString(): string;
}

View File

@@ -2,12 +2,13 @@
declare(strict_types=1);
namespace PmConverter\Exporters;
namespace PmConverter\Converters;
use PmConverter\Exporters\Curl\CurlConverter;
use PmConverter\Exporters\Http\HttpConverter;
use PmConverter\Exporters\Wget\WgetConverter;
use PmConverter\Converters\{
Curl\CurlConverter,
Http\HttpConverter,
Wget\WgetConverter};
enum ConvertFormat: string
{

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace PmConverter\Exporters;
namespace PmConverter\Converters;
interface ConverterContract
{

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Http;
use PmConverter\Converters\Abstract\AbstractRequest;
use PmConverter\Exceptions\{
EmptyHttpVerbException};
/**
* Class to determine file content with http request format
*/
class HttpRequest extends AbstractRequest
{
/**
* @inheritDoc
*/
protected function prepareDescription(): array
{
return empty($this->description)
? []
: ['# ' . str_replace("\n", "\n# ", $this->description), ''];
}
/**
* @inheritDoc
* @throws EmptyHttpVerbException
*/
protected function prepareHeaders(): array
{
$output[] = sprintf('%s %s HTTP/%s', $this->getVerb(), $this->getUrl(), $this->getHttpVersion());
foreach ($this->headers as $name => $data) {
$output[] = sprintf('%s%s: %s', $data['disabled'] ? '# ' : '', $name, $data['value']);
}
return $output;
}
/**
* @inheritDoc
*/
protected function prepareBody(): array
{
switch ($this->getBodymode()) {
case 'formdata':
$output = [''];
foreach ($this->body as $key => $data) {
$output[] = sprintf(
'%s%s=%s',
empty($data['disabled']) ? '' : '# ',
$key,
$data['type'] === 'file' ? '@' . $data['value'] : $data['value']
);
}
return $output;
default:
return ['', $this->body];
}
}
/**
* @inheritDoc
* @throws EmptyHttpVerbException
*/
public function __toString(): string
{
$output = array_merge(
$this->prepareDescription(),
$this->prepareHeaders(),
$this->prepareBody()
);
return implode(PHP_EOL, $output);
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters;
use PmConverter\Converters\Http\HttpRequest;
use PmConverter\Exceptions\{
EmptyHttpVerbException,
InvalidHttpVersionException};
interface RequestContract
{
/**
* Sets HTTP protocol version
*
* @param float $version
* @return $this
* @throws InvalidHttpVersionException
*/
public function setHttpVersion(float $version): static;
/**
* Returns HTTP protocol version
*
* @return float
*/
public function getHttpVersion(): float;
/**
* Sets name from collection item to request object
*
* @param string $name
* @return HttpRequest
*/
public function setName(string $name): static;
/**
* Returns name of request
*
* @return string
*/
public function getName(): string;
/**
* Sets description from collection item to request object
*
* @param string|null $description
* @return HttpRequest
*/
public function setDescription(?string $description): static;
/**
* Returns description request
*
* @return string|null
*/
public function getDescription(): ?string;
/**
* Sets HTTP verb from collection item to request object
*
* @param string $verb
* @return HttpRequest
*/
public function setVerb(string $verb): static;
/**
* Returns HTTP verb of request
*
* @return string
* @throws EmptyHttpVerbException
*/
public function getVerb(): string;
/**
* Sets URL from collection item to request object
*
* @param string $url
* @return HttpRequest
*/
public function setUrl(string $url): static;
/**
* Returns URL of request
*
* @return string
*/
public function getUrl(): string;
/**
* Sets headers from collection item to request object
*
* @param object[]|null $headers
* @return $this
*/
public function setHeaders(?array $headers): static;
/**
* Sets one header to request object
*
* @param string $name Header's name
* @param mixed $value Header's value
* @param bool $disabled Pass true to skip (or comment out) this header
* @return $this
*/
public function setHeader(string $name, mixed $value, bool $disabled = false): static;
/**
* Returns array of prepared headers
*
* @return array
*/
public function getHeaders(): array;
/**
* Sets authorization headers
*
* @param object|null $auth
* @return $this
*/
public function setAuth(object $auth): static;
/**
* Sets body mode from collection item to request object
*
* @param string $bodymode
* @return HttpRequest
*/
public function setBodymode(string $bodymode): static;
/**
* Returns body mode of request
*
* @return HttpRequest
*/
public function getBodymode(): string;
/**
* Sets body from collection item to request object
*
* @param object $body
* @return $this
*/
public function setBody(object $body): static;
/**
* Returns body content
*
* @return mixed
*/
public function getBody(): mixed;
}

View File

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

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Wget;
use PmConverter\Converters\Abstract\AbstractRequest;
use PmConverter\Exceptions\EmptyHttpVerbException;
/**
* Class to determine file content with wget request format
*/
class WgetRequest extends AbstractRequest
{
/**
* @inheritDoc
*/
protected function prepareDescription(): array
{
return empty($this->description)
? []
: ['# ' . str_replace("\n", "\n# ", $this->description), ''];
}
/**
* @inheritDoc
*/
protected function prepareHeaders(): array
{
$output = [];
foreach ($this->headers as $header_key => $header) {
if ($header['disabled']) {
continue;
}
$output[] = sprintf("\t--header '%s: %s' \ ", $header_key, $header['value']);
}
return $output;
}
/**
* @inheritDoc
*/
protected function prepareBody(): array
{
switch ($this->bodymode) {
case 'formdata':
$output = [];
foreach ($this->body as $key => $data) {
if ($data['type'] === 'file') {
continue;
}
$output[$key] = $data['value'];
}
return $output;
default:
return [$this->body];
}
}
/**
* @inheritDoc
* @throws EmptyHttpVerbException
*/
public function __toString(): string
{
$output = array_merge(
['#!/bin/sh'],
$this->prepareDescription(),
[
'wget \ ',
"\t--no-check-certificate \ ",
"\t--timeout 0 \ ",
"\t--method $this->verb \ ",
],
$this->prepareHeaders(),
);
if ($this->getBodymode() === 'formdata') {
if ($this->getBody()) {
if ($this->getVerb() === 'GET') {
$output[] = sprintf("\t%s?%s", $this->getUrl(), http_build_query($this->prepareBody()));
} else {
$output[] = sprintf("\t--body-data '%s' \ ", http_build_query($this->prepareBody()));
$output[] = sprintf("\t%s", $this->getUrl());
}
}
} else {
if ($this->getVerb() !== 'GET') {
$output[] = sprintf("\t--body-data '%s' \ ", implode("\n", $this->prepareBody()));
$output[] = sprintf("\t%s", $this->getUrl());
}
}
return implode(PHP_EOL, array_merge($output, ['']));
}
}

76
src/Environment.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace PmConverter;
class Environment implements \ArrayAccess
{
/**
* @var array
*/
protected array $vars = [];
/**
* @param object $env
*/
public function __construct(protected object $env)
{
foreach ($env->values as $var) {
$this->vars[static::formatKey($var->key)] = $var->value;
}
}
/**
* Tells if there are some vars or not
*
* @return bool
*/
public function hasVars(): bool
{
return !empty($this->vars);
}
/**
* @inheritDoc
*/
public function offsetExists(mixed $offset): bool
{
return array_key_exists(static::formatKey($offset), $this->vars);
}
/**
* @inheritDoc
*/
public function offsetGet(mixed $offset): mixed
{
return $this->vars[static::formatKey($offset)] ?? null;
}
/**
* @inheritDoc
*/
public function offsetSet(mixed $offset, mixed $value): void
{
$this->vars[static::formatKey($offset)] = $value;
}
/**
* @inheritDoc
*/
public function offsetUnset(mixed $offset): void
{
unset($this->vars[static::formatKey($offset)]);
}
/**
* Returns correct variable {{name}}
*
* @param string $key
* @return string
*/
public static function formatKey(string $key): string
{
return sprintf('{{%s}}', str_replace(['{', '}'], '', $key));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,164 +0,0 @@
<?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

@@ -1,65 +0,0 @@
<?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

@@ -1,53 +0,0 @@
<?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

@@ -1,18 +0,0 @@
<?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

@@ -1,63 +0,0 @@
<?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);
}
}

View File

@@ -1,6 +1,6 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace PmConverter;
@@ -10,8 +10,17 @@ use PmConverter\Exceptions\{
DirectoryIsNotWriteableException,
DirectoryNotExistsException};
/**
* Helper class to work with files and directories
*/
class FileSystem
{
/**
* Normalizes a given path
*
* @param string $path
* @return string
*/
public static function normalizePath(string $path): string
{
$path = str_replace('~', $_SERVER['HOME'], $path);
@@ -19,6 +28,8 @@ class FileSystem
}
/**
* Recursively creates a new directory by given path
*
* @param string $path
* @return string
* @throws CannotCreateDirectoryException
@@ -38,6 +49,8 @@ class FileSystem
}
/**
* Recursively removes a given directory
*
* @param string $path
* @return void
* @throws DirectoryIsNotReadableException
@@ -65,23 +78,22 @@ class FileSystem
{
$path = static::normalizePath($path);
if (!file_exists($path)) {
throw new DirectoryNotExistsException("output directory is not exist: $path");
throw new DirectoryNotExistsException("directory does not exist: $path");
}
if (!is_readable($path)) {
throw new DirectoryIsNotReadableException("output directory permissions are not valid: $path");
throw new DirectoryIsNotReadableException("directory permissions are not valid: $path");
}
if (!is_writable($path)) {
throw new DirectoryIsNotWriteableException("output directory permissions are not valid: $path");
throw new DirectoryIsNotWriteableException("directory permissions are not valid: $path");
}
return true;
}
/**
* Returns content of given directory path
*
* @param string $path
* @return array
* @throws DirectoryIsNotReadableException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
*/
public static function dirContents(string $path): array
{
@@ -92,4 +104,22 @@ class FileSystem
}
return $records;
}
/**
* Checks if a given file is a valid collection json file
*
* @param string $path
* @return bool
*/
public static function isCollectionFile(string $path): bool
{
$path = static::normalizePath($path);
return !empty($path = trim($path))
&& str_ends_with($path, '.postman_collection.json')
&& file_exists($path)
&& is_readable($path)
&& ($json = json_decode(file_get_contents($path), true))
&& json_last_error() === JSON_ERROR_NONE
&& isset($json['collection']['info']['name']);
}
}

21
src/HttpVersions.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PmConverter;
enum HttpVersions: string
{
case Version10 = '1.0';
case Version11 = '1.1';
case Version2 = '2';
case Version3 = '3';
public static function values(): array
{
return array_combine(
array_column(self::cases(), 'name'),
array_column(self::cases(), 'value'),
);
}
}

View File

@@ -7,27 +7,26 @@ 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;
use PmConverter\Converters\{
ConverterContract,
ConvertFormat};
use PmConverter\Exceptions\{
CannotCreateDirectoryException,
DirectoryIsNotReadableException,
DirectoryIsNotWriteableException,
DirectoryNotExistsException};
/**
*
*/
class Processor
{
/**
* Converter version
*/
public const VERSION = '1.0.0';
public const VERSION = '1.4.0';
/**
* @var string[] Paths to collection files
*/
protected array $collectionPaths;
protected array $collectionPaths = [];
/**
* @var string Output path where to put results in
@@ -39,6 +38,11 @@ class Processor
*/
protected bool $preserveOutput = false;
/**
* @var string[] Additional variables
*/
protected array $vars;
/**
* @var ConvertFormat[] Formats to convert a collections into
*/
@@ -52,7 +56,27 @@ class Processor
/**
* @var object[] Collections that will be converted into choosen formats
*/
protected array $collections;
protected array $collections = [];
/**
* @var int Initial timestamp
*/
protected int $initTime;
/**
* @var int Initial RAM usage
*/
protected int $initRam;
/**
* @var string Path to environment file
*/
protected string $envFile;
/**
* @var Environment
*/
protected Environment $env;
/**
* Constructor
@@ -61,15 +85,14 @@ class Processor
*/
public function __construct(protected array $argv)
{
$this->initTime = hrtime(true);
$this->initRam = memory_get_usage(true);
}
/**
* Parses an array of arguments came from cli
*
* @return void
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
*/
protected function parseArgs(): void
{
@@ -80,60 +103,87 @@ class Processor
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)');
$rawpath = $this->argv[$idx + 1];
$normpath = FileSystem::normalizePath($rawpath);
if (!FileSystem::isCollectionFile($normpath)) {
throw new InvalidArgumentException(
sprintf("this is not a valid collection file:%s\t%s %s", PHP_EOL, $arg, $rawpath)
);
}
$this->collectionPaths[] = $this->argv[$idx + 1];
break;
case '-o':
case '--output':
if (empty($this->argv[$idx + 1])) {
throw new InvalidArgumentException('-o expected');
throw new InvalidArgumentException('-o is required');
}
$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));
}
$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));
break;
case '-e':
case '--env':
$this->envFile = FileSystem::normalizePath($this->argv[$idx + 1]);
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 '--var':
[$var, $value] = explode('=', trim($this->argv[$idx + 1]));
$this->vars[$var] = $value;
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->collectionPaths)) {
throw new InvalidArgumentException('there are no collections to convert');
}
if (empty($this->outputPath)) {
throw new InvalidArgumentException('-o is required');
}
if (empty($this->formats)) {
$this->formats = [ConvertFormat::Http->name => ConvertFormat::Http];
}
}
/**
* Initializes output directory
*
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
@@ -158,9 +208,12 @@ class Processor
foreach ($this->formats as $type) {
$this->converters[$type->name] = new $type->value($this->preserveOutput);
}
unset($this->formats);
}
/**
* Initializes collection objects
*
* @throws JsonException
*/
protected function initCollections(): void
@@ -173,6 +226,30 @@ class Processor
}
$this->collections[$content->collection->info->name] = $content->collection;
}
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);
}
/**
@@ -180,28 +257,54 @@ class Processor
*
* @throws Exception
*/
public function start(): void
public function convert(): void
{
$this->parseArgs();
$this->initOutputDirectory();
$this->initConverters();
$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);
foreach ($this->collections as $collectionName => $collection) {
print("Converting '$collectionName':" . PHP_EOL);
++$current;
printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, PHP_EOL);
foreach ($this->converters as $type => $exporter) {
print("\t-> " . strtolower($type));
printf('> %s%s', strtolower($type), PHP_EOL);
$outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName);
if (!empty($this->env)) {
$exporter->withEnv($this->env);
}
$exporter->convert($collection, $outputPath);
printf("\t- OK: %s%s", $exporter->getOutputPath(), PHP_EOL);
printf(' OK: %s%s', $exporter->getOutputPath(), PHP_EOL);
}
print(PHP_EOL);
++$success;
}
unset($this->converters, $type, $exporter, $outputPath, $this->collections, $collectionName, $collection);
$this->printStats($success, $current);
}
/**
* Outputs some statistics
*
* @param int $success
* @param int $count
* @return void
*/
protected function printStats(int $success, int $count): void
{
$time = (hrtime(true) - $this->initTime) / 1_000_000;
$ram = (memory_get_peak_usage(true) - $this->initRam) / 1024 / 1024;
printf('Converted %d of %d in %.3f ms using up to %.3f MiB RAM%s', $success, $count, $time, $ram, PHP_EOL);
}
/**
* @return string[]
*/
protected function version(): array
public function version(): array
{
return ["Postman collection converter v" . self::VERSION];
}
@@ -209,7 +312,7 @@ class Processor
/**
* @return string[]
*/
protected function copyright(): array
public function copyright(): array
{
return [
'Anthony Axenov (c) ' . date('Y') . ", MIT license",
@@ -220,7 +323,7 @@ class Processor
/**
* @return array
*/
protected function usage(): array
public function usage(): array
{
return array_merge($this->version(), [
'Usage:',
@@ -230,33 +333,48 @@ class Processor
"\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",
"\t-f, --file - a PATH to a single collection file to convert from",
"\t-d, --dir - a PATH to a directory with collections to convert from",
"\t-o, --output - a directory OUTPUT_PATH to put results in",
"\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 (see interpolation notes below)",
"\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.',
'If both -f and -d are specified then only unique set of files from both arguments 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.',
'If -o or -e was 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.',
'',
'Notes about variable interpolation:',
"\t1. You can use -e to tell where to find variables to replace in requests.",
"\t2. You can use one or several --var to replace specific env variables to your own value.",
"\t3. Correct syntax is `--var \"NAME=VALUE\". NAME may be in curly braces like {{NAME}}.",
"\t4. Since -e is optional, a bunch of --var will emulate an environment. Also it does not",
"\t matter if there is --var in environment file you provided or not.",
"\t5. Even if you (not) provided -e and/or --var, any of variable may still be overridden",
"\t from collection (if any), so last ones has top priority.",
'',
'Example:',
" ./pm-convert \ ",
" -f ~/dir1/first.postman_collection.json \ ",
" --directory ~/team \ ",
" --file ~/dir2/second.postman_collection.json \ ",
" --env ~/localhost.postman_environment.json \ ",
" -d ~/personal \ ",
" --var \"myvar=some value\" \ ",
" -o ~/postman_export ",
"",
], $this->copyright());

View File

@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PmConverter\Exceptions\{
EmptyHttpVerbException,
InvalidHttpVersionException};
class AbstractRequestTest extends TestCase
{
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\HttpVersions
* @return void
* @throws InvalidHttpVersionException
*/
public function testHttpVersion(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setHttpVersion(2.0);
$this->assertSame(2.0, $request->getHttpVersion());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::getVerb()
* @covers PmConverter\HttpVersions
* @return void
* @throws InvalidHttpVersionException
*/
public function testInvalidHttpVersionException(): void
{
$this->expectException(InvalidHttpVersionException::class);
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setHttpVersion(5);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setVerb()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getVerb()
* @return void
* @throws EmptyHttpVerbException
*/
public function testVerb(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setVerb('GET');
$this->assertSame('GET', $request->getVerb());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::getVerb()
* @return void
* @throws EmptyHttpVerbException
*/
public function testEmptyHttpVerbException(): void
{
$this->expectException(EmptyHttpVerbException::class);
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->getVerb();
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setUrl()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getUrl()
* @return void
*/
public function testUrl(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setUrl('http://localhost');
$this->assertSame('http://localhost', $request->getUrl());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setName()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getName()
* @return void
*/
public function testName(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setName('lorem ipsum');
$this->assertSame('lorem ipsum', $request->getName());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setDescription()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getDescription()
* @return void
*/
public function testDescription(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setDescription("lorem ipsum\ndolor sit\namet");
$this->assertSame("lorem ipsum\ndolor sit\namet", $request->getDescription());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setBodymode()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getBodymode()
* @return void
*/
public function testBodyMode(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setBodymode('raw');
$this->assertSame('raw', $request->getBodymode());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setHeaders()
* @covers PmConverter\Converters\Abstract\AbstractRequest::setHeader()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getHeaders()
* @return void
*/
public function testHeaders(): void
{
$headers = [
(object)[
'key' => 'Header 1',
'value' => 'Value 1',
'disabled' => true,
],
(object)[
'key' => 'Header 2',
'value' => 'Value 2',
'disabled' => false,
],
(object)[
'key' => 'Header 3',
'value' => 'Value 3',
],
];
$expected = [
'Header 1' => [
'value' => 'Value 1',
'disabled' => true,
],
'Header 2' => [
'value' => 'Value 2',
'disabled' => false,
],
'Header 3' => [
'value' => 'Value 3',
'disabled' => false,
],
'Header 4' => [
'value' => 'Value 4',
'disabled' => false,
],
'Header 5' => [
'value' => 'Value 5',
'disabled' => true,
],
];
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setHeaders($headers)
->setHeader('Header 4', 'Value 4')
->setHeader('Header 5', 'Value 5', true);
$this->assertSame($expected, $request->getHeaders());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setAuth()
* @return void
*/
public function testAuthBearer(): void
{
$auth = (object)[
'type' => 'bearer',
'bearer' => [
(object)[
'key' => 'token',
'value' => 'qwerty',
'type' => 'string',
]
]
];
$expected = [
'Authorization' => [
'value' => 'Bearer qwerty',
'disabled' => false,
],
];
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setAuth($auth);
$this->assertSame($expected, $request->getHeaders());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setBodymode()
* @covers PmConverter\Converters\Abstract\AbstractRequest::setHeader()
* @covers PmConverter\Converters\Abstract\AbstractRequest::setBody()
* @covers PmConverter\Converters\Abstract\AbstractRequest::setJsonBody()
* @covers PmConverter\Converters\Abstract\AbstractRequest::getBody()
* @return void
*/
public function testJson(): void
{
$body = (object)[
'mode' => 'raw',
'raw' => $expectedBody = '["lorem ipsum dolor sit amet"]',
'options' => (object)[
'raw' => (object)[
'language' => 'json',
]
]
];
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setBody($body);
$expectedHeaders = [
'Content-Type' => [
'value' => 'application/json',
'disabled' => false,
],
];
$this->assertSame($expectedHeaders, $request->getHeaders());
$this->assertSame($expectedBody, $request->getBody());
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::setBodymode
* @covers PmConverter\Converters\Abstract\AbstractRequest::setHeader
* @covers PmConverter\Converters\Abstract\AbstractRequest::setBody
* @covers PmConverter\Converters\Abstract\AbstractRequest::setFormdataBody
* @covers PmConverter\Converters\Abstract\AbstractRequest::getBody
* @return void
*/
public function testFormdata(): void
{
$body = (object)[
'mode' => 'formdata',
'formdata' => [
(object)[
'key' => 'param1',
'value' => 'value1',
'type' => 'text',
],
(object)[
'key' => 'param2',
'src' => '/tmp/somefile.txt',
'type' => 'file',
],
],
];
$expectedBody = [
'param1' => [
'value' => 'value1',
'type' => 'text',
],
'param2' => [
'value' => '/tmp/somefile.txt',
'type' => 'file',
],
];
$expectedHeaders = [
'Content-Type' => [
'value' => 'multipart/form-data',
'disabled' => false,
],
];
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setBody($body);
$this->assertSame($expectedHeaders, $request->getHeaders());
$this->assertSame($expectedBody, $request->getBody());
}
}

140
tests/HttpRequestTest.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PmConverter\Converters\Http\HttpRequest;
class HttpRequestTest extends TestCase
{
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Http\HttpRequest
* @covers PmConverter\Converters\Http\HttpRequest::prepareDescription()
* @covers PmConverter\Converters\Http\HttpRequest::__toString()
* @return void
*/
public function testPrepareDescription()
{
$description = [
'lorem ipsum',
'dolor sit',
'amet',
];
$needle = implode("\n", [
'# lorem ipsum',
'# dolor sit',
'# amet',
]);
$request = (new HttpRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setDescription(implode("\n", $description));
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Http\HttpRequest
* @covers PmConverter\Converters\Http\HttpRequest::prepareHeaders()
* @covers PmConverter\Converters\Http\HttpRequest::__toString()
* @return void
*/
public function testPrepareHeaders()
{
$headers = [
(object)[
'key' => 'Header1',
'value' => 'Value 1',
'disabled' => true,
],
(object)[
'key' => 'Header2',
'value' => 'Value 2',
'disabled' => false,
],
(object)[
'key' => 'Header3',
'value' => 'Value 3',
],
];
$needle = implode("\n", [
'# Header1: Value 1',
'Header2: Value 2',
'Header3: Value 3',
]);
$request = (new HttpRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setHeaders($headers);
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Http\HttpRequest
* @covers PmConverter\Converters\Http\HttpRequest::prepareBody()
* @covers PmConverter\Converters\Http\HttpRequest::__toString()
* @return void
*/
public function testPrepareFormdataBody()
{
$body = (object)[
'mode' => 'formdata',
'formdata' => [
(object)[
'key' => 'param1',
'value' => 'value1',
'type' => 'text',
],
(object)[
'key' => 'param2',
'src' => '/tmp/somefile.txt',
'type' => 'file',
],
],
];
$needle = implode("\n", [
'param1=value1',
'param2=@/tmp/somefile.txt',
]);
$request = (new HttpRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Http\HttpRequest
* @covers PmConverter\Converters\Http\HttpRequest::prepareBody()
* @covers PmConverter\Converters\Http\HttpRequest::__toString()
* @return void
*/
public function testPrepareJsonBody()
{
$body = (object)[
'mode' => 'raw',
'raw' => $needle = '["lorem ipsum dolor sit amet"]',
'options' => (object)[
'raw' => (object)[
'language' => 'json',
]
]
];
$request = (new HttpRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringContainsString($needle, (string)$request);
}
}

232
tests/WgetRequestTest.php Normal file
View File

@@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PmConverter\Converters\Wget\WgetRequest;
class WgetRequestTest extends TestCase
{
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareDescription()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareNotEmptyDescription()
{
$request = (new WgetRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setDescription(null);
$result = explode("\n", (string)$request);
$this->assertFalse(str_starts_with($result[0], '# ') && str_starts_with($result[1], '# '));
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareDescription()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareEmptyDescription()
{
$description = [
'lorem ipsum',
'dolor sit',
'amet',
];
$needle = implode("\n", [
'# lorem ipsum',
'# dolor sit',
'# amet',
]);
$request = (new WgetRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setDescription(implode("\n", $description));
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareHeaders()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareHeaders()
{
$headers = [
(object)[
'key' => 'Header1',
'value' => 'Value 1',
'disabled' => true,
],
(object)[
'key' => 'Header2',
'value' => 'Value 2',
'disabled' => false,
],
(object)[
'key' => 'Header3',
'value' => 'Value 3',
],
];
$needle = implode("\n", [
"\t--header 'Header2: Value 2' \ ",
"\t--header 'Header3: Value 3' \ ",
]);
$request = (new WgetRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setHeaders($headers);
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareBody()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareFormdataBodyGet()
{
$body = (object)[
'mode' => 'formdata',
'formdata' => [
(object)[
'key' => 'param1',
'value' => 'value1',
'type' => 'text',
],
(object)[
'key' => 'param2',
'src' => '/tmp/somefile.txt',
'type' => 'file',
],
(object)[
'key' => 'param3',
'value' => 'value3',
'type' => 'text',
],
],
];
$needle = 'http://localhost?' . http_build_query([
'param1' => 'value1',
'param3' => 'value3',
]);
$request = (new WgetRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareBody()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareFormdataBodyPost()
{
$body = (object)[
'mode' => 'formdata',
'formdata' => [
(object)[
'key' => 'param1',
'value' => 'value1',
'type' => 'text',
],
(object)[
'key' => 'param2',
'src' => '/tmp/somefile.txt',
'type' => 'file',
],
(object)[
'key' => 'param3',
'value' => 'value3',
'type' => 'text',
],
],
];
$needle = http_build_query([
'param1' => 'value1',
'param3' => 'value3',
]);
$request = (new WgetRequest())
->setVerb('POST')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareBody()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareJsonBodyGet()
{
$body = (object)[
'mode' => 'raw',
'raw' => $needle = '["lorem ipsum dolor sit amet"]',
'options' => (object)[
'raw' => (object)[
'language' => 'json',
]
]
];
$request = (new WgetRequest())
->setVerb('GET')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringNotContainsString($needle, (string)$request);
}
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Wget\WgetRequest
* @covers PmConverter\Converters\Wget\WgetRequest::prepareBody()
* @covers PmConverter\Converters\Wget\WgetRequest::__toString()
* @return void
*/
public function testPrepareJsonBodyPost()
{
$body = (object)[
'mode' => 'raw',
'raw' => $needle = '["lorem ipsum dolor sit amet"]',
'options' => (object)[
'raw' => (object)[
'language' => 'json',
]
]
];
$request = (new WgetRequest())
->setVerb('POST')
->setUrl('http://localhost')
->setBody($body);
$this->assertStringContainsString($needle, (string)$request);
}
}