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.
This commit is contained in:
Anthony Axenov 2023-09-17 21:42:06 +08:00
parent 5c85f23514
commit 3c1871ce1f
Signed by: anthony
GPG Key ID: EA9EC32FF7CCD4EC
11 changed files with 296 additions and 26 deletions

98
src/Collection.php Normal file
View File

@ -0,0 +1,98 @@
<?php
declare(strict_types = 1);
namespace PmConverter;
use JsonException;
use Stringable;
/**
* Class that describes a request collection
*
* @property array|object $item
* @property object $info
*/
class Collection implements Stringable
{
/**
* 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 (isset($json->collection)) {
$json = $json->collection;
}
$this->json = $json;
}
/**
* 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
*/
public function version(): 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
};
}
}

12
src/CollectionVersion.php Normal file
View File

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

View File

@ -5,11 +5,15 @@ declare(strict_types=1);
namespace PmConverter\Converters\Abstract; namespace PmConverter\Converters\Abstract;
use Exception; use Exception;
use PmConverter\Collection;
use PmConverter\Converters\{ use PmConverter\Converters\{
ConverterContract, ConverterContract,
RequestContract}; RequestContract};
use PmConverter\Environment; use PmConverter\Environment;
use PmConverter\Exceptions\InvalidHttpVersionException; use PmConverter\Exceptions\{
CannotCreateDirectoryException,
DirectoryIsNotWriteableException,
InvalidHttpVersionException};
use PmConverter\FileSystem; use PmConverter\FileSystem;
/** /**
@ -18,9 +22,9 @@ use PmConverter\FileSystem;
abstract class AbstractConverter implements ConverterContract abstract class AbstractConverter implements ConverterContract
{ {
/** /**
* @var object|null * @var Collection|null
*/ */
protected ?object $collection = null; protected ?Collection $collection = null;
/** /**
* @var string * @var string
@ -45,14 +49,32 @@ abstract class AbstractConverter implements ConverterContract
} }
/** /**
* Converts collection requests * Creates a new directory to save a converted collection into
* *
* @throws Exception * @param string $outputPath
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/ */
public function convert(object $collection, string $outputPath): void protected function prepareOutputDir(string $outputPath): void
{ {
$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);
}
/**
* Converts collection requests
*
* @param Collection $collection
* @param string $outputPath
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
* @throws Exception
*/
public function convert(Collection $collection, string $outputPath): void
{
$this->prepareOutputDir($outputPath);
$this->collection = $collection; $this->collection = $collection;
$this->setVariables(); $this->setVariables();
foreach ($collection->item as $item) { foreach ($collection->item as $item) {
@ -94,7 +116,9 @@ abstract class AbstractConverter implements ConverterContract
*/ */
protected function isItemFolder(object $item): bool protected function isItemFolder(object $item): bool
{ {
return !empty($item->item) && empty($item->request); return !empty($item->item)
&& is_array($item->item)
&& empty($item->request);
} }
/** /**

View File

@ -8,7 +8,7 @@ use PmConverter\Converters\RequestContract;
use PmConverter\Exceptions\{ use PmConverter\Exceptions\{
EmptyHttpVerbException, EmptyHttpVerbException,
InvalidHttpVersionException}; InvalidHttpVersionException};
use PmConverter\HttpVersions; use PmConverter\HttpVersion;
use Stringable; use Stringable;
/** /**
@ -61,9 +61,9 @@ abstract class AbstractRequest implements Stringable, RequestContract
*/ */
public function setHttpVersion(float $version): static public function setHttpVersion(float $version): static
{ {
if (!in_array($version, HttpVersions::values())) { if (!in_array($version, HttpVersion::values())) {
throw new InvalidHttpVersionException( throw new InvalidHttpVersionException(
'Only these HTTP versions are supported: ' . implode(', ', HttpVersions::values()) 'Only these HTTP versions are supported: ' . implode(', ', HttpVersion::values())
); );
} }
$this->httpVersion = $version; $this->httpVersion = $version;

View File

@ -8,6 +8,7 @@ namespace PmConverter\Converters;
use PmConverter\Converters\{ use PmConverter\Converters\{
Curl\CurlConverter, Curl\CurlConverter,
Http\HttpConverter, Http\HttpConverter,
Postman20\Postman20Converter,
Wget\WgetConverter}; Wget\WgetConverter};
enum ConvertFormat: string enum ConvertFormat: string
@ -15,4 +16,5 @@ enum ConvertFormat: string
case Http = HttpConverter::class; case Http = HttpConverter::class;
case Curl = CurlConverter::class; case Curl = CurlConverter::class;
case Wget = WgetConverter::class; case Wget = WgetConverter::class;
case Postman20 = Postman20Converter::class;
} }

View File

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace PmConverter\Converters; namespace PmConverter\Converters;
use PmConverter\Collection;
interface ConverterContract interface ConverterContract
{ {
public function convert(object $collection, string $outputPath): void; public function convert(Collection $collection, string $outputPath): void;
public function getOutputPath(): string; public function getOutputPath(): string;
} }

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace PmConverter\Converters\Postman20;
use PmConverter\Collection;
use PmConverter\Converters\{
Abstract\AbstractConverter,
ConverterContract};
use PmConverter\Exceptions\CannotCreateDirectoryException;
use PmConverter\Exceptions\DirectoryIsNotWriteableException;
use PmConverter\FileSystem;
/**
* Converts Postman Collection v2.1 to v2.0
*/
class Postman20Converter extends AbstractConverter implements ConverterContract
{
protected const FILE_EXT = 'v20.postman_collection.json';
protected const OUTPUT_DIR = 'v2.0';
/**
* Converts collection requests
*
* @param Collection $collection
* @param string $outputPath
* @return void
* @throws CannotCreateDirectoryException
* @throws DirectoryIsNotWriteableException
*/
public function convert(Collection $collection, string $outputPath): void
{
$this->collection = $collection;
$this->collection->info->schema = str_replace('/v2.1.', '/v2.0.', $this->collection->info->schema);
$this->prepareOutputDir($outputPath);
$this->convertAuth($this->collection->raw());
foreach ($this->collection->item as $item) {
$this->convertItem($item);
}
$this->writeCollection();
}
/**
* 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, DIRECTORY_SEPARATOR, $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;
}
$type = $request->auth->type;
if ($type !== 'noauth' && is_array($request->auth->$type)) { // bearer
$auth = [];
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)) {
$raw = $response->originalRequest->url->raw;
$response->originalRequest->url = $raw;
}
}
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace PmConverter; namespace PmConverter;
use JsonException;
use PmConverter\Exceptions\{ use PmConverter\Exceptions\{
CannotCreateDirectoryException, CannotCreateDirectoryException,
DirectoryIsNotReadableException, DirectoryIsNotReadableException,
@ -110,16 +111,14 @@ class FileSystem
* *
* @param string $path * @param string $path
* @return bool * @return bool
* @throws JsonException
*/ */
public static function isCollectionFile(string $path): bool public static function isCollectionFile(string $path): bool
{ {
$path = static::normalizePath($path); return (!empty($path = trim(static::normalizePath($path))))
return !empty($path = trim($path))
&& str_ends_with($path, '.postman_collection.json') && str_ends_with($path, '.postman_collection.json')
&& file_exists($path) && file_exists($path)
&& is_readable($path) && is_readable($path)
&& ($json = json_decode(file_get_contents($path), true)) && Collection::fromFile($path)->version() !== CollectionVersion::Unknown;
&& json_last_error() === JSON_ERROR_NONE
&& isset($json['collection']['info']['name']);
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace PmConverter; namespace PmConverter;
enum HttpVersions: string enum HttpVersion: string
{ {
case Version10 = '1.0'; case Version10 = '1.0';
case Version11 = '1.1'; case Version11 = '1.1';

View File

@ -107,7 +107,7 @@ class Processor
$normpath = FileSystem::normalizePath($rawpath); $normpath = FileSystem::normalizePath($rawpath);
if (!FileSystem::isCollectionFile($normpath)) { if (!FileSystem::isCollectionFile($normpath)) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
sprintf("this is not a valid collection file:%s\t%s %s", PHP_EOL, $arg, $rawpath) sprintf("not a valid collection:%s\t%s %s", PHP_EOL, $arg, $rawpath)
); );
} }
$this->collectionPaths[] = $this->argv[$idx + 1]; $this->collectionPaths[] = $this->argv[$idx + 1];
@ -156,6 +156,10 @@ class Processor
$this->formats[ConvertFormat::Wget->name] = ConvertFormat::Wget; $this->formats[ConvertFormat::Wget->name] = ConvertFormat::Wget;
break; break;
case '--v2.0':
$this->formats[ConvertFormat::Postman20->name] = ConvertFormat::Postman20;
break;
case '--var': case '--var':
[$var, $value] = explode('=', trim($this->argv[$idx + 1])); [$var, $value] = explode('=', trim($this->argv[$idx + 1]));
$this->vars[$var] = $value; $this->vars[$var] = $value;
@ -219,12 +223,8 @@ class Processor
protected function initCollections(): void protected function initCollections(): void
{ {
foreach ($this->collectionPaths as $collectionPath) { foreach ($this->collectionPaths as $collectionPath) {
$content = file_get_contents(FileSystem::normalizePath($collectionPath)); $collection = Collection::fromFile($collectionPath);
$content = json_decode($content, flags: JSON_THROW_ON_ERROR); $this->collections[$collection->name()] = $collection;
if (!property_exists($content, 'collection') || empty($content?->collection)) {
throw new JsonException("not a valid collection: $collectionPath");
}
$this->collections[$content->collection->info->name] = $content->collection;
} }
unset($this->collectionPaths, $content); unset($this->collectionPaths, $content);
} }

View File

@ -11,7 +11,7 @@ class AbstractRequestTest extends TestCase
{ {
/** /**
* @covers PmConverter\Converters\Abstract\AbstractRequest * @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\HttpVersions * @covers PmConverter\HttpVersion
* @return void * @return void
* @throws InvalidHttpVersionException * @throws InvalidHttpVersionException
*/ */
@ -26,7 +26,7 @@ class AbstractRequestTest extends TestCase
/** /**
* @covers PmConverter\Converters\Abstract\AbstractRequest * @covers PmConverter\Converters\Abstract\AbstractRequest
* @covers PmConverter\Converters\Abstract\AbstractRequest::getVerb() * @covers PmConverter\Converters\Abstract\AbstractRequest::getVerb()
* @covers PmConverter\HttpVersions * @covers PmConverter\HttpVersion
* @return void * @return void
* @throws InvalidHttpVersionException * @throws InvalidHttpVersionException
*/ */