Skip to content

Commit 4645ca4

Browse files
committed
Inherit type information from enum values if the property containing the enum doesn't provide type information (#30)
Throw a schema exception if an empty enum is detected
1 parent 13967ca commit 4645ca4

File tree

8 files changed

+162
-26
lines changed

8 files changed

+162
-26
lines changed

src/Model/Validator/TypeCheckValidator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ class TypeCheckValidator extends PropertyValidator implements TypeCheckInterface
2626
*/
2727
public function __construct(string $type, PropertyInterface $property, bool $allowImplicitNull)
2828
{
29-
$this->type = $type;
29+
$this->type = strtolower($type);
3030

3131
parent::__construct(
3232
$property,
33-
'!is_' . strtolower($type) . '($value)' . ($allowImplicitNull ? ' && $value !== null' : ''),
33+
"!is_$type(\$value)" . ($allowImplicitNull ? ' && $value !== null' : ''),
3434
InvalidTypeException::class,
3535
[$type]
3636
);

src/PropertyProcessor/Property/AbstractPropertyProcessor.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
use PHPModelGenerator\Model\Validator\SchemaDependencyValidator;
1616
use PHPModelGenerator\PropertyProcessor\ComposedValueProcessorFactory;
1717
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
18+
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator;
1819
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintTransferDecorator;
1920
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
2021
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
2122
use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface;
2223
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
24+
use PHPModelGenerator\Utils\TypeConverter;
2325

2426
/**
2527
* Class AbstractPropertyProcessor
@@ -82,14 +84,43 @@ protected function generateValidators(PropertyInterface $property, JsonSchema $p
8284
*
8385
* @param PropertyInterface $property
8486
* @param array $allowedValues
87+
*
88+
* @throws SchemaException
8589
*/
8690
protected function addEnumValidator(PropertyInterface $property, array $allowedValues): void
8791
{
88-
if ($this->isImplicitNullAllowed($property)) {
92+
if (empty($allowedValues)) {
93+
throw new SchemaException(
94+
sprintf(
95+
"Empty enum property %s in file %s",
96+
$property->getName(),
97+
$property->getJsonSchema()->getFile()
98+
)
99+
);
100+
}
101+
102+
$allowedValues = array_unique($allowedValues);
103+
104+
// no type information provided - inherit the types from the enum values
105+
if (empty($property->getType())) {
106+
$typesOfEnum = array_unique(array_map(
107+
function ($value): string {
108+
return TypeConverter::gettypeToInternal(gettype($value));
109+
},
110+
$allowedValues
111+
));
112+
113+
if (count($typesOfEnum) === 1) {
114+
$property->setType($typesOfEnum[0]);
115+
}
116+
$property->addTypeHintDecorator(new TypeHintDecorator($typesOfEnum));
117+
}
118+
119+
if ($this->isImplicitNullAllowed($property) && !in_array(null, $allowedValues)) {
89120
$allowedValues[] = null;
90121
}
91122

92-
$property->addValidator(new EnumValidator($property, array_unique($allowedValues)), 3);
123+
$property->addValidator(new EnumValidator($property, $allowedValues), 3);
93124
}
94125

95126
/**

src/PropertyProcessor/Property/ConstProcessor.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
1111
use PHPModelGenerator\Model\Validator\PropertyValidator;
1212
use PHPModelGenerator\PropertyProcessor\PropertyProcessorInterface;
13+
use PHPModelGenerator\Utils\TypeConverter;
1314

1415
/**
1516
* Class ConstProcessor
@@ -18,23 +19,16 @@
1819
*/
1920
class ConstProcessor implements PropertyProcessorInterface
2021
{
21-
const TYPE_MAP = [
22-
'boolean' => 'bool',
23-
'integer' => 'int',
24-
'double' => 'float',
25-
];
26-
2722
/**
2823
* @inheritdoc
2924
*/
3025
public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface
3126
{
3227
$json = $propertySchema->getJson();
33-
$type = gettype($json['const']);
3428

3529
$property = new Property(
3630
$propertyName,
37-
self::TYPE_MAP[gettype($json['const'])] ?? $type,
31+
TypeConverter::gettypeToInternal(gettype($json['const'])),
3832
$propertySchema,
3933
$json['description'] ?? ''
4034
);

src/Utils/TypeConverter.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\Utils;
6+
7+
class TypeConverter
8+
{
9+
public static function gettypeToInternal(string $type): string
10+
{
11+
return [
12+
'boolean' => 'bool',
13+
'integer' => 'int',
14+
'double' => 'float',
15+
][$type] ?? $type;
16+
}
17+
}

tests/Objects/EnumPropertyTest.php

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPModelGenerator\Exception\ValidationException;
77
use PHPModelGenerator\Exception\RenderException;
88
use PHPModelGenerator\Exception\SchemaException;
9+
use PHPModelGenerator\Model\GeneratorConfiguration;
910
use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest;
1011
use stdClass;
1112

@@ -171,12 +172,28 @@ public function testNullProvidedForRequiredEnumThrowsAnException(): void
171172
* @throws RenderException
172173
* @throws SchemaException
173174
*/
174-
public function testNotProvidedEnumItemIsValidInOptionalUntypedEnum(): void
175+
public function testNotProvidedEnumItemIsValidInOptionalUntypedEnum(bool $implicitNull): void
175176
{
176-
$className = $this->generateClassFromFile('UntypedEnumProperty.json');
177+
$className = $this->generateClassFromFile(
178+
'UntypedEnumProperty.json',
179+
(new GeneratorConfiguration())->setImmutable(false),
180+
false,
181+
$implicitNull
182+
);
177183

178184
$object = new $className([]);
179185
$this->assertSame(null, $object->getProperty());
186+
187+
$this->assertSame('string|int|null', $this->getPropertyTypeAnnotation($object, 'property'));
188+
189+
$this->assertSame('string|int|null', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
190+
$this->assertNull($this->getReturnType($object, 'getProperty'));
191+
192+
$this->assertSame(
193+
$implicitNull ? 'string|int|null' : 'string|int',
194+
$this->getMethodParameterTypeAnnotation($object, 'setProperty')
195+
);
196+
$this->assertNull($this->getParameterType($object, 'setProperty'));
180197
}
181198

182199
/**
@@ -252,11 +269,13 @@ public function invalidEnumEntriesUntypedEnumDataProvider(): array
252269
}
253270

254271
/**
272+
* @dataProvider implicitNullDataProvider
273+
*
255274
* @throws FileSystemException
256275
* @throws RenderException
257276
* @throws SchemaException
258277
*/
259-
public function testNotProvidedEnumItemInRequiredUntypedEnumThrowsAnException(): void
278+
public function testNotProvidedEnumItemInRequiredUntypedEnumThrowsAnException(bool $implicitNull): void
260279
{
261280
$this->expectException(ValidationException::class);
262281
$this->expectExceptionMessage('Missing required value for property');
@@ -266,6 +285,74 @@ public function testNotProvidedEnumItemInRequiredUntypedEnumThrowsAnException():
266285
new $className([]);
267286
}
268287

288+
/**
289+
* @dataProvider implicitNullDataProvider
290+
*
291+
* @param bool $implicitNull
292+
*
293+
* @throws FileSystemException
294+
* @throws RenderException
295+
* @throws SchemaException
296+
*/
297+
public function testProvidedEnumItemForRequiredUntypedEnumIsValid(bool $implicitNull): void
298+
{
299+
$className = $this->generateClassFromFile(
300+
'RequiredUntypedEnumProperty.json',
301+
(new GeneratorConfiguration())->setImmutable(false),
302+
false,
303+
$implicitNull
304+
);
305+
306+
$object = new $className(['property' => 'red']);
307+
$this->assertSame('red', $object->getProperty());
308+
309+
$this->assertSame('string|int', $this->getPropertyTypeAnnotation($object, 'property'));
310+
311+
$this->assertSame('string|int', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
312+
$this->assertNull($this->getReturnType($object, 'getProperty'));
313+
314+
$this->assertSame('string|int', $this->getMethodParameterTypeAnnotation($object, 'setProperty'));
315+
$this->assertNull($this->getParameterType($object, 'setProperty'));
316+
}
317+
318+
/**
319+
* @dataProvider implicitNullDataProvider
320+
*
321+
* @param bool $implicitNull
322+
*
323+
* @throws FileSystemException
324+
* @throws RenderException
325+
* @throws SchemaException
326+
*/
327+
public function testTypesAreInheritedFromEnumValuesForUntypedProperties(bool $implicitNull): void
328+
{
329+
$className = $this->generateClassFromFile(
330+
'UntypedEnumPropertyTypeInheritance.json',
331+
(new GeneratorConfiguration())->setImmutable(false),
332+
false,
333+
$implicitNull
334+
);
335+
336+
$object = new $className(['property' => 'red']);
337+
$this->assertSame('red', $object->getProperty());
338+
339+
// property may be null if the optional property is not provided
340+
$this->assertSame('string|null', $this->getPropertyTypeAnnotation($object, 'property'));
341+
342+
$this->assertSame('string|null', $this->getMethodReturnTypeAnnotation($object, 'getProperty'));
343+
$returnType = $this->getReturnType($object, 'getProperty');
344+
$this->assertSame('string', $returnType->getName());
345+
$this->assertTrue($returnType->allowsNull());
346+
347+
$this->assertSame(
348+
$implicitNull ? 'string|null' : 'string',
349+
$this->getMethodParameterTypeAnnotation($object, 'setProperty')
350+
);
351+
$parameterType = $this->getParameterType($object, 'setProperty');
352+
$this->assertSame('string', $parameterType->getName());
353+
$this->assertSame($implicitNull, $parameterType->allowsNull());
354+
}
355+
269356
/**
270357
* @dataProvider implicitNullDataProvider
271358
*
@@ -285,6 +372,13 @@ public function testNullProvidedEnumItemInRequiredUntypedEnumThrowsAnException(b
285372
new $className(['property' => null]);
286373
}
287374

375+
public function testEmptyEnumThrowsSchemaException(): void
376+
{
377+
$this->expectException(SchemaException::class);
378+
$this->expectExceptionMessage('Empty enum property property in file');
379+
$this->generateEnumClass('string', []);
380+
}
381+
288382
protected function generateEnumClass(string $type, array $enumValues, $required = false): string
289383
{
290384
$enumValues = array_map(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"property": {
5+
"enum": ["red", "green", "blue"]
6+
}
7+
}
8+
}

tests/manual/schema/person.json

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,9 @@
22
"$id": "Person",
33
"type": "object",
44
"properties": {
5-
"name": {
6-
"type": "string",
7-
"description": "The name of the person",
8-
"example": "Lawrence"
9-
},
105
"age": {
116
"type": "integer",
12-
"description": "The age of the person",
13-
"example": 42
7+
"enum": [42, 10]
148
}
15-
},
16-
"required": [
17-
"name"
18-
]
9+
}
1910
}

tests/manual/test.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
$generator = new ModelGenerator((new GeneratorConfiguration())
1010
->setNamespacePrefix('\\ManualSchema')
1111
->setImmutable(false)
12+
->setImplicitNull(true)
1213
);
1314

1415
$generator

0 commit comments

Comments
 (0)