v1.2.0 #3
16
README.md
16
README.md
@ -15,6 +15,8 @@ These formats are supported for now: `http`, `curl`, `wget`.
|
|||||||
## Supported features
|
## Supported features
|
||||||
|
|
||||||
* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json);
|
* [collection schema **v2.1**](https://schema.postman.com/json/collection/v2.1.0/collection.json);
|
||||||
|
* `Bearer` auth;
|
||||||
|
* replace vars in requests by stored in collection and environment file;
|
||||||
* export one or several collections (or even whole directories) into one or all of formats supported at the same time;
|
* 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);
|
* all headers (including disabled for `http`-format);
|
||||||
* `json` body (forces header `Content-Type` to `application/json`);
|
* `json` body (forces header `Content-Type` to `application/json`);
|
||||||
@ -22,12 +24,10 @@ These formats are supported for now: `http`, `curl`, `wget`.
|
|||||||
|
|
||||||
## Planned features
|
## Planned features
|
||||||
|
|
||||||
- support as many as possible/necessary of authentication kinds (_currently no ones_);
|
- support as many as possible/necessary of authentication kinds (_currently only `Bearer` supported_);
|
||||||
- support as many as possible/necessary of body formats (_currently only `json` and `formdata`_);
|
- support as many as possible/necessary of body formats (_currently only `json` and `formdata`_);
|
||||||
- documentation generation support (markdown) with responce examples (if present);
|
- documentation generation support (markdown) with responce examples (if present);
|
||||||
- maybe some another convert formats (like httpie or something...);
|
- maybe some another convert formats (like httpie or something...);
|
||||||
- replace `{{vars}}` from folder;
|
|
||||||
- replace `{{vars}}` from environment;
|
|
||||||
- better logging;
|
- better logging;
|
||||||
- tests, phpcs, psalm, etc.;
|
- tests, phpcs, psalm, etc.;
|
||||||
- web version.
|
- web version.
|
||||||
@ -61,15 +61,19 @@ Possible ARGUMENTS:
|
|||||||
-f, --file - a PATH to single collection located in PATH to convert from
|
-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
|
-d, --dir - a directory with collections located in COLLECTION_FILEPATH to convert from
|
||||||
-o, --output - a directory OUTPUT_PATH to put results in
|
-o, --output - a directory OUTPUT_PATH to put results in
|
||||||
|
-e, --env - use environment file with variable values to replace in request
|
||||||
-p, --preserve - do not delete OUTPUT_PATH (if exists)
|
-p, --preserve - do not delete OUTPUT_PATH (if exists)
|
||||||
-h, --help - show this help message and exit
|
-h, --help - show this help message and exit
|
||||||
-v, --version - show version info and exit
|
-v, --version - show version info and exit
|
||||||
|
|
||||||
If both -c and -d are specified then only unique set of files will be converted.
|
If no ARGUMENTS passed then --help implied.
|
||||||
|
If both -f and -d are specified then only unique set of files will be converted.
|
||||||
-f or -d are required to be specified at least once, but each may be specified multiple times.
|
-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.
|
PATH must be a valid path to readable json-file or directory.
|
||||||
OUTPUT_PATH must be a valid path to writeable 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 is specified several times then only last one will be used.
|
||||||
|
If -e is specified several times then only last one will be used.
|
||||||
|
If -e is not specified then only collection vars will be replaced (if any).
|
||||||
|
|
||||||
Possible FORMATS:
|
Possible FORMATS:
|
||||||
--http - generate raw *.http files (default)
|
--http - generate raw *.http files (default)
|
||||||
@ -83,8 +87,10 @@ Example:
|
|||||||
-f ~/dir1/first.postman_collection.json \
|
-f ~/dir1/first.postman_collection.json \
|
||||||
--directory ~/team \
|
--directory ~/team \
|
||||||
--file ~/dir2/second.postman_collection.json \
|
--file ~/dir2/second.postman_collection.json \
|
||||||
|
--env ~/localhost.postman_environment.json \
|
||||||
-d ~/personal \
|
-d ~/personal \
|
||||||
-o ~/postman_export
|
-o ~/postman_export
|
||||||
|
|
||||||
```
|
```
|
||||||
### Notice
|
### Notice
|
||||||
|
|
||||||
|
65
src/Environment.php
Normal file
65
src/Environment.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PmConverter;
|
||||||
|
|
||||||
|
class Environment implements \ArrayAccess
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $vars = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param object $env
|
||||||
|
*/
|
||||||
|
public function __construct(protected object $env)
|
||||||
|
{
|
||||||
|
foreach ($env->values as $var) {
|
||||||
|
$this->vars["{{{$var->key}}}"] = $var->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if there are some vars or not
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasVars(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function offsetExists(mixed $offset): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($offset, $this->vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function offsetGet(mixed $offset): mixed
|
||||||
|
{
|
||||||
|
return $this->vars[$offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->vars[$offset] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
unset($this->vars[$offset]);
|
||||||
|
}
|
||||||
|
}
|
@ -5,14 +5,16 @@ declare(strict_types=1);
|
|||||||
namespace PmConverter\Exporters\Abstract;
|
namespace PmConverter\Exporters\Abstract;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use PmConverter\Environment;
|
||||||
use PmConverter\Exporters\{
|
use PmConverter\Exporters\{
|
||||||
|
ConverterContract,
|
||||||
RequestContract};
|
RequestContract};
|
||||||
use PmConverter\FileSystem;
|
use PmConverter\FileSystem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
abstract class AbstractConverter
|
abstract class AbstractConverter implements ConverterContract
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var object|null
|
* @var object|null
|
||||||
@ -25,6 +27,30 @@ abstract class AbstractConverter
|
|||||||
protected string $outputPath;
|
protected string $outputPath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected array $vars = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Environment
|
||||||
|
*/
|
||||||
|
protected Environment $env;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an environment with vars
|
||||||
|
*
|
||||||
|
* @param Environment $env
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function withEnv(Environment $env): static
|
||||||
|
{
|
||||||
|
$this->env = $env;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts collection requests
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function convert(object $collection, string $outputPath): void
|
public function convert(object $collection, string $outputPath): void
|
||||||
@ -32,12 +58,30 @@ abstract class AbstractConverter
|
|||||||
$outputPath = sprintf('%s%s%s', $outputPath, DIRECTORY_SEPARATOR, static::OUTPUT_DIR);
|
$outputPath = sprintf('%s%s%s', $outputPath, DIRECTORY_SEPARATOR, static::OUTPUT_DIR);
|
||||||
$this->outputPath = FileSystem::makeDir($outputPath);
|
$this->outputPath = FileSystem::makeDir($outputPath);
|
||||||
$this->collection = $collection;
|
$this->collection = $collection;
|
||||||
|
$this->setVariables();
|
||||||
foreach ($collection->item as $item) {
|
foreach ($collection->item as $item) {
|
||||||
$this->convertItem($item);
|
$this->convertItem($item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Prepares collection variables
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
protected function setVariables(): static
|
||||||
|
{
|
||||||
|
if ($this->collection->variable) {
|
||||||
|
foreach ($this->collection->variable as $var) {
|
||||||
|
$this->vars["{{{$var->key}}}"] = $var->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns output path
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getOutputPath(): string
|
public function getOutputPath(): string
|
||||||
@ -46,6 +90,8 @@ abstract class AbstractConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Checks whether item contains another items or not
|
||||||
|
*
|
||||||
* @param object $item
|
* @param object $item
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
@ -55,6 +101,8 @@ abstract class AbstractConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Converts an item to request object and writes it into file
|
||||||
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
protected function convertItem(mixed $item): void
|
protected function convertItem(mixed $item): void
|
||||||
@ -77,18 +125,23 @@ abstract class AbstractConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Initialiazes request object to be written in file
|
||||||
|
*
|
||||||
* @param object $item
|
* @param object $item
|
||||||
* @return RequestContract
|
* @return RequestContract
|
||||||
*/
|
*/
|
||||||
protected function initRequest(object $item): RequestContract
|
protected function initRequest(object $item): RequestContract
|
||||||
{
|
{
|
||||||
$request_class = static::REQUEST;
|
$request_class = static::REQUEST_CLASS;
|
||||||
$result = new $request_class();
|
|
||||||
|
/** @var RequestContract $result */
|
||||||
|
$result = new $request_class($this->vars);
|
||||||
$result->setName($item->name);
|
$result->setName($item->name);
|
||||||
$result->setDescription($item->request?->description ?? null);
|
$result->setDescription($item->request?->description ?? null);
|
||||||
$result->setVerb($item->request->method);
|
$result->setVerb($item->request->method);
|
||||||
$result->setUrl($item->request->url->raw);
|
$result->setUrl($item->request->url->raw);
|
||||||
$result->setHeaders($item->request->header);
|
$result->setHeaders($item->request->header);
|
||||||
|
$result->setAuth($item->request?->auth ?? $this->collection?->auth ?? null);
|
||||||
if ($item->request->method !== 'GET' && !empty($item->request->body)) {
|
if ($item->request->method !== 'GET' && !empty($item->request->body)) {
|
||||||
$result->setBody($item->request->body);
|
$result->setBody($item->request->body);
|
||||||
}
|
}
|
||||||
@ -96,6 +149,8 @@ abstract class AbstractConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Writes converted request object to file
|
||||||
|
*
|
||||||
* @param RequestContract $request
|
* @param RequestContract $request
|
||||||
* @param string|null $subpath
|
* @param string|null $subpath
|
||||||
* @return bool
|
* @return bool
|
||||||
@ -106,6 +161,33 @@ abstract class AbstractConverter
|
|||||||
$filedir = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $subpath);
|
$filedir = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $subpath);
|
||||||
$filedir = FileSystem::makeDir($filedir);
|
$filedir = FileSystem::makeDir($filedir);
|
||||||
$filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $request->getName(), static::FILE_EXT);
|
$filepath = sprintf('%s%s%s.%s', $filedir, DIRECTORY_SEPARATOR, $request->getName(), static::FILE_EXT);
|
||||||
return file_put_contents($filepath, (string)$request) > 0;
|
$content = $this->interpolate((string)$request);
|
||||||
|
return file_put_contents($filepath, $content) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces variables in request with values from collection or environment
|
||||||
|
*
|
||||||
|
* @param string $content
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function interpolate(string $content): string
|
||||||
|
{
|
||||||
|
if (empty($this->vars) && $this->env->hasVars()) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
$matches = [];
|
||||||
|
if (preg_match_all('/\{\{[a-zA-Z][a-zA-Z0-9]+}}/', $content, $matches, PREG_PATTERN_ORDER) > 0) {
|
||||||
|
foreach ($matches[0] as $key => $var) {
|
||||||
|
if (str_contains($content, $var)) {
|
||||||
|
$content = str_replace($var, $this->vars[$var] ?? $this->env[$var] ?? $var, $content);
|
||||||
|
unset($matches[0][$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (!empty($matches[0])) {
|
||||||
|
// fwrite(STDERR, sprintf(' No values found: %s%s', implode(', ', $matches[0]), PHP_EOL));
|
||||||
|
// }
|
||||||
|
return $content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@ use PmConverter\Exporters\Http\HttpRequest;
|
|||||||
use PmConverter\Exporters\RequestContract;
|
use PmConverter\Exporters\RequestContract;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Class to determine file content with any request format
|
||||||
*/
|
*/
|
||||||
abstract class AbstractRequest implements RequestContract
|
abstract class AbstractRequest implements RequestContract
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected string $http = 'HTTP/1.1'; //TODO verb
|
protected string $http = 'HTTP/1.1'; //TODO proto
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
@ -53,6 +53,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
protected string $url;
|
protected string $url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets name from collection item to request object
|
||||||
|
*
|
||||||
* @param string $name
|
* @param string $name
|
||||||
* @return HttpRequest
|
* @return HttpRequest
|
||||||
*/
|
*/
|
||||||
@ -63,6 +65,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Returns name of request
|
||||||
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
@ -71,6 +75,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets description from collection item to request object
|
||||||
|
*
|
||||||
* @param string|null $description
|
* @param string|null $description
|
||||||
* @return HttpRequest
|
* @return HttpRequest
|
||||||
*/
|
*/
|
||||||
@ -81,6 +87,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets HTTP verb from collection item to request object
|
||||||
|
*
|
||||||
* @param string $verb
|
* @param string $verb
|
||||||
* @return HttpRequest
|
* @return HttpRequest
|
||||||
*/
|
*/
|
||||||
@ -91,6 +99,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets URL from collection item to request object
|
||||||
|
*
|
||||||
* @param string $url
|
* @param string $url
|
||||||
* @return HttpRequest
|
* @return HttpRequest
|
||||||
*/
|
*/
|
||||||
@ -101,6 +111,8 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets headers from collection item to request object
|
||||||
|
*
|
||||||
* @param object[]|null $headers
|
* @param object[]|null $headers
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
@ -116,6 +128,32 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets authorization headers
|
||||||
|
*
|
||||||
|
* @param object|null $auth
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setAuth(?object $auth): static
|
||||||
|
{
|
||||||
|
if (!empty($auth)) {
|
||||||
|
switch ($auth->type) {
|
||||||
|
case 'bearer':
|
||||||
|
$this->headers['Authorization'] = [
|
||||||
|
'value' => 'Bearer ' . $auth->{$auth->type}[0]->value,
|
||||||
|
'disabled' => false,
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets body mode from collection item to request object
|
||||||
|
*
|
||||||
* @param string $bodymode
|
* @param string $bodymode
|
||||||
* @return HttpRequest
|
* @return HttpRequest
|
||||||
*/
|
*/
|
||||||
@ -126,8 +164,10 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $body
|
* Sets body from collection item to request object
|
||||||
* @return HttpRequest
|
*
|
||||||
|
* @param object $body
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setBody(object $body): static
|
public function setBody(object $body): static
|
||||||
{
|
{
|
||||||
@ -153,11 +193,29 @@ abstract class AbstractRequest implements RequestContract
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* Returns array of description lines
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
*/
|
*/
|
||||||
abstract protected function prepareBody(): ?string;
|
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
|
* @return string
|
||||||
*/
|
*/
|
||||||
abstract public function __toString(): string;
|
abstract public function __toString(): string;
|
||||||
|
@ -11,7 +11,8 @@ use PmConverter\Exporters\{
|
|||||||
class CurlConverter extends AbstractConverter implements ConverterContract
|
class CurlConverter extends AbstractConverter implements ConverterContract
|
||||||
{
|
{
|
||||||
protected const FILE_EXT = 'sh';
|
protected const FILE_EXT = 'sh';
|
||||||
|
|
||||||
protected const OUTPUT_DIR = 'curl';
|
protected const OUTPUT_DIR = 'curl';
|
||||||
|
|
||||||
protected const REQUEST = CurlRequest::class;
|
protected const REQUEST_CLASS = CurlRequest::class;
|
||||||
}
|
}
|
||||||
|
@ -7,59 +7,77 @@ namespace PmConverter\Exporters\Curl;
|
|||||||
use PmConverter\Exporters\Abstract\AbstractRequest;
|
use PmConverter\Exporters\Abstract\AbstractRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Class to determine file content with curl request format
|
||||||
*/
|
*/
|
||||||
class CurlRequest extends AbstractRequest
|
class CurlRequest extends AbstractRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
protected function prepareBody(): ?string
|
protected function prepareDescription(): array
|
||||||
{
|
{
|
||||||
switch ($this->bodymode) {
|
return empty($this->description)
|
||||||
case 'formdata':
|
? []
|
||||||
$body = [];
|
: ['# ' . str_replace("\n", "\n# ", $this->description), ''];
|
||||||
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
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function __toString(): string
|
protected function prepareHeaders(): array
|
||||||
{
|
{
|
||||||
$output[] = '#!/bin/sh';
|
$output = [];
|
||||||
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) {
|
foreach ($this->headers as $header_key => $header) {
|
||||||
if ($header['disabled']) {
|
if ($header['disabled']) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
|
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
|
||||||
}
|
}
|
||||||
if (!is_null($body = $this->prepareBody())) {
|
return $output;
|
||||||
$output[] = match ($this->bodymode) {
|
|
||||||
'formdata' => $body,
|
|
||||||
default => "\t--data '$body'",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
protected function prepareBody(): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
switch ($this->bodymode) {
|
||||||
|
case 'formdata':
|
||||||
|
foreach ($this->body as $data) {
|
||||||
|
$output[] = sprintf(
|
||||||
|
"%s\t--form '%s=%s' \ ",
|
||||||
|
isset($data->disabled) ? '# ' : '',
|
||||||
|
$data->key,
|
||||||
|
$data->type === 'file' ? "@$data->src" : $data->value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$output = ["\t--data '$this->body'"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$output = array_merge(
|
||||||
|
['#!/bin/sh'],
|
||||||
|
$this->prepareDescription(),
|
||||||
|
[
|
||||||
|
"curl \ ",
|
||||||
|
"\t--http1.1 \ ", //TODO proto
|
||||||
|
"\t--request $this->verb \ ",
|
||||||
|
"\t--location $this->url \ ",
|
||||||
|
],
|
||||||
|
$this->prepareHeaders(),
|
||||||
|
$this->prepareBody()
|
||||||
|
);
|
||||||
$output[] = rtrim(array_pop($output), '\ ');
|
$output[] = rtrim(array_pop($output), '\ ');
|
||||||
return implode(PHP_EOL, $output);
|
return implode(PHP_EOL, array_merge($output, ['']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,8 @@ use PmConverter\Exporters\{
|
|||||||
class HttpConverter extends AbstractConverter implements ConverterContract
|
class HttpConverter extends AbstractConverter implements ConverterContract
|
||||||
{
|
{
|
||||||
protected const FILE_EXT = 'http';
|
protected const FILE_EXT = 'http';
|
||||||
|
|
||||||
protected const OUTPUT_DIR = 'http';
|
protected const OUTPUT_DIR = 'http';
|
||||||
|
|
||||||
protected const REQUEST = HttpRequest::class;
|
protected const REQUEST_CLASS = HttpRequest::class;
|
||||||
}
|
}
|
||||||
|
@ -7,47 +7,64 @@ namespace PmConverter\Exporters\Http;
|
|||||||
use PmConverter\Exporters\Abstract\AbstractRequest;
|
use PmConverter\Exporters\Abstract\AbstractRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Class to determine file content with http request format
|
||||||
*/
|
*/
|
||||||
class HttpRequest extends AbstractRequest
|
class HttpRequest extends AbstractRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
protected function prepareBody(): ?string
|
protected function prepareDescription(): array
|
||||||
{
|
{
|
||||||
switch ($this->bodymode) {
|
return empty($this->description)
|
||||||
case 'formdata':
|
? []
|
||||||
$body = [];
|
: ['# ' . str_replace("\n", "\n# ", $this->description), ''];
|
||||||
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
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function __toString(): string
|
protected function prepareHeaders(): array
|
||||||
{
|
{
|
||||||
if ($this->description) {
|
|
||||||
$output[] = '# ' . str_replace("\n", "\n# ", $this->description);
|
|
||||||
$output[] = '';
|
|
||||||
}
|
|
||||||
$output[] = "$this->verb $this->url $this->http";
|
$output[] = "$this->verb $this->url $this->http";
|
||||||
foreach ($this->headers as $header_key => $header) {
|
foreach ($this->headers as $header_key => $header) {
|
||||||
$output[] = sprintf('%s%s: %s', $header['disabled'] ? '# ' : '', $header_key, $header['value']);
|
$output[] = sprintf('%s%s: %s', $header['disabled'] ? '# ' : '', $header_key, $header['value']);
|
||||||
}
|
}
|
||||||
$output[] = '';
|
return $output;
|
||||||
$output[] = (string)$this->prepareBody();
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
protected function prepareBody(): array
|
||||||
|
{
|
||||||
|
switch ($this->bodymode) {
|
||||||
|
case 'formdata':
|
||||||
|
$output = [''];
|
||||||
|
foreach ($this->body as $data) {
|
||||||
|
$output[] = sprintf(
|
||||||
|
'%s%s=%s',
|
||||||
|
empty($data->disabled) ? '' : '# ',
|
||||||
|
$data->key,
|
||||||
|
$data->type === 'file' ? $data->src : $data->value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
default:
|
||||||
|
return ['', $this->body];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$output = array_merge(
|
||||||
|
$this->prepareDescription(),
|
||||||
|
$this->prepareHeaders(),
|
||||||
|
$this->prepareBody()
|
||||||
|
);
|
||||||
return implode(PHP_EOL, $output);
|
return implode(PHP_EOL, $output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ interface RequestContract
|
|||||||
public function setDescription(?string $description): static;
|
public function setDescription(?string $description): static;
|
||||||
public function setVerb(string $verb): static;
|
public function setVerb(string $verb): static;
|
||||||
public function setUrl(string $url): static;
|
public function setUrl(string $url): static;
|
||||||
|
public function setAuth(object $auth): static;
|
||||||
public function setHeaders(?array $headers): static;
|
public function setHeaders(?array $headers): static;
|
||||||
public function setBodymode(string $bodymode): static;
|
public function setBodymode(string $bodymode): static;
|
||||||
public function setBody(object $body): static;
|
public function setBody(object $body): static;
|
||||||
|
@ -11,7 +11,8 @@ use PmConverter\Exporters\{
|
|||||||
class WgetConverter extends AbstractConverter implements ConverterContract
|
class WgetConverter extends AbstractConverter implements ConverterContract
|
||||||
{
|
{
|
||||||
protected const FILE_EXT = 'sh';
|
protected const FILE_EXT = 'sh';
|
||||||
|
|
||||||
protected const OUTPUT_DIR = 'wget';
|
protected const OUTPUT_DIR = 'wget';
|
||||||
|
|
||||||
protected const REQUEST = WgetRequest::class;
|
protected const REQUEST_CLASS = WgetRequest::class;
|
||||||
}
|
}
|
||||||
|
@ -7,57 +7,76 @@ namespace PmConverter\Exporters\Wget;
|
|||||||
use PmConverter\Exporters\Abstract\AbstractRequest;
|
use PmConverter\Exporters\Abstract\AbstractRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Class to determine file content with wget request format
|
||||||
*/
|
*/
|
||||||
class WgetRequest extends AbstractRequest
|
class WgetRequest extends AbstractRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
protected function prepareBody(): ?string
|
protected function prepareDescription(): array
|
||||||
{
|
{
|
||||||
switch ($this->bodymode) {
|
return empty($this->description)
|
||||||
case 'formdata':
|
? []
|
||||||
$lines = [];
|
: ['# ' . str_replace("\n", "\n# ", $this->description), ''];
|
||||||
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
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function __toString(): string
|
protected function prepareHeaders(): array
|
||||||
{
|
{
|
||||||
$output[] = '#!/bin/sh';
|
$output = [];
|
||||||
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) {
|
foreach ($this->headers as $header_key => $header) {
|
||||||
if ($header['disabled']) {
|
if ($header['disabled']) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
|
$output[] = sprintf("\t--header '%s=%s' \ ", $header_key, $header['value']);
|
||||||
}
|
}
|
||||||
if (!is_null($body = $this->prepareBody())) {
|
return $output;
|
||||||
$output[] = "\t--body-data '$body' \ ";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
protected function prepareBody(): array
|
||||||
|
{
|
||||||
|
switch ($this->bodymode) {
|
||||||
|
case 'formdata':
|
||||||
|
$params = [];
|
||||||
|
foreach ($this->body as $data) {
|
||||||
|
if ($data->type === 'file') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$params[$data->key] = $data->value;
|
||||||
|
}
|
||||||
|
$output[] = http_build_query($params);
|
||||||
|
return $output;
|
||||||
|
default:
|
||||||
|
return ["\t$this->body"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$output = array_merge(
|
||||||
|
['#!/bin/sh'],
|
||||||
|
$this->prepareDescription(),
|
||||||
|
[
|
||||||
|
'wget \ ',
|
||||||
|
"\t--no-check-certificate \ ",
|
||||||
|
"\t--quiet \ ",
|
||||||
|
"\t--timeout=0 \ ",
|
||||||
|
"\t--method $this->verb \ ",
|
||||||
|
],
|
||||||
|
$this->prepareHeaders(),
|
||||||
|
$this->prepareBody()
|
||||||
|
);
|
||||||
$output[] = rtrim(array_pop($output), '\ ');
|
$output[] = rtrim(array_pop($output), '\ ');
|
||||||
$output[] = "\t'$this->url'";
|
$output[] = "\t'$this->url'";
|
||||||
return implode(PHP_EOL, $output);
|
return implode(PHP_EOL, array_merge($output, ['']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ class Processor
|
|||||||
/**
|
/**
|
||||||
* Converter version
|
* Converter version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '1.1.1';
|
public const VERSION = '1.2.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string[] Paths to collection files
|
* @var string[] Paths to collection files
|
||||||
@ -57,12 +57,22 @@ class Processor
|
|||||||
/**
|
/**
|
||||||
* @var int Initial timestamp
|
* @var int Initial timestamp
|
||||||
*/
|
*/
|
||||||
protected int $init_time;
|
protected int $initTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int Initial RAM usage
|
* @var int Initial RAM usage
|
||||||
*/
|
*/
|
||||||
protected int $init_ram;
|
protected int $initRam;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $envFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Environment
|
||||||
|
*/
|
||||||
|
protected Environment $env;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -71,8 +81,8 @@ class Processor
|
|||||||
*/
|
*/
|
||||||
public function __construct(protected array $argv)
|
public function __construct(protected array $argv)
|
||||||
{
|
{
|
||||||
$this->init_time = hrtime(true);
|
$this->initTime = hrtime(true);
|
||||||
$this->init_ram = memory_get_usage(true);
|
$this->initRam = memory_get_usage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,14 +125,19 @@ class Processor
|
|||||||
if (empty($this->argv[$idx + 1])) {
|
if (empty($this->argv[$idx + 1])) {
|
||||||
throw new InvalidArgumentException('a directory path is expected for -d (--dir)');
|
throw new InvalidArgumentException('a directory path is expected for -d (--dir)');
|
||||||
}
|
}
|
||||||
$normpath = $this->argv[$idx + 1];
|
$rawpath = $this->argv[$idx + 1];
|
||||||
$files = array_filter(
|
$files = array_filter(
|
||||||
FileSystem::dirContents($normpath),
|
FileSystem::dirContents($rawpath),
|
||||||
static fn($filename) => FileSystem::isCollectionFile($filename)
|
static fn($filename) => FileSystem::isCollectionFile($filename)
|
||||||
);
|
);
|
||||||
$this->collectionPaths = array_unique(array_merge($this?->collectionPaths ?? [], $files));
|
$this->collectionPaths = array_unique(array_merge($this?->collectionPaths ?? [], $files));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case '-e':
|
||||||
|
case '--env':
|
||||||
|
$this->envFile = FileSystem::normalizePath($this->argv[$idx + 1]);
|
||||||
|
break;
|
||||||
|
|
||||||
case '-p':
|
case '-p':
|
||||||
case '--preserve':
|
case '--preserve':
|
||||||
$this->preserveOutput = true;
|
$this->preserveOutput = true;
|
||||||
@ -158,6 +173,8 @@ class Processor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Initializes output directory
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
* @throws CannotCreateDirectoryException
|
* @throws CannotCreateDirectoryException
|
||||||
* @throws DirectoryIsNotWriteableException
|
* @throws DirectoryIsNotWriteableException
|
||||||
@ -185,6 +202,8 @@ class Processor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Initializes collection objects
|
||||||
|
*
|
||||||
* @throws JsonException
|
* @throws JsonException
|
||||||
*/
|
*/
|
||||||
protected function initCollections(): void
|
protected function initCollections(): void
|
||||||
@ -199,6 +218,22 @@ class Processor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes environment object
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws JsonException
|
||||||
|
*/
|
||||||
|
protected function initEnv(): void
|
||||||
|
{
|
||||||
|
$content = file_get_contents(FileSystem::normalizePath($this->envFile));
|
||||||
|
$content = json_decode($content, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if (!property_exists($content, 'environment') || empty($content?->environment)) {
|
||||||
|
throw new JsonException("not a valid environment: $this->envFile");
|
||||||
|
}
|
||||||
|
$this->env = new Environment($content->environment);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begins a conversion
|
* Begins a conversion
|
||||||
*
|
*
|
||||||
@ -210,6 +245,7 @@ class Processor
|
|||||||
$this->initOutputDirectory();
|
$this->initOutputDirectory();
|
||||||
$this->initConverters();
|
$this->initConverters();
|
||||||
$this->initCollections();
|
$this->initCollections();
|
||||||
|
$this->initEnv();
|
||||||
$count = count($this->collections);
|
$count = count($this->collections);
|
||||||
$current = 0;
|
$current = 0;
|
||||||
$success = 0;
|
$success = 0;
|
||||||
@ -218,10 +254,13 @@ class Processor
|
|||||||
++$current;
|
++$current;
|
||||||
printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, PHP_EOL);
|
printf("Converting '%s' (%d/%d):%s", $collectionName, $current, $count, PHP_EOL);
|
||||||
foreach ($this->converters as $type => $exporter) {
|
foreach ($this->converters as $type => $exporter) {
|
||||||
printf(' > %s', strtolower($type));
|
printf('> %s%s', strtolower($type), PHP_EOL);
|
||||||
$outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName);
|
$outputPath = sprintf('%s%s%s', $this->outputPath, DIRECTORY_SEPARATOR, $collectionName);
|
||||||
|
if (!empty($this->env)) {
|
||||||
|
$exporter->withEnv($this->env);
|
||||||
|
}
|
||||||
$exporter->convert($collection, $outputPath);
|
$exporter->convert($collection, $outputPath);
|
||||||
printf(' - OK: %s%s', $exporter->getOutputPath(), PHP_EOL);
|
printf(' OK: %s%s', $exporter->getOutputPath(), PHP_EOL);
|
||||||
}
|
}
|
||||||
print(PHP_EOL);
|
print(PHP_EOL);
|
||||||
++$success;
|
++$success;
|
||||||
@ -238,8 +277,8 @@ class Processor
|
|||||||
*/
|
*/
|
||||||
protected function printStats(int $success, int $count): void
|
protected function printStats(int $success, int $count): void
|
||||||
{
|
{
|
||||||
$time = (hrtime(true) - $this->init_time) / 1_000_000;
|
$time = (hrtime(true) - $this->initTime) / 1_000_000;
|
||||||
$ram = (memory_get_peak_usage(true) - $this->init_ram) / 1024 / 1024;
|
$ram = (memory_get_peak_usage(true) - $this->initRam) / 1024 / 1024;
|
||||||
printf('Converted %d of %d in %.3f ms using %.3f MiB RAM', $success, $count, $time, $ram);
|
printf('Converted %d of %d in %.3f ms using %.3f MiB RAM', $success, $count, $time, $ram);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,16 +317,19 @@ class Processor
|
|||||||
"\t-f, --file - a PATH to single collection located in PATH to convert from",
|
"\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-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-o, --output - a directory OUTPUT_PATH to put results in",
|
||||||
|
"\t-e, --env - use environment file with variable values to replace in request",
|
||||||
"\t-p, --preserve - do not delete OUTPUT_PATH (if exists)",
|
"\t-p, --preserve - do not delete OUTPUT_PATH (if exists)",
|
||||||
"\t-h, --help - show this help message and exit",
|
"\t-h, --help - show this help message and exit",
|
||||||
"\t-v, --version - show version info and exit",
|
"\t-v, --version - show version info and exit",
|
||||||
'',
|
'',
|
||||||
'If no ARGUMENTS passed then --help implied.',
|
'If no ARGUMENTS passed then --help implied.',
|
||||||
'If both -c and -d are specified then only unique set of files will be converted.',
|
'If both -f and -d are specified then only unique set of files will be converted.',
|
||||||
'-f or -d are required to be specified at least once, but each may be specified multiple times.',
|
'-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.',
|
'PATH must be a valid path to readable json-file or directory.',
|
||||||
'OUTPUT_PATH must be a valid path to writeable directory.',
|
'OUTPUT_PATH must be a valid path to writeable directory.',
|
||||||
'If -o is specified several times then only last one will be used.',
|
'If -o is specified several times then only last one will be used.',
|
||||||
|
'If -e is specified several times then only last one will be used.',
|
||||||
|
'If -e is not specified then only collection vars will be replaced (if any).',
|
||||||
'',
|
'',
|
||||||
'Possible FORMATS:',
|
'Possible FORMATS:',
|
||||||
"\t--http - generate raw *.http files (default)",
|
"\t--http - generate raw *.http files (default)",
|
||||||
@ -301,6 +343,7 @@ class Processor
|
|||||||
" -f ~/dir1/first.postman_collection.json \ ",
|
" -f ~/dir1/first.postman_collection.json \ ",
|
||||||
" --directory ~/team \ ",
|
" --directory ~/team \ ",
|
||||||
" --file ~/dir2/second.postman_collection.json \ ",
|
" --file ~/dir2/second.postman_collection.json \ ",
|
||||||
|
" --env ~/localhost.postman_environment.json \ ",
|
||||||
" -d ~/personal \ ",
|
" -d ~/personal \ ",
|
||||||
" -o ~/postman_export ",
|
" -o ~/postman_export ",
|
||||||
"",
|
"",
|
||||||
|
Loading…
Reference in New Issue
Block a user