Skip to content

Commit 20fa704

Browse files
committed
Support voNamespace in nested definitions (implicit objects) - #12
1 parent 9e6e955 commit 20fa704

4 files changed

Lines changed: 157 additions & 25 deletions

File tree

src/Shorthand/Shorthand.php

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,18 @@ public static function convertToJsonSchema($shorthand, array $customData = []):
8888
break;
8989
}
9090

91+
// implicit sub value objects have to use voNamespace as namespace
9192
if (\is_array($shorthandDefinition)) {
92-
$schema['properties'][$schemaProperty] = self::convertToJsonSchema($shorthandDefinition);
93+
$subCustomData = $customData;
94+
$namespace = $customData['voNamespace'] ?? '';
95+
96+
if ($namespace !== '') {
97+
$namespace = \rtrim($namespace, '/');
98+
unset($subCustomData['ns']); // remove abbreviation if any
99+
$subCustomData['namespace'] = $namespace;
100+
}
101+
102+
$schema['properties'][$schemaProperty] = self::convertToJsonSchema($shorthandDefinition, $subCustomData);
93103
} elseif (\is_string($shorthandDefinition)) {
94104
$schema['properties'][$schemaProperty] = self::convertShorthandStringToJsonSchema($shorthandDefinition, $customData);
95105
} else {
@@ -117,26 +127,35 @@ private static function convertShorthandStringToJsonSchema(string $shorthandStr,
117127

118128
$parts = \explode('|', $shorthandStr);
119129

120-
if (\mb_substr($parts[0], -2) === '[]') {
121-
$itemsParts = [\mb_substr($parts[0], 0, -2)];
122-
\array_push($itemsParts, ...\array_slice($parts, 1));
123-
124-
return [
125-
'type' => 'array',
126-
'items' => self::convertShorthandStringToJsonSchema(\implode('|', $itemsParts), $customData),
127-
];
128-
}
129-
130130
$type = $parts[0];
131131
$namespace = $customData['voNamespace'] ?? '';
132-
$namespaceDetected = false !== \strpos($parts[0], '/');
132+
$typeNamespaceDetected = false !== \strpos($type, '/');
133133

134134
if ($namespace !== '') {
135-
$namespace = \rtrim($namespace, '/') . '/';
135+
$namespace = \rtrim($namespace, '/');
136+
}
137+
138+
// it's an array, the collection / list class is created implicitly
139+
if (\mb_substr($type, -2) === '[]') {
140+
$itemsParts = [\mb_substr($type, 0, -2)];
141+
\array_push($itemsParts, ...\array_slice($parts, 1));
142+
143+
$schema = [];
144+
145+
if ($namespace !== '') {
146+
$schema['namespace'] = $namespace;
147+
}
148+
149+
$schema['type'] = 'array';
150+
$schema['items'] = self::convertShorthandStringToJsonSchema(\implode('|', $itemsParts), $customData);
151+
152+
return $schema;
136153
}
137154

138-
if ($namespaceDetected) {
155+
if ($typeNamespaceDetected) {
139156
$namespace = self::extractNamespace($type);
157+
$namespace = $namespace === '' ? '/' : $namespace;
158+
140159
$type = self::extractType($type);
141160
}
142161

@@ -163,18 +182,19 @@ private static function convertShorthandStringToJsonSchema(string $shorthandStr,
163182
$schema = self::populateSchema($parts);
164183
$schema[$typeKey] = $typeValue;
165184

166-
if ($namespaceDetected) {
167-
$schema['namespace'] = \strlen($namespace) > 1 ? \rtrim($namespace, '/') : $namespace;
185+
if ($namespace) {
186+
$schema['namespace'] = $namespace;
168187
}
169188

170189
return $schema;
171190
default:
172191
$schema = self::populateSchema($parts);
173192

174-
$schema['$ref'] = '#/definitions/' . \ltrim($namespace, '/') . $type;
193+
$schema['$ref'] = '#/definitions/' . $type;
175194

176-
if ($namespaceDetected) {
177-
$schema['namespace'] = \strlen($namespace) > 1 ? \rtrim($namespace, '/') : $namespace;
195+
if ($namespace) {
196+
$schema['namespace'] = $namespace;
197+
$schema['$ref'] = '#/definitions/' . \ltrim($namespace, '/') . '/' . $type;
178198
}
179199

180200
return $schema;
@@ -245,6 +265,6 @@ private static function extractType(string $type): string
245265

246266
private static function extractNamespace(string $type): string
247267
{
248-
return \substr($type, 0, \strrpos($type, '/') + 1);
268+
return \substr($type, 0, \strrpos($type, '/'));
249269
}
250270
}

src/Type/Type.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,27 @@ final class Type
1717
{
1818
/**
1919
* @param array<string, mixed> $shorthand
20-
* @param string|null $name
21-
* @param string|null $namespace
20+
* @param string|null $name Name
21+
* @param string|null $namespace Namespace
22+
* @param string|null $voNamespace Value object namespace e.g. for properties of type object
2223
* @return TypeSet
2324
*/
24-
public static function fromShorthand(array $shorthand, ?string $name = null, ?string $namespace = null): TypeSet
25-
{
26-
return self::fromDefinition(Shorthand::convertToJsonSchema($shorthand), $name);
25+
public static function fromShorthand(
26+
array $shorthand,
27+
?string $name = null,
28+
?string $namespace = null,
29+
?string $voNamespace = null
30+
): TypeSet {
31+
$customData = [];
32+
33+
if ($namespace !== null) {
34+
$customData['namespace'] = $namespace;
35+
}
36+
if ($voNamespace !== null) {
37+
$customData['voNamespace'] = $voNamespace;
38+
}
39+
40+
return self::fromDefinition(Shorthand::convertToJsonSchema($shorthand, $customData), $name);
2741
}
2842

2943
/**

tests/Type/ObjectTypeTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,92 @@ private function assertAddressObject(ReferenceType $address, bool $required): vo
335335

336336
$this->assertSame(['namespace' => 'Address'], $streetAddress->custom());
337337
}
338+
339+
/**
340+
* @test
341+
*/
342+
public function it_supports_definition_of_objects_shorthand_nested_ns(): void
343+
{
344+
$json = \file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'schema_with_objects_shorthand_nested_ns.json');
345+
$decodedJson = \json_decode($json, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR);
346+
347+
$typeSet = Type::fromShorthand($decodedJson, 'checkout', '/Order', '/Shipping');
348+
349+
$this->assertCount(1, $typeSet);
350+
351+
/** @var ObjectType $type */
352+
$type = $typeSet->first();
353+
$this->assertInstanceOf(ObjectType::class, $type);
354+
$this->assertFalse($type->additionalProperties());
355+
356+
$required = $type->required();
357+
$this->assertCount(4, $required);
358+
$this->assertContains('order', $required);
359+
$this->assertContains('salutation', $required);
360+
$this->assertContains('name', $required);
361+
$this->assertContains('items', $required);
362+
363+
$properties = $type->properties();
364+
$this->assertCount(4, $properties);
365+
$this->assertArrayHasKey('order', $properties);
366+
$this->assertArrayHasKey('salutation', $properties);
367+
$this->assertArrayHasKey('name', $properties);
368+
$this->assertArrayHasKey('items', $properties);
369+
370+
/** @var ObjectType $order */
371+
$order = $properties['order']->first();
372+
$this->assertInstanceOf(ObjectType::class, $order);
373+
// implicit object Order is in namespace /Shipping and value objects with no namespace also in /Shipping
374+
$this->assertSame(['namespace' => '/Shipping', 'voNamespace' => '/Shipping'], $order->custom());
375+
376+
$subProperties = $order->properties();
377+
$this->assertCount(3, $subProperties);
378+
$this->assertArrayHasKey('billing_address', $subProperties);
379+
$this->assertArrayHasKey('shipping_address', $subProperties);
380+
$this->assertArrayHasKey('payment_address', $subProperties);
381+
382+
/** @var StringType $paymentAddress */
383+
$paymentAddress = $subProperties['payment_address']->first();
384+
$this->assertInstanceOf(StringType::class, $paymentAddress);
385+
// value object defines it's own namespace
386+
$this->assertSame(['namespace' => 'Payment'], $paymentAddress->custom());
387+
388+
/** @var ReferenceType $billingAddress */
389+
$billingAddress = $subProperties['billing_address']->first();
390+
$this->assertInstanceOf(ReferenceType::class, $billingAddress);
391+
// reference value object use defined voNamespace because of missing namespace definition
392+
$this->assertSame(['namespace' => '/Shipping'], $billingAddress->custom());
393+
$this->assertSame('#/definitions/Shipping/Address', $billingAddress->ref());
394+
395+
/** @var ArrayType $shippingAddress */
396+
$shippingAddress = $subProperties['shipping_address']->first();
397+
$this->assertInstanceOf(ArrayType::class, $shippingAddress);
398+
// implicit array value object uses defined voNamespace
399+
$this->assertSame(['namespace' => '/Shipping'], $shippingAddress->custom());
400+
401+
/** @var ReferenceType $items */
402+
$items = $shippingAddress->items()[0]->first();
403+
$this->assertInstanceOf(ReferenceType::class, $items);
404+
// item defines it's own namespace
405+
$this->assertSame(['namespace' => '/Order'], $items->custom());
406+
$this->assertSame('#/definitions/Order/Address', $items->ref());
407+
408+
/** @var StringType $salutation */
409+
$salutation = $properties['salutation']->first();
410+
$this->assertInstanceOf(StringType::class, $salutation);
411+
$this->assertSame(['MR', 'MRS'], $salutation->enum());
412+
$this->assertSame(['namespace' => '/Contact'], $salutation->custom());
413+
414+
/** @var StringType $name */
415+
$name = $properties['name']->first();
416+
$this->assertInstanceOf(StringType::class, $name);
417+
// value objects with no namespace are in voNamespace /Shipping
418+
$this->assertSame(['namespace' => '/Shipping'], $name->custom());
419+
420+
/** @var ArrayType $items */
421+
$items = $properties['items']->first();
422+
$this->assertInstanceOf(ArrayType::class, $items);
423+
// implicit array value object uses defined voNamespace
424+
$this->assertSame(['namespace' => '/Shipping'], $items->custom());
425+
}
338426
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"order": {
3+
"billing_address": "Address",
4+
"payment_address": "Payment/string",
5+
"shipping_address": "/Order/Address[]"
6+
},
7+
"salutation": "/Contact/enum:MR,MRS",
8+
"name": "string",
9+
"items": "Items[]"
10+
}

0 commit comments

Comments
 (0)