Skip to content

Commit 870a8b0

Browse files
committed
Generate exception class for enum types - Close #34
1 parent 32ace9f commit 870a8b0

File tree

4 files changed

+623
-319
lines changed

4 files changed

+623
-319
lines changed

src/ExceptionFactory.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/json-schema-to-php-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModeling\JsonSchemaToPhpAst;
12+
13+
use OpenCodeModeling\CodeAst\Builder\ClassBuilder;
14+
use OpenCodeModeling\CodeAst\Builder\ClassMethodBuilder;
15+
use OpenCodeModeling\CodeAst\Builder\ParameterBuilder;
16+
use OpenCodeModeling\CodeAst\Package\ClassInfoList;
17+
use OpenCodeModeling\JsonSchemaToPhp\Type\StringType;
18+
use OpenCodeModeling\JsonSchemaToPhp\Type\TypeDefinition;
19+
20+
final class ExceptionFactory
21+
{
22+
private ClassInfoList $classInfoList;
23+
24+
private bool $typed;
25+
26+
/**
27+
* @var callable
28+
*/
29+
private $classNameFilter;
30+
31+
/**
32+
* @var callable
33+
*/
34+
private $propertyNameFilter;
35+
36+
/**
37+
* @var callable
38+
*/
39+
private $methodNameFilter;
40+
41+
/**
42+
* @param bool $typed
43+
* @param callable $classNameFilter Converts the name of the type to a proper class name
44+
* @param callable $propertyNameFilter Converts the name to a proper class property name
45+
* @param callable $methodNameFilter Converts the name to a proper class method name
46+
*/
47+
public function __construct(
48+
ClassInfoList $classInfoList,
49+
bool $typed,
50+
callable $classNameFilter,
51+
callable $propertyNameFilter,
52+
callable $methodNameFilter
53+
) {
54+
$this->classInfoList = $classInfoList;
55+
$this->typed = $typed;
56+
$this->classNameFilter = $classNameFilter;
57+
$this->propertyNameFilter = $propertyNameFilter;
58+
$this->methodNameFilter = $methodNameFilter;
59+
}
60+
61+
/**
62+
* @param TypeDefinition $typeDefinition
63+
* @param string $valueObjectFqcn FQCN of the value object (needed for determine exception class namespace and namespace imports)
64+
* @return ClassBuilder
65+
*/
66+
public function classBuilder(
67+
TypeDefinition $typeDefinition,
68+
string $valueObjectFqcn
69+
): ClassBuilder {
70+
$classInfo = $this->classInfoList->classInfoForNamespace($valueObjectFqcn);
71+
$namespace = $classInfo->getClassNamespace($valueObjectFqcn) . '\\Exception';
72+
73+
switch (true) {
74+
case $typeDefinition instanceof StringType:
75+
if ($typeDefinition->enum() !== null) {
76+
return $this->exceptionClassForEnum($typeDefinition, $namespace)
77+
->addNamespaceImport($valueObjectFqcn);
78+
}
79+
// no break
80+
default:
81+
throw new \RuntimeException(\sprintf('Type "%s" not supported', \get_class($typeDefinition)));
82+
}
83+
}
84+
85+
private function exceptionClassForEnum(
86+
TypeDefinition $typeDefinition,
87+
string $namespace
88+
): ClassBuilder {
89+
$name = $typeDefinition->name() ?: 'text';
90+
91+
$argumentName = ($this->propertyNameFilter)($name);
92+
$className = ($this->classNameFilter)($name);
93+
94+
$body = <<<EOF
95+
return new self(sprintf('Invalid value for "$className" given. Got "%s", but allowed values are ' . implode(', ', $className::CHOICES), \$$argumentName, StatusCodeInterface::STATUS_BAD_REQUEST));
96+
EOF;
97+
98+
$invalidMethodFor = ClassMethodBuilder::fromScratch(($this->methodNameFilter)('for_' . $name), $this->typed)
99+
->setStatic(true)
100+
->setParameters(ParameterBuilder::fromScratch($argumentName, 'string'))
101+
->setReturnType('self')
102+
->setBody($body);
103+
104+
$classBuilder = ClassBuilder::fromScratch('Invalid' . ($this->classNameFilter)($name), $namespace)
105+
->setFinal(true)
106+
->setExtends('InvalidArgumentException');
107+
$classBuilder->addNamespaceImport(
108+
'Fig\Http\Message\StatusCodeInterface',
109+
'InvalidArgumentException'
110+
);
111+
112+
$classBuilder->addMethod($invalidMethodFor);
113+
114+
return $classBuilder;
115+
}
116+
}

