Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ fix-style: php-cs-fixer.phar
install: composer.lock
$(DOCKER_PHP) composer install --prefer-dist --no-interaction --no-progress --ansi

test: unit test-recursion.json test-recursion2.yaml test-recursion3_index.yaml test-empty-maps.json test-const.json
test: unit test-recursion.json test-recursion2.yaml test-recursion3_index.yaml test-empty-maps.json test-const.json test-openapi-3.1.yaml

unit:
$(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) vendor/bin/phpunit --colors=always $(TESTCASE)

test-debug: unit-debug test-recursion.json test-recursion2.yaml test-recursion3_index.yaml test-empty-maps.json test-const.json
test-debug: unit-debug test-recursion.json test-recursion2.yaml test-recursion3_index.yaml test-empty-maps.json test-const.json test-openapi-3.1.yaml

unit-debug:
$(DOCKER_PHP) php $(PHPARGS) $(XPHPARGS) vendor/bin/phpunit --debug --testdox --colors=always $(TESTCASE)
Expand Down
8 changes: 7 additions & 1 deletion src/ReferenceContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\json\JsonPointer;
use cebe\openapi\spec\Reference;
use cebe\openapi\spec\Schema;
use Symfony\Component\Yaml\Yaml;

/**
Expand Down Expand Up @@ -302,7 +303,7 @@ private function getUriScheme(string $uri): string
* @param JsonPointer $pointer
* @param array $data
* @param string|null $toType
* @return SpecObjectInterface|array|null
* @return SpecObjectInterface|array|bool|null a boolean for an OpenAPI 3.1 boolean schema
*/
public function resolveReferenceData($uri, JsonPointer $pointer, $data, $toType)
{
Expand All @@ -317,6 +318,11 @@ public function resolveReferenceData($uri, JsonPointer $pointer, $data, $toType)
return null;
}

// OpenAPI 3.1: a Schema Object can also be a boolean
if ($toType === Schema::class && is_bool($referencedData)) {
return $referencedData;
}

