Skip to content

Commit 99b71eb

Browse files
committed
Fix missing required list in OpenAPI for nested body params
1 parent cb4c2e5 commit 99b71eb

File tree

2 files changed

+122
-17
lines changed

2 files changed

+122
-17
lines changed

src/Writing/OpenAPISpecWriter.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ protected function generateEndpointParametersSpec(OutputEndpointData $endpoint):
197197
return $parameters;
198198
}
199199

200-
protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint)
200+
protected function generateEndpointRequestBodySpec(OutputEndpointData $endpoint): array|\stdClass
201201
{
202202
$body = [];
203203

@@ -405,9 +405,9 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE
405405

406406
case 'object':
407407
$properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) {
408-
return [$key => $this->generateSchemaForValue($value, $endpoint, $key)];
408+
return [$key => $this->generateSchemaForResponseValue($value, $endpoint, $key)];
409409
})->toArray();
410-
$required = $this->filterRequiredFields($endpoint, array_keys($properties));
410+
$required = $this->filterRequiredResponseFields($endpoint, array_keys($properties));
411411

412412
$data = [
413413
'application/json' => [
@@ -551,6 +551,7 @@ public function generateFieldData($field): array
551551
'properties' => $this->objectIfEmpty(collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) {
552552
return [$subfieldName => $this->generateFieldData($subfield)];
553553
})->all()),
554+
'required' => collect($field->__fields)->filter(fn ($f) => $f['required'])->keys()->toArray(),
554555
];
555556
} else {
556557
$schema = [
@@ -589,17 +590,18 @@ protected function objectIfEmpty(array $field): array|\stdClass
589590
* object)}, and possibly a description for each property. The $endpoint and $path are used for looking up response
590591
* field descriptions.
591592
*/
592-
public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoint, string $path): array
593+
public function generateSchemaForResponseValue(mixed $value, OutputEndpointData $endpoint, string $path): array
593594
{
595+
// If $value is a JSON object
594596
if ($value instanceof \stdClass) {
595597
$value = (array)$value;
596598
$properties = [];
597599
// Recurse into the object
598600
foreach ($value as $subField => $subValue) {
599601
$subFieldPath = sprintf('%s.%s', $path, $subField);
600-
$properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath);
602+
$properties[$subField] = $this->generateSchemaForResponseValue($subValue, $endpoint, $subFieldPath);
601603
}
602-
$required = $this->filterRequiredFields($endpoint, array_keys($properties), $path);
604+
$required = $this->filterRequiredResponseFields($endpoint, array_keys($properties), $path);
603605

604606
$schema = [
605607
'type' => 'object',
@@ -633,10 +635,10 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin
633635

634636
if ($typeOfEachItem === 'object') {
635637
$schema['items']['properties'] = collect($sample)->mapWithKeys(function ($v, $k) use ($endpoint, $path) {
636-
return [$k => $this->generateSchemaForValue($v, $endpoint, "$path.$k")];
638+
return [$k => $this->generateSchemaForResponseValue($v, $endpoint, "$path.$k")];
637639
})->toArray();
638640

639-
$required = $this->filterRequiredFields($endpoint, array_keys($schema['items']['properties']), $path);
641+
$required = $this->filterRequiredResponseFields($endpoint, array_keys($schema['items']['properties']), $path);
640642
if ($required) {
641643
$schema['required'] = $required;
642644
}
@@ -649,7 +651,7 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin
649651
/**
650652
* Given an enpoint and a set of object keys at a path, return the properties that are specified as required.
651653
*/
652-
public function filterRequiredFields(OutputEndpointData $endpoint, array $properties, string $path = ''): array
654+
public function filterRequiredResponseFields(OutputEndpointData $endpoint, array $properties, string $path = ''): array
653655
{
654656
$required = [];
655657
foreach ($properties as $property) {

tests/Unit/OpenAPISpecWriterTest.php

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -861,17 +861,39 @@ public function adds_more_than_two_answers_correctly_using_oneOf()
861861
public function adds_enum_values_to_response_properties()
862862
{
863863
$endpointData = $this->createMockEndpointData([
864-
'uri' => '/path',
865-
'httpMethods' => ['POST'],
864+
'httpMethods' => ['GEt'],
865+
'uri' => '/path1',
866866
'responses' => [
867867
[
868868
'status' => 200,
869-
'description' => 'This one',
870-
'content' => '{"status": "one"}',
869+
'description' => 'List of entities',
870+
'content' => '{"data":[{"name":"Resource name","uuid":"UUID","primary":true}]}',
871871
],
872872
],
873873
'responseFields' => [
874-
'status' => ['enumValues' => ['one', 'two', 'three']],
874+
'data' => [
875+
'name' => 'data',
876+
'type' => 'array',
877+
'description' => 'Data wrapper',
878+
],
879+
'data.name' => [
880+
'name' => 'Resource name',
881+
'type' => 'string',
882+
'description' => 'Name of the resource object',
883+
'required' => true,
884+
],
885+
'data.uuid' => [
886+
'name' => 'Resource UUID',
887+
'type' => 'string',
888+
'description' => 'Unique ID for the resource',
889+
'required' => true,
890+
],
891+
'data.primary' => [
892+
'name' => 'Is primary',
893+
'type' => 'bool',
894+
'description' => 'Is primary resource',
895+
'required' => true,
896+
],
875897
],
876898
]);
877899

@@ -881,19 +903,100 @@ public function adds_enum_values_to_response_properties()
881903

882904
$this->assertArraySubset([
883905
'200' => [
906+
'description' => 'List of entities',
907+
'content' => [
908+
'application/json' => [
909+
'schema' => [
910+
'type' => 'object',
911+
'properties' => [
912+
'data' => [
913+
'type' => 'array',
914+
'description' => 'Data wrapper',
915+
'items' => [
916+
'type' => 'object',
917+
'properties' => [
918+
'name' => [
919+
'type' => 'string',
920+
'description' => 'Name of the resource object',
921+
],
922+
'uuid' => [
923+
'type' => 'string',
924+
'description' => 'Unique ID for the resource',
925+
],
926+
'primary' => [
927+
'type' => 'boolean',
928+
'description' => 'Is primary resource',
929+
],
930+
],
931+
],
932+
'required' => [
933+
'name',
934+
'uuid',
935+
'primary',
936+
]
937+
],
938+
],
939+
],
940+
],
941+
],
942+
],
943+
], $results['paths']['/path1']['get']['responses']);
944+
}
945+
946+
/** @test */
947+
public function lists_required_properties_in_request_body()
948+
{
949+
$endpointData = $this->createMockEndpointData([
950+
'uri' => '/path',
951+
'httpMethods' => ['POST'],
952+
'bodyParameters' => [
953+
'my_field' => [
954+
'name' => 'my_field',
955+
'description' => '',
956+
'required' => true,
957+
'example' => 'abc',
958+
'type' => 'string',
959+
'nullable' => false,
960+
],
961+
'other_field.nested_field' => [
962+
'name' => 'nested_field',
963+
'description' => '',
964+
'required' => true,
965+
'example' => 'abc',
966+
'type' => 'string',
967+
'nullable' => false,
968+
],
969+
],
970+
]);
971+
$groups = [$this->createGroup([$endpointData])];
972+
$results = $this->generate($groups);
973+
974+
$this->assertArraySubset([
975+
'requestBody' => [
884976
'content' => [
885977
'application/json' => [
886978
'schema' => [
979+
'type' => 'object',
887980
'properties' => [
888-
'status' => [
889-
'enum' => ['one', 'two', 'three'],
981+
'my_field' => [
982+
'type' => 'string',
983+
],
984+
'other_field' => [
985+
'type' => 'object',
986+
'properties' => [
987+
'nested_field' => [
988+
'type' => 'string',
989+
],
990+
],
991+
'required' => ['nested_field'],
890992
],
891993
],
994+
'required' => ['my_field']
892995
],
893996
],
894997
],
895998
],
896-
], $results['paths']['/path']['post']['responses']);
999+
], $results['paths']['/path']['post']);
8971000
}
8981001

8991002
protected function createMockEndpointData(array $custom = []): OutputEndpointData

0 commit comments

Comments
 (0)