Skip to content

Commit 6189637

Browse files
authored
Merge pull request #37 from cebe/fix-schema-properties
Add all valid properties to Schema
2 parents 13b9175 + 5eba2c1 commit 6189637

File tree

5 files changed

+162
-37
lines changed

5 files changed

+162
-37
lines changed

src/SpecBaseObject.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ public function __construct(array $data)
6262
continue;
6363
}
6464

65-
if ($type === Type::STRING || $type === Type::ANY) {
66-
$this->_properties[$property] = $data[$property];
67-
} elseif ($type === Type::BOOLEAN) {
65+
if ($type === Type::BOOLEAN) {
6866
if (!\is_bool($data[$property])) {
6967
$this->_errors[] = "property '$property' must be boolean, but " . gettype($data[$property]) . " given.";
7068
continue;
@@ -85,7 +83,7 @@ public function __construct(array $data)
8583
$this->_errors[] = "property '$property' must be array of strings, but array has " . gettype($item) . " element.";
8684
}
8785
$this->_properties[$property][] = $item;
88-
} elseif ($type[0] === Type::ANY || $type[0] === Type::BOOLEAN || $type[0] === Type::INTEGER) { // TODO simplify handling of scalar types
86+
} elseif ($type[0] === Type::ANY || Type::isScalar($type[0])) {
8987
$this->_properties[$property][] = $item;
9088
} else {
9189
$this->_properties[$property][] = $this->instantiate($type[0], $item);
@@ -104,14 +102,16 @@ public function __construct(array $data)
104102
$this->_errors[] = "property '$property' must be map<string, string>, but entry '$key' is of type " . \gettype($item) . '.';
105103
}
106104
$this->_properties[$property][$key] = $item;
107-
} elseif ($type[1] === Type::ANY || $type[1] === Type::BOOLEAN || $type[1] === Type::INTEGER) { // TODO simplify handling of scalar types
105+
} elseif ($type[1] === Type::ANY || Type::isScalar($type[1])) {
108106
$this->_properties[$property][$key] = $item;
109107
} else {
110108
$this->_properties[$property][$key] = $this->instantiate($type[1], $item);
111109
}
112110
}
113111
break;
114112
}
113+
} elseif ($type === Type::ANY || Type::isScalar($type)) {
114+
$this->_properties[$property] = $data[$property];
115115
} else {
116116
$this->_properties[$property] = $this->instantiate($type, $data[$property]);
117117
}
@@ -125,9 +125,9 @@ public function __construct(array $data)
125125
/**
126126
* @throws TypeErrorException
127127
*/
128-
private function instantiate($type, $data)
128+
protected function instantiate($type, $data)
129129
{
130-
if ($data instanceof $type) {
130+
if ($data instanceof $type || $data instanceof Reference) {
131131
return $data;
132132
}
133133

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

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

278278
protected function requireProperties(array $names)
@@ -303,13 +303,14 @@ public function __get($name)
303303
if (isset($this->_properties[$name])) {
304304
return $this->_properties[$name];
305305
}
306-
if (isset(static::attributeDefaults()[$name])) {
307-
return static::attributeDefaults()[$name];
306+
$defaults = $this->attributeDefaults();
307+
if (array_key_exists($name, $defaults)) {
308+
return $defaults[$name];
308309
}
309-
if (isset(static::attributes()[$name])) {
310-
if (is_array(static::attributes()[$name])) {
310+
if (isset($this->attributes()[$name])) {
311+
if (is_array($this->attributes()[$name])) {
311312
return [];
312-
} elseif (static::attributes()[$name] === Type::BOOLEAN) {
313+
} elseif ($this->attributes()[$name] === Type::BOOLEAN) {
313314
return false;
314315
}
315316
return null;
@@ -324,7 +325,7 @@ public function __set($name, $value)
324325

325326
public function __isset($name)
326327
{
327-
if (isset($this->_properties[$name]) || isset(static::attributeDefaults()[$name]) || isset(static::attributes()[$name])) {
328+
if (isset($this->_properties[$name]) || isset($this->attributeDefaults()[$name]) || isset($this->attributes()[$name])) {
328329
return $this->__get($name) !== null;
329330
}
330331

src/spec/PathItem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ protected function performValidation()
109109
public function getOperations()
110110
{
111111
$operations = [];
112-
foreach (static::attributes() as $attribute => $type) {
112+
foreach ($this->attributes() as $attribute => $type) {
113113
if ($type === Operation::class && isset($this->$attribute)) {
114114
$operations[$attribute] = $this->$attribute;
115115
}

src/spec/Schema.php

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@
4141
* @property array $enum
4242
*
4343
* @property string $type
44-
* @property Schema[] $allOf
45-
* @property Schema[] $oneOf
46-
* @property Schema[] $anyOf
47-
* @property Schema|null $not
48-
* @property Schema|null $items
49-
* @property Schema[] $properties
50-
* @property Schema|bool $additionalProperties
44+
* @property Schema[]|Reference[] $allOf
45+
* @property Schema[]|Reference[] $oneOf
46+
* @property Schema[]|Reference[] $anyOf
47+
* @property Schema|Reference|null $not
48+
* @property Schema|Reference|null $items
49+
* @property Schema[]|Reference[] $properties
50+
* @property Schema|Reference|bool $additionalProperties
5151
* @property string $description
5252
* @property string $format
5353
* @property mixed $default
@@ -70,6 +70,25 @@ class Schema extends SpecBaseObject
7070
protected function attributes(): array
7171
{
7272
return [
73+
// The following properties are taken directly from the JSON Schema definition and follow the same specifications:
74+
// types from https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-4 ff.
75+
'title' => Type::STRING,
76+
'multipleOf' => Type::NUMBER,
77+
'maximum' => Type::NUMBER,
78+
'exclusiveMaximum' => Type::BOOLEAN,
79+
'minimum' => Type::NUMBER,
80+
'exclusiveMinimum' => Type::BOOLEAN,
81+
'maxLength' => Type::INTEGER,
82+
'minLength' => Type::INTEGER,
83+
'pattern' => Type::STRING,
84+
'maxItems' => Type::INTEGER,
85+
'minItems' => Type::INTEGER,
86+
'uniqueItems' => Type::BOOLEAN,
87+
'maxProperties' => Type::INTEGER,
88+
'minProperties' => Type::INTEGER,
89+
'required' => [Type::STRING],
90+
'enum' => [Type::ANY],
91+
// The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification.
7392
'type' => Type::STRING,
7493
'allOf' => [Schema::class],
7594
'oneOf' => [Schema::class],
@@ -81,7 +100,7 @@ protected function attributes(): array
81100
'description' => Type::STRING,
82101
'format' => Type::STRING,
83102
'default' => Type::ANY,
84-
103+
// Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation:
85104
'nullable' => Type::BOOLEAN,
86105
'discriminator' => Discriminator::class,
87106
'readOnly' => Type::BOOLEAN,
@@ -100,6 +119,11 @@ protected function attributeDefaults(): array
100119
{
101120
return [
102121
'additionalProperties' => true,
122+
'required' => null,
123+
'enum' => null,
124+
'allOf' => null,
125+
'oneOf' => null,
126+
'anyOf' => null,
103127
];
104128
}
105129

@@ -112,21 +136,13 @@ public function __construct(array $data)
112136
{
113137
if (isset($data['additionalProperties'])) {
114138
if (is_array($data['additionalProperties'])) {
115-
try {
116-
$data['additionalProperties'] = new Schema($data['additionalProperties']);
117-
} catch (\TypeError $e) {
118-
throw new TypeErrorException(
119-
"Unable to instantiate Schema Object with data '" . print_r($data['additionalProperties'], true) . "'",
120-
$e->getCode(),
121-
$e
122-
);
123-
}
124-
} elseif (!($data['additionalProperties'] instanceof Schema || is_bool($data['additionalProperties']))) {
139+
$data['additionalProperties'] = $this->instantiate(Schema::class, $data['additionalProperties']);
140+
} elseif (!($data['additionalProperties'] instanceof Schema || $data['additionalProperties'] instanceof Reference || is_bool($data['additionalProperties']))) {
125141
$givenType = gettype($data['additionalProperties']);
126142
if ($givenType === 'object') {
127143
$givenType = get_class($data['additionalProperties']);
128144
}
129-
throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either array, boolean or a Schema object, "%s" given', $givenType));
145+
throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "%s" given', $givenType));
130146
}
131147
}
132148
parent::__construct($data);

src/spec/Type.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,23 @@ class Type
2121
const BOOLEAN = 'boolean';
2222
const OBJECT = 'object';
2323
const ARRAY = 'array';
24+
25+
/**
26+
* Indicate whether a type is a scalar type, i.e. not an array or object.
27+
*
28+
* For ANY this will return false.
29+
*
30+
* @param string $type value from one of the type constants defined in this class.
31+
* @return bool whether the type is a scalar type.
32+
* @since 1.2.1
33+
*/
34+
public static function isScalar(string $type): bool
35+
{
36+
return in_array($type, [
37+
self::INTEGER,
38+
self::NUMBER,
39+
self::STRING,
40+
self::BOOLEAN,
41+
]);
42+
}
2443
}

tests/spec/SchemaTest.php

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ public function badSchemaProvider()
167167
yield [['properties' => ['a' => false]], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'\''];
168168
yield [['properties' => ['a' => new stdClass()]], "Unable to instantiate cebe\openapi\spec\Schema Object with data 'stdClass Object\n(\n)\n'"];
169169

170-
yield [['additionalProperties' => 'foo'], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "string" given'];
171-
yield [['additionalProperties' => 42], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "integer" given'];
172-
yield [['additionalProperties' => new stdClass()], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "stdClass" given'];
170+
yield [['additionalProperties' => 'foo'], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "string" given'];
171+
yield [['additionalProperties' => 42], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "integer" given'];
172+
yield [['additionalProperties' => new stdClass()], 'Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "stdClass" given'];
173173
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
174174
}
175175

@@ -231,4 +231,93 @@ public function testAllOf()
231231
$this->assertArrayHasKey('id', $refResolved->properties);
232232
$this->assertArrayHasKey('name', $person->allOf[1]->properties);
233233
}
234+
235+
/**
236+
* Ensure Schema properties are accessable and have default values.
237+
*/
238+
public function testSchemaProperties()
239+
{
240+
$schema = new Schema([]);
241+
$validProperties = [
242+
// https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#schema-object
243+
// The following properties are taken directly from the JSON Schema definition and follow the same specifications:
244+
'title' => null,
245+
'multipleOf' => null,
246+
'maximum' => null,
247+
'exclusiveMaximum' => false,
248+
'minimum' => null,
249+
'exclusiveMinimum' => false,
250+
'maxLength' => null,
251+
'minLength' => null,
252+
'pattern' => null,
253+
'maxItems' => null,
254+
'minItems' => null,
255+
'uniqueItems' => false,
256+
'maxProperties' => null,
257+
'minProperties' => null,
258+
'required' => null, // if set, it should not be an empty array, according to the spec
259+
'enum' => null, // if it is an array, it means restriction of values
260+
// The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification.
261+
'type' => null,
262+
'allOf' => null,
263+
'oneOf' => null,
264+
'anyOf' => null,
265+
'not' => null,
266+
'items' => null,
267+
'properties' => [],
268+
'additionalProperties' => true,
269+
'description' => null,
270+
'format' => null,
271+
'default' => null,
272+
// Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation:
273+
'nullable' => false,
274+
'readOnly' => false,
275+
'writeOnly' => false,
276+
'xml' => null,
277+
'externalDocs' => null,
278+
'example' => null,
279+
'deprecated' => false,
280+
];
281+
282+
foreach($validProperties as $property => $defaultValue) {
283+
$this->assertEquals($defaultValue, $schema->$property, "testing property '$property'");
284+
}
285+
}
286+
287+
public function testRefAdditionalProperties()
288+
{
289+
$json = <<<'JSON'
290+
{
291+
"components": {
292+
"schemas": {
293+
"booleanProperties": {
294+
"type": "boolean"
295+
},
296+
"person": {
297+
"type": "object",
298+
"properties": {
299+
"name": {
300+
"type": "string"
301+
}
302+
},
303+
"additionalProperties": {"$ref": "#/components/schemas/booleanProperties"}
304+
}
305+
}
306+
}
307+
}
308+
JSON;
309+
$openApi = Reader::readFromJson($json);
310+
$this->assertInstanceOf(Schema::class, $booleanProperties = $openApi->components->schemas['booleanProperties']);
311+
$this->assertInstanceOf(Schema::class, $person = $openApi->components->schemas['person']);
312+
313+
$this->assertEquals('boolean', $booleanProperties->type);
314+
$this->assertInstanceOf(Reference::class, $person->additionalProperties);
315+
316+
$this->assertInstanceOf(Schema::class, $refResolved = $person->additionalProperties->resolve(new ReferenceContext($openApi, 'tmp://openapi.yaml')));
317+
318+
$this->assertEquals('boolean', $refResolved->type);
319+
320+
$schema = new Schema(['additionalProperties' => new Reference(['$ref' => '#/here'], Schema::class)]);
321+
$this->assertInstanceOf(Reference::class, $schema->additionalProperties);
322+
}
234323
}

0 commit comments

Comments
 (0)