Skip to content

Commit 0562db3

Browse files
authored
Merge pull request #59 from php-api-clients/responses
Generate response validation and hydration
2 parents 04f37bc + ffff530 commit 0562db3

File tree

6 files changed

+256
-24
lines changed

6 files changed

+256
-24
lines changed

composer.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openapi-client.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml
1+
#spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions-next/api.github.com/api.github.com.yaml
2+
spec: file:///home/wyrihaximus/Projects/PHPAPIClients/openapi-client-generator/api.github.com.yaml
23
namespace: ApiClients\Client\Github
34
destination: generated

src/Generator.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ private function all(string $namespace): iterable
110110
$path,
111111
$method,
112112
$this->dirname($namespace . 'Operation/' . $operationClassName),
113+
$namespace,
113114
$this->basename($namespace . 'Operation/' . $operationClassName),
114-
$operation
115+
$operation,
116+
$schemaClassNameMap
115117
);
116118

117119
[$operationGroup, $operationOperation] = explode('/', $operationClassName);

src/Generator/Client.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,44 @@ public static function generate(string $operationGroup, string $namespace, strin
2222
$factory = new BuilderFactory();
2323
$stmt = $factory->namespace($namespace);
2424

25-
$class = $factory->class($className)->makeFinal();
25+
$class = $factory->class($className)->makeFinal()->addStmt(
26+
$factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
27+
)->addStmt(
28+
$factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
29+
)->addStmt(
30+
$factory->method('__construct')->makePublic()->addParam(
31+
(new Param('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')
32+
)->addStmt(
33+
new Node\Expr\Assign(
34+
new Node\Expr\PropertyFetch(
35+
new Node\Expr\Variable('this'),
36+
'requestSchemaValidator'
37+
),
38+
new Node\Expr\Variable('requestSchemaValidator'),
39+
)
40+
)->addParam(
41+
(new Param('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')
42+
)->addStmt(
43+
new Node\Expr\Assign(
44+
new Node\Expr\PropertyFetch(
45+
new Node\Expr\Variable('this'),
46+
'responseSchemaValidator'
47+
),
48+
new Node\Expr\Variable('responseSchemaValidator'),
49+
)
50+
)
51+
);
2652

2753
foreach ($operations as $operationOperation => $operationDetails) {
2854
$params = [];
55+
$params[] = new Node\Expr\PropertyFetch(
56+
new Node\Expr\Variable('this'),
57+
'requestSchemaValidator'
58+
);
59+
$params[] = new Node\Expr\PropertyFetch(
60+
new Node\Expr\Variable('this'),
61+
'responseSchemaValidator'
62+
);
2963
$cn = str_replace('/', '\\', '\\' . $namespace . '\\' . $operationDetails['class']);
3064
$method = $factory->method(lcfirst($operationOperation))->setReturnType($cn)->makePublic();
3165
foreach ($operationDetails['operation']->parameters as $parameter) {

src/Generator/Clients.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,45 @@ public static function generate(string $namespace, array $clients): iterable
2626
$factory = new BuilderFactory();
2727
$stmt = $factory->namespace(rtrim($namespace, '\\'));
2828

29-
$class = $factory->class('Client')->makeFinal();
29+
$class = $factory->class('Client')->makeFinal()->addStmt(
30+
$factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
31+
)->addStmt(
32+
$factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
33+
)->addStmt(
34+
$factory->method('__construct')->makePublic()->addStmt(
35+
new Node\Expr\Assign(
36+
new Node\Expr\PropertyFetch(
37+
new Node\Expr\Variable('this'),
38+
'requestSchemaValidator'
39+
),
40+
new Node\Expr\New_(
41+
new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'),
42+
[
43+
new Node\Arg(new Node\Expr\ClassConstFetch(
44+
new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'),
45+
new Node\Name('VALIDATE_AS_REQUEST'),
46+
))
47+
]
48+
),
49+
)
50+
)->addStmt(
51+
new Node\Expr\Assign(
52+
new Node\Expr\PropertyFetch(
53+
new Node\Expr\Variable('this'),
54+
'responseSchemaValidator'
55+
),
56+
new Node\Expr\New_(
57+
new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'),
58+
[
59+
new Node\Arg(new Node\Expr\ClassConstFetch(
60+
new Node\Name('\League\OpenAPIValidation\Schema\SchemaValidator'),
61+
new Node\Name('VALIDATE_AS_RESPONSE'),
62+
))
63+
]
64+
),
65+
)
66+
)
67+
);
3068

3169

3270
foreach ($clients as $operationGroup => $operations) {
@@ -38,6 +76,16 @@ public static function generate(string $namespace, array $clients): iterable
3876
new Node\Name(
3977
$cn
4078
),
79+
[
80+
new Node\Arg(new Node\Expr\PropertyFetch(
81+
new Node\Expr\Variable('this'),
82+
'requestSchemaValidator'
83+
)),
84+
new Node\Arg(new Node\Expr\PropertyFetch(
85+
new Node\Expr\Variable('this'),
86+
'responseSchemaValidator'
87+
)),
88+
]
4189
)
4290
)
4391
)->makePublic()

src/Generator/Operation.php

Lines changed: 150 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
use cebe\openapi\spec\Operation as OpenAPiOperation;
77
use PhpParser\Builder\Param;
88
use PhpParser\BuilderFactory;
9+
use PhpParser\Comment\Doc;
910
use PhpParser\Node;
11+
use PhpParser\Node\Arg;
1012
use PhpParser\Node\Stmt\Class_;
1113
use Psr\Http\Message\RequestInterface;
14+
use Psr\Http\Message\ResponseInterface;
1215
use RingCentral\Psr7\Request;
16+
use WyriHaximus\Hydrator\Hydrator;
1317

1418
final class Operation
1519
{
@@ -21,7 +25,7 @@ final class Operation
2125
* @param OpenAPiOperation $operation
2226
* @return iterable<Node>
2327
*/
24-
public static function generate(string $path, string $method, string $namespace, string $className, OpenAPiOperation $operation): iterable
28+
public static function generate(string $path, string $method, string $namespace, string $rootNamespace, string $className, OpenAPiOperation $operation, array $schemaClassNameMap): iterable
2529
{
2630
$factory = new BuilderFactory();
2731
$stmt = $factory->namespace($namespace);
@@ -47,10 +51,33 @@ public static function generate(string $path, string $method, string $namespace,
4751
)
4852
)
4953
)
50-
// )->setDocComment('/**' . var_export($operation, true) . '**/');
54+
)->addStmt(
55+
$factory->property('requestSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
56+
)->addStmt(
57+
$factory->property('responseSchemaValidator')->setType('\League\OpenAPIValidation\Schema\SchemaValidator')->makeReadonly()->makePrivate()
5158
);
5259

53-
$constructor = $factory->method('__construct');
60+
$constructor = $factory->method('__construct')->makePublic()->addParam(
61+
(new Param('requestSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')
62+
)->addStmt(
63+
new Node\Expr\Assign(
64+
new Node\Expr\PropertyFetch(
65+
new Node\Expr\Variable('this'),
66+
'requestSchemaValidator'
67+
),
68+
new Node\Expr\Variable('requestSchemaValidator'),
69+
)
70+
)->addParam(
71+
(new Param('responseSchemaValidator'))->setType('\League\OpenAPIValidation\Schema\SchemaValidator')
72+
)->addStmt(
73+
new Node\Expr\Assign(
74+
new Node\Expr\PropertyFetch(
75+
new Node\Expr\Variable('this'),
76+
'responseSchemaValidator'
77+
),
78+
new Node\Expr\Variable('responseSchemaValidator'),
79+
)
80+
);
5481
$requestReplaces = [];
5582
$query = [];
5683
foreach ($operation->parameters as $parameter) {
@@ -110,30 +137,133 @@ public static function generate(string $path, string $method, string $namespace,
110137
}
111138
}
112139
$class->addStmt($constructor);
113-
$class->addStmt(
114-
$factory->method('createRequest')->setReturnType('\\' . RequestInterface::class)->addStmt(
115-
new Node\Stmt\Return_(
116-
new Node\Expr\New_(
117-
new Node\Name(
118-
'\\' . Request::class
140+
$requestParameters = [
141+
new Node\Arg(new Node\Scalar\String_($method)),
142+
new Node\Arg(new Node\Expr\FuncCall(
143+
new Node\Name('\str_replace'),
144+
[
145+
new Node\Expr\Array_(array_map(static fn (string $key): Node\Expr\ArrayItem => new Node\Expr\ArrayItem(new Node\Scalar\String_($key)), array_keys($requestReplaces))),
146+
new Node\Expr\Array_(array_values($requestReplaces)),
147+
new Node\Scalar\String_(rtrim($path . '?' . implode('&', $query), '?')),
148+
]
149+
)),
150+
];
151+
152+
$createRequestMethod = $factory->method('createRequest')->setReturnType('\\' . RequestInterface::class)->addParam(
153+
$factory->param('data')->setType('array')->setDefault([])
154+
);
155+
156+
if ($operation->requestBody !== null) {
157+
foreach ($operation->requestBody->content as $requestBodyContentType => $requestBodyContent) {
158+
$requestParameters[] = new Node\Expr\Array_([
159+
new Node\Expr\ArrayItem(new Node\Scalar\String_($requestBodyContentType), new Node\Scalar\String_('Content-Type'))
160+
]);
161+
$requestParameters[] = new Node\Expr\FuncCall(new Node\Name('json_encode'), [new Arg(new Node\Expr\Variable('data'))]);
162+
$createRequestMethod->addStmt(
163+
new Node\Stmt\Expression(new Node\Expr\MethodCall(
164+
new Node\Expr\PropertyFetch(
165+
new Node\Expr\Variable('this'),
166+
'requestSchemaValidator'
119167
),
168+
new Node\Name('validate'),
120169
[
121-
new Node\Arg(new Node\Scalar\String_($method)),
122-
new Node\Arg(new Node\Expr\FuncCall(
123-
new Node\Name('\str_replace'),
124-
[
125-
new Node\Expr\Array_(array_map(static fn (string $key): Node\Expr\ArrayItem => new Node\Expr\ArrayItem(new Node\Scalar\String_($key)), array_keys($requestReplaces))),
126-
new Node\Expr\Array_(array_values($requestReplaces)),
127-
new Node\Scalar\String_(rtrim($path . '?' . implode('&', $query), '?')),
128-
]
129-
)),
170+
new Node\Arg(new Node\Expr\Variable('data')),
171+
new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [new Node\Scalar\String_(json_encode($requestBodyContent->schema->getSerializableData())), new Node\Scalar\String_('\cebe\openapi\spec\Schema')])),
130172
]
131-
)
173+
))
174+
);
175+
break;
176+
}
177+
}
178+
179+
$createRequestMethod->addStmt(
180+
new Node\Stmt\Return_(
181+
new Node\Expr\New_(
182+
new Node\Name(
183+
'\\' . Request::class
184+
),
185+
$requestParameters
132186
)
133187
)
134188
);
189+
135190
$class->addStmt(
136-
$factory->method('validateResponse')
191+
$createRequestMethod
192+
);
193+
$cases = [];
194+
$returnType = [];
195+
foreach ($operation->responses as $code => $spec) {
196+
$contentTypeCases = [];
197+
foreach ($spec->content as $contentType => $contentTypeSchema) {
198+
$returnType[] = $object = '\\' . $rootNamespace . 'Schema\\' . $schemaClassNameMap[spl_object_hash($contentTypeSchema->schema)];
199+
$ctc = new Node\Stmt\Case_(
200+
new Node\Scalar\String_($contentType),
201+
[
202+
new Node\Stmt\Expression(new Node\Expr\MethodCall(
203+
new Node\Expr\PropertyFetch(
204+
new Node\Expr\Variable('this'),
205+
'responseSchemaValidator'
206+
),
207+
new Node\Name('validate'),
208+
[
209+
new Node\Arg(new Node\Expr\Variable('body')),
210+
new Node\Arg(new Node\Expr\StaticCall(new Node\Name('\cebe\openapi\Reader'), new Node\Name('readFromJson'), [new Node\Scalar\String_(json_encode($contentTypeSchema->schema->getSerializableData())), new Node\Scalar\String_('\cebe\openapi\spec\Schema')])),
211+
]
212+
)),
213+
new Node\Stmt\Return_(new Node\Expr\MethodCall(
214+
new Node\Expr\Variable('hydrator'),
215+
new Node\Name('hydrate'),
216+
[
217+
new Node\Arg(new Node\Scalar\String_($object)),
218+
new Node\Arg(new Node\Expr\Variable('body')),
219+
]
220+
)),
221+
// new Node\Stmt\Break_()
222+
]
223+
);
224+
$contentTypeCases[] = $ctc;
225+
// $ctc->setDocComment(new Doc('/**' . @var_export($contentTypeSchema->getSerializableData(), true) . '**/'));
226+
}
227+
$case = new Node\Stmt\Case_(
228+
new Node\Scalar\LNumber($code),
229+
[
230+
new Node\Stmt\Switch_(
231+
new Node\Expr\Variable('contentType'),
232+
$contentTypeCases
233+
),
234+
new Node\Stmt\Break_()
235+
]
236+
);
237+
$cases[] = $case;
238+
$case->setDocComment(new Doc('/**' . $spec->description . '**/'));
239+
// $case->setDocComment(new Doc('/**' . @var_export($spec->getSerializableData(), true) . '**/'));
240+
}
241+
$class->addStmt(
242+
$factory->method('createResponse')->addParam(
243+
$factory->param('response')->setType('\\' . ResponseInterface::class)
244+
)->setReturnType(
245+
new Node\UnionType(array_map(static fn (string $object): Node\Name => new Node\Name($object), array_unique($returnType)))
246+
)->addStmt(
247+
new Node\Expr\Assign(new Node\Expr\Variable('contentType'), new Node\Expr\MethodCall(new Node\Expr\Variable('response'), 'getHeaderLine', [new Arg(new Node\Scalar\String_('Content-Type'))]))
248+
)->addStmt(
249+
new Node\Expr\Assign(new Node\Expr\Variable('body'), new Node\Expr\FuncCall(new Node\Name('json_decode'), [new Node\Expr\MethodCall(new Node\Expr\MethodCall(new Node\Expr\Variable('response'), 'getBody'), 'getContents'), new Node\Expr\ConstFetch(new Node\Name('true'))]))
250+
)->addStmt(
251+
new Node\Expr\Assign(new Node\Expr\Variable('hydrator'), new Node\Expr\New_(new Node\Name('\\' . Hydrator::class)))
252+
)->addStmt(
253+
new Node\Stmt\Switch_(
254+
new Node\Expr\MethodCall(new Node\Expr\Variable('response'), 'getStatusCode'),
255+
$cases
256+
)
257+
)->addStmt(
258+
new Node\Stmt\Throw_(
259+
new Node\Expr\New_(
260+
new Node\Name('\\' . \RuntimeException::class),
261+
[
262+
new Arg(new Node\Scalar\String_('Unable to find matching reponse code and content type'))
263+
]
264+
)
265+
)
266+
)
137267
);
138268

139269
yield new File($namespace . '\\' . $className, $stmt->addStmt($class)->getNode());

0 commit comments

Comments
 (0)