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
29 changes: 15 additions & 14 deletions src/SpecBaseObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ public function __construct(array $data)
continue;
}

if ($type === Type::STRING || $type === Type::ANY) {
$this->_properties[$property] = $data[$property];
} elseif ($type === Type::BOOLEAN) {
if ($type === Type::BOOLEAN) {
if (!\is_bool($data[$property])) {
$this->_errors[] = "property '$property' must be boolean, but " . gettype($data[$property]) . " given.";
continue;
Expand All @@ -85,7 +83,7 @@ public function __construct(array $data)
$this->_errors[] = "property '$property' must be array of strings, but array has " . gettype($item) . " element.";
}
$this->_properties[$property][] = $item;
} elseif ($type[0] === Type::ANY || $type[0] === Type::BOOLEAN || $type[0] === Type::INTEGER) { // TODO simplify handling of scalar types
} elseif ($type[0] === Type::ANY || Type::isScalar($type[0])) {
$this->_properties[$property][] = $item;
} else {
$this->_properties[$property][] = $this->instantiate($type[0], $item);
Expand All @@ -104,14 +102,16 @@ public function __construct(array $data)
$this->_errors[] = "property '$property' must be map<string, string>, but entry '$key' is of type " . \gettype($item) . '.';
}
$this->_properties[$property][$key] = $item;
} elseif ($type[1] === Type::ANY || $type[1] === Type::BOOLEAN || $type[1] === Type::INTEGER) { // TODO simplify handling of scalar types
} elseif ($type[1] === Type::ANY || Type::isScalar($type[1])) {
$this->_properties[$property][$key] = $item;
} else {
$this->_properties[$property][$key] = $this->instantiate($type[1], $item);
}
}
break;
}
} elseif ($type === Type::ANY || Type::isScalar($type)) {
$this->_properties[$property] = $data[$property];
} else {
$this->_properties[$property] = $this->instantiate($type, $data[$property]);
}
Expand All @@ -125,9 +125,9 @@ public function __construct(array $data)
/**
* @throws TypeErrorException
*/
private function instantiate($type, $data)
protected function instantiate($type, $data)
{
if ($data instanceof $type) {
if ($data instanceof $type || $data instanceof Reference) {
return $data;
}

Expand Down Expand Up @@ -272,7 +272,7 @@ protected function addError(string $error, $class = '')

protected function hasProperty(string $name): bool
{
return isset($this->_properties[$name]) || isset(static::attributes()[$name]);
return isset($this->_properties[$name]) || isset($this->attributes()[$name]);
}

protected function requireProperties(array $names)
Expand Down Expand Up @@ -303,13 +303,14 @@ public function __get($name)
if (isset($this->_properties[$name])) {
return $this->_properties[$name];
}
if (isset(static::attributeDefaults()[$name])) {
return static::attributeDefaults()[$name];
$defaults = $this->attributeDefaults();
if (array_key_exists($name, $defaults)) {
return $defaults[$name];
}
if (isset(static::attributes()[$name])) {
if (is_array(static::attributes()[$name])) {
if (isset($this->attributes()[$name])) {
if (is_array($this->attributes()[$name])) {
return [];
} elseif (static::attributes()[$name] === Type::BOOLEAN) {
} elseif ($this->attributes()[$name] === Type::BOOLEAN) {
return false;
}
return null;
Expand All @@ -324,7 +325,7 @@ public function __set($name, $value)

public function __isset($name)
{
if (isset($this->_properties[$name]) || isset(static::attributeDefaults()[$name]) || isset(static::attributes()[$name])) {
if (isset($this->_properties[$name]) || isset($this->attributeDefaults()[$name]) || isset($this->attributes()[$name])) {
return $this->__get($name) !== null;
}

Expand Down
2 changes: 1 addition & 1 deletion src/spec/PathItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ protected function performValidation()
public function getOperations()
{
$operations = [];
foreach (static::attributes() as $attribute => $type) {
foreach ($this->attributes() as $attribute => $type) {
if ($type === Operation::class && isset($this->$attribute)) {
$operations[$attribute] = $this->$attribute;
}
Expand Down
54 changes: 35 additions & 19 deletions src/spec/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@
* @property array $enum
*
* @property string $type
* @property Schema[] $allOf
* @property Schema[] $oneOf
* @property Schema[] $anyOf
* @property Schema|null $not
* @property Schema|null $items
* @property Schema[] $properties
* @property Schema|bool $additionalProperties
* @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
* @property Schema|Reference|bool $additionalProperties
* @property string $description
* @property string $format
* @property mixed $default
Expand All @@ -70,6 +70,25 @@ class Schema extends SpecBaseObject
protected function attributes(): array
{
return [
// The following properties are taken directly from the JSON Schema definition and follow the same specifications:
// types from https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-4 ff.
'title' => Type::STRING,
'multipleOf' => Type::NUMBER,
'maximum' => Type::NUMBER,
'exclusiveMaximum' => Type::BOOLEAN,
'minimum' => Type::NUMBER,
'exclusiveMinimum' => Type::BOOLEAN,
'maxLength' => Type::INTEGER,
'minLength' => Type::INTEGER,
'pattern' => Type::STRING,
'maxItems' => Type::INTEGER,
'minItems' => Type::INTEGER,
'uniqueItems' => Type::BOOLEAN,
'maxProperties' => Type::INTEGER,
'minProperties' => Type::INTEGER,
'required' => [Type::STRING],
'enum' => [Type::ANY],
// The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification.
'type' => Type::STRING,
'allOf' => [Schema::class],
'oneOf' => [Schema::class],
Expand All @@ -81,7 +100,7 @@ protected function attributes(): array
'description' => Type::STRING,
'format' => Type::STRING,
'default' => Type::ANY,

// Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation:
'nullable' => Type::BOOLEAN,
'discriminator' => Discriminator::class,
'readOnly' => Type::BOOLEAN,
Expand All @@ -100,6 +119,11 @@ protected function attributeDefaults(): array
{
return [
'additionalProperties' => true,
'required' => null,
'enum' => null,
'allOf' => null,
'oneOf' => null,
'anyOf' => null,
];
}

Expand All @@ -112,21 +136,13 @@ public function __construct(array $data)
{
if (isset($data['additionalProperties'])) {
if (is_array($data['additionalProperties'])) {
try {
$data['additionalProperties'] = new Schema($data['additionalProperties']);
} catch (\TypeError $e) {
throw new TypeErrorException(
"Unable to instantiate Schema Object with data '" . print_r($data['additionalProperties'], true) . "'",
$e->getCode(),
$e
);
}
} elseif (!($data['additionalProperties'] instanceof Schema || is_bool($data['additionalProperties']))) {
$data['additionalProperties'] = $this->instantiate(Schema::class, $data['additionalProperties']);
} elseif (!($data['additionalProperties'] instanceof Schema || $data['additionalProperties'] instanceof Reference || is_bool($data['additionalProperties']))) {
$givenType = gettype($data['additionalProperties']);
if ($givenType === 'object') {
$givenType = get_class($data['additionalProperties']);
}
throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either array, boolean or a Schema object, "%s" given', $givenType));
throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "%s" given', $givenType));
}
}
parent::__construct($data);
Expand Down
19 changes: 19 additions & 0 deletions src/spec/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,23 @@ class Type
const BOOLEAN = 'boolean';
const OBJECT = 'object';
const ARRAY = 'array';

/**
* Indicate whether a type is a scalar type, i.e. not an array or object.
*
* For ANY this will return false.
*
* @param string $type value from one of the type constants defined in this class.
* @return bool whether the type is a scalar type.
* @since 1.2.1
*/
public static function isScalar(string $type): bool
{
return in_array($type, [
self::INTEGER,
self::NUMBER,
self::STRING,
self::BOOLEAN,
]);
}
}
95 changes: 92 additions & 3 deletions tests/spec/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ public function badSchemaProvider()
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 array, boolean or a Schema object, "string" given'];
yield [['additionalProperties' => 42], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "integer" given'];
yield [['additionalProperties' => new stdClass()], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "stdClass" given'];
yield [['additionalProperties' => 'foo'], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "string" given'];
yield [['additionalProperties' => 42], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "integer" given'];
yield [['additionalProperties' => new stdClass()], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "stdClass" given'];
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
}

Expand Down Expand Up @@ -231,4 +231,93 @@ public function testAllOf()
$this->assertArrayHasKey('id', $refResolved->properties);
$this->assertArrayHasKey('name', $person->allOf[1]->properties);
}

/**
* Ensure Schema properties are accessable and have default values.
*/
public function testSchemaProperties()
{
$schema = new Schema([]);
$validProperties = [
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#schema-object
// The following properties are taken directly from the JSON Schema definition and follow the same specifications:
'title' => null,
'multipleOf' => null,
'maximum' => null,
'exclusiveMaximum' => false,
'minimum' => null,
'exclusiveMinimum' => false,
'maxLength' => null,
'minLength' => null,
'pattern' => null,
'maxItems' => null,
'minItems' => null,
'uniqueItems' => false,
'maxProperties' => null,
'minProperties' => null,
'required' => null, // if set, it should not be an empty array, according to the spec
'enum' => null, // if it is an array, it means restriction of values
// The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification.
'type' => null,
'allOf' => null,
'oneOf' => null,
'anyOf' => null,
'not' => null,
'items' => null,
'properties' => [],
'additionalProperties' => true,
'description' => null,
'format' => null,
'default' => null,
// Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation:
'nullable' => false,
'readOnly' => false,
'writeOnly' => false,
'xml' => null,
'externalDocs' => null,
'example' => null,
'deprecated' => false,
];

foreach($validProperties as $property => $defaultValue) {
$this->assertEquals($defaultValue, $schema->$property, "testing property '$property'");
}
}

public function testRefAdditionalProperties()
{
$json = <<<'JSON'
{
"components": {
"schemas": {
"booleanProperties": {
"type": "boolean"
},
"person": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": {"$ref": "#/components/schemas/booleanProperties"}
}
}
}
}
JSON;
$openApi = Reader::readFromJson($json);
$this->assertInstanceOf(Schema::class, $booleanProperties = $openApi->components->schemas['booleanProperties']);
$this->assertInstanceOf(Schema::class, $person = $openApi->components->schemas['person']);

$this->assertEquals('boolean', $booleanProperties->type);
$this->assertInstanceOf(Reference::class, $person->additionalProperties);

$this->assertInstanceOf(Schema::class, $refResolved = $person->additionalProperties->resolve(new ReferenceContext($openApi, 'tmp://openapi.yaml')));

$this->assertEquals('boolean', $refResolved->type);

$schema = new Schema(['additionalProperties' => new Reference(['$ref' => '#/here'], Schema::class)]);
$this->assertInstanceOf(Reference::class, $schema->additionalProperties);
}
}