38 Commits
v1.1.1 ... dev

Author SHA1 Message Date
a2cc18a434 Misc 2024-11-09 19:05:37 +08:00
58e6157f40 Some refactorings related to main Processor class 2024-08-09 12:21:39 +08:00
2bf9345f69 Minimum version php8.1 => 8.2 + dev-dependecies updated 2024-08-04 23:17:04 +08:00
e5dad3f2d6 Fixed execution without params 2024-07-08 00:14:37 +08:00
47930b010f Introducing settings file, some refactorings and stabilization 2024-07-07 23:44:59 +08:00
5666702ccc Error stacktrace with --dev enabled while conversion is in progress
Also shorter constants.
2023-10-03 09:05:03 +08:00
771fe4931a Bump version 2023-09-18 00:40:23 +08:00
c6bdcfe7cc New flag --all, corrections in --help and README 2023-09-18 00:39:33 +08:00
60cad4b501 Skip postman schema conversion if it is already same as specified (#10) 2023-09-18 00:33:37 +08:00
01d29ee023 Initial and very naive conversion v2.0 => v2.1 (#10) 2023-09-17 23:59:37 +08:00
3c1871ce1f Conversion to 2.0 (--v2.0), improvements and fixes (#10)
First of all, now you can convert a collection manually exported from Postman UI.

Until this commit, any collection json had to be inside of root 'collection' object. Postman API returns collections in a such way and that was my case. So any collection exported using UI was mistakenly not detected as correct one.

The second thing that it is now possible to convert collections from v2.1 to v2.0 using --v2.0 flag. Even if they are exported via Postman API, of course.

Also some important refactorings are here.
2023-09-17 23:09:19 +08:00
5c85f23514 Bump version 2023-09-10 15:58:31 +08:00
35e1984326 JSON body & header was fixed 2023-09-10 15:57:39 +08: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
48 changed files with 5241 additions and 832 deletions

4
.gitignore vendored
View File

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

176
README.md
View File

@@ -8,13 +8,18 @@ Without 3rd-party dependencies.
These formats are supported for now: `http`, `curl`, `wget`.
> This project was quickly written in my spare time to solve one exact problem in one NDA-project, so it may
> contain stupid errors and (for sure) doesn't cover all possible cases according to collection schema.
> So feel free to propose your improvements.
> This project has been started and quickly written in my spare time to solve one exact problem in one NDA-project,
> so it may contain stupid errors and (for sure) doesn't cover all possible cases according to collection schema.
> Feel free to propose your improvements.
Versions older than the latest are not supported, only current one is.
If you found an error in old version please ensure if an error you found has been fixed in latest version.
So please always use the latest version of `pm-convert`.
## Supported features
* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json);
* collection schemas [**v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json) and [**v2.0**](https://schema.postman.com/json/collection/v2.0.0/collection.json);
* replace vars in requests by stored in collection and environment file;
* export one or several collections (or even whole directories) into one or all of formats supported at the same time;
* all headers (including disabled for `http`-format);
* `json` body (forces header `Content-Type` to `application/json`);
@@ -22,20 +27,19 @@ 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 only `json` and `formdata`_);
- documentation generation support (markdown) with responce examples (if present);
- 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` supported_);
- documentation generation support (markdown) with response examples (if present) (#6);
- maybe some another convert formats (like httpie or something...);
- replace `{{vars}}` from folder;
- replace `{{vars}}` from environment;
- 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:
@@ -52,45 +56,151 @@ 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
-p, --preserve - do not delete OUTPUT_PATH (if exists)
--dump - convert provided arguments into settings file in `pwd`
-h, --help - show this help message and exit
--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
--v2.0 - convert from Postman Collection Schema v2.1 into v2.0
--v2.1 - convert from Postman Collection Schema v2.0 into v2.1
-a, --all - convert to all of formats listed above
If no FORMATS specified then --http implied.
Any of FORMATS can be specified at the same time.
Any of FORMATS can be specified at the same time or replaced by --all.
Example:
./pm-convert \
-f ~/dir1/first.postman_collection.json \
--directory ~/team \
--file ~/dir2/second.postman_collection.json \
--env ~/localhost.postman_environment.json \
-d ~/personal \
-o ~/postman_export
--var "myvar=some value" \
-o ~/postman_export \
--all
```
### 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.
### 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.
## 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.
### Notes about conversion between Postman Schemas
You can use `--v2.1` to convert v2.1 into v2.1 (and this is not a typo).
Same applies to `--v2.0`.
There is a case when a collection has been exported via Postman API.
In such case collection itself places in single root object called `collection` like this:
```
{
"collection": {
// your actual collection here
}
}
```
So, pm-convert will just raise actual data up on top level and write into disk.
## Settings file
You may want to specify parameters once and just use them everytime without explicit defining arguments to `pm-convert`.
This might be done in several ways.
1. Save this file as `pm-convert-settings.json` in your project directory:
```json
{
"directories": [],
"files": [],
"environment": "",
"output": "",
"preserveOutput": false,
"formats": [],
"vars": {}
}
```
Fill it with values you need.
2. Add `--dump` at the end of your command and all arguments you provided will be converted and saved as
`pm-convert-settings.json` in your curent working directory. For example in `--help` file will contain this:
```json
{
"directories": [
"~/team",
"~/personal"
],
"files": [
"~/dir1/first.postman_collection.json",
"~/dir2/second.postman_collection.json"
],
"environment": "~/localhost.postman_environment.json",
"output": "~/postman_export",
"preserveOutput": false,
"formats": [
"http",
"curl",
"wget",
"v2.0",
"v2.1"
],
"vars": {
"myvar": "some value"
}
}
```
If settings file already exists then you will be asked what to do: overwrite it, back it up or exit.
Once settings file saved in current you can just run `pm-convert`.
Settings will be applied like if you pass them explicitly via arguments.
## How to implement a new format
1. Create new namespace in `./src/Converters` and name it according to format of your choice.
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. Add your converter class name in `Converters\ConvertFormat`.
5. Write your own logic in converter, 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": [
@@ -12,12 +12,12 @@
],
"keywords": ["postman", "collection", "converter", "http", "wget", "curl", "api", "convert"],
"require": {
"php": "^8.1",
"ext-json": "*"
"php": "^8.2",
"ext-json": "*",
"ext-mbstring": "*",
"ext-readline": "*"
},
"bin": [
"pm-convert"
],
"bin": ["pm-convert"],
"autoload": {
"psr-4": {
"PmConverter\\": "src\\"
@@ -30,5 +30,8 @@
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"require-dev": {
"phpunit/phpunit": "^10.3"
}
}

1635
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,9 +1,13 @@
#!/usr/bin/env php
<?php
declare(strict_types = 1);
declare(strict_types=1);
use PmConverter\Processor;
use PmConverter\Handler;
const EOL = PHP_EOL;
const DS = DIRECTORY_SEPARATOR;
const PM_VERSION = '1.7';
$paths = [
__DIR__ . '/../../autoload.php',
@@ -20,9 +24,13 @@ foreach ($paths as $path) {
is_null($file) && throw new RuntimeException('Unable to locate autoload.php file.');
$handler = new Handler();
$handler::printVersion();
$handler::printCopyright();
try {
(new Processor($argv))->convert();
$handler->init($argv);
$handler->start();
} catch (Exception $e) {
fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), PHP_EOL));
die(1);
fwrite(STDERR, sprintf('ERROR: %s%s', $e->getMessage(), EOL));
exit(1);
}

178
src/ArgumentParser.php Normal file
View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace PmConverter;
use InvalidArgumentException;
use PmConverter\Converters\ConvertFormat;
use PmConverter\Enums\ArgumentNames as AN;
class ArgumentParser
{
/**
* @var array Raw arguments passed from cli ($argv)
*/
protected readonly array $raw;
/**
* @var array Parsed and ready to use
*/
protected array $parsed = [];
/**
* Constructor
*
* @param array $argv Raw arguments passed from cli ($argv)
*/
public function __construct(array $argv)
{
$this->raw = array_slice($argv, 1);
}
/**
* Parses raw arguments
*
* @return array Settings according to settings file format
*/
public function parse(): array
{
foreach ($this->raw as $idx => $arg) {
switch ($arg) {
case '-c':
case '--config':
if (empty($this->raw[$idx + 1])) {
throw new InvalidArgumentException('a configuration file path is expected for -c (--config)');
}
if (isset($this->parsed[AN::Config])) {
printf(
"INFO: Config file is already set to '%s' and will be overwritten to '%s'",
$this->parsed[AN::Config],
$this->raw[$idx + 1],
);
}
$this->parsed[AN::Config] = $this->raw[$idx + 1];
break;
case '-d':
case '--dir':
if (empty($this->raw[$idx + 1])) {
throw new InvalidArgumentException('a directory path is expected for -d (--dir)');
}
$this->parsed[AN::Directories][] = $this->raw[$idx + 1];
break;
case '-f':
case '--file':
if (empty($this->raw[$idx + 1])) {
throw new InvalidArgumentException('a directory path is expected for -f (--file)');
}
$this->parsed[AN::Files][] = $this->raw[$idx + 1];
break;
case '-e':
case '--env':
if (empty($this->raw[$idx + 1])) {
throw new InvalidArgumentException('an environment file path is expected for -e (--env)');
}
$this->parsed[AN::Environment][] = $this->raw[$idx + 1];
break;
case '-o':
case '--output':
if (empty($this->raw[$idx + 1])) {
throw new InvalidArgumentException('an output path is expected for -o (--output)');
}
$this->parsed[AN::Output][] = $this->raw[$idx + 1];
break;
case '-p':
case '--preserve':
$this->parsed[AN::PreserveOutput] = true;
break;
case '--http':
$this->parsed[AN::Formats][] = ConvertFormat::Http;
break;
case '--curl':
$this->parsed[AN::Formats][] = ConvertFormat::Curl;
break;
case '--wget':
$this->parsed[AN::Formats][] = ConvertFormat::Wget;
break;
case '--v2.0':
$this->parsed[AN::Formats][] = ConvertFormat::Postman20;
break;
case '--v2.1':
$this->parsed[AN::Formats][] = ConvertFormat::Postman21;
break;
case '-a':
case '--all':
foreach (ConvertFormat::cases() as $format) {
$this->parsed[AN::Formats][] = $format;
}
break;
case '--var':
$definition = trim($this->raw[$idx + 1]);
$name = strtok($definition, '='); // take first part before equal sign as var name
$value = strtok(''); // take the rest of argument as var value
if (isset($this->parsed[AN::Vars][$name])) {
printf(
"INFO: Variable '%s' is already set to '%s' and will be overwritten to '%s'",
$name,
$this->parsed[AN::Vars][$name],
$value,
);
}
$this->parsed[AN::Vars][$name] = $value;
break;
case '--dev':
$this->parsed[AN::DevMode] = true;
break;
case '-v':
case '--verbose':
$this->parsed[AN::Verbose] = true;
break;
case '--dump':
$this->parsed[AN::Dump] = true;
break;
case '--version':
$this->parsed[AN::Version] = true;
break;
case '-h':
case '--help':
$this->parsed[AN::Help] = true;
break;
}
}
foreach ([AN::Directories, AN::Files, AN::Formats] as $field) {
if (!empty($this->parsed[$field])) {
$this->parsed[$field] = array_unique($this->parsed[$field] ?? []);
}
}
return $this->parsed;
}
/**
* Returns parsed arguments (if set) or parses raw ones
*
* @return array
*/
public function parsed(): array
{
return $this->parsed ??= $this->parse();
}
}

170
src/Collection.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types = 1);
namespace PmConverter;
use Exception;
use Generator;
use JsonException;
use PmConverter\Enums\CollectionVersion;
use Stringable;
/**
* Class that describes a request collection
*
* @property array|object $item
* @property object $info
* @property object|null $variable
*/
class Collection implements Stringable
{
public readonly CollectionVersion $version;
/**
* Closed constructor so that we could use factory methods
*
* @param object $json
*/
private function __construct(protected object $json)
{
// specific case when collection has been exported via postman api
if (property_exists($json, 'collection')) {
$json = $json->collection;
}
$this->json = $json;
$this->version = $this->detectVersion();
}
/**
* Factory that creates new Collection from content read from file path
*
* @param string $path
* @return static
* @throws JsonException
*/
public static function fromFile(string $path): static
{
$content = file_get_contents(FileSystem::normalizePath($path));
$json = json_decode($content, flags: JSON_THROW_ON_ERROR);
return new static($json);
}
/**
* @inheritDoc
*/
public function __toString(): string
{
return json_encode($this->json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
/**
* Returns reference to the parsed json structure
*
* @return object
*/
public function &raw(): object
{
return $this->json;
}
/**
* Returns reference to any part of the parsed json structure
*
* @param string $name
* @return mixed
*/
public function &__get(string $name): mixed
{
return $this->json->$name;
}
/**
* Returns collection name
*
* @return string
*/
public function name(): string
{
return $this->json->info->name;
}
/**
* Returns the collection version
*
* @return CollectionVersion
*/
protected function detectVersion(): CollectionVersion
{
return match (true) {
str_contains($this->json->info->schema, '/v2.0.') => CollectionVersion::Version20,
str_contains($this->json->info->schema, '/v2.1.') => CollectionVersion::Version21,
default => CollectionVersion::Unknown
};
}
/**
* Returns the collection version from raw file
*
* @param string $filepath
* @return CollectionVersion
* @throws Exception
*/
public static function detectFileVersion(string $filepath): CollectionVersion
{
$content = file_get_contents($filepath);
if ($content === false) {
throw new Exception("cannot read file: $filepath");
}
$json = json_decode($content, true);
$schema = $json['info']['schema'] ?? '';
if (str_ends_with($schema, 'v2.0.0/collection.json')) {
return CollectionVersion::Version20;
}
if (str_ends_with($schema, 'v2.1.0/collection.json')) {
return CollectionVersion::Version21;
}
return CollectionVersion::Unknown;
}
/**
* Iterates over collection request items and returns item associated by its path in folder
*
* @param mixed|null $item
* @return Generator
*/
public function iterate(mixed $item = null): Generator
{
$is_recursive = !is_null($item);
$folder = $is_recursive ? $item : $this->json;
static $dir_tree;
$path = DS . ($is_recursive ? implode(DS, $dir_tree ?? []) : '');
foreach ($folder->item as $subitem) {
if ($this->isItemFolder($subitem)) {
$dir_tree[] = $subitem->name;
yield from $this->iterate($subitem);
continue;
}
yield $path => $subitem;
}
$is_recursive && array_pop($dir_tree);
}
/**
* Checks whether item contains another items or not
*
* @param object $item
* @return bool
*/
protected function isItemFolder(object $item): bool
{
return !empty($item->item)
&& is_array($item->item)
&& empty($item->request);
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Abstract;
use Exception;
use Iterator;
use PmConverter\Collection;
use PmConverter\Converters\RequestContract;
use PmConverter\Environment;
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\Exceptions\InvalidHttpVersionException;
use PmConverter\FileSystem;
/**
*
*/
abstract class AbstractConverter
{
/**
* @var Collection|null
*/
protected ?Collection $collection = null;
/**
* @var string
*/
protected string $outputPath;
/**
* @var RequestContract[] Converted requests
*/
protected array $requests = [];
/**
* Sets output path
*
* @param string $outputPath
* @return $this
*/
public function to(string $outputPath): self
{
$this->outputPath = sprintf('%s%s%s', $outputPath, DS, static::OUTPUT_DIR);
return $this;
}
/**
* Converts requests from collection
*
* @param Collection $collection
* @return static
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
* @throws Exception
*/
public function convert(Collection $collection): static
{
$this->collection = $collection;
$this->outputPath = FileSystem::makeDir($this->outputPath);
$this->setCollectionVars();
foreach ($collection->iterate() as $path => $item) {
// $this->requests[$path][] = $this->makeRequest($item);
$request = $this->makeRequest($item);
$this->writeRequest($request, $path);
}
return $this;
}
/**
* Returns converted requests
*
* @return Iterator<string, RequestContract>
*/
public function converted(): Iterator
{
foreach ($this->requests as $path => $requests) {
foreach ($requests as $request) {
yield $path => $request;
}
}
}
/**
* Writes requests on disk
*
* @throws Exception
*/
public function flush(): void
{
foreach ($this->converted() as $path => $request) {
$this->writeRequest($request, $path);
}
}
/**
* Prepares collection variables
*
* @return $this
*/
protected function setCollectionVars(): static
{
foreach ($this->collection?->variable ?? [] as $var) {
Environment::instance()->setCustomVar($var->key, $var->value);
}
return $this;
}
/**
* Returns output path
*
* @return string
*/
public function getOutputPath(): string
{
return $this->outputPath;
}
/**
* Checks whether item contains another items or not
*
* @param object $item
* @return bool
*/
protected function isItemFolder(object $item): bool
{
return !empty($item->item)
&& is_array($item->item)
&& empty($item->request);
}
/**
* Initialiazes request object to be written in file
*
* @param object $item
* @return RequestContract
* @throws InvalidHttpVersionException
*/
protected function makeRequest(object $item): RequestContract
{
$request_class = static::REQUEST_CLASS;
/** @var RequestContract $request */
$request = new $request_class();
$request->setName($item->name);
$request->setVersion($this->collection->version);
$request->setHttpVersion(1.1); //TODO http version?
$request->setDescription($item->request?->description ?? null);
$request->setVerb($item->request->method);
$request->setUrl($item->request->url);
$request->setHeaders($item->request->header);
$request->setAuth($item->request?->auth ?? $this->collection?->auth ?? null);
if ($item->request->method !== 'GET' && !empty($item->request->body)) {
$request->setBody($item->request->body);
}
return $request;
}
/**
* Writes converted request object to file
*
* @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, DS, $subpath);
$filedir = FileSystem::makeDir($filedir);
$filepath = sprintf('%s%s%s.%s', $filedir, DS, $request->getName(), static::FILE_EXT);
$content = $this->interpolate((string)$request);
return file_put_contents($filepath, $content) > 0;
}
/**
* Replaces variables in request with values from collection or environment
*
* @param string $content
* @return string
*/
protected function interpolate(string $content): string
{
$replace = static fn ($a) => Environment::instance()->var($var = $a[0]) ?: $var;
return Environment::instance()->hasVars()
? preg_replace_callback('/\{\{.*}}/m', $replace, $content)
: $content;
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Abstract;
use PmConverter\Converters\RequestContract;
use PmConverter\Enums\CollectionVersion;
use PmConverter\Enums\HttpVersion;
use PmConverter\Exceptions\EmptyHttpVerbException;
use PmConverter\Exceptions\InvalidHttpVersionException;
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 object|string URL where to send a request
*/
protected object|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';
protected CollectionVersion $version;
public function setVersion(CollectionVersion $version): static
{
$this->version = $version;
return $this;
}
/**
* @inheritDoc
*/
public function setHttpVersion(float $version): static
{
if (!in_array($version, HttpVersion::values())) {
throw new InvalidHttpVersionException(
'Only these HTTP versions are supported: ' . implode(', ', HttpVersion::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(DS, '_', $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(object|string $url): static
{
$this->url = $url;
return $this;
}
/**
* @inheritDoc
*/
public function getRawUrl(): string
{
return is_object($this->url) ? $this->url->raw : $this->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 ' . match ($this->version) {
CollectionVersion::Version20 => $auth->{$auth->type}->token,
CollectionVersion::Version21 => $auth->{$auth->type}[0]->value,
default => null
});
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 ($body->mode === 'raw') {
$this->setBodyAsIs($body);
if (!empty($body->options) && $body->options->{$this->bodymode}->language === 'json') {
$this->setHeader('Content-Type', 'application/json');
}
}
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 setBodyAsIs(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

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters;
use PmConverter\Converters\Curl\CurlConverter;
use PmConverter\Converters\Http\HttpConverter;
use PmConverter\Converters\Postman20\Postman20Converter;
use PmConverter\Converters\Postman21\Postman21Converter;
use PmConverter\Converters\Wget\WgetConverter;
enum ConvertFormat: string
{
case Http = HttpConverter::class;
case Curl = CurlConverter::class;
case Wget = WgetConverter::class;
case Postman20 = Postman20Converter::class;
case Postman21 = Postman21Converter::class;
public static function fromArg(string $arg): self
{
return match ($arg) {
'http' => ConvertFormat::Http,
'curl' => ConvertFormat::Curl,
'wget' => ConvertFormat::Wget,
'v2.0' => ConvertFormat::Postman20,
'v2.1' => ConvertFormat::Postman21,
};
}
public function toArg(): string
{
return match ($this) {
ConvertFormat::Http => 'http',
ConvertFormat::Curl => 'curl',
ConvertFormat::Wget => 'wget',
ConvertFormat::Postman20 => 'v2.0',
ConvertFormat::Postman21 => 'v2.1',
};
}
}

View File

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

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Curl;
use PmConverter\Converters\Abstract\AbstractConverter;
class CurlConverter extends AbstractConverter
{
protected const FILE_EXT = 'sh';
protected const OUTPUT_DIR = 'curl';
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->getRawUrl()} \ ",
],
$this->prepareHeaders(),
$this->prepareBody()
);
$output[] = rtrim(array_pop($output), '\ ');
return implode(EOL, array_merge($output, ['']));
}
}

View File

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

View File

@@ -0,0 +1,73 @@
<?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->getRawUrl(), $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(EOL, $output);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Postman20;
use PmConverter\Collection;
use PmConverter\Converters\Abstract\AbstractConverter;
use PmConverter\Enums\CollectionVersion;
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\FileSystem;
/**
* Converts Postman Collection v2.1 to v2.0
*/
class Postman20Converter extends AbstractConverter
{
protected const FILE_EXT = 'v20.postman_collection.json';
protected const OUTPUT_DIR = 'pm-v2.0';
/**
* Converts collection requests
*
* @param Collection $collection
* @return static
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
public function convert(Collection $collection): static
{
$this->collection = $collection;
// if data was exported from API, here is already valid json to
// just flush it in file, otherwise we need to convert it deeper
if ($this->collection->version === CollectionVersion::Version21) {
$this->collection->info->schema = str_replace('/v2.1.', '/v2.0.', $this->collection->info->schema);
$this->convertAuth($this->collection->raw());
foreach ($this->collection->item as $item) {
$this->convertItem($item);
}
}
$this->outputPath = FileSystem::makeDir($this->outputPath);
$this->writeCollection();
return $this;
}
/**
* Writes converted collection into file
*
* @return bool
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
protected function writeCollection(): bool
{
$filedir = FileSystem::makeDir($this->outputPath);
$filepath = sprintf('%s%s%s.%s', $filedir, DS, $this->collection->name(), static::FILE_EXT);
return file_put_contents($filepath, $this->collection) > 0;
}
/**
* Changes some requests fields in place
*
* @param mixed $item
* @return void
*/
protected function convertItem(mixed $item): void
{
if ($this->isItemFolder($item)) {
foreach ($item->item as $subitem) {
if ($this->isItemFolder($subitem)) {
$this->convertItem($subitem);
} else {
$this->convertAuth($subitem->request);
$this->convertRequestUrl($subitem->request);
$this->convertResponseUrls($subitem->response);
}
}
} else {
$this->convertAuth($item->request);
$this->convertRequestUrl($item->request);
$this->convertResponseUrls($item->response);
}
}
/**
* Converts auth object from v2.1 to v2.0
*
* @param object $request
* @return void
*/
protected function convertAuth(object $request): void
{
if (empty($request->auth)) {
return;
}
$auth = ['type' => 'noauth'];
$type = strtolower($request->auth->type);
if ($type !== 'noauth') {
foreach ($request->auth->$type as $param) {
$auth[$param->key] = $param->value ?? '';
}
$request->auth->$type = (object)$auth;
}
}
/**
* Converts requests URLs from object v2.1 to string v2.0
*
* @param object $request
* @return void
*/
protected function convertRequestUrl(object $request): void
{
if (is_object($request->url)) {
$request->url = $request->url->raw;
}
}
/**
* Converts URLs response examples from object v2.1 to string v2.0
*
* @param array $responses
* @return void
*/
protected function convertResponseUrls(array $responses): void
{
foreach ($responses as $response) {
if (is_object($response->originalRequest->url)) {
$response->originalRequest->url = $response->originalRequest->url->raw;
}
}
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Postman21;
use PmConverter\Collection;
use PmConverter\Converters\Abstract\AbstractConverter;
use PmConverter\Enums\CollectionVersion;
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\FileSystem;
/**
* Converts Postman Collection v2.0 to v2.1
*/
class Postman21Converter extends AbstractConverter
{
protected const FILE_EXT = 'v21.postman_collection.json';
protected const OUTPUT_DIR = 'pm-v2.1';
/**
* Converts collection requests
*
* @param Collection $collection
* @return static
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
public function convert(Collection $collection): static
{
$this->collection = $collection;
// if data was exported from API, here is already valid json to
// just flush it in file, otherwise we need to convert it deeper
if ($this->collection->version === CollectionVersion::Version20) {
$this->collection->info->schema = str_replace('/v2.0.', '/v2.1.', $this->collection->info->schema);
$this->convertAuth($this->collection->raw());
foreach ($this->collection->item as $item) {
$this->convertItem($item);
}
}
$this->outputPath = FileSystem::makeDir($this->outputPath);
$this->writeCollection();
return $this;
}
/**
* Writes converted collection into file
*
* @return bool
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
protected function writeCollection(): bool
{
$filedir = FileSystem::makeDir($this->outputPath);
$filepath = sprintf('%s%s%s.%s', $filedir, DS, $this->collection->name(), static::FILE_EXT);
return file_put_contents($filepath, $this->collection) > 0;
}
/**
* Changes some requests fields in place
*
* @param mixed $item
* @return void
*/
protected function convertItem(mixed $item): void
{
if ($this->isItemFolder($item)) {
foreach ($item->item as $subitem) {
if ($this->isItemFolder($subitem)) {
$this->convertItem($subitem);
} else {
$this->convertAuth($subitem->request);
$this->convertRequestUrl($subitem->request);
$this->convertResponseUrls($subitem->response);
}
}
} else {
$this->convertAuth($item->request);
$this->convertRequestUrl($item->request);
$this->convertResponseUrls($item->response);
}
}
/**
* Converts auth object from v2.0 to v2.1
*
* @param object $request
* @return void
*/
protected function convertAuth(object $request): void
{
if (empty($request->auth)) {
return;
}
$type = $request->auth->type;
if ($type !== 'noauth' && isset($request->auth->$type)) {
$auth = [];
foreach ($request->auth->$type as $key => $value) {
$auth[] = (object)[
'key' => $key,
'value' => $value,
'type' => 'string',
];
}
$request->auth->$type = $auth;
}
}
/**
* Converts requests URLs from string v2.0 to object v2.1
*
* @param object $request
* @return void
*/
protected function convertRequestUrl(object $request): void
{
if (is_string($request->url) && mb_strlen($request->url) > 0) {
$data = array_values(array_filter(explode('/', $request->url))); //TODO URL parsing
if (count($data) === 1) {
$url = [
'raw' => $request->url,
'host' => [$data[0] ?? $request->url],
];
} else {
$url = [
'raw' => $request->url,
'protocol' => str_replace(':', '', $data[0]),
'host' => [$data[1] ?? $request->url],
'path' => array_slice($data, 2),
];
}
$request->url = (object)$url;
}
}
/**
* Converts URLs response examples from string v2.0 to object v2.1
*
* @param array $responses
* @return void
*/
protected function convertResponseUrls(array $responses): void
{
foreach ($responses as $response) {
if (is_string($response->originalRequest->url)) {
$data = array_values(array_filter(explode('/', $response->originalRequest->url))); //TODO URL parsing
if (count($data) === 1) {
$url = [
'raw' => $response->originalRequest->url,
'host' => [$data[0] ?? $response->originalRequest->url],
];
} else {
$url = [
'raw' => $response->originalRequest->url,
'protocol' => str_replace(':', '', $data[0]),
'host' => [$data[1] ?? $response->originalRequest->url],
'path' => array_slice($data, 2),
];
}
$response->originalRequest->url = (object)$url;
}
}
}
}

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 getRawUrl(): 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 $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

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Wget;
use PmConverter\Converters\Abstract\AbstractConverter;
class WgetConverter extends AbstractConverter
{
protected const FILE_EXT = 'sh';
protected const OUTPUT_DIR = 'wget';
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->getRawUrl(), http_build_query($this->prepareBody()));
} else {
$output[] = sprintf("\t--body-data '%s' \ ", http_build_query($this->prepareBody()));
$output[] = sprintf("\t%s", $this->getRawUrl());
}
}
} else {
if ($this->getVerb() !== 'GET') {
$output[] = sprintf("\t--body-data '%s' \ ", implode("\n", $this->prepareBody()));
$output[] = sprintf("\t%s", $this->getRawUrl());
}
}
return implode(EOL, array_merge($output, ['']));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace PmConverter\Enums;
/**
* Argument names
*/
class ArgumentNames
{
public const Config = 'config';
public const Directories = 'directories';
public const Files = 'files';
public const Environment = 'environment';
public const Output = 'output';
public const PreserveOutput = 'preserveOutput';
public const Formats = 'formats';
public const Vars = 'vars';
public const DevMode = 'devMode';
public const Verbose = 'verbose';
public const Dump = 'dump';
public const Version = 'version';
public const Help = 'help';
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace PmConverter\Enums;
enum CollectionVersion: string
{
case Version20 = 'v2.0';
case Version21 = 'v2.1';
case Unknown = 'unknown';
}

21
src/Enums/HttpVersion.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PmConverter\Enums;
enum HttpVersion: 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'),
);
}
}

216
src/Environment.php Normal file
View File

@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
namespace PmConverter;
use ArrayAccess;
use JsonException;
/**
*
*/
class Environment implements ArrayAccess
{
/**
* @var string Path to env file
*/
protected static string $filepath = '';
/**
* @var Environment
*/
protected static Environment $instance;
/**
* @var array
*/
protected array $ownVars = [];
/**
* @var array
*/
protected array $customVars = [];
public static function instance(): static
{
return static::$instance ??= new static();
}
/**
* @param string|null $filepath
* @return $this
* @throws JsonException
*/
public function readFromFile(?string $filepath): static
{
if (empty($filepath)) {
return $this;
}
$content = file_get_contents(static::$filepath = $filepath);
$content = json_decode($content, flags: JSON_THROW_ON_ERROR); //TODO try-catch
$content || throw new JsonException("not a valid environment: $filepath");
property_exists($content, 'environment') && $content = $content->environment;
if (!property_exists($content, 'id') && !property_exists($content, 'name')) {
throw new JsonException("not a valid environment: $filepath");
}
return $this->setOwnVars($content->values);
}
/**
* @param array $vars
* @return $this
*/
protected function setOwnVars(array $vars): static
{
foreach ($vars as $key => $value) {
is_object($value) && [$key, $value] = [$value->key, $value->value];
$this->setOwnVar($key, $value);
}
return $this;
}
/**
* Sets value to some environment own variable
*
* @param string $name
* @param string $value
* @return $this
*/
protected function setOwnVar(string $name, string $value): static
{
$this->ownVars[static::formatKey($name)] = $value;
return $this;
}
/**
* @param array $vars
* @return $this
*/
public function setCustomVars(array $vars): static
{
foreach ($vars as $key => $value) {
is_object($value) && [$key, $value] = [$value->key, $value->value];
$this->setCustomVar($key, $value);
}
return $this;
}
/**
* Sets value to some environment own variable
*
* @param string $name
* @param string $value
* @return $this
*/
public function setCustomVar(string $name, string $value): static
{
$this->customVars[static::formatKey($name)] = $value;
return $this;
}
/**
* Returns value of specific variable
*
* @param string $name
* @return mixed
*/
public function var(string $name): mixed
{
$format_key = static::formatKey($name);
return $this->ownVars[$format_key] ?? $this->customVars[$format_key] ?? null;
}
/**
* Returns array of own and custom variables
*
* @return string[]
*/
public function vars(): array
{
return array_merge($this->ownVars, $this->customVars);
}
/**
* Returns array of custom variables
*
* @return string[]
*/
public function customVars(): array
{
return $this->customVars;
}
/**
* Tells if there are some vars or not
*
* @return bool
*/
public function hasVars(): bool
{
return !empty($this->ownVars) && !empty($this->customVars);
}
/**
* Closed constructor
*/
protected function __construct()
{
}
/**
* @inheritDoc
*/
public function offsetExists(mixed $offset): bool
{
return array_key_exists(static::formatKey($offset), $this->vars());
}
/**
* @inheritDoc
*/
public function offsetGet(mixed $offset): mixed
{
return $this->var($offset);
}
/**
* @inheritDoc
*/
public function offsetSet(mixed $offset, mixed $value): void
{
$this->customVars[static::formatKey($offset)] = $value;
}
/**
* @inheritDoc
*/
public function offsetUnset(mixed $offset): void
{
unset($this->customVars[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 IncorrectSettingsFileException 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,111 +0,0 @@
<?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

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

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

View File

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

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

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

@@ -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,14 +1,16 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
namespace PmConverter;
use PmConverter\Exceptions\{
CannotCreateDirectoryException,
DirectoryIsNotReadableException,
DirectoryIsNotWriteableException,
DirectoryNotExistsException};
use Exception;
use JsonException;
use PmConverter\Enums\CollectionVersion;
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotReadableException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\Exceptions\DirectoryNotExistsException;
/**
* Helper class to work with files and directories
@@ -23,8 +25,17 @@ class FileSystem
*/
public static function normalizePath(string $path): string
{
$path = str_replace('~', $_SERVER['HOME'], $path);
return rtrim($path, DIRECTORY_SEPARATOR);
$path = trim($path);
if (str_starts_with($path, '~' . DS)) {
$path = str_replace('~' . DS, $_SERVER['HOME'] . DS, $path);
} elseif (str_starts_with($path, '.' . DS)) {
$path = str_replace('.' . DS, $_SERVER['PWD'] . DS, $path);
} elseif (!str_starts_with($path, DS)) {
$path = $_SERVER['PWD'] . DS . $path;
}
return rtrim($path, DS);
}
/**
@@ -100,7 +111,7 @@ class FileSystem
$path = static::normalizePath($path);
$records = array_diff(@scandir($path) ?: [], ['.', '..']);
foreach ($records as &$record) {
$record = sprintf('%s%s%s', $path, DIRECTORY_SEPARATOR, $record);
$record = sprintf('%s%s%s', $path, DS, $record);
}
return $records;
}
@@ -110,16 +121,17 @@ class FileSystem
*
* @param string $path
* @return bool
* @throws JsonException
* @throws Exception
*/
public static function isCollectionFile(string $path): bool
{
$path = static::normalizePath($path);
return !empty($path = trim($path))
return !empty($path)
&& str_ends_with($path, '.postman_collection.json')
&& file_exists($path)
&& is_file($path)
&& is_readable($path)
&& ($json = json_decode(file_get_contents($path), true))
&& json_last_error() === JSON_ERROR_NONE
&& isset($json['collection']['info']['name']);
&& Collection::detectFileVersion($path) !== CollectionVersion::Unknown;
}
}

238
src/Handler.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types = 1);
namespace PmConverter;
use JsonException;
use PmConverter\Enums\ArgumentNames as AN;
class Handler
{
/**
* @var array Ready to use arguments
*/
protected array $arguments;
/**
* @var Settings Settings read from file and merged with provided in cli
*/
protected Settings $settings;
/**
* @var Environment Environment laoded from file with custom vars provided
*/
protected Environment $env;
/**
* @var Processor Object that do convertions according to settings
*/
protected Processor $processor;
/**
* Initializes main flow
*
* @param array $argv Raw arguments passed from cli
* @return void
* @throws JsonException
* @throws \Exception
*/
public function init(array $argv): void
{
$this->arguments = (new ArgumentParser($argv))->parsed();
if (!empty($this->arguments[AN::Help])) {
self::printHelp();
exit;
}
if (!empty($this->arguments[AN::Version])) {
self::printVersion();
exit;
}
$this->settings = new Settings();
$this->settings->loadFromFile($this->arguments[AN::Config] ?? null);
$this->settings->override($this->arguments);
if (empty($this->settings->collectionPaths())) {
throw new \Exception('at least 1 collection file must be defined');
}
if (!empty($arguments[AN::Dump])) {
$this->handleSettingsDump();
}
$this->env = Environment::instance()
->readFromFile($this->settings->envFilepath())
->setCustomVars($this->settings->vars());
}
/**
* Starts convertions
*
* @return void
* @throws Exceptions\CannotCreateDirectoryException
* @throws Exceptions\DirectoryIsNotReadableException
* @throws Exceptions\DirectoryIsNotWriteableException
* @throws Exceptions\DirectoryNotExistsException
* @throws Exceptions\IncorrectSettingsFileException
* @throws JsonException
*/
public function start(): void
{
$this->processor = new Processor($this->settings, $this->env);
$this->processor->start();
}
/**
* Handles settings file saving when requested by --dump
*
* @return never
*/
protected function handleSettingsDump(): never
{
$answer = 'o';
if ($this->settings->fileExists()) {
echo 'Settings file already exists: ' . $this->settings->filePath() . EOL;
echo 'Do you want to (o)verwrite it, (b)ackup it and create new one or (c)ancel (default)?' . EOL;
$answer = strtolower(trim(readline('> ')));
}
if (!in_array($answer, ['o', 'b'])) {
die('Current settings file has not been changed' . EOL);
}
if ($answer === 'b') {
$filepath = $this->settings->backup();
printf("Settings file has been backed up to file:%s\t%s%s", EOL, $filepath, EOL);
}
$this->settings->dump();
printf("Arguments has been converted into settings file:%s\t%s%s", EOL, $this->settings->filePath(), EOL);
die('Review and edit it if needed.' . EOL);
}
/**
* Returns usage help strings
*
* @return array
*/
protected static function help(): array
{
return array_merge(self::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 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-p, --preserve - do not delete OUTPUT_PATH (if exists)",
"\t --dump - convert provided arguments into settings file in `pwd",
"\t-h, --help - show this help message and exit",
"\t-v, --version - show version info and exit",
'',
'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 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",
"\t--v2.0 - convert from Postman Collection Schema v2.1 into v2.0",
"\t--v2.1 - convert from Postman Collection Schema v2.0 into v2.1",
"\t-a, --all - convert to all of formats listed above",
'',
'If no FORMATS specified then --http implied.',
'Any of FORMATS can be specified at the same time or replaced by --all.',
'',
'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 \ ",
" --all",
"",
], self::copyright());
}
/**
* Prints usage help message in stdout
*
* @return void
*/
public static function printHelp(): void
{
self::printArray(self::help());
}
/**
* Returns version strings
*
* @return string[]
*/
protected static function version(): array
{
return ['Postman collection converter v' . PM_VERSION, ''];
}
/**
* Prints version message in stdout
*
* @return void
*/
public static function printVersion(): void
{
self::printArray(self::version());
}
/**
* Returns copyright strings
*
* @return string[]
*/
protected static function copyright(): array
{
return [
'Anthony Axenov (c) 2023 - ' . (int)date('Y') . ', MIT license',
'https://git.axenov.dev/anthony/pm-convert',
'',
];
}
/**
* Prints copyright message in stdout
*
* @return void
*/
public static function printCopyright(): void
{
self::printArray(self::copyright());
}
/**
* Prints an arrays of string to stdout
*
* @param ...$strings
* @return void
*/
protected static function printArray(...$strings): void
{
fwrite(STDOUT, implode("\n", array_merge(...$strings)));
}
}

View File

@@ -5,227 +5,142 @@ declare(strict_types=1);
namespace PmConverter;
use Exception;
use InvalidArgumentException;
use Generator;
use JsonException;
use PmConverter\Converters\Abstract\AbstractConverter;
use PmConverter\Converters\ConverterContract;
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\Exceptions\IncorrectSettingsFileException;
/**
*
* Processor class
*/
class Processor
{
/**
* Converter version
*/
public const VERSION = '1.1.1';
/**
* @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 = [];
/**
* @var int Initial timestamp
*/
protected int $init_time;
protected readonly int $initTime;
/**
* @var int Initial RAM usage
*/
protected int $init_ram;
protected readonly int $initRam;
/**
* @var ConverterContract[] Converters will be used for conversion according to chosen formats
*/
protected array $converters = [];
/**
* Constructor
*
* @param array $argv Arguments came from cli
* @param Settings $settings Settings (lol)
* @param Environment $env Environment
*/
public function __construct(protected array $argv)
{
$this->init_time = hrtime(true);
$this->init_ram = memory_get_usage(true);
public function __construct(
protected Settings $settings,
protected Environment $env,
) {
$this->initTime = hrtime(true);
$this->initRam = memory_get_usage(true);
}
/**
* Parses an array of arguments came from cli
* Handles input command
*
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotReadableException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
* @throws JsonException
*/
protected function parseArgs(): void
public function start(): 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':
$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 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)');
}
$normpath = $this->argv[$idx + 1];
$files = array_filter(
FileSystem::dirContents($normpath),
static fn($filename) => FileSystem::isCollectionFile($filename)
);
$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->collectionPaths)) {
throw new InvalidArgumentException('there are no collections to convert');
}
if (empty($this->formats)) {
$this->formats = [ConvertFormat::Http->name => ConvertFormat::Http];
}
$this->prepareOutputDirectory();
$this->initConverters();
$this->convert();
}
/**
* Initializes output directory
*
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
* @throws DirectoryNotExistsException
* @throws DirectoryIsNotReadableException
*/
protected function initOutputDirectory(): void
protected function prepareOutputDirectory(): void
{
if (isset($this?->outputPath) && !$this->preserveOutput) {
FileSystem::removeDir($this->outputPath);
if (!$this->settings->isPreserveOutput()) {
FileSystem::removeDir($this->settings->outputPath());
}
FileSystem::makeDir($this->outputPath);
FileSystem::makeDir($this->settings->outputPath());
}
/**
* Initializes converters according to choosen formats
* Initializes converters according to chosen formats
*
* @return void
*/
protected function initConverters(): void
{
foreach ($this->formats as $type) {
$this->converters[$type->name] = new $type->value($this->preserveOutput);
foreach ($this->settings->formats() as $type) {
$this->converters[$type->name] = new $type->value($this->settings->isPreserveOutput());
}
unset($this->formats);
}
/**
* Generates collections from settings
*
* @return Generator<Collection>
* @throws JsonException
*/
protected function initCollections(): void
protected function nextCollection(): Generator
{
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;
foreach ($this->settings->collectionPaths() as $collectionPath) {
yield Collection::fromFile($collectionPath);
}
}
/**
* Begins a conversion
*
* @throws Exception
* @throws JsonException
*/
public function convert(): void
{
$this->parseArgs();
$this->initOutputDirectory();
$this->initConverters();
$this->initCollections();
$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) {
$count = count($this->settings->collectionPaths());
$current = $success = 0;
foreach ($this->nextCollection() as $collection) {
++$current;
printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, PHP_EOL);
foreach ($this->converters as $type => $exporter) {
printf(' > %s', strtolower($type));
$outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName);
$exporter->convert($collection, $outputPath);
printf(' - OK: %s%s', $exporter->getOutputPath(), PHP_EOL);
printf("Converting '%s' (%d/%d):%s", $collection->name(), $current, $count, EOL);
foreach ($this->converters as $type => $converter) {
/** @var AbstractConverter $converter */
printf('> %s%s', strtolower($type), EOL);
$outputPath = sprintf('%s%s%s', $this->settings->outputPath(), DS, $collection->name());
try {
$converter = $converter->to($outputPath);
$converter = $converter->convert($collection);
$converter->flush();
printf(' OK: %s%s', $converter->getOutputPath(), EOL);
} catch (Exception $e) {
printf(' ERROR %s: %s%s', $e->getCode(), $e->getMessage(), EOL);
if ($this->settings->isDevMode()) {
array_map(static fn (string $line) => printf(' %s%s', $line, EOL), $e->getTrace());
}
}
}
print(PHP_EOL);
print(EOL);
++$success;
}
unset($this->converters, $type, $converter, $outputPath, $this->collections, $collectionName, $collection);
$this->printStats($success, $current);
}
@@ -238,72 +153,13 @@ class Processor
*/
protected function printStats(int $success, int $count): void
{
$time = (hrtime(true) - $this->init_time) / 1_000_000;
$ram = (memory_get_peak_usage(true) - $this->init_ram) / 1024 / 1024;
printf('Converted %d of %d in %.3f ms using %.3f MiB RAM', $success, $count, $time, $ram);
}
/**
* @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());
$time = (hrtime(true) - $this->initTime) / 1_000_000;
$timeFmt = 'ms';
if ($time > 1000) {
$time /= 1000;
$timeFmt = 'sec';
}
$ram = (memory_get_peak_usage(true) - $this->initRam) / 1024 / 1024;
printf("Converted %d/%d in %.2f $timeFmt using up to %.2f MiB RAM%s", $success, $count, $time, $ram, EOL);
}
}

425
src/Settings.php Normal file
View File

@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
namespace PmConverter;
use Exception;
use InvalidArgumentException;
use JsonException;
use PmConverter\Converters\ConvertFormat;
use PmConverter\Enums\ArgumentNames as AN;
/**
* Class responsible for settings storage and dumping
*/
class Settings
{
/**
* @var string|null Full path to settings file
*/
protected ?string $filePath = null;
/**
* @var bool Flag to output some debug-specific messages
*/
protected bool $devMode = false;
/**
* @var string[] Paths to collection directories
*/
protected array $dirPaths = [];
/**
* @var string[] Paths to collection files
*/
protected array $collectionPaths = [];
/**
* @var string|null 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;
/**
* @var string[] Additional variables
*/
protected array $vars = [];
/**
* @var ConvertFormat[] Formats to convert a collections into
*/
protected array $formats = [];
/**
* @var string|null Path to environment file
*/
protected ?string $envFilePath = null;
/**
* @throws JsonException
*/
public function __construct()
{
$this->loadFromDefaults();
}
/**
* Loads settings from file
*
* @param string|null $filePath
* @return void
* @throws Exception
*/
public function loadFromFile(?string $filePath = null): void
{
if (is_null($filePath)) {
$filePath = sprintf('%s%spm-convert-settings.json', $_SERVER['PWD'], DS);
}
$filePath = trim($filePath);
if (!file_exists($filePath)) {
throw new Exception("file does not exist: $filePath");
}
if (!is_file($filePath)) {
throw new Exception("not a file: $filePath");
}
if (!is_readable($filePath)) {
throw new Exception("file is not readable: $filePath");
}
$content = file_get_contents($filePath);
$settings = json_decode($content ?: '{}', true, JSON_THROW_ON_ERROR);
$this->setFromArray($settings);
$this->filePath = $filePath;
}
/**
* Rewrites some defined settings with new values
*
* @param array $settings
* @return void
* @throws JsonException
*/
public function override(array $settings): void
{
$settings = array_replace_recursive($this->__serialize(), $settings);
$this->setFromArray($settings);
}
/**
* Loads settings with default values
*
* @return void
* @throws JsonException
*/
public function loadFromDefaults(): void
{
$this->setFromArray(self::defaults());
}
/**
* Returns default settings values
*
* @return array
*/
public static function defaults(?string $key = null): mixed
{
$values = [
AN::Config => null,
AN::Directories => [],
AN::Files => [],
AN::Environment => null,
AN::Output => null,
AN::PreserveOutput => false,
AN::Formats => ['http'],
AN::Vars => [],
AN::DevMode => false,
AN::Verbose => false,
];
return $key ? $values[$key] : $values;
}
/**
* Set settings from array
*
* @param array $settings
* @return void
* @throws JsonException
*/
protected function setFromArray(array $settings): void
{
foreach ($settings[AN::Directories] ?? self::defaults(AN::Directories) as $path) {
$this->addDirPath($path);
}
foreach ($settings[AN::Files] ?? self::defaults(AN::Files) ?? [] as $path) {
$this->addFilePath($path);
}
$this->setEnvFilePath($settings[AN::Environment] ?? self::defaults(AN::Environment));
$this->setOutputPath($settings[AN::Output] ?? self::defaults(AN::Output));
$this->setPreserveOutput($settings[AN::PreserveOutput] ?? self::defaults(AN::PreserveOutput));
foreach ($settings[AN::Formats] ?? self::defaults(AN::Formats) as $format) {
$this->addFormat(ConvertFormat::fromArg($format));
}
$this->vars = $settings[AN::Vars] ?? self::defaults(AN::Vars);
$this->setDevMode($settings[AN::DevMode] ?? self::defaults(AN::DevMode));
}
/**
* Checks wether settings file exists or not
*
* @return bool
*/
public function fileExists(): bool
{
return is_file($this->filePath)
&& is_readable($this->filePath)
&& is_writable($this->filePath);
}
/**
* Returns full path to settings file
*
* @return string
*/
public function filePath(): string
{
return $this->filePath;
}
/**
* Adds directory path into current settings array and fills files array with its content
*
* @param string $path
* @return void
* @throws JsonException
*/
public function addDirPath(string $path): void
{
$this->dirPaths = array_unique(array_merge(
$this->dirPaths ?? [],
[FileSystem::normalizePath($path)]
));
$files = array_filter(
FileSystem::dirContents($path),
static fn ($filename) => FileSystem::isCollectionFile($filename)
);
$this->collectionPaths = array_unique(array_merge($this->collectionPaths ?? [], $files));
}
/**
* Adds collection file into current settings array
*
* @param string $path
* @return void
* @throws JsonException
*/
public function addFilePath(string $path): void
{
if (!FileSystem::isCollectionFile($path)) {
throw new InvalidArgumentException("not a valid collection: $path");
}
if (!in_array($path, $this->collectionPaths)) {
$this->collectionPaths[] = FileSystem::normalizePath($path);
}
}
/**
* Sets output directory path
*
* @param string|null $outputPath
* @return void
*/
public function setOutputPath(?string $outputPath): void
{
$this->outputPath = $outputPath;
}
/**
* Sets developer mode setting
*
* @param bool $devMode
* @return void
*/
public function setDevMode(bool $devMode): void
{
$this->devMode = $devMode;
}
/**
* Adds a format to convert to into current settings array
*
* @param ConvertFormat $format
* @return void
*/
public function addFormat(ConvertFormat $format): void
{
$this->formats[$format->name] = $format;
}
/**
* Returns array of variables
*
* @return string[]
*/
public function vars(): array
{
return $this->vars;
}
/**
* Sets a setting responsible for saving old convertion results
*
* @param bool $preserveOutput
* @return void
*/
public function setPreserveOutput(bool $preserveOutput): void
{
$this->preserveOutput = $preserveOutput;
}
/**
* Sets environment filepath setting
*
* @param string|null $filepath
* @return void
*/
public function setEnvFilePath(?string $filepath): void
{
$this->envFilePath = is_string($filepath)
? FileSystem::normalizePath($filepath)
: $filepath;
}
/**
* Returns current value of developer mode setting
*
* @return bool
*/
public function isDevMode(): bool
{
return $this->devMode;
}
/**
* Returns current value of collection files setting
*
* @return string[]
*/
public function collectionPaths(): array
{
return $this->collectionPaths;
}
/**
* Returns current value of output directory path setting
*
* @return string|null
*/
public function outputPath(): ?string
{
return $this->outputPath;
}
/**
* Returns current value of preserve output setting
*
* @return bool
*/
public function isPreserveOutput(): bool
{
return $this->preserveOutput;
}
/**
* Returns current convert formats
*
* @return ConvertFormat[]
*/
public function formats(): array
{
return $this->formats;
}
/**
* Returns current value of environment filepath setting
*
* @return string|null
*/
public function envFilepath(): ?string
{
return $this->envFilePath;
}
/**
* Determines fieldset of settings JSON
*
* @return array
*/
public function __serialize(): array
{
return [
AN::DevMode => $this->isDevMode(),
AN::Directories => $this->dirPaths,
AN::Files => $this->collectionPaths(),
AN::Environment => $this->envFilepath(),
AN::Output => $this->outputPath(),
AN::PreserveOutput => $this->isPreserveOutput(),
AN::Formats => array_values(array_map(
static fn (ConvertFormat $format) => $format->toArg(),
$this->formats(),
)),
AN::Vars => $this->vars,
];
}
/**
* Converts settings into JSON format
*
* @return string
*/
public function __toString(): string
{
return json_encode($this->__serialize(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* Writes settings in JSON format into settings file
*
* @param array $vars
* @return bool
*/
public function dump(array $vars = []): bool
{
count($vars) > 0 && $this->vars = $vars;
return file_put_contents(self::$filepath, (string)$this) > 0;
}
/**
* Makes a backup file of current settings file
*
* @return string
*/
public function backup(): string
{
$newFilePath = $this->filePath() . '.bak.' . time();
copy($this->filePath(), $newFilePath);
return $newFilePath;
}
}

View File

@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PmConverter\Exceptions\EmptyHttpVerbException;
use PmConverter\Exceptions\InvalidHttpVersionException;
class AbstractRequestTest extends TestCase
{
/**
* @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers \PmConverter\Enums\HttpVersion
* @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\Enums\HttpVersion
* @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::getRawUrl()
* @return void
*/
public function testUrl(): void
{
$request = new \PmConverter\Converters\Http\HttpRequest();
$request->setUrl('http://localhost');
$this->assertSame('http://localhost', $request->getRawUrl());
}
/**
* @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::setBodyAsIs()
* @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);
}
}

View File

@@ -0,0 +1,10 @@
{
"files": [
"collections/20-API Lifecycle.postman_collection.json"
],
"output": "converted",
"preserveOutput": false,
"formats": [
"http"
]
}