diff --git a/src/SpecBaseObject.php b/src/SpecBaseObject.php index 9227cc7a..d494486f 100644 --- a/src/SpecBaseObject.php +++ b/src/SpecBaseObject.php @@ -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; @@ -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); @@ -104,7 +102,7 @@ public function __construct(array $data) $this->_errors[] = "property '$property' must be map, 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); @@ -112,6 +110,8 @@ public function __construct(array $data) } break; } + } elseif ($type === Type::ANY || Type::isScalar($type)) { + $this->_properties[$property] = $data[$property]; } else { $this->_properties[$property] = $this->instantiate($type, $data[$property]); } @@ -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; } @@ -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) @@ -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; @@ -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; } diff --git a/src/spec/PathItem.php b/src/spec/PathItem.php index 72120037..7b97bb86 100644 --- a/src/spec/PathItem.php +++ b/src/spec/PathItem.php @@ -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; } diff --git a/src/spec/Schema.php b/src/spec/Schema.php index 5488425a..8f69a8d5 100644 --- a/src/spec/Schema.php +++ b/src/spec/Schema.php @@ -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 @@ -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], @@ -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, @@ -100,6 +119,11 @@ protected function attributeDefaults(): array { return [ 'additionalProperties' => true, + 'required' => null, + 'enum' => null, + 'allOf' => null, + 'oneOf' => null, + 'anyOf' => null, ]; } @@ -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); diff --git a/src/spec/Type.php b/src/spec/Type.php index cea7e11c..98610000 100644 --- a/src/spec/Type.php +++ b/src/spec/Type.php @@ -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, + ]); + } } diff --git a/tests/spec/SchemaTest.php b/tests/spec/SchemaTest.php index 6d542077..cd3d4e96 100644 --- a/tests/spec/SchemaTest.php +++ b/tests/spec/SchemaTest.php @@ -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 } @@ -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); + } }