// transitive reference
if (isset($referencedData['$ref'])) {
return new Reference($referencedData, $toType);
Expand Down
6 changes: 6 additions & 0 deletions src/SpecBaseObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use cebe\openapi\json\JsonPointer;
use cebe\openapi\json\JsonReference;
use cebe\openapi\spec\Reference;
use cebe\openapi\spec\Schema;
use cebe\openapi\spec\Type;

/**
Expand Down Expand Up @@ -153,6 +154,11 @@ protected function instantiate($type, $data)
return $data;
}

// OpenAPI 3.1: a Schema Object can also be a boolean, which is kept as it is.
if ($type === Schema::class && is_bool($data)) {
return $data;
}

if (is_array($data) && isset($data['$ref'])) {
return new Reference($data, $type);
}
Expand Down
4 changes: 3 additions & 1 deletion src/spec/Components.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#componentsObject
*
* @property Schema[]|Reference[] $schemas
* @property (Schema|Reference|bool)[] $schemas
* @property Response[]|Reference[] $responses
* @property Parameter[]|Reference[] $parameters
* @property Example[]|Reference[] $examples
Expand All @@ -26,6 +26,7 @@
* @property SecurityScheme[]|Reference[] $securitySchemes
* @property Link[]|Reference[] $links
* @property Callback[]|Reference[] $callbacks
* @property PathItem[]|Reference[] $pathItems
*
*
*/
Expand All @@ -46,6 +47,7 @@ protected function attributes(): array
'securitySchemes' => [Type::STRING, SecurityScheme::class],
'links' => [Type::STRING, Link::class],
'callbacks' => [Type::STRING, Callback::class],
'pathItems' => [Type::STRING, PathItem::class],
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/spec/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function performValidation()
if (!empty($this->in)) {
$this->addError("'in' must not be specified in Header Object.");
}
if (!empty($this->content) && !empty($this->schema)) {
if (!empty($this->content) && $this->schema !== null) {
$this->addError("A Header Object MUST contain either a schema property, or a content property, but not both. ");
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/spec/Info.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#infoObject
*
* @property string $title
* @property string $summary
* @property string $description
* @property string $termsOfService
* @property Contact|null $contact
Expand All @@ -33,6 +34,7 @@ protected function attributes(): array
{
return [
'title' => Type::STRING,
'summary' => Type::STRING,
'description' => Type::STRING,
'termsOfService' => Type::STRING,
'contact' => Contact::class,
Expand Down
5 changes: 5 additions & 0 deletions src/spec/License.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#licenseObject
*
* @property string $name
* @property string $identifier
* @property string $url
*
*/
Expand All @@ -27,6 +28,7 @@ protected function attributes(): array
{
return [
'name' => Type::STRING,
'identifier' => Type::STRING,
'url' => Type::STRING,
];
}
Expand All @@ -38,5 +40,8 @@ protected function performValidation()
{
$this->requireProperties(['name']);
$this->validateUrl('url');
if (!empty($this->identifier) && !empty($this->url)) {
$this->addError('A License Object MUST NOT contain both an identifier and a url.');
}
}
}
10 changes: 5 additions & 5 deletions src/spec/MediaType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#mediaTypeObject
*
* @property Schema|Reference|null $schema
* @property Schema|Reference|bool|null $schema
* @property mixed $example
* @property Example[]|Reference[] $examples
* @property Encoding[] $encoding
Expand Down Expand Up @@ -54,11 +54,11 @@ public function __construct(array $data)
$encoding[$property] = $encodingData;
} elseif (is_array($encodingData)) {
$schema = $this->schema->properties[$property] ?? null;
// Don't pass the schema if it's still an unresolved reference.
if ($schema instanceof Reference) {
$encoding[$property] = new Encoding($encodingData);
} else {
// Don't pass the schema if it's still an unresolved reference or a boolean schema.
if ($schema instanceof Schema) {
$encoding[$property] = new Encoding($encodingData, $schema);
} else {
$encoding[$property] = new Encoding($encodingData);
}
} else {
$givenType = gettype($encodingData);
Expand Down
6 changes: 4 additions & 2 deletions src/spec/OpenApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
*
* @property string $openapi
* @property Info $info
* @property string|null $jsonSchemaDialect
* @property Server[] $servers
* @property Paths|PathItem[] $paths
* @property Components|null $components
* @property PathItem[]|null $webhooks
* @property (PathItem|Reference)[] $webhooks
* @property SecurityRequirement[] $security
* @property Tag[] $tags
* @property ExternalDocumentation|null $externalDocs
Expand All @@ -45,9 +46,10 @@ protected function attributes(): array
return [
'openapi' => Type::STRING,
'info' => Info::class,
'jsonSchemaDialect' => Type::STRING,
'servers' => [Server::class],
'paths' => Paths::class,
'webhooks' => [PathItem::class],
'webhooks' => [Type::STRING, PathItem::class],
'components' => Components::class,
'security' => [SecurityRequirement::class],
'tags' => [Tag::class],
Expand Down
4 changes: 2 additions & 2 deletions src/spec/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* @property string $style
* @property boolean $explode
* @property boolean $allowReserved
* @property Schema|Reference|null $schema
* @property Schema|Reference|bool|null $schema
* @property mixed $example
* @property Example[] $examples
*
Expand Down Expand Up @@ -114,7 +114,7 @@ protected function performValidation()
$this->addError("Parameter 'required' must be true for 'in': 'path'.");
}
}
if (!empty($this->content) && !empty($this->schema)) {
if (!empty($this->content) && $this->schema !== null) {
$this->addError('A Parameter Object MUST contain either a schema property, or a content property, but not both.');
}
if (!empty($this->content) && count($this->content) !== 1) {
Expand Down
5 changes: 5 additions & 0 deletions src/spec/Reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ private function resolveTransitiveReference(Reference $referencedObject, Referen
*/
private function adjustRelativeReferences($referencedDocument, $basePath, $baseDocument = null, $oContext = null)
{
// nothing to adjust in scalar values, for example a boolean schema
if (!is_array($referencedDocument)) {
return $referencedDocument;
}

$context = new ReferenceContext(null, $basePath);
if ($baseDocument === null) {
$baseDocument = $referencedDocument;
Expand Down
14 changes: 8 additions & 6 deletions src/spec/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
* @property mixed $const
*
* @property string|string[] $type type can only be `string` in OpenAPI 3.0, but can be an array of strings since OpenAPI 3.1
* @property Schema[]|Reference[] $allOf
* @property Schema[]|Reference[] $oneOf
* @property Schema[]|Reference[] $anyOf
* @property Schema|Reference|null $not
* @property Schema|Reference|null $items
* @property Schema[]|Reference[] $properties
* Since OpenAPI 3.1 a Schema Object can also be a boolean, so every property below that holds Schema Objects can contain `true` or `false` too.
*
* @property (Schema|Reference|bool)[] $allOf
* @property (Schema|Reference|bool)[] $oneOf
* @property (Schema|Reference|bool)[] $anyOf
* @property Schema|Reference|bool|null $not
* @property Schema|Reference|bool|null $items
* @property (Schema|Reference|bool)[] $properties
* @property Schema|Reference|bool $additionalProperties
* @property string $description
* @property string $format
Expand Down
3 changes: 2 additions & 1 deletion src/spec/SecurityScheme.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class SecurityScheme extends SpecBaseObject
"apiKey",
"http",
"oauth2",
"openIdConnect"
"openIdConnect",
"mutualTLS"
];

/**
Expand Down
131 changes: 131 additions & 0 deletions tests/spec/OpenApi31Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

use cebe\openapi\Reader;
use cebe\openapi\Writer;
use cebe\openapi\spec\Encoding;
use cebe\openapi\spec\License;
use cebe\openapi\spec\MediaType;
use cebe\openapi\spec\OpenApi;
use cebe\openapi\spec\Parameter;
use cebe\openapi\spec\PathItem;
use cebe\openapi\spec\Schema;

/**
* Features added in OpenAPI 3.1 that are not specific to a single spec object.
*/
class OpenApi31Test extends \PHPUnit\Framework\TestCase
{
private function readDocument(): OpenApi
{
return Reader::readFromYamlFile(realpath(__DIR__ . '/data/openapi-3.1.yaml'));
}

public function testDocumentIsValid()
{
$openapi = $this->readDocument();

$this->assertTrue($openapi->validate(), implode("\n", $openapi->getErrors()));
$this->assertSame([], $openapi->getErrors());
}

public function testBooleanSchemas()
{
$schemas = $this->readDocument()->components->schemas;

$this->assertTrue($schemas['Anything']);
$this->assertFalse($schemas['Nothing']);

$this->assertInstanceOf(Schema::class, $schemas['EmptyList']);
$this->assertFalse($schemas['EmptyList']->items);

$combined = $schemas['Combined'];
$this->assertTrue($combined->properties['anything']);
$this->assertFalse($combined->properties['nothing']);
$this->assertSame([true], $combined->allOf);
$this->assertFalse($combined->not);
$this->assertFalse($combined->additionalProperties);
}

public function testExternalReferenceToBooleanSchema()
{
$combined = $this->readDocument()->components->schemas['Combined'];

$this->assertTrue($combined->properties['external']);
$this->assertTrue($combined->properties['externalAlias']);
}

public function testBooleanSchemasAreWritten()
{
$data = json_decode(Writer::writeToJson($this->readDocument()), true);
$schemas = $data['components']['schemas'];

$this->assertTrue($schemas['Anything']);
$this->assertFalse($schemas['Nothing']);
$this->assertFalse($schemas['EmptyList']['items']);
$this->assertSame(['anything' => true, 'nothing' => false, 'external' => true, 'externalAlias' => true], $schemas['Combined']['properties']);
$this->assertSame([true], $schemas['Combined']['allOf']);
$this->assertFalse($schemas['Combined']['not']);
}

public function testMediaTypeWithBooleanSchemas()
{
$mediaType = new MediaType(['schema' => true]);
$this->assertTrue($mediaType->schema);

$mediaType = new MediaType([
'schema' => ['properties' => ['file' => true]],
'encoding' => ['file' => ['contentType' => 'text/plain']],
]);
$this->assertInstanceOf(Encoding::class, $mediaType->encoding['file']);
}

public function testParameterDoesNotAllowBooleanSchemaAndContent()
{
$parameter = new Parameter(['name' => 'a', 'in' => 'query', 'schema' => true]);
$this->assertTrue($parameter->validate(), implode("\n", $parameter->getErrors()));

$parameter = new Parameter(['name' => 'a', 'in' => 'query', 'schema' => false, 'content' => ['text/plain' => []]]);
$this->assertFalse($parameter->validate());
}

public function testFields()
{
$openapi = $this->readDocument();

$this->assertSame('https://json-schema.org/draft/2020-12/schema', $openapi->jsonSchemaDialect);
$this->assertSame('Fields and boolean schemas added in OpenAPI 3.1', $openapi->info->summary);
$this->assertSame('Apache-2.0', $openapi->info->license->identifier);
$this->assertInstanceOf(PathItem::class, $openapi->components->pathItems['Shared']);
$this->assertInstanceOf(PathItem::class, $openapi->webhooks['newPet']);
$this->assertSame('mutualTLS', $openapi->components->securitySchemes['tls']->type);
}

public function testFieldsAreOptional()
{
$openapi = new OpenApi([]);

$this->assertNull($openapi->jsonSchemaDialect);
$this->assertSame([], $openapi->webhooks);
}

public function testEmptyWebhooksAreWrittenAsMap()
{
$openapi = new OpenApi([
'openapi' => '3.1.0',
'info' => ['title' => 'Test', 'version' => '1.0.0'],
'webhooks' => [],
]);

$this->assertEquals(new stdClass(), json_decode(Writer::writeToJson($openapi))->webhooks);
}

public function testLicenseDoesNotAllowIdentifierAndUrl()
{
$license = new License(['name' => 'MIT', 'identifier' => 'MIT']);
$this->assertTrue($license->validate(), implode("\n", $license->getErrors()));

$license = new License(['name' => 'MIT', 'identifier' => 'MIT', 'url' => 'https://opensource.org/license/mit']);
$this->assertFalse($license->validate());
$this->assertSame(['A License Object MUST NOT contain both an identifier and a url.'], $license->getErrors());
}
}
1 change: 0 additions & 1 deletion tests/spec/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ public static function badSchemaProvider()
{
yield [['properties' => ['a' => 'foo']], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'foo\''];
yield [['properties' => ['a' => 42]], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'42\''];
yield [['properties' => ['a' => false]], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'\''];
yield [['properties' => ['a' => new stdClass()]], "Unable to instantiate cebe\openapi\spec\Schema Object with data 'stdClass Object\n(\n)\n'"];

yield [['additionalProperties' => 'foo'], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "string" given'];
Expand Down
3 changes: 3 additions & 0 deletions tests/spec/data/openapi-3.1-definitions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Anything: true
Alias:
$ref: '#/Anything'
Loading
Loading