Skip to content

Commit 3797d8a

Browse files
committed
Enhance type system:
* anyOf * arrays of scalars in response bodies * oneOf Other enhancements: * Pretty print schema and example data JSON * Add operation test `cover` annotation * Handle a second / in an oprationId better
1 parent 6f56366 commit 3797d8a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3549
-319
lines changed

example/openapi-client-one.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ state:
33
additionalFiles:
44
- composer.json
55
- composer.lock
6-
spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml
7-
#spec: api.github.com.yaml
6+
#spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml
7+
spec: api.github.com.yaml
88
entryPoints:
99
call: true
1010
operations: true

src/Gatherer/Operation.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public static function gather(
150150
}
151151

152152
$name = lcfirst(trim(Utils::basename($className), '\\'));
153-
$group = strlen(trim(trim(Utils::dirname($className), '\\'), '.')) > 0 ? trim(Utils::dirname($className), '\\') : null;
153+
$group = strlen(trim(trim(Utils::dirname($className), '\\'), '.')) > 0 ? trim(str_replace('\\', '', Utils::dirname($className)), '\\') : null;
154154

155155
return new \ApiClients\Tools\OpenApiClientGenerator\Representation\Operation(
156156
ClassString::factory($baseNamespace, 'Operation\\' . Utils::fixKeyword($className)),

src/Generator/Client.php

+63-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use ApiClients\Tools\OpenApiClientGenerator\Generator\Client\Routers;
1313
use ApiClients\Tools\OpenApiClientGenerator\Generator\Client\Routers\RouterClass;
1414
use ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Operation;
15+
use ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Types;
1516
use ApiClients\Tools\OpenApiClientGenerator\PrivatePromotedPropertyAsParam;
1617
use ApiClients\Tools\OpenApiClientGenerator\Representation;
1718
use ApiClients\Tools\OpenApiClientGenerator\Utils;
@@ -25,6 +26,8 @@
2526
use PhpParser\Node;
2627
use PhpParser\Node\Arg;
2728
use PhpParser\Node\Expr;
29+
use PhpParser\Node\Name;
30+
use PhpParser\Node\UnionType;
2831
use React\EventLoop\Loop;
2932
use React\Http\Browser;
3033
use ReflectionClass;
@@ -326,7 +329,7 @@ public static function generate(Configuration $configuration, string $pathPrefix
326329
array_map(
327330
'trim',
328331
array_unique(
329-
$returnTypes,
332+
[...Types::filterDuplicatesAndIncompatibleRawTypes(...$returnTypes)],
330333
),
331334
),
332335
);
@@ -336,7 +339,7 @@ public static function generate(Configuration $configuration, string $pathPrefix
336339
'trim',
337340
array_filter(
338341
array_unique(
339-
$returnTypes,
342+
[...Types::filterDuplicatesAndIncompatibleRawTypes(...$returnTypes)],
340343
),
341344
static fn (string $type): bool => $type !== 'void',
342345
),
@@ -494,7 +497,6 @@ public static function generate(Configuration $configuration, string $pathPrefix
494497
],
495498
),
496499
),
497-
// ...($returnTypesUnfilterred === 'void' ? [new Node\Stmt\Return_()] : []),
498500
],
499501
];
500502
}
@@ -553,11 +555,7 @@ public static function generate(Configuration $configuration, string $pathPrefix
553555
$left = '';
554556
$right = '';
555557
for ($i = 0; $i < $count; $i++) {
556-
$returnType = implode('|', [
557-
...($operations[$i]->matchMethod === 'STREAM' ? ['iterable<string>'] : []),
558-
...array_unique($operations[$i]->returnType),
559-
]);
560-
$returnType = ($operations[$i]->matchMethod === 'LIST' ? 'iterable<' . $returnType . '>' : $returnType);
558+
$returnType = Operation::getDocBlockResultTypeFromOperation($operations[$i]);
561559
if ($i !== $lastItem) {
562560
$left .= '($call is Operation\\' . $operations[$i]->classNameSanitized->relative . '::OPERATION_MATCH ? ' . $returnType . ' : ';
563561
} else {
@@ -572,7 +570,22 @@ public static function generate(Configuration $configuration, string $pathPrefix
572570
' */',
573571
'// phpcs:enable',
574572
])),
575-
)->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->addStmt(
573+
)->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->setReturnType(
574+
new UnionType(
575+
array_map(
576+
static fn (string $type): Name => new Name($type),
577+
array_unique(
578+
[
579+
...Types::filterDuplicatesAndIncompatibleRawTypes(...(static function (array $operations): iterable {
580+
foreach ($operations as $operation) {
581+
yield from explode('|', Operation::getResultTypeFromOperation($operation));
582+
}
583+
})($operations)),
584+
],
585+
),
586+
),
587+
),
588+
)->addStmt(
576589
new Node\Expr\Assign(
577590
new Node\Expr\Array_([
578591
new Node\Expr\ArrayItem(
@@ -743,6 +756,19 @@ private static function traverseOperationPaths(array $operations, array &$operat
743756
return $operations;
744757
}
745758

759+
/** @param array<Representation\Path> $paths */
760+
private static function operationsInThisThree(array $paths, int $level, Routers $routers): iterable
761+
{
762+
foreach ($paths as $path) {
763+
yield from $path['operations'];
764+
yield from self::operationsInThisThree(
765+
$path['paths'], /** @phpstan-ignore-line */
766+
$level + 1,
767+
$routers,
768+
);
769+
}
770+
}
771+
746772
/**
747773
* @param array<Representation\Operation> $operations
748774
* @param array<Representation\Path> $paths
@@ -760,7 +786,20 @@ private static function traverseOperations(array $operations, array $paths, int
760786
$nonArgumentPathChunks[] = new Node\Expr\ArrayItem(new Node\Scalar\String_($pathChunk));
761787
}
762788

789+
$opsInTree = [
790+
...self::operationsInThisThree(
791+
$paths,
792+
$level,
793+
$routers,
794+
),
795+
];
796+
763797
$ifs = [];
798+
799+
// if (count($opsIntree) === 13) {
800+
// $operations = [...$operations, ...$opsIntree];
801+
// }
802+
764803
foreach ($operations as $operation) {
765804
$ifs[] = [
766805
new Node\Expr\BinaryOp\Equal(
@@ -774,6 +813,7 @@ private static function traverseOperations(array $operations, array $paths, int
774813
];
775814
}
776815

816+
// if (count($opsIntree) > 13) {
777817
foreach ($paths as $pathChunk => $path) {
778818
$ifs[] = [
779819
new Node\Expr\BinaryOp\Equal(
@@ -784,14 +824,16 @@ private static function traverseOperations(array $operations, array $paths, int
784824
new Node\Scalar\String_($pathChunk),
785825
),
786826
self::traverseOperations(
787-
$path['operations'], /** @phpstan-ignore-line */
827+
$path['operations'], /** @phpstan-ignore-line */
788828
$path['paths'], /** @phpstan-ignore-line */
789829
$level + 1,
790830
$routers,
791831
),
792832
];
793833
}
794834

835+
// }
836+
795837
if (count($ifs) === 0) {
796838
return [];
797839
}
@@ -816,7 +858,17 @@ private static function traverseOperations(array $operations, array $paths, int
816858
/** @return array<Node\Stmt> */
817859
private static function callOperation(Routers $routers, Representation\Operation $operation, Representation\Path $path): array
818860
{
819-
$returnType = Operation::getResultTypeFromOperation($operation);
861+
$returnType = implode(
862+
'|',
863+
[
864+
...Types::filterDuplicatesAndIncompatibleRawTypes(
865+
...explode(
866+
'|',
867+
Operation::getResultTypeFromOperation($operation),
868+
),
869+
),
870+
],
871+
);
820872
$router = $routers->add(
821873
$operation->method,
822874
$operation->group,

src/Generator/ClientInterface.php

+22-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
use ApiClients\Contracts\OpenAPI\WebHooksInterface;
88
use ApiClients\Tools\OpenApiClientGenerator\Configuration;
99
use ApiClients\Tools\OpenApiClientGenerator\File;
10+
use ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Types;
1011
use ApiClients\Tools\OpenApiClientGenerator\Representation\Operation;
1112
use PhpParser\Builder\Param;
1213
use PhpParser\BuilderFactory;
1314
use PhpParser\Comment\Doc;
15+
use PhpParser\Node\Name;
16+
use PhpParser\Node\UnionType;
1417

18+
use function array_map;
1519
use function array_unique;
1620
use function count;
21+
use function explode;
1722
use function implode;
1823
use function trim;
1924

@@ -45,11 +50,7 @@ public static function generate(Configuration $configuration, string $pathPrefix
4550
$left = '';
4651
$right = '';
4752
for ($i = 0; $i < $count; $i++) {
48-
$returnType = implode('|', [
49-
...($operations[$i]->matchMethod === 'STREAM' ? ['iterable<string>'] : []),
50-
...array_unique($operations[$i]->returnType),
51-
]);
52-
$returnType = ($operations[$i]->matchMethod === 'LIST' ? 'iterable<' . $returnType . '>' : $returnType);
53+
$returnType = \ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Operation::getDocBlockResultTypeFromOperation($operations[$i]);
5354
if ($i !== $lastItem) {
5455
$left .= '($call is Operation\\' . $operations[$i]->classNameSanitized->relative . '::OPERATION_MATCH ? ' . $returnType . ' : ';
5556
} else {
@@ -64,7 +65,22 @@ public static function generate(Configuration $configuration, string $pathPrefix
6465
' */',
6566
'// phpcs:enable',
6667
])),
67-
)->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([])),
68+
)->addParam((new Param('call'))->setType('string'))->addParam((new Param('params'))->setType('array')->setDefault([]))->setReturnType(
69+
new UnionType(
70+
array_map(
71+
static fn (string $type): Name => new Name($type),
72+
array_unique(
73+
[
74+
...Types::filterDuplicatesAndIncompatibleRawTypes(...(static function (array $operations): iterable {
75+
foreach ($operations as $operation) {
76+
yield from explode('|', \ApiClients\Tools\OpenApiClientGenerator\Generator\Helper\Operation::getResultTypeFromOperation($operation));
77+
}
78+
})($operations)),
79+
],
80+
),
81+
),
82+
),
83+
),
6884
);
6985
}
7086

src/Generator/Helper/Operation.php

+21-10
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
use PHPStan\PhpDocParser\Parser\TokenIterator;
1919
use PHPStan\PhpDocParser\Parser\TypeParser;
2020
use ReflectionClass;
21+
use Rx\Observable;
2122

2223
use function array_map;
2324
use function count;
2425
use function explode;
2526
use function implode;
2627
use function is_string;
28+
use function str_replace;
2729
use function strpos;
2830
use function ucfirst;
2931

@@ -167,11 +169,13 @@ public static function getResultTypeFromOperation(Representation\Operation $oper
167169
return (string) $returnType;
168170
}
169171

170-
return implode(
171-
'|',
172-
array_map(
173-
static fn (string $object): Node\Name => new Node\Name((strpos($object, '\\') > 0 ? '\\' : '') . $object),
174-
explode('|', (string) $returnType),
172+
return self::convertObservableIntoIterable(
173+
implode(
174+
'|',
175+
array_map(
176+
static fn (string $object): Node\Name => new Node\Name((strpos($object, '\\') > 0 ? '\\' : '') . $object),
177+
explode('|', (string) $returnType),
178+
),
175179
),
176180
);
177181
}
@@ -209,12 +213,19 @@ public static function getDocBlockResultTypeFromOperation(Representation\Operati
209213
$tokens = new TokenIterator($lexer->tokenize($docComment));
210214
$phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode
211215

212-
return implode(
213-
'|',
214-
array_map(
215-
static fn (ReturnTagValueNode $returnTagValueNode): string => (string) $returnTagValueNode->type,
216-
$phpDocNode->getReturnTagValues(),
216+
return self::convertObservableIntoIterable(
217+
implode(
218+
'|',
219+
array_map(
220+
static fn (ReturnTagValueNode $returnTagValueNode): string => (string) $returnTagValueNode->type,
221+
$phpDocNode->getReturnTagValues(),
222+
),
217223
),
218224
);
219225
}
226+
227+
private static function convertObservableIntoIterable(string $string): string
228+
{
229+
return str_replace('\\' . Observable::class, 'iterable', $string);
230+
}
220231
}
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ApiClients\Tools\OpenApiClientGenerator\Generator\Helper;
6+
7+
use ApiClients\Tools\OpenApiClientGenerator\Representation\PropertyType;
8+
use ApiClients\Tools\OpenApiClientGenerator\Representation\Schema;
9+
use cebe\openapi\spec\Schema as cebeSchema;
10+
use PhpParser\Node;
11+
use PhpParser\Node\Arg;
12+
13+
use function is_array;
14+
use function is_string;
15+
16+
final class OperationArray
17+
{
18+
public static function hydrate(string $className): Node\Expr\MethodCall
19+
{
20+
return new Node\Expr\MethodCall(
21+
new Node\Expr\PropertyFetch(
22+
new Node\Expr\Variable('this'),
23+
'hydrator',
24+
),
25+
'hydrateObject',
26+
[
27+
new Node\Arg(new Node\Expr\ClassConstFetch(
28+
new Node\Name($className),
29+
'class',
30+
)),
31+
new Node\Arg(new Node\Expr\Variable('body')),
32+
],
33+
);
34+
}
35+
36+
public static function validate(string $className, bool $isArray): Node\Stmt\Expression
37+
{
38+
return new Node\Stmt\Expression(new Node\Expr\MethodCall(
39+
new Node\Expr\PropertyFetch(
40+
new Node\Expr\Variable('this'),
41+
'responseSchemaValidator',
42+
),
43+
'validate',
44+
[
45+
new Node\Arg(new Node\Expr\Variable($isArray ? 'bodyItem' : 'body')),
46+
new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), 'readFromJson', [
47+
new Arg(new Node\Expr\ClassConstFetch(
48+
new Node\Name($className),
49+
'SCHEMA_JSON',
50+
)),
51+
new Arg(new Node\Expr\ClassConstFetch(
52+
new Node\Name('\\' . cebeSchema::class),
53+
'class',
54+
)),
55+
])),
56+
],
57+
));
58+
}
59+
60+
/** @return iterable<Schema> */
61+
public static function uniqueSchemas(string|Schema|PropertyType ...$propertyTypes): iterable
62+
{
63+
$schemas = [];
64+
65+
foreach ($propertyTypes as $propertyType) {
66+
if (is_string($propertyType)) {
67+
$schemas[$propertyType] = $propertyType;
68+
continue;
69+
}
70+
71+
if ($propertyType instanceof Schema) {
72+
$schemas[$propertyType->className->fullyQualified->source] = $propertyType;
73+
continue;
74+
}
75+
76+
foreach (
77+
static::uniqueSchemas(...is_array($propertyType->payload) ? $propertyType->payload : [$propertyType->payload]) as $nestedPropertyType
78+
) {
79+
$schemas[$nestedPropertyType instanceof Schema ? $nestedPropertyType->className->fullyQualified->source : $nestedPropertyType] = $nestedPropertyType;
80+
}
81+
}
82+
83+
yield from $schemas;
84+
}
85+
}

0 commit comments

Comments
 (0)