Skip to content

Commit 0943eba

Browse files
authored
Merge pull request #28 from SilverFire/feature/pass-objects
Allow passing prepared objects instead of array definition
2 parents 8199098 + 526c580 commit 0943eba

10 files changed

+221
-11
lines changed

src/SpecBaseObject.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public function __construct(array $data)
9999
}
100100
$this->_properties[$property] = [];
101101
foreach ($data[$property] as $key => $item) {
102-
if ($type[1] === 'string') {
102+
if ($type[1] === Type::STRING) {
103103
if (!is_string($item)) {
104104
$this->_errors[] = "property '$property' must be map<string, string>, but entry '$key' is of type " . \gettype($item) . '.';
105105
}
@@ -127,9 +127,14 @@ public function __construct(array $data)
127127
*/
128128
private function instantiate($type, $data)
129129
{
130-
if (isset($data['$ref'])) {
130+
if ($data instanceof $type) {
131+
return $data;
132+
}
133+
134+
if (is_array($data) && isset($data['$ref'])) {
131135
return new Reference($data, $type);
132136
}
137+
133138
try {
134139
return new $type($data);
135140
} catch (\TypeError $e) {

src/spec/MediaType.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,17 @@ public function __construct(array $data)
5050

5151
if (!empty($encoding)) {
5252
foreach ($encoding as $property => $encodingData) {
53-
$encoding[$property] = new Encoding($encodingData, $this->schema->properties[$property] ?? null);
53+
if ($encodingData instanceof Encoding) {
54+
$encoding[$property] = $encodingData;
55+
} elseif (is_array($encodingData)) {
56+
$encoding[$property] = new Encoding($encodingData, $this->schema->properties[$property] ?? null);
57+
} else {
58+
$givenType = gettype($encodingData);
59+
if ($givenType === 'object') {
60+
$givenType = get_class($encodingData);
61+
}
62+
throw new TypeErrorException(sprintf('Encoding MUST be either array or Encoding object, "%s" given', $givenType));
63+
}
5464
}
5565
$this->encoding = $encoding;
5666
}

src/spec/PathItem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public function resolveReferences(ReferenceContext $context = null)
150150
$pathItem = $this->_ref->resolve($context);
151151
$this->_ref = null;
152152
// The properties of the referenced structure are merged with the local Path Item Object.
153-
foreach(self::attributes() as $attribute => $type) {
153+
foreach (self::attributes() as $attribute => $type) {
154154
if (!isset($pathItem->$attribute)) {
155155
continue;
156156
}

src/spec/Paths.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,24 @@ class Paths implements SpecObjectInterface, DocumentContextInterface, ArrayAcces
4343

4444
/**
4545
* Create an object from spec data.
46-
* @param array $data spec data read from YAML or JSON
46+
* @param PathItem[]|array[] $data spec data read from YAML or JSON
4747
* @throws TypeErrorException in case invalid data is supplied.
4848
*/
4949
public function __construct(array $data)
5050
{
5151
foreach ($data as $path => $object) {
5252
if ($object === null) {
5353
$this->_paths[$path] = null;
54-
} else {
54+
} elseif (is_array($object)) {
5555
$this->_paths[$path] = new PathItem($object);
56+
} elseif ($object instanceof PathItem) {
57+
$this->_paths[$path] = $object;
58+
} else {
59+
$givenType = gettype($object);
60+
if ($givenType === 'object') {
61+
$givenType = get_class($object);
62+
}
63+
throw new TypeErrorException(sprintf('Path MUST be either array or PathItem object, "%s" given', $givenType));
5664
}
5765
}
5866
}

src/spec/Responses.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use ArrayAccess;
1111
use ArrayIterator;
1212
use cebe\openapi\DocumentContextInterface;
13+
use cebe\openapi\exceptions\TypeErrorException;
1314
use cebe\openapi\exceptions\UnresolvableReferenceException;
1415
use cebe\openapi\json\JsonPointer;
1516
use cebe\openapi\ReferenceContext;
@@ -37,19 +38,27 @@ class Responses implements SpecObjectInterface, DocumentContextInterface, ArrayA
3738

3839
/**
3940
* Create an object from spec data.
40-
* @param array $data spec data read from YAML or JSON
41-
* @throws \cebe\openapi\exceptions\TypeErrorException in case invalid data is supplied.
41+
* @param Response[]|Reference[]|array[] $data spec data read from YAML or JSON
42+
* @throws TypeErrorException in case invalid data is supplied.
4243
*/
4344
public function __construct(array $data)
4445
{
4546
foreach ($data as $statusCode => $response) {
4647
// From Spec: This field MUST be enclosed in quotation marks (for example, "200") for compatibility between JSON and YAML.
4748
$statusCode = (string) $statusCode;
4849
if (preg_match('~^(?:default|[1-5](?:[0-9][0-9]|XX))$~', $statusCode)) {
49-
if (isset($response['$ref'])) {
50+
if ($response instanceof Response || $response instanceof Reference) {
51+
$this->_responses[$statusCode] = $response;
52+
} elseif (is_array($response) && isset($response['$ref'])) {
5053
$this->_responses[$statusCode] = new Reference($response, Response::class);
51-
} else {
54+
} elseif (is_array($response)) {
5255
$this->_responses[$statusCode] = new Response($response);
56+
} else {
57+
$givenType = gettype($response);
58+
if ($givenType === 'object') {
59+
$givenType = get_class($response);
60+
}
61+
throw new TypeErrorException(sprintf('Response MUST be either an array, a Response or a Reference object, "%s" given', $givenType));
5362
}
5463
} else {
5564
$this->_errors[] = "Responses: $statusCode is not a valid HTTP status code.";

src/spec/Schema.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ public function __construct(array $data)
121121
$e
122122
);
123123
}
124+
} elseif (!($data['additionalProperties'] instanceof Schema || is_bool($data['additionalProperties']))) {
125+
$givenType = gettype($data['additionalProperties']);
126+
if ($givenType === 'object') {
127+
$givenType = get_class($data['additionalProperties']);
128+
}
129+
throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either array, boolean or a Schema object, "%s" given', $givenType));
124130
}
125131
}
126132
parent::__construct($data);

tests/spec/MediaTypeTest.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,54 @@ public function testRead()
6464
$this->assertEquals($expectedCat, $mediaType->examples['cat']->value);
6565

6666
}
67-
}
67+
68+
public function testCreateionFromObjects()
69+
{
70+
$mediaType = new MediaType([
71+
'schema' => new \cebe\openapi\spec\Schema([
72+
'type' => \cebe\openapi\spec\Type::OBJECT,
73+
'properties' => [
74+
'id' => new \cebe\openapi\spec\Schema(['type' => 'string', 'format' => 'uuid']),
75+
'profileImage' => new \cebe\openapi\spec\Schema(['type' => 'string', 'format' => 'binary']),
76+
],
77+
]),
78+
'encoding' => [
79+
'id' => [],
80+
'profileImage' => new \cebe\openapi\spec\Encoding([
81+
'contentType' => 'image/png, image/jpeg',
82+
'headers' => [
83+
'X-Rate-Limit-Limit' => new \cebe\openapi\spec\Header([
84+
'description' => 'The number of allowed requests in the current period',
85+
'schema' => new \cebe\openapi\spec\Schema(['type' => 'integer']),
86+
]),
87+
],
88+
]),
89+
],
90+
]);
91+
92+
// default value should be extracted
93+
$this->assertEquals('text/plain', $mediaType->encoding['id']->contentType);
94+
// object should be passed.
95+
$this->assertInstanceOf(\cebe\openapi\spec\Encoding::class, $mediaType->encoding['profileImage']);
96+
}
97+
98+
public function badEncodingProvider()
99+
{
100+
yield [['encoding' => ['id' => 'foo']], 'Encoding MUST be either array or Encoding object, "string" given'];
101+
yield [['encoding' => ['id' => 42]], 'Encoding MUST be either array or Encoding object, "integer" given'];
102+
yield [['encoding' => ['id' => false]], 'Encoding MUST be either array or Encoding object, "boolean" given'];
103+
yield [['encoding' => ['id' => new stdClass()]], 'Encoding MUST be either array or Encoding object, "stdClass" given'];
104+
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
105+
}
106+
107+
/**
108+
* @dataProvider badEncodingProvider
109+
*/
110+
public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException)
111+
{
112+
$this->expectException(\cebe\openapi\exceptions\TypeErrorException::class);
113+
$this->expectExceptionMessage($expectedException);
114+
115+
new MediaType($config);
116+
}
117+
}

tests/spec/PathTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use cebe\openapi\spec\PathItem;
66
use cebe\openapi\spec\Paths;
77
use cebe\openapi\spec\Reference;
8+
use cebe\openapi\spec\Response;
9+
use cebe\openapi\spec\Responses;
810

911
/**
1012
* @covers \cebe\openapi\spec\Paths
@@ -64,6 +66,48 @@ public function testRead()
6466
}
6567
}
6668

69+
public function testCreateionFromObjects()
70+
{
71+
$paths = new Paths([
72+
'/pets' => new PathItem([
73+
'get' => new Operation([
74+
'responses' => new Responses([
75+
200 => new Response(['description' => 'A list of pets.']),
76+
404 => ['description' => 'The pets list is gone 🙀'],
77+
])
78+
])
79+
])
80+
]);
81+
82+
$this->assertTrue($paths->hasPath('/pets'));
83+
$this->assertInstanceOf(PathItem::class, $paths->getPath('/pets'));
84+
$this->assertInstanceOf(PathItem::class, $paths['/pets']);
85+
$this->assertInstanceOf(Operation::class, $paths->getPath('/pets')->get);
86+
87+
$this->assertSame('A list of pets.', $paths->getPath('/pets')->get->responses->getResponse(200)->description);
88+
$this->assertSame('The pets list is gone 🙀', $paths->getPath('/pets')->get->responses->getResponse(404)->description);
89+
}
90+
91+
public function badPathsConfigProvider()
92+
{
93+
yield [['/pets' => 'foo'], 'Path MUST be either array or PathItem object, "string" given'];
94+
yield [['/pets' => 42], 'Path MUST be either array or PathItem object, "integer" given'];
95+
yield [['/pets' => false], 'Path MUST be either array or PathItem object, "boolean" given'];
96+
yield [['/pets' => new stdClass()], 'Path MUST be either array or PathItem object, "stdClass" given'];
97+
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
98+
}
99+
100+
/**
101+
* @dataProvider badPathsConfigProvider
102+
*/
103+
public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException)
104+
{
105+
$this->expectException(\cebe\openapi\exceptions\TypeErrorException::class);
106+
$this->expectExceptionMessage($expectedException);
107+
108+
new Paths($config);
109+
}
110+
67111
public function testInvalidPath()
68112
{
69113
/** @var $paths Paths */

tests/spec/ResponseTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,35 @@ public function testResponseCodes()
162162
$this->assertFalse($result);
163163

164164
}
165+
166+
public function testCreateionFromObjects()
167+
{
168+
$responses = new Responses([
169+
200 => new Response(['description' => 'A list of pets.']),
170+
404 => ['description' => 'The pets list is gone 🙀'],
171+
]);
172+
173+
$this->assertSame('A list of pets.', $responses->getResponse(200)->description);
174+
$this->assertSame('The pets list is gone 🙀', $responses->getResponse(404)->description);
175+
}
176+
177+
public function badResponseProvider()
178+
{
179+
yield [['200' => 'foo'], 'Response MUST be either an array, a Response or a Reference object, "string" given'];
180+
yield [['200' => 42], 'Response MUST be either an array, a Response or a Reference object, "integer" given'];
181+
yield [['200' => false], 'Response MUST be either an array, a Response or a Reference object, "boolean" given'];
182+
yield [['200' => new stdClass()], 'Response MUST be either an array, a Response or a Reference object, "stdClass" given'];
183+
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
184+
}
185+
186+
/**
187+
* @dataProvider badResponseProvider
188+
*/
189+
public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException)
190+
{
191+
$this->expectException(\cebe\openapi\exceptions\TypeErrorException::class);
192+
$this->expectExceptionMessage($expectedException);
193+
194+
new Responses($config);
195+
}
165196
}

tests/spec/SchemaTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,51 @@ public function testDiscriminator()
134134
'monster' => 'https://gigantic-server.com/schemas/Monster/schema.json',
135135
], $schema->discriminator->mapping);
136136
}
137+
138+
public function testCreateionFromObjects()
139+
{
140+
$schema = new Schema([
141+
'allOf' => [
142+
new Schema(['type' => 'integer']),
143+
new Schema(['type' => 'string']),
144+
],
145+
'additionalProperties' => new Schema([
146+
'type' => 'object',
147+
]),
148+
'discriminator' => new Discriminator([
149+
'mapping' => ['A' => 'B'],
150+
]),
151+
]);
152+
153+
$this->assertSame('integer', $schema->allOf[0]->type);
154+
$this->assertSame('string', $schema->allOf[1]->type);
155+
$this->assertInstanceOf(Schema::class, $schema->additionalProperties);
156+
$this->assertSame('object', $schema->additionalProperties->type);
157+
$this->assertSame(['A' => 'B'], $schema->discriminator->mapping);
158+
}
159+
160+
161+
public function badSchemaProvider()
162+
{
163+
yield [['properties' => ['a' => 'foo']], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'foo\''];
164+
yield [['properties' => ['a' => 42]], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'42\''];
165+
yield [['properties' => ['a' => false]], 'Unable to instantiate cebe\openapi\spec\Schema Object with data \'\''];
166+
yield [['properties' => ['a' => new stdClass()]], "Unable to instantiate cebe\openapi\spec\Schema Object with data 'stdClass Object\n(\n)\n'"];
167+
168+
yield [['additionalProperties' => 'foo'], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "string" given'];
169+
yield [['additionalProperties' => 42], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "integer" given'];
170+
yield [['additionalProperties' => new stdClass()], 'Schema::$additionalProperties MUST be either array, boolean or a Schema object, "stdClass" given'];
171+
// The last one can be supported in future, but now SpecBaseObjects::__construct() requires array explicitly
172+
}
173+
174+
/**
175+
* @dataProvider badSchemaProvider
176+
*/
177+
public function testPathsCanNotBeCreatedFromBullshit($config, $expectedException)
178+
{
179+
$this->expectException(\cebe\openapi\exceptions\TypeErrorException::class);
180+
$this->expectExceptionMessage($expectedException);
181+
182+
new Schema($config);
183+
}
137184
}

0 commit comments

Comments
 (0)