diff --git a/Makefile b/Makefile index e9b15d0..9a07de5 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,14 @@ install: test: vendor/bin/phpunit +# copy openapi3 json schema +schemas/openapi-v3.0.json: vendor/oai/openapi-specification/schemas/v3.0/schema.json + cp $< $@ + +schemas/openapi-v3.0.yaml: vendor/oai/openapi-specification/schemas/v3.0/schema.yaml + cp $< $@ + + # find spec classes that are not mentioned in tests with @covers yet coverage: .php-openapi-covA .php-openapi-covB diff $^ diff --git a/README.md b/README.md index c9551c7..93d2b00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # php-openapi -READ [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the content accessible in PHP objects. +Read and write [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the content accessible in PHP objects. + +It also provides a CLI tool for validating and converting OpenAPI 3.0.x YAML and JSON files. [![Latest Stable Version](https://poser.pugx.org/cebe/php-openapi/v/stable)](https://packagist.org/packages/cebe/php-openapi) [![Build Status](https://travis-ci.org/cebe/php-openapi.svg?branch=master)](https://travis-ci.org/cebe/php-openapi) @@ -17,23 +19,66 @@ READ [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the ## Used by -This library provides a low level API for reading OpenAPI files. It is used by higher level tools to +This library provides a low level API for reading and writing OpenAPI files. It is used by higher level tools to do awesome work: -- https://github.com/cebe/yii2-openapi Code Generator for REST API from OpenAPI spec +- https://github.com/cebe/yii2-openapi Code Generator for REST API from OpenAPI spec, includes fake data generator. - https://github.com/cebe/yii2-app-api Yii framework application template for developing API-first applications - ... ([add yours](https://github.com/cebe/php-openapi/edit/master/README.md#L24)) ## Usage +### CLI tool + + $ vendor/bin/php-openapi help + PHP OpenAPI 3 tool + ------------------ + by Carsten Brandt + + Usage: + php-openapi [] [input.yml|input.json] [output.yml|output.json] + + The following commands are available: + + validate Validate the API description in the specified input file against the OpenAPI v3.0 schema. + Note: the validation is performed in two steps. The results is composed of + (1) structural errors found while reading the API description file, and + (2) violations of the OpenAPI v3.0 schema. + + If no input file is specified input will be read from STDIN. + The tool will try to auto-detect the content type of the input, but may fail + to do so, you may specify --read-yaml or --read-json to force the file type. + + Exits with code 2 on validation errors, 1 on other errors and 0 on success. + + convert Convert a JSON or YAML input file to JSON or YAML output file. + References are being resolved so the output will be a single specification file. + + If no input file is specified input will be read from STDIN. + If no output file is specified output will be written to STDOUT. + The tool will try to auto-detect the content type of the input and output file, but may fail + to do so, you may specify --read-yaml or --read-json to force the input file type. + and --write-yaml or --write-json to force the output file type. + + help Shows this usage information. + + Options: + + --read-json force reading input as JSON. Auto-detect if not specified. + --read-yaml force reading input as YAML. Auto-detect if not specified. + --write-json force writing output as JSON. Auto-detect if not specified. + --write-yaml force writing output as YAML. Auto-detect if not specified. + + ### Reading Specification information -Read OpenAPI spec from JSON: +Read OpenAPI spec from JSON file: ```php use cebe\openapi\Reader; -$openapi = Reader::readFromJson(file_get_contents('openapi.json')); +// realpath is needed for resolving references with relative Paths or URLs +$openapi = Reader::readFromJsonFile(realpath('openapi.json')); ``` Read OpenAPI spec from YAML: @@ -41,7 +86,10 @@ Read OpenAPI spec from YAML: ```php use cebe\openapi\Reader; -$openapi = Reader::readFromYaml(file_get_contents('openapi.yaml')); +// realpath is needed for resolving references with relative Paths or URLs +$openapi = Reader::readFromYamlFile(realpath('openapi.json')); +// you may also specify the URL to your description file +$openapi = Reader::readFromYamlFile('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/3.0.2/examples/v3.0/petstore-expanded.yaml'); ``` Access specification data: @@ -57,6 +105,44 @@ foreach($openapi->paths as $path => $definition) { Object properties are exactly like in the [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#openapi-specification). You may also access additional properties added by specification extensions. +### Writing Specification files + +```php +// create base description +$openapi = new \cebe\openapi\spec\OpenApi([ + 'openapi' => '3.0.2', + 'info' => [ + 'title' => 'Test API', + 'version' => '1.0.0', + ], + 'paths' => [], +]); +// manipulate description as needed +$openapi->paths['/test'] = new \cebe\openapi\spec\PathItem([ + 'description' => 'something' +]); +// ... + +$json = \cebe\openapi\Writer::writeToJson($openapi); +``` + +results in the following JSON data: + +```json +{ + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/test": { + "description": "something" + } + } +} +``` + ### Reading Specification Files and Resolving References In the above we have passed the raw JSON or YAML data to the Reader. In order to be able to resolve @@ -82,6 +168,8 @@ $openapi->resolveReferences( ### Validation The library provides simple validation operations, that check basic OpenAPI spec requirements. +This is the same as "structural errors found while reading the API description file" from the CLI tool. +This validation does not include checking against the OpenAPI v3.0 JSON schema. ``` // return `true` in case no errors have been found, `false` in case of errors. diff --git a/bin/php-openapi b/bin/php-openapi new file mode 100755 index 0000000..e7685bd --- /dev/null +++ b/bin/php-openapi @@ -0,0 +1,287 @@ +#!/usr/bin/env php + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +$composerAutoload = [ + __DIR__ . '/../vendor/autoload.php', // standalone with "composer install" run + __DIR__ . '/../../../autoload.php', // script is installed as a composer binary +]; +foreach ($composerAutoload as $autoload) { + if (file_exists($autoload)) { + require($autoload); + break; + } +} + +// Send all errors to stderr +ini_set('display_errors', 'stderr'); +// open streams if not in CLI sapi +defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w')); +defined('STDERR') or define('STDERR', fopen('php://stderr', 'w')); + +$command = null; +$inputFile = null; +$inputFormat = null; +$outputFile = null; +$outputFormat = null; +foreach($argv as $k => $arg) { + if ($k == 0) { + continue; + } + if ($arg[0] == '-' || $arg === 'help') { + $arg = explode('=', $arg); + switch($arg[0]) { + case '--read-yaml': + if ($inputFormat === null) { + $inputFormat = 'yaml'; + } else { + error("Conflicting arguments: only one of --read-json or --read-yaml is allowed!", "usage"); + } + break; + case '--read-json': + if ($inputFormat === null) { + $inputFormat = 'json'; + } else { + error("Conflicting arguments: only one of --read-json or --read-yaml is allowed!", "usage"); + } + break; + case '--write-yaml': + if ($outputFormat === null) { + $outputFormat = 'yaml'; + } else { + error("Conflicting arguments: only one of --write-json or --write-yaml is allowed!", "usage"); + } + break; + case '--write-json': + if ($outputFormat === null) { + $outputFormat = 'json'; + } else { + error("Conflicting arguments: only one of --write-json or --write-yaml is allowed!", "usage"); + } + break; + case '-h': + case '--help': + case 'help': + print_formatted( + "\BPHP OpenAPI 3 tool\C\n" + . "\B------------------\C\n" + . "by Carsten Brandt \n\n", + STDERR + ); + usage(); + break; + default: + error("Unknown argument " . $arg[0], "usage"); + } + } else { + if ($command === null) { + $command = $arg; + } elseif ($inputFile === null) { + $inputFile = $arg; + } elseif ($outputFile === null) { + if ($command !== 'convert') { + error("Too many arguments: " . $arg, "usage"); + } + $outputFile = $arg; + } else { + error("Too many arguments: " . $arg, "usage"); + } + } +} +switch ($command) { + case 'validate': + + $openApi = read_input($inputFile, $inputFormat); + + // Validate + + $openApi->validate(); + $errors = $openApi->getErrors(); + + $validator = new JsonSchema\Validator; + $validator->validate($openApi->getSerializableData(), (object)['$ref' => 'file://' . dirname(__DIR__) . '/schemas/openapi-v3.0.json']); + + if ($validator->isValid() && empty($errors)) { + print_formatted("The supplied API Description \B\Gvalidates\C against the OpenAPI v3.0 schema.\n", STDERR); + exit(0); + } + + if (!empty($errors)) { + print_formatted("\BErrors found while reading the API Description:\C\n", STDERR); + foreach ($errors as $error) { + fwrite(STDERR, "- $error\n"); + } + } + if (!$validator->isValid()) { + print_formatted("\BOpenAPI v3.0 schema violations:\C\n", STDERR); + foreach ($validator->getErrors() as $error) { + print_formatted(sprintf("- [\Y%s\C] %s\n", $error['property'], $error['message']), STDERR); + } + } + exit(2); + + break; + case 'convert': + + $openApi = read_input($inputFile, $inputFormat); + + if ($outputFile === null) { + if ($outputFormat === null) { + error("No output fromat specified, please specify --write-json or --write-yaml.", "usage"); + } elseif ($outputFormat === 'json') { + fwrite(STDOUT, \cebe\openapi\Writer::writeToJson($openApi)); + } else { + fwrite(STDOUT, \cebe\openapi\Writer::writeToYaml($openApi)); + } + fclose(STDOUT); + exit(0); + } + + if ($outputFormat === null) { + if (strtolower(substr($outputFile, -5, 5)) === '.json') { + $outputFormat = 'json'; + } elseif (strtolower(substr($outputFile, -5, 5)) === '.yaml') { + $outputFormat = 'yaml'; + } elseif (strtolower(substr($outputFile, -4, 4)) === '.yml') { + $outputFormat = 'yaml'; + } else { + error("Failed to detect output format from file name, please specify --write-json or --write-yaml.", "usage"); + } + } + if ($outputFormat === 'json') { + \cebe\openapi\Writer::writeToJsonFile($openApi, $outputFile); + } else { + \cebe\openapi\Writer::writeToYamlFile($openApi, $outputFile); + } + exit(0); + + break; + case null: + error("No command specified.", "usage"); + break; + default: + error("Unknown command " . $command, "usage"); +} + + + +// functions + +function read_input($inputFile, $inputFormat) +{ + try { + if ($inputFile === null) { + $fileContent = file_get_contents("php://stdin"); + if ($inputFormat === null) { + $inputFormat = (ltrim($fileContent) === '{' && rtrim($fileContent) === '}') ? 'json' : 'yaml'; + } + if ($inputFormat === 'json') { + $openApi = \cebe\openapi\Reader::readFromJson($fileContent); + } else { + $openApi = \cebe\openapi\Reader::readFromYaml($fileContent); + } + } else { + if (!file_exists($inputFile)) { + error("File does not exist: " . $inputFile); + } + if ($inputFormat === null) { + if (strtolower(substr($inputFile, -5, 5)) === '.json') { + $inputFormat = 'json'; + } elseif (strtolower(substr($inputFile, -5, 5)) === '.yaml') { + $inputFormat = 'yaml'; + } elseif (strtolower(substr($inputFile, -4, 4)) === '.yml') { + $inputFormat = 'yaml'; + } else { + error("Failed to detect input format from file name, please specify --read-json or --read-yaml.", "usage"); + } + } + if ($inputFormat === 'json') { + $openApi = \cebe\openapi\Reader::readFromJsonFile(realpath($inputFile)); + } else { + $openApi = \cebe\openapi\Reader::readFromYamlFile(realpath($inputFile)); + } + } + } catch (Symfony\Component\Yaml\Exception\ParseException $e) { + error($e->getMessage()); + exit(1); + } + return $openApi; +} + +/** + * Display usage information + */ +function usage() { + global $argv; + $cmd = basename($argv[0]); + print_formatted(<<\C [\Y\C] [\Ginput.yml\C|\Ginput.json\C] [\Goutput.yml\C|\Goutput.json\C] + + The following commands are available: + + \Bvalidate\C Validate the API description in the specified \Ginput file\C against the OpenAPI v3.0 schema. + Note: the validation is performed in two steps. The results is composed of + (1) structural errors found while reading the API description file, and + (2) violations of the OpenAPI v3.0 schema. + + If no input file is specified input will be read from STDIN. + The tool will try to auto-detect the content type of the input, but may fail + to do so, you may specify \Y--read-yaml\C or \Y--read-json\C to force the file type. + + Exits with code 2 on validation errors, 1 on other errors and 0 on success. + + \Bconvert\C Convert a JSON or YAML input file to JSON or YAML output file. + References are being resolved so the output will be a single specification file. + + If no input file is specified input will be read from STDIN. + If no output file is specified output will be written to STDOUT. + The tool will try to auto-detect the content type of the input and output file, but may fail + to do so, you may specify \Y--read-yaml\C or \Y--read-json\C to force the input file type. + and \Y--write-yaml\C or \Y--write-json\C to force the output file type. + + \Bhelp\C Shows this usage information. + + Options: + + \Y--read-json\C force reading input as JSON. Auto-detect if not specified. + \Y--read-yaml\C force reading input as YAML. Auto-detect if not specified. + \Y--write-json\C force writing output as JSON. Auto-detect if not specified. + \Y--write-yaml\C force writing output as YAML. Auto-detect if not specified. + + +EOF + , STDERR +); + exit(1); +} + +/** + * Send custom error message to stderr + * @param $message string + * @param $callback mixed called before script exit + * @return void + */ +function error($message, $callback = null) { + print_formatted("\B\RError\C: " . $message . "\n", STDERR); + if (is_callable($callback)) { + call_user_func($callback); + } + exit(1); +} + +function print_formatted($string, $stream) { + fwrite($stream, strtr($string, [ + '\\Y' => "\033[33m", // yellow + '\\G' => "\033[32m", // green + '\\R' => "\033[31m", // green + '\\B' => "\033[1m", // bold + '\\C' => "\033[0m", // clear + ])); +} diff --git a/composer.json b/composer.json index eff5cd9..dd959d7 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "require": { "php": ">=7.1.0", "ext-json": "*", - "symfony/yaml": "^4.0" + "symfony/yaml": "^4.0", + "justinrainbow/json-schema": "^5.0" }, "require-dev": { "cebe/indent": "*", @@ -43,6 +44,9 @@ "dev-master": "1.0.x-dev" } }, + "bin": [ + "bin/php-openapi" + ], "repositories": [ { "type": "package", diff --git a/schemas/openapi-v3.0.json b/schemas/openapi-v3.0.json new file mode 100644 index 0000000..7180840 --- /dev/null +++ b/schemas/openapi-v3.0.json @@ -0,0 +1,1654 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/schema/2019-04-02", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Validation schema for OpenAPI Specification 3.0.X.", + "type": "object", + "required": [ + "openapi", + "info", + "paths" + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "type": "object", + "required": [ + "title", + "version" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": [ + "default" + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": { + }, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": { + }, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": { + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": { + }, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": [ + "simple" + ], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/definitions/Operation" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": [ + "responses" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": { + } + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": [ + "schema", + "content" + ] + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": [ + "style" + ] + } + }, + { + "not": { + "required": [ + "explode" + ] + } + }, + { + "not": { + "required": [ + "allowReserved" + ] + } + }, + { + "not": { + "required": [ + "example" + ] + } + }, + { + "not": { + "required": [ + "examples" + ] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "required": [ + "name", + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + }, + { + "$ref": "#/definitions/ParameterLocation" + } + ] + }, + "ParameterLocation": { + "description": "Parameter location", + "oneOf": [ + { + "description": "Parameter in path", + "required": [ + "required" + ], + "properties": { + "in": { + "enum": [ + "path" + ] + }, + "style": { + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + }, + "required": { + "enum": [ + true + ] + } + } + }, + { + "description": "Parameter in query", + "properties": { + "in": { + "enum": [ + "query" + ] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + { + "description": "Parameter in header", + "properties": { + "in": { + "enum": [ + "header" + ] + }, + "style": { + "enum": [ + "simple" + ], + "default": "simple" + } + } + }, + { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": [ + "cookie" + ] + }, + "style": { + "enum": [ + "form" + ], + "default": "form" + } + } + } + ] + }, + "RequestBody": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "enum": [ + "bearer" + ] + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": [ + "bearerFormat" + ] + }, + "properties": { + "scheme": { + "not": { + "enum": [ + "bearer" + ] + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": [ + "tokenUrl" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": [ + "tokenUrl" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": { + } + }, + "requestBody": { + }, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": [ + "operationId", + "operationRef" + ] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": { + } + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Header" + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/schemas/openapi-v3.0.yaml b/schemas/openapi-v3.0.yaml new file mode 100644 index 0000000..13e47ff --- /dev/null +++ b/schemas/openapi-v3.0.yaml @@ -0,0 +1,1003 @@ +id: https://spec.openapis.org/oas/3.0/schema/2019-04-02 +$schema: http://json-schema.org/draft-04/schema# +description: Validation schema for OpenAPI Specification 3.0.X. +type: object +required: + - openapi + - info + - paths +properties: + openapi: + type: string + pattern: ^3\.0\.\d(-.+)?$ + info: + $ref: '#/definitions/Info' + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + servers: + type: array + items: + $ref: '#/definitions/Server' + security: + type: array + items: + $ref: '#/definitions/SecurityRequirement' + tags: + type: array + items: + $ref: '#/definitions/Tag' + uniqueItems: true + paths: + $ref: '#/definitions/Paths' + components: + $ref: '#/definitions/Components' +patternProperties: + '^x-': {} +additionalProperties: false +definitions: + Reference: + type: object + required: + - $ref + patternProperties: + '^\$ref$': + type: string + format: uri-reference + Info: + type: object + required: + - title + - version + properties: + title: + type: string + description: + type: string + termsOfService: + type: string + format: uri-reference + contact: + $ref: '#/definitions/Contact' + license: + $ref: '#/definitions/License' + version: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + + Contact: + type: object + properties: + name: + type: string + url: + type: string + format: uri-reference + email: + type: string + format: email + patternProperties: + '^x-': {} + additionalProperties: false + + License: + type: object + required: + - name + properties: + name: + type: string + url: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + Server: + type: object + required: + - url + properties: + url: + type: string + description: + type: string + variables: + type: object + additionalProperties: + $ref: '#/definitions/ServerVariable' + patternProperties: + '^x-': {} + additionalProperties: false + + ServerVariable: + type: object + required: + - default + properties: + enum: + type: array + items: + type: string + default: + type: string + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + Components: + type: object + properties: + schemas: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + responses: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Response' + parameters: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Parameter' + examples: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Example' + requestBodies: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/RequestBody' + headers: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Header' + securitySchemes: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/SecurityScheme' + links: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Link' + callbacks: + type: object + patternProperties: + '^[a-zA-Z0-9\.\-_]+$': + oneOf: + - $ref: '#/definitions/Reference' + - $ref: '#/definitions/Callback' + patternProperties: + '^x-': {} + additionalProperties: false + + Schema: + type: object + properties: + title: + type: string + multipleOf: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: + type: number + exclusiveMaximum: + type: boolean + default: false + minimum: + type: number + exclusiveMinimum: + type: boolean + default: false + maxLength: + type: integer + minimum: 0 + minLength: + type: integer + minimum: 0 + default: 0 + pattern: + type: string + format: regex + maxItems: + type: integer + minimum: 0 + minItems: + type: integer + minimum: 0 + default: 0 + uniqueItems: + type: boolean + default: false + maxProperties: + type: integer + minimum: 0 + minProperties: + type: integer + minimum: 0 + default: 0 + required: + type: array + items: + type: string + minItems: 1 + uniqueItems: true + enum: + type: array + items: {} + minItems: 1 + uniqueItems: false + type: + type: string + enum: + - array + - boolean + - integer + - number + - object + - string + not: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + allOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + oneOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + anyOf: + type: array + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + items: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + properties: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + additionalProperties: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + - type: boolean + default: true + description: + type: string + format: + type: string + default: {} + nullable: + type: boolean + default: false + discriminator: + $ref: '#/definitions/Discriminator' + readOnly: + type: boolean + default: false + writeOnly: + type: boolean + default: false + example: {} + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + deprecated: + type: boolean + default: false + xml: + $ref: '#/definitions/XML' + patternProperties: + '^x-': {} + additionalProperties: false + + Discriminator: + type: object + required: + - propertyName + properties: + propertyName: + type: string + mapping: + type: object + additionalProperties: + type: string + + XML: + type: object + properties: + name: + type: string + namespace: + type: string + format: uri + prefix: + type: string + attribute: + type: boolean + default: false + wrapped: + type: boolean + default: false + patternProperties: + '^x-': {} + additionalProperties: false + + Response: + type: object + required: + - description + properties: + description: + type: string + headers: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Header' + - $ref: '#/definitions/Reference' + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + links: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Link' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + + MediaType: + type: object + properties: + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + example: {} + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + encoding: + type: object + additionalProperties: + $ref: '#/definitions/Encoding' + patternProperties: + '^x-': {} + additionalProperties: false + allOf: + - $ref: '#/definitions/ExampleXORExamples' + + Example: + type: object + properties: + summary: + type: string + description: + type: string + value: {} + externalValue: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + Header: + type: object + properties: + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + enum: + - simple + default: simple + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + minProperties: 1 + maxProperties: 1 + example: {} + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + allOf: + - $ref: '#/definitions/ExampleXORExamples' + - $ref: '#/definitions/SchemaXORContent' + + Paths: + type: object + patternProperties: + '^\/': + $ref: '#/definitions/PathItem' + '^x-': {} + additionalProperties: false + + PathItem: + type: object + properties: + $ref: + type: string + summary: + type: string + description: + type: string + servers: + type: array + items: + $ref: '#/definitions/Server' + parameters: + type: array + items: + oneOf: + - $ref: '#/definitions/Parameter' + - $ref: '#/definitions/Reference' + uniqueItems: true + patternProperties: + '^(get|put|post|delete|options|head|patch|trace)$': + $ref: '#/definitions/Operation' + '^x-': {} + additionalProperties: false + + Operation: + type: object + required: + - responses + properties: + tags: + type: array + items: + type: string + summary: + type: string + description: + type: string + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + operationId: + type: string + parameters: + type: array + items: + oneOf: + - $ref: '#/definitions/Parameter' + - $ref: '#/definitions/Reference' + uniqueItems: true + requestBody: + oneOf: + - $ref: '#/definitions/RequestBody' + - $ref: '#/definitions/Reference' + responses: + $ref: '#/definitions/Responses' + callbacks: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Callback' + - $ref: '#/definitions/Reference' + deprecated: + type: boolean + default: false + security: + type: array + items: + $ref: '#/definitions/SecurityRequirement' + servers: + type: array + items: + $ref: '#/definitions/Server' + patternProperties: + '^x-': {} + additionalProperties: false + + Responses: + type: object + properties: + default: + oneOf: + - $ref: '#/definitions/Response' + - $ref: '#/definitions/Reference' + patternProperties: + '^[1-5](?:\d{2}|XX)$': + oneOf: + - $ref: '#/definitions/Response' + - $ref: '#/definitions/Reference' + '^x-': {} + minProperties: 1 + additionalProperties: false + + + SecurityRequirement: + type: object + additionalProperties: + type: array + items: + type: string + + Tag: + type: object + required: + - name + properties: + name: + type: string + description: + type: string + externalDocs: + $ref: '#/definitions/ExternalDocumentation' + patternProperties: + '^x-': {} + additionalProperties: false + + ExternalDocumentation: + type: object + required: + - url + properties: + description: + type: string + url: + type: string + format: uri-reference + patternProperties: + '^x-': {} + additionalProperties: false + + ExampleXORExamples: + description: Example and examples are mutually exclusive + not: + required: [example, examples] + + SchemaXORContent: + description: Schema and content are mutually exclusive, at least one is required + not: + required: [schema, content] + oneOf: + - required: [schema] + - required: [content] + description: Some properties are not allowed if content is present + allOf: + - not: + required: [style] + - not: + required: [explode] + - not: + required: [allowReserved] + - not: + required: [example] + - not: + required: [examples] + + Parameter: + type: object + properties: + name: + type: string + in: + type: string + description: + type: string + required: + type: boolean + default: false + deprecated: + type: boolean + default: false + allowEmptyValue: + type: boolean + default: false + style: + type: string + explode: + type: boolean + allowReserved: + type: boolean + default: false + schema: + oneOf: + - $ref: '#/definitions/Schema' + - $ref: '#/definitions/Reference' + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + minProperties: 1 + maxProperties: 1 + example: {} + examples: + type: object + additionalProperties: + oneOf: + - $ref: '#/definitions/Example' + - $ref: '#/definitions/Reference' + patternProperties: + '^x-': {} + additionalProperties: false + required: + - name + - in + allOf: + - $ref: '#/definitions/ExampleXORExamples' + - $ref: '#/definitions/SchemaXORContent' + - $ref: '#/definitions/ParameterLocation' + + ParameterLocation: + description: Parameter location + oneOf: + - description: Parameter in path + required: + - required + properties: + in: + enum: [path] + style: + enum: [matrix, label, simple] + default: simple + required: + enum: [true] + + - description: Parameter in query + properties: + in: + enum: [query] + style: + enum: [form, spaceDelimited, pipeDelimited, deepObject] + default: form + + - description: Parameter in header + properties: + in: + enum: [header] + style: + enum: [simple] + default: simple + + - description: Parameter in cookie + properties: + in: + enum: [cookie] + style: + enum: [form] + default: form + + RequestBody: + type: object + required: + - content + properties: + description: + type: string + content: + type: object + additionalProperties: + $ref: '#/definitions/MediaType' + required: + type: boolean + default: false + patternProperties: + '^x-': {} + additionalProperties: false + + SecurityScheme: + oneOf: + - $ref: '#/definitions/APIKeySecurityScheme' + - $ref: '#/definitions/HTTPSecurityScheme' + - $ref: '#/definitions/OAuth2SecurityScheme' + - $ref: '#/definitions/OpenIdConnectSecurityScheme' + + APIKeySecurityScheme: + type: object + required: + - type + - name + - in + properties: + type: + type: string + enum: + - apiKey + name: + type: string + in: + type: string + enum: + - header + - query + - cookie + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + HTTPSecurityScheme: + type: object + required: + - scheme + - type + properties: + scheme: + type: string + bearerFormat: + type: string + description: + type: string + type: + type: string + enum: + - http + patternProperties: + '^x-': {} + additionalProperties: false + oneOf: + - description: Bearer + properties: + scheme: + enum: [bearer] + + - description: Non Bearer + not: + required: [bearerFormat] + properties: + scheme: + not: + enum: [bearer] + + OAuth2SecurityScheme: + type: object + required: + - type + - flows + properties: + type: + type: string + enum: + - oauth2 + flows: + $ref: '#/definitions/OAuthFlows' + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + OpenIdConnectSecurityScheme: + type: object + required: + - type + - openIdConnectUrl + properties: + type: + type: string + enum: + - openIdConnect + openIdConnectUrl: + type: string + format: uri-reference + description: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + OAuthFlows: + type: object + properties: + implicit: + $ref: '#/definitions/ImplicitOAuthFlow' + password: + $ref: '#/definitions/PasswordOAuthFlow' + clientCredentials: + $ref: '#/definitions/ClientCredentialsFlow' + authorizationCode: + $ref: '#/definitions/AuthorizationCodeOAuthFlow' + patternProperties: + '^x-': {} + additionalProperties: false + + ImplicitOAuthFlow: + type: object + required: + - authorizationUrl + - scopes + properties: + authorizationUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + PasswordOAuthFlow: + type: object + required: + - tokenUrl + properties: + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + ClientCredentialsFlow: + type: object + required: + - tokenUrl + properties: + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + AuthorizationCodeOAuthFlow: + type: object + required: + - authorizationUrl + - tokenUrl + properties: + authorizationUrl: + type: string + format: uri-reference + tokenUrl: + type: string + format: uri-reference + refreshUrl: + type: string + format: uri-reference + scopes: + type: object + additionalProperties: + type: string + patternProperties: + '^x-': {} + additionalProperties: false + + Link: + type: object + properties: + operationId: + type: string + operationRef: + type: string + format: uri-reference + parameters: + type: object + additionalProperties: {} + requestBody: {} + description: + type: string + server: + $ref: '#/definitions/Server' + patternProperties: + '^x-': {} + additionalProperties: false + not: + description: Operation Id and Operation Ref are mutually exclusive + required: [operationId, operationRef] + + Callback: + type: object + additionalProperties: + $ref: '#/definitions/PathItem' + patternProperties: + '^x-': {} + + Encoding: + type: object + properties: + contentType: + type: string + headers: + type: object + additionalProperties: + $ref: '#/definitions/Header' + style: + type: string + enum: + - form + - spaceDelimited + - pipeDelimited + - deepObject + explode: + type: boolean + allowReserved: + type: boolean + default: false + additionalProperties: false diff --git a/src/SpecBaseObject.php b/src/SpecBaseObject.php index eaa2a4d..9a4e4a0 100644 --- a/src/SpecBaseObject.php +++ b/src/SpecBaseObject.php @@ -29,6 +29,14 @@ abstract class SpecBaseObject implements SpecObjectInterface */ abstract protected function attributes(): array; + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return []; + } + /** * Perform validation on this object, check data against OpenAPI Specification rules. * @@ -127,6 +135,35 @@ private function instantiate($type, $data) } } + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + $data = $this->_properties; + foreach ($data as $k => $v) { + if ($v instanceof SpecObjectInterface) { + $data[$k] = $v->getSerializableData(); + } elseif (is_array($v)) { + $toObject = false; + $j = 0; + foreach ($v as $i => $d) { + if ($j++ !== $i) { + $toObject = true; + } + if ($d instanceof SpecObjectInterface) { + $data[$k][$i] = $d->getSerializableData(); + } + } + if ($toObject) { + $data[$k] = (object) $data[$k]; + } + } + } + return (object) $data; + } + /** * Validate object data according to OpenAPI spec. * @return bool whether the loaded data is valid according to OpenAPI spec @@ -212,6 +249,9 @@ public function __get($name) if (isset($this->_properties[$name])) { return $this->_properties[$name]; } + if (isset(static::attributeDefaults()[$name])) { + return static::attributeDefaults()[$name]; + } if (isset(static::attributes()[$name])) { if (is_array(static::attributes()[$name])) { return []; @@ -225,12 +265,12 @@ public function __get($name) public function __set($name, $value) { - throw new ReadonlyPropertyException('Setting read-only property: ' . \get_class($this) . '::' . $name); + $this->_properties[$name] = $value; } public function __isset($name) { - if (isset($this->_properties[$name]) || isset(static::attributes()[$name])) { + if (isset($this->_properties[$name]) || isset(static::attributeDefaults()[$name]) || isset(static::attributes()[$name])) { return $this->__get($name) !== null; } @@ -239,7 +279,7 @@ public function __isset($name) public function __unset($name) { - throw new ReadonlyPropertyException('Unsetting read-only property: ' . \get_class($this) . '::' . $name); + unset($this->_properties[$name]); } /** diff --git a/src/SpecObjectInterface.php b/src/SpecObjectInterface.php index 981da13..a96aa74 100644 --- a/src/SpecObjectInterface.php +++ b/src/SpecObjectInterface.php @@ -18,6 +18,12 @@ interface SpecObjectInterface */ public function __construct(array $data); + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData(); + /** * Validate object data according to OpenAPI spec. * @return bool whether the loaded data is valid according to OpenAPI spec diff --git a/src/Writer.php b/src/Writer.php new file mode 100644 index 0000000..7388f4b --- /dev/null +++ b/src/Writer.php @@ -0,0 +1,58 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi; + +use cebe\openapi\spec\OpenApi; +use Symfony\Component\Yaml\Yaml; + +/** + * Utility class to simplify writing JSON or YAML OpenAPI specs. + * + */ +class Writer +{ + /** + * Convert OpenAPI spec object to JSON data. + * @param SpecObjectInterface|OpenApi the OpenApi object instance. + * @return string JSON string. + */ + public static function writeToJson(SpecObjectInterface $object): string + { + return json_encode($object->getSerializableData(), JSON_PRETTY_PRINT); + } + + /** + * Convert OpenAPI spec object to YAML data. + * @param SpecObjectInterface|OpenApi the OpenApi object instance. + * @return string YAML string. + */ + public static function writeToYaml(SpecObjectInterface $object): string + { + return Yaml::dump($object->getSerializableData(), 256, 2, Yaml::DUMP_OBJECT_AS_MAP); + } + + /** + * Write OpenAPI spec object to JSON file. + * @param SpecObjectInterface|OpenApi the OpenApi object instance. + * @param string $fileName file name to write to. + */ + public static function writeToJsonFile(SpecObjectInterface $object, string $fileName): void + { + file_put_contents($fileName, static::writeToJson($object)); + } + + /** + * Write OpenAPI spec object to YAML file. + * @param SpecObjectInterface|OpenApi the OpenApi object instance. + * @param string $fileName file name to write to. + */ + public static function writeToYamlFile(SpecObjectInterface $object, string $fileName): void + { + file_put_contents($fileName, static::writeToYaml($object)); + } +} diff --git a/src/spec/Callback.php b/src/spec/Callback.php index add751e..fe4cad8 100644 --- a/src/spec/Callback.php +++ b/src/spec/Callback.php @@ -41,6 +41,15 @@ public function __construct(array $data) $this->_url = key($data); } + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + return (object) [$this->_url => ($this->_pathItem === null) ? null : $this->_pathItem->getSerializableData()]; + } + /** * @return string */ @@ -49,14 +58,30 @@ public function getUrl() return $this->_url; } + /** + * @param string $url + */ + public function setUrl(string $url): void + { + $this->_url = $url; + } + /** * @return PathItem */ - public function getRequest() + public function getRequest(): ?PathItem { return $this->_pathItem; } + /** + * @param PathItem $request + */ + public function setRequest(?PathItem $request): void + { + $this->_pathItem = $request; + } + /** * Validate object data according to OpenAPI spec. * @return bool whether the loaded data is valid according to OpenAPI spec diff --git a/src/spec/Encoding.php b/src/spec/Encoding.php index 4707632..7b4c034 100644 --- a/src/spec/Encoding.php +++ b/src/spec/Encoding.php @@ -30,8 +30,6 @@ class Encoding extends SpecBaseObject protected function attributes(): array { return [ - // TODO implement default values for contentType - // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#encodingObject 'contentType' => Type::STRING, 'headers' => [Type::STRING, Header::class], // TODO implement default values for style @@ -42,16 +40,49 @@ protected function attributes(): array ]; } + private $_attributeDefaults = []; + + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return $this->_attributeDefaults; + } + /** * Create an object from spec data. * @param array $data spec data read from YAML or JSON * @throws TypeErrorException in case invalid data is supplied. */ - public function __construct(array $data) + public function __construct(array $data, ?Schema $schema = null) { - if (!isset($data['explode']) && isset($data['style'])) { + if (isset($data['style'])) { // Spec: When style is form, the default value is true. - $data['explode'] = ($data['style'] === 'form'); + $this->_attributeDefaults['explode'] = ($data['style'] === 'form'); + } + if ($schema !== null) { + // Spec: Default value depends on the property type: + // for string with format being binary – application/octet-stream; + // for other primitive types – text/plain; + // for object - application/json; + // for array – the default is defined based on the inner type. + switch ($schema->type === 'array' ? ($schema->items->type ?? 'array') : $schema->type) { + case Type::STRING: + if ($schema->format === 'binary') { + $this->_attributeDefaults['contentType'] = 'application/octet-stream'; + break; + } + // no break here + case Type::BOOLEAN: + case Type::INTEGER: + case Type::NUMBER: + $this->_attributeDefaults['contentType'] = 'text/plain'; + break; + case 'object': + $this->_attributeDefaults['contentType'] = 'application/json'; + break; + } } parent::__construct($data); } diff --git a/src/spec/MediaType.php b/src/spec/MediaType.php index 2818d29..ad47394 100644 --- a/src/spec/MediaType.php +++ b/src/spec/MediaType.php @@ -7,6 +7,7 @@ namespace cebe\openapi\spec; +use cebe\openapi\exceptions\TypeErrorException; use cebe\openapi\SpecBaseObject; /** @@ -34,6 +35,27 @@ protected function attributes(): array ]; } + /** + * Create an object from spec data. + * @param array $data spec data read from YAML or JSON + * @throws TypeErrorException in case invalid data is supplied. + */ + public function __construct(array $data) + { + // instantiate Encoding by passing the schema for extracting default values + $encoding = $data['encoding'] ?? null; + unset($data['encoding']); + + parent::__construct($data); + + if (!empty($encoding)) { + foreach($encoding as $property => $encodingData) { + $encoding[$property] = new Encoding($encodingData, $this->schema->properties[$property] ?? null); + } + $this->encoding = $encoding; + } + } + /** * Perform validation on this object, check data against OpenAPI Specification rules. */ diff --git a/src/spec/OpenApi.php b/src/spec/OpenApi.php index e4be96b..24c551a 100644 --- a/src/spec/OpenApi.php +++ b/src/spec/OpenApi.php @@ -45,19 +45,28 @@ protected function attributes(): array } /** - * Create an object from spec data. - * @param array $data spec data read from YAML or JSON - * @throws TypeErrorException in case invalid data is supplied. + * @return array array of attributes default values. */ - public function __construct(array $data) + protected function attributeDefaults(): array { - if (empty($data['servers'])) { - // Spec: If the servers property is not provided, or is an empty array, the default value would be a Server Object with a url value of /. - $data['servers'] = [ - ['url' => '/'], - ]; + return [ + // Spec: If the servers property is not provided, or is an empty array, + // the default value would be a Server Object with a url value of /. + 'servers' => [ + new Server(['url' => '/']) + ], + ]; + } + + public function __get($name) + { + $ret = parent::__get($name); + // Spec: If the servers property is not provided, or is an empty array, + // the default value would be a Server Object with a url value of /. + if ($name === 'servers' && $ret === []) { + return $this->attributeDefaults()['servers']; } - parent::__construct($data); + return $ret; } /** diff --git a/src/spec/Parameter.php b/src/spec/Parameter.php index 05509dc..3afed47 100644 --- a/src/spec/Parameter.php +++ b/src/spec/Parameter.php @@ -57,6 +57,16 @@ protected function attributes(): array ]; } + private $_attributeDefaults = []; + + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return $this->_attributeDefaults; + } + /** * Create an object from spec data. * @param array $data spec data read from YAML or JSON @@ -64,7 +74,7 @@ protected function attributes(): array */ public function __construct(array $data) { - if (!isset($data['style']) && isset($data['in'])) { + if (isset($data['in'])) { // Spec: Default values (based on value of in): // for query - form; // for path - simple; @@ -73,17 +83,19 @@ public function __construct(array $data) switch ($data['in']) { case 'query': case 'cookie': - $data['style'] = 'form'; + $this->_attributeDefaults['style'] = 'form'; + $this->_attributeDefaults['explode'] = true; break; case 'path': case 'header': - $data['style'] = 'simple'; + $this->_attributeDefaults['style'] = 'simple'; + $this->_attributeDefaults['explode'] = false; break; } } - if (!isset($data['explode']) && isset($data['style'])) { + if (isset($data['style'])) { // Spec: When style is form, the default value is true. For all other styles, the default value is false. - $data['explode'] = ($data['style'] === 'form'); + $this->_attributeDefaults['explode'] = ($data['style'] === 'form'); } parent::__construct($data); } diff --git a/src/spec/Paths.php b/src/spec/Paths.php index af03d6b..dc3899b 100644 --- a/src/spec/Paths.php +++ b/src/spec/Paths.php @@ -53,6 +53,19 @@ public function __construct(array $data) } } + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + $data = []; + foreach ($this->_paths as $path => $pathItem) { + $data[$path] = ($pathItem === null) ? null : $pathItem->getSerializableData(); + } + return (object) $data; + } + /** * @param string $name path name * @return bool @@ -71,6 +84,23 @@ public function getPath(string $name): ?PathItem return $this->_paths[$name] ?? null; } + /** + * @param string $name path name + * @param PathItem $pathItem the path item to add + */ + public function addPath(string $name, PathItem $pathItem): void + { + $this->_paths[$name] = $pathItem; + } + + /** + * @param string $name path name + */ + public function removePath(string $name): void + { + unset($this->_paths[$name]); + } + /** * @return PathItem[] */ @@ -150,7 +180,7 @@ public function offsetGet($offset) */ public function offsetSet($offset, $value) { - throw new ReadonlyPropertyException('Setting read-only property: ' . \get_class($this) . '::' . $offset); + $this->addPath($offset, $value); } /** @@ -161,7 +191,7 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { - throw new ReadonlyPropertyException('Unsetting read-only property: ' . \get_class($this) . '::' . $offset); + $this->removePath($offset); } /** diff --git a/src/spec/Reference.php b/src/spec/Reference.php index d024453..a52b2b8 100644 --- a/src/spec/Reference.php +++ b/src/spec/Reference.php @@ -60,6 +60,15 @@ public function __construct(array $data, string $to = null) $this->_ref = $data['$ref']; } + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + return (object) ['$ref' => $this->_ref]; + } + /** * Validate object data according to OpenAPI spec. * @return bool whether the loaded data is valid according to OpenAPI spec diff --git a/src/spec/Responses.php b/src/spec/Responses.php index e80f156..e9e044b 100644 --- a/src/spec/Responses.php +++ b/src/spec/Responses.php @@ -53,6 +53,19 @@ public function __construct(array $data) } } + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + $data = []; + foreach ($this->_responses as $statusCode => $response) { + $data[$statusCode] = ($response === null) ? null : $response->getSerializableData(); + } + return (object) $data; + } + /** * @param string $statusCode HTTP status code * @return bool @@ -71,6 +84,23 @@ public function getResponse($statusCode) return $this->_responses[$statusCode] ?? null; } + /** + * @param string $statusCode HTTP status code + * @param Response|Reference $response + */ + public function addResponse($statusCode, $response): void + { + $this->_responses[$statusCode] = $response; + } + + /** + * @param string $statusCode HTTP status code + */ + public function removeResponse($statusCode) + { + unset($this->_responses[$statusCode]); + } + /** * @return Response[]|Reference[] */ @@ -146,7 +176,7 @@ public function offsetGet($offset) */ public function offsetSet($offset, $value) { - throw new ReadonlyPropertyException('Setting read-only property: ' . \get_class($this) . '::' . $offset); + $this->addResponse($offset, $value); } /** @@ -157,7 +187,7 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { - throw new ReadonlyPropertyException('Unsetting read-only property: ' . \get_class($this) . '::' . $offset); + $this->removeResponse($offset); } /** diff --git a/src/spec/Schema.php b/src/spec/Schema.php index 7138526..d5fd896 100644 --- a/src/spec/Schema.php +++ b/src/spec/Schema.php @@ -93,6 +93,16 @@ protected function attributes(): array ]; } + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return [ + 'additionalProperties' => true, + ]; + } + /** * Create an object from spec data. * @param array $data spec data read from YAML or JSON @@ -112,9 +122,6 @@ public function __construct(array $data) ); } } - } else { - // additionalProperties defaults to true. - $data['additionalProperties'] = true; } parent::__construct($data); } diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index 9bae111..64c992d 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,10 +1,5 @@ - */ class ReaderTest extends \PHPUnit\Framework\TestCase { public function testReadJson() diff --git a/tests/WriterTest.php b/tests/WriterTest.php new file mode 100644 index 0000000..9ae2221 --- /dev/null +++ b/tests/WriterTest.php @@ -0,0 +1,86 @@ + '3.0.0', + 'info' => [ + 'title' => 'Test API', + 'version' => '1.0.0', + ], + 'paths' => [], + ]); + } + + public function testWriteJson() + { + $openapi = $this->createOpenAPI(); + + $json = \cebe\openapi\Writer::writeToJson($openapi); + + $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI(); + + $openapi->paths['/test'] = new \cebe\openapi\spec\PathItem([ + 'description' => 'something' + ]); + + $json = \cebe\openapi\Writer::writeToJson($openapi); + + $this->assertEquals(preg_replace('~\R~', "\n", <<createOpenAPI(); + + $yaml = \cebe\openapi\Writer::writeToYaml($openapi); + + + $this->assertEquals(preg_replace('~\R~', "\n", <<