src/ValueObjectFactory.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ final class ValueObjectFactory
5656

5757
private ClassInfoList $classInfoList;
5858
private FileCodeGenerator $fileCodeGenerator;
59+
private ExceptionFactory $exceptionFactory;
5960

6061
/**
6162
* @var callable
@@ -132,6 +133,13 @@ public function __construct(
132133
$this->constValueFilter = $constValueFilter;
133134

134135
$this->fileCodeGenerator = new FileCodeGenerator($parser, $printer, $classInfoList);
136+
$this->exceptionFactory = new ExceptionFactory(
137+
$this->classInfoList,
138+
$typed,
139+
$this->classNameFilter,
140+
$this->propertyNameFilter,
141+
$this->methodNameFilter,
142+
);
135143

136144
$this->isValueObject = static function (ClassBuilder $classBuilder): bool {
137145
return $classBuilder->hasMethod('fromItems')
@@ -335,13 +343,22 @@ public function generateClasses(
335343
$this->addNamespaceImport($classBuilder, $propertyClassNamespace . '\\' . $propertyClassName);
336344
break;
337345
case $propertyType instanceof ScalarType:
338-
$fileCollection->add(
339-
$this->generateValueObject($propertyClassName, $propertyClassNamespace, $propertyType)
340-
);
346+
$valueObjectClassBuilder = $this->generateValueObject($propertyClassName, $propertyClassNamespace, $propertyType);
347+
$fileCollection->add($valueObjectClassBuilder);
348+
341349
$this->addNamespaceImport($classBuilder, $propertyClassNamespace . '\\' . $propertyClassName);
342350
$classBuilder->addProperty(
343351
$this->determineProperty($propertyPropertyName, $propertyClassName, $propertyType)
344352
);
353+
354+
if ($propertyType instanceof StringType && $propertyType->enum() !== null) {
355+
$enumExceptionClassBuilder = $this->exceptionFactory->classBuilder(
356+
$propertyType,
357+
$propertyClassNamespace . '\\' . $propertyClassName
358+
);
359+
$fileCollection->add($enumExceptionClassBuilder);
360+
$valueObjectClassBuilder->addNamespaceImport($enumExceptionClassBuilder->getFqcn());
361+
}
345362
break;
346363
default:
347364
break;
@@ -350,13 +367,19 @@ public function generateClasses(
350367
$fileCollection->add($classBuilder);
351368
break;
352369
case $type instanceof ScalarType:
353-
$fileCollection->add(
354-
$this->generateValueObject(
355-
($this->classNameFilter)($className),
356-
$this->extractNamespace($classNamespacePath, $rootClassNamespacePath, $type),
357-
$type
358-
)
359-
);
370+
$namespace = $this->extractNamespace($classNamespacePath, $rootClassNamespacePath, $type);
371+
$valueObjectClassBuilder = $this->generateValueObject(($this->classNameFilter)($className), $namespace, $type);
372+
373+
$fileCollection->add($valueObjectClassBuilder);
374+
375+
if ($type instanceof StringType && $type->enum() !== null) {
376+
$enumExceptionClassBuilder = $this->exceptionFactory->classBuilder(
377+
$type,
378+
$namespace . '\\' . ($this->classNameFilter)($className)
379+
);
380+
$fileCollection->add($enumExceptionClassBuilder);
381+
$valueObjectClassBuilder->addNamespaceImport($enumExceptionClassBuilder->getFqcn());
382+
}
360383
break;
361384
case $type instanceof ArrayType:
362385
$arrayClassBuilder = $this->generateValueObject(

tests/ExceptionFactoryTest.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/**
4+
* @see https://github.com/open-code-modeling/json-schema-to-php-ast for the canonical source repository
5+
* @copyright https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/COPYRIGHT.md
6+
* @license https://github.com/open-code-modeling/json-schema-to-php-ast/blob/master/LICENSE.md MIT License
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace OpenCodeModelingTest\JsonSchemaToPhpAst;
12+
13+
use OpenCodeModeling\CodeAst\Builder\ClassBuilder;
14+
use OpenCodeModeling\CodeAst\Builder\FileCollection;
15+
use OpenCodeModeling\CodeAst\FileCodeGenerator;
16+
use OpenCodeModeling\CodeAst\Package\ClassInfoList;
17+
use OpenCodeModeling\CodeAst\Package\Psr4Info;
18+
use OpenCodeModeling\Filter\FilterFactory;
19+
use OpenCodeModeling\JsonSchemaToPhp\Shorthand\Shorthand;
20+
use OpenCodeModeling\JsonSchemaToPhp\Type\Type;
21+
use OpenCodeModeling\JsonSchemaToPhpAst\ExceptionFactory;
22+
use PhpParser\Parser;
23+
use PhpParser\ParserFactory;
24+
use PhpParser\PrettyPrinter\Standard;
25+
use PhpParser\PrettyPrinterAbstract;
26+
use PHPUnit\Framework\TestCase;
27+
28+
final class ExceptionFactoryTest extends TestCase
29+
{
30+
private Parser $parser;
31+
private PrettyPrinterAbstract $printer;
32+
private ExceptionFactory $exceptionFactory;
33+
private ClassInfoList $classInfoList;
34+
private FileCodeGenerator $fileCodeGenerator;
35+
36+
public function setUp(): void
37+
{
38+
$this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
39+
$this->printer = new Standard(['shortArraySyntax' => true]);
40+
41+
$this->classInfoList = new ClassInfoList(
42+
new Psr4Info(
43+
'tmp/',
44+
'Acme',
45+
FilterFactory::directoryToNamespaceFilter(),
46+
FilterFactory::namespaceToDirectoryFilter(),
47+
)
48+
);
49+
50+
$classNameFilter = FilterFactory::classNameFilter();
51+
$propertyNameFilter = FilterFactory::propertyNameFilter();
52+
$methodNameFilter = FilterFactory::methodNameFilter();
53+
54+
$this->fileCodeGenerator = new FileCodeGenerator(
55+
$this->parser,
56+
$this->printer,
57+
$this->classInfoList
58+
);
59+
60+
$this->exceptionFactory = new ExceptionFactory(
61+
$this->classInfoList,
62+
true,
63+
$classNameFilter,
64+
$propertyNameFilter,
65+
$methodNameFilter
66+
);
67+
}
68+
69+
/**
70+
* @test
71+
*/
72+
public function it_generates_exception_class_for_enums(): void
73+
{
74+
$typeSet = Type::fromDefinition(Shorthand::convertToJsonSchema('enum:not_interested,invalid|ns:Contact'), 'reason_type');
75+
76+
$fileCollection = FileCollection::emptyList();
77+
78+
$classBuilder = $this->exceptionFactory->classBuilder($typeSet->first(), 'Acme\Contact\ReasonType');
79+
80+
$this->assertEnumException($classBuilder);
81+
$fileCollection->add($classBuilder);
82+
83+
$files = $this->fileCodeGenerator->generateFiles($fileCollection);
84+
85+
$this->assertEnumExceptionFile($files['tmp/Contact/Exception/InvalidReasonType.php']);
86+
}
87+
88+
private function assertEnumException(ClassBuilder $classBuilder): void
89+
{
90+
$this->assertSame('InvalidReasonType', $classBuilder->getName());
91+
}
92+
93+
private function assertEnumExceptionFile(string $code): void
94+
{
95+
$expected = <<<'PHP'
96+
<?php
97+
98+
declare (strict_types=1);
99+
namespace Acme\Contact\Exception;
100+
101+
use Fig\Http\Message\StatusCodeInterface;
102+
use InvalidArgumentException;
103+
use Acme\Contact\ReasonType;
104+
final class InvalidReasonType extends InvalidArgumentException
105+
{
106+
public static function forReasonType(string $reasonType) : self
107+
{
108+
return new self(sprintf('Invalid value for "ReasonType" given. Got "%s", but allowed values are ' . implode(', ', ReasonType::CHOICES), $reasonType, StatusCodeInterface::STATUS_BAD_REQUEST));
109+
}
110+
}
111+
PHP;
112+
$this->assertSame($expected, $code);
113+
}
114+
}

0 commit comments

Comments
 (0)