Skip to content

Commit 6450a54

Browse files
committed
Resolve deep nested references - Close #5
1 parent d9eda7c commit 6450a54

File tree

10 files changed

+241
-47
lines changed

10 files changed

+241
-47
lines changed

src/Type/ArrayType.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ private function __construct()
4242
{
4343
}
4444

45-
public static function fromDefinition(array $definition, ?string $name = null): self
45+
/**
46+
* @param array<string, mixed> $definition
47+
* @param string|null $name
48+
* @param array<string, TypeSet> $rootDefinitions
49+
* @return static
50+
*/
51+
public static function fromDefinition(array $definition, ?string $name = null, array $rootDefinitions = []): self
4652
{
4753
if (! isset($definition['type']) && ! isset($definition['$ref'])) {
4854
throw new \RuntimeException(\sprintf('The "type" is missing in schema definition for "%s"', $name));
@@ -61,24 +67,28 @@ public static function fromDefinition(array $definition, ?string $name = null):
6167

6268
if (isset($definition['definitions'])) {
6369
foreach ($definition['definitions'] as $propertyName => $propertyDefinition) {
64-
$self->definitions[$propertyName] = Type::fromDefinition($propertyDefinition, $propertyName);
70+
$self->definitions[$propertyName] = Type::fromDefinition($propertyDefinition, $propertyName, $rootDefinitions);
6571
}
6672
}
6773

6874
// definitions can be shared and must be cloned to not override defaults e. g. required
69-
$resolveReference = static function (string $ref) use ($self) {
75+
$resolveReference = static function (string $ref) use ($self, $rootDefinitions) {
7076
$referencePath = \explode('/', $ref);
7177
$name = \array_pop($referencePath);
7278

7379
$resolvedType = $self->definitions[$name] ?? null;
7480

81+
if ($resolvedType === null) {
82+
$resolvedType = $rootDefinitions[$name] ?? null;
83+
}
84+
7585
return $resolvedType ? clone $resolvedType : null;
7686
};
7787

7888
$populateArrayType = static function (string $key, array $definitionValue) use ($resolveReference, $self) {
7989
switch (true) {
8090
case isset($definitionValue['type']):
81-
$self->$key[] = Type::fromDefinition($definitionValue, '');
91+
$self->$key[] = Type::fromDefinition($definitionValue, '', $self->definitions());
8292
break;
8393
case isset($definitionValue['$ref']):
8494
$ref = ReferenceType::fromDefinition($definitionValue, '');
@@ -103,7 +113,8 @@ public static function fromDefinition(array $definition, ?string $name = null):
103113
isset($propertyDefinition[0])
104114
? $definitionValue
105115
: $propertyDefinition,
106-
''
116+
'',
117+
$self->definitions()
107118
);
108119
}
109120
}
@@ -120,7 +131,7 @@ public static function fromDefinition(array $definition, ?string $name = null):
120131
$populateArrayType('contains', $definitionValue);
121132
break;
122133
case 'additionalItems':
123-
$self->additionalItems = Type::fromDefinition($definitionValue, '');
134+
$self->additionalItems = Type::fromDefinition($definitionValue, '', $self->definitions());
124135
break;
125136
case 'definitions':
126137
// handled beforehand

src/Type/NotType.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@ public function setName(?string $name): void
4040
$this->name = $name;
4141
}
4242

43-
public static function fromDefinition(array $definition, ?string $name = null): self
43+
/**
44+
* @param array<string, mixed> $definition
45+
* @param string|null $name
46+
* @param array<string, TypeSet> $rootDefinitions
47+
* @return static
48+
*/
49+
public static function fromDefinition(array $definition, ?string $name = null, array $rootDefinitions = []): self
4450
{
4551
$self = new static();
4652
$self->name = $name;
47-
$self->typeSet = Type::fromDefinition($definition['not']);
53+
$self->typeSet = Type::fromDefinition($definition['not'], null, $rootDefinitions);
4854

4955
return $self;
5056
}

src/Type/ObjectType.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ private function __construct()
4848
{
4949
}
5050

51-
public static function fromDefinition(array $definition, ?string $name = null): self
51+
/**
52+
* @param array<string, mixed> $definition
53+
* @param string|null $name
54+
* @param array<string, TypeSet> $rootDefinitions
55+
* @return static
56+
*/
57+
public static function fromDefinition(array $definition, ?string $name = null, array $rootDefinitions = []): self
5258
{
5359
if (! isset($definition['type'])) {
5460
throw new \RuntimeException(\sprintf('The "type" is missing in schema definition for "%s"', $name));
@@ -67,17 +73,21 @@ public static function fromDefinition(array $definition, ?string $name = null):
6773

6874
if (isset($definition['definitions'])) {
6975
foreach ($definition['definitions'] as $propertyName => $propertyDefinition) {
70-
$self->definitions[$propertyName] = Type::fromDefinition($propertyDefinition, $propertyName);
76+
$self->definitions[$propertyName] = Type::fromDefinition($propertyDefinition, $propertyName, $rootDefinitions);
7177
}
7278
}
7379

7480
// definitions can be shared and must be cloned to not override defaults e. g. required
75-
$resolveReference = static function (string $ref) use ($self) {
81+
$resolveReference = static function (string $ref) use ($self, $rootDefinitions) {
7682
$referencePath = \explode('/', $ref);
7783
$name = \array_pop($referencePath);
7884

7985
$resolvedType = $self->definitions[$name] ?? null;
8086

87+
if ($resolvedType === null) {
88+
$resolvedType = $rootDefinitions[$name] ?? null;
89+
}
90+
8191
return $resolvedType ? clone $resolvedType : null;
8292
};
8393

@@ -95,14 +105,15 @@ public static function fromDefinition(array $definition, ?string $name = null):
95105
} else {
96106
$self->properties[$propertyName] = Type::fromDefinition(
97107
$propertyDefinition,
98-
$propertyName
108+
$propertyName,
109+
$self->definitions
99110
);
100111
}
101112
}
102113
break;
103114
case 'additionalProperties':
104115
$self->additionalProperties = \is_array($definitionValue)
105-
? Type::fromDefinition($definitionValue, '')
116+
? Type::fromDefinition($definitionValue, '', $self->definitions)
106117
: $definitionValue;
107118
break;
108119
case 'definitions':

src/Type/OfType.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,21 @@ public function setName(?string $name): void
4646
$this->name = $name;
4747
}
4848

49-
public static function fromDefinition(array $definition, ?string $name = null): self
49+
/**
50+
* @param array<string, mixed> $definition
51+
* @param string|null $name
52+
* @param array<string, TypeSet> $rootDefinitions
53+
* @return static
54+
*/
55+
public static function fromDefinition(array $definition, ?string $name = null, array $rootDefinitions = []): self
5056
{
5157
$self = new static();
5258
$self->name = $name;
5359

5460
$type = $definition['type'] ?: static::type();
5561

5662
foreach ($definition[$type] as $typeDefinition) {
57-
$self->typeSets[] = Type::fromDefinition($typeDefinition);
63+
$self->typeSets[] = Type::fromDefinition($typeDefinition, null, $rootDefinitions);
5864
}
5965

6066
return $self;

src/Type/ReferenceType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static function fromDefinition(array $definition, ?string $name = null):
3434

3535
$self = new static();
3636
$self->setName($name);
37+
$self->ref = $definition['$ref'];
3738

3839
foreach ($definition as $definitionKey => $definitionValue) {
3940
if (\property_exists($self, $definitionKey)) {

src/Type/Type.php

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ public static function fromShorthand(array $shorthand, ?string $name = null): Ty
2727
/**
2828
* @param array<string, mixed> $definition
2929
* @param string|null $name
30+
* @param array<string, TypeSet> $rootDefinitions
3031
* @return TypeSet
3132
*/
32-
public static function fromDefinition(array $definition, ?string $name = null): TypeSet
33+
public static function fromDefinition(array $definition, ?string $name = null, array $rootDefinitions = []): TypeSet
3334
{
3435
if (! isset($definition['type'])) {
3536
switch (true) {
@@ -86,22 +87,22 @@ public static function fromDefinition(array $definition, ?string $name = null):
8687
$types[] = BooleanType::fromDefinition($definition, $name);
8788
break;
8889
case 'object':
89-
$types[] = ObjectType::fromDefinition($definition, $name);
90+
$types[] = ObjectType::fromDefinition($definition, $name, $rootDefinitions);
9091
break;
9192
case 'array':
92-
$types[] = ArrayType::fromDefinition($definition, $name);
93+
$types[] = ArrayType::fromDefinition($definition, $name, $rootDefinitions);
9394
break;
9495
case 'oneOf':
95-
$types[] = OneOfType::fromDefinition($definition, $name);
96+
$types[] = OneOfType::fromDefinition($definition, $name, $rootDefinitions);
9697
break;
9798
case 'anyOf':
98-
$types[] = AnyOfType::fromDefinition($definition, $name);
99+
$types[] = AnyOfType::fromDefinition($definition, $name, $rootDefinitions);
99100
break;
100101
case 'allOf':
101-
$types[] = AllOfType::fromDefinition($definition, $name);
102+
$types[] = AllOfType::fromDefinition($definition, $name, $rootDefinitions);
102103
break;
103104
case 'not':
104-
$types[] = NotType::fromDefinition($definition, $name);
105+
$types[] = NotType::fromDefinition($definition, $name, $rootDefinitions);
105106
break;
106107
case 'const':
107108
$types[] = ConstType::fromDefinition($definition, $name);
@@ -122,12 +123,89 @@ public static function fromDefinition(array $definition, ?string $name = null):
122123
throw new \RuntimeException('Could not determine type of JSON schema');
123124
}
124125

126+
$typeSet = new TypeSet(...$types);
127+
125128
foreach ($types as $type) {
126129
if ($type instanceof NullableAware) {
127130
$type->setNullable($isNullable);
128131
}
129132
}
130133

131-
return new TypeSet(...$types);
134+
self::populateReferences($typeSet);
135+
136+
return $typeSet;
137+
}
138+
139+
/**
140+
* @param TypeSet $typeSet
141+
* @param array<string, TypeSet> $rootDefinitions
142+
*/
143+
private static function populateReferences(TypeSet $typeSet, array $rootDefinitions = []): void
144+
{
145+
foreach ($typeSet as $typeDefinition) {
146+
switch (true) {
147+
case $typeDefinition instanceof ObjectType:
148+
foreach ($typeDefinition->definitions() as $property) {
149+
self::populateReferences($property, $typeDefinition->definitions());
150+
}
151+
foreach ($typeDefinition->properties() as $property) {
152+
if (\count($typeDefinition->definitions()) > 0) {
153+
self::populateReferences($property, $typeDefinition->definitions());
154+
}
155+
if (\count($rootDefinitions) > 0) {
156+
self::populateReferences($property, $rootDefinitions);
157+
}
158+
}
159+
$additionalProperties = $typeDefinition->additionalProperties();
160+
161+
if ($additionalProperties instanceof TypeSet) {
162+
if (\count($typeDefinition->definitions()) > 0) {
163+
self::populateReferences($additionalProperties, $typeDefinition->definitions());
164+
}
165+
if (\count($rootDefinitions) > 0) {
166+
self::populateReferences($additionalProperties, $rootDefinitions);
167+
}
168+
}
169+
break;
170+
case $typeDefinition instanceof ArrayType:
171+
foreach ($typeDefinition->definitions() as $property) {
172+
self::populateReferences($property, $typeDefinition->definitions());
173+
}
174+
foreach ($typeDefinition->items() as $item) {
175+
if (\count($typeDefinition->definitions()) > 0) {
176+
self::populateReferences($item, $typeDefinition->definitions());
177+
}
178+
if (\count($rootDefinitions) > 0) {
179+
self::populateReferences($item, $rootDefinitions);
180+
}
181+
}
182+
break;
183+
case $typeDefinition instanceof NotType:
184+
if (\count($rootDefinitions) > 0) {
185+
self::populateReferences($typeDefinition->getTypeSet(), $rootDefinitions);
186+
}
187+
break;
188+
case $typeDefinition instanceof OfType:
189+
foreach ($typeDefinition->getTypeSets() as $item) {
190+
if (\count($rootDefinitions) > 0) {
191+
self::populateReferences($item, $rootDefinitions);
192+
}
193+
}
194+
break;
195+
case $typeDefinition instanceof ReferenceType:
196+
$referencePath = \explode('/', $typeDefinition->ref());
197+
$name = \array_pop($referencePath);
198+
199+
$resolvedType = $rootDefinitions[$name] ?? null;
200+
201+
if ($resolvedType !== null) {
202+
$typeDefinition->setResolvedType(clone $resolvedType);
203+
$typeDefinition->setIsRequired($typeDefinition->isRequired());
204+
}
205+
break;
206+
default:
207+
break;
208+
}
209+
}
132210
}
133211
}

tests/Type/ArrayTypeTest.php

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use OpenCodeModeling\JsonSchemaToPhp\Type\ArrayType;
88
use OpenCodeModeling\JsonSchemaToPhp\Type\NumberType;
9+
use OpenCodeModeling\JsonSchemaToPhp\Type\ObjectType;
910
use OpenCodeModeling\JsonSchemaToPhp\Type\ReferenceType;
1011
use OpenCodeModeling\JsonSchemaToPhp\Type\StringType;
1112
use OpenCodeModeling\JsonSchemaToPhp\Type\Type;
@@ -85,7 +86,15 @@ public function it_supports_array_with_one_type_ref(): void
8586
$items = $type->items();
8687
$this->assertCount(1, $items);
8788

88-
$this->assertItemThree($items[0]);
89+
/** @var ReferenceType $itemThreeType */
90+
$itemThreeType = $items[0]->first();
91+
$this->assertInstanceOf(ReferenceType::class, $itemThreeType);
92+
$this->assertCount(1, $itemThreeType->resolvedType());
93+
94+
/** @var StringType $resolvedType */
95+
$resolvedType = $itemThreeType->resolvedType()->first();
96+
97+
$this->assertSame(2, $resolvedType->minLength());
8998
}
9099

91100
/**
@@ -135,17 +144,44 @@ private function assertItemTwo(TypeSet $itemTwo): void
135144

136145
private function assertItemThree(TypeSet $itemThree): void
137146
{
138-
$this->assertCount(1, $itemThree);
139-
140-
/** @var ReferenceType $itemThreeType */
141-
$itemThreeType = $itemThree->first();
142-
$this->assertInstanceOf(ReferenceType::class, $itemThreeType);
143-
$this->assertCount(1, $itemThreeType->resolvedType());
144-
145-
/** @var StringType $resolvedType */
146-
$resolvedType = $itemThreeType->resolvedType()->first();
147-
148-
$this->assertSame(2, $resolvedType->minLength());
147+
/** @var ReferenceType $address */
148+
$address = $itemThree->first();
149+
$this->assertInstanceOf(ReferenceType::class, $address);
150+
$this->assertCount(1, $address->resolvedType());
151+
152+
/** @var ObjectType $resolvedType */
153+
$resolvedType = $address->resolvedType()->first();
154+
$this->assertInstanceOf(ObjectType::class, $resolvedType);
155+
156+
$this->assertFalse($address->isRequired());
157+
$this->assertFalse($resolvedType->isRequired());
158+
$this->assertFalse($resolvedType->isNullable());
159+
160+
$properties = $resolvedType->properties();
161+
$this->assertArrayHasKey('street_address', $properties);
162+
$this->assertArrayHasKey('city', $properties);
163+
$this->assertArrayHasKey('state', $properties);
164+
165+
$stateTypeSet = $properties['state'];
166+
$this->assertCount(1, $stateTypeSet);
167+
168+
/** @var ReferenceType $state */
169+
$state = $stateTypeSet->first();
170+
$this->assertInstanceOf(ReferenceType::class, $state);
171+
$this->assertTrue($state->isRequired());
172+
$this->assertFalse($state->isNullable());
173+
174+
$resolvedTypeSet = $state->resolvedType();
175+
$this->assertCount(1, $resolvedTypeSet);
176+
177+
/** @var StringType $state */
178+
$state = $resolvedTypeSet->first();
179+
$this->assertInstanceOf(StringType::class, $state);
180+
$this->assertTrue($state->isRequired());
181+
$this->assertFalse($state->isNullable());
182+
$this->assertCount(2, $state->enum());
183+
$this->assertContains('NY', $state->enum());
184+
$this->assertContains('DC', $state->enum());
149185
}
150186

151187
private function assertItemFour(TypeSet $itemFour): void
@@ -157,4 +193,5 @@ private function assertItemFour(TypeSet $itemFour): void
157193
$this->assertCount(4, $itemFourType->enum());
158194

159195
}
196+
160197
}

0 commit comments

Comments
 (0)