Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add required to responseField tag and append the required fields to generateResponseContentSpec for object types #814

Merged
merged 11 commits into from
Oct 18, 2024
Merged
3 changes: 3 additions & 0 deletions camel/Extraction/ResponseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class ResponseField extends BaseDTO
/** @var string */
public $type;

/** @var boolean */
public $required;

/** @var mixed */
public $example;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,28 @@ class GetFromResponseFieldTag extends GetFieldsFromTagStrategy
protected function parseTag(string $tagContent): array
{
// Format:
// @responseField <name> <type> <description>
// @responseField <name> <type> <"required" (optional)> <description>
// Examples:
// @responseField text string The text.
// @responseField text string required The text.
// @responseField user_id integer The ID of the user.
preg_match('/(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content);
preg_match('/(.+?)\s+(.+?)\s+(.+?)\s+([\s\S]*)/', $tagContent, $content);
if (empty($content)) {
// This means only name and type were supplied
[$name, $type] = preg_split('/\s+/', $tagContent);
$description = '';
$required = false;
} else {
[$_, $name, $type, $description] = $content;
[$_, $name, $type, $required, $description] = $content;
if($required !== "required"){
$description = $required . " " . $description;
}

$required = $required === "required";
$description = trim($description);
}

$type = static::normalizeTypeName($type);
$data = compact('name', 'type', 'description');
$data = compact('name', 'type', 'required', 'description');

// Support optional type in annotation
// The type can also be a union or nullable type (eg ?string or string|null)
Expand Down
33 changes: 30 additions & 3 deletions src/Writing/OpenAPISpecWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,16 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE
],
'example' => $decoded,
],
],
],
];

case 'object':
$properties = collect($decoded)->mapWithKeys(function ($value, $key) use ($endpoint) {
return [$key => $this->generateSchemaForValue($value, $endpoint, $key)];
})->toArray();
$required = $this->filterRequiredFields($endpoint, array_keys($properties));

return [
$data = [
'application/json' => [
'schema' => [
'type' => 'object',
Expand All @@ -417,6 +418,11 @@ protected function generateResponseContentSpec(?string $responseContent, OutputE
],
],
];
if ($required) {
$data['application/json']['schema']['required'] = $required;
}

return $data;
}
}

Expand Down Expand Up @@ -585,11 +591,16 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin
$subFieldPath = sprintf('%s.%s', $path, $subField);
$properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath);
}
$required = $this->filterRequiredFields($endpoint, array_keys($properties), $path);

return [
$schema = [
'type' => 'object',
'properties' => $this->objectIfEmpty($properties),
];
if ($required) {
$schema['required'] = $required;
}
return $schema;
}

$schema = [
Expand All @@ -616,4 +627,20 @@ public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoin

return $schema;
}

/**
* Given an enpoint and a set of object keys at a path, return the properties that are specified as required.
*/
public function filterRequiredFields(OutputEndpointData $endpoint, array $properties, string $path = ''): array
{
$required = [];
foreach ($properties as $property) {
$responseField = $endpoint->responseFields["$path.$property"] ?? $endpoint->responseFields[$property] ?? null;
if ($responseField && $responseField->required) {
$required[] = $property;
}
}

return $required;
}
}
37 changes: 32 additions & 5 deletions tests/Unit/OpenAPISpecWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ public function adds_responses_correctly_as_responses_on_operation_object()
[
'status' => 201,
'description' => '',
'content' => '{"this": "shouldn\'t be ignored", "and this": "too", "sub level 0": { "sub level 1 key 1": "sl0_sl1k1", "sub level 1 key 2": [ { "sub level 2 key 1": "sl0_sl1k2_sl2k1", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k2_sl2k2_sl3k1" } } ], "sub level 1 key 3": { "sub level 2 key 1": "sl0_sl1k3_sl2k2", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k3_sl2k2_sl3k1", "sub level 3 key null": null, "sub level 3 key integer": 99 } } } }',
'content' => '{"this": "shouldn\'t be ignored", "and this": "too", "also this": "too", "sub level 0": { "sub level 1 key 1": "sl0_sl1k1", "sub level 1 key 2": [ { "sub level 2 key 1": "sl0_sl1k2_sl2k1", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k2_sl2k2_sl3k1" } } ], "sub level 1 key 3": { "sub level 2 key 1": "sl0_sl1k3_sl2k2", "sub level 2 key 2": { "sub level 3 key 1": "sl0_sl1k3_sl2k2_sl3k1", "sub level 3 key null": null, "sub level 3 key integer": 99 }, "sub level 2 key 3 required" : "sl0_sl1k3_sl2k3" } } }',
],
],
'responseFields' => [
Expand All @@ -445,9 +445,19 @@ public function adds_responses_correctly_as_responses_on_operation_object()
'type' => 'string',
'description' => 'Parameter description, ha!',
],
'also this' => [
'name' => 'also this',
'type' => 'string',
'description' => 'This response parameter is required.',
'required' => true,
],
'sub level 0.sub level 1 key 3.sub level 2 key 1' => [
'description' => 'This is description of nested object',
]
'description' => 'This is a description of a nested object',
],
'sub level 0.sub level 1 key 3.sub level 2 key 3 required' => [
'description' => 'This is a description of a required nested object',
'required' => true,
],
],
]);
$endpointData2 = $this->createMockEndpointData([
Expand Down Expand Up @@ -485,6 +495,11 @@ public function adds_responses_correctly_as_responses_on_operation_object()
'example' => "too",
'type' => 'string',
],
'also this' => [
'description' => 'This response parameter is required.',
'example' => "too",
'type' => 'string',
],
'sub level 0' => [
'type' => 'object',
'properties' => [
Expand Down Expand Up @@ -512,7 +527,7 @@ public function adds_responses_correctly_as_responses_on_operation_object()
'sub level 2 key 1' => [
'type' => 'string',
'example' => 'sl0_sl1k3_sl2k2',
'description' => 'This is description of nested object'
'description' => 'This is a description of a nested object'
],
'sub level 2 key 2' => [
'type' => 'object',
Expand All @@ -530,12 +545,24 @@ public function adds_responses_correctly_as_responses_on_operation_object()
'example' => 99
]
]
]
],
'sub level 2 key 3 required' => [
'type' => 'string',
'example' => 'sl0_sl1k3_sl2k3',
'description' => 'This is a description of a required nested object'
],

],
'required' => [
'sub level 2 key 3 required'
]
]
]
]
],
'required' => [
'also this'
]
],
],
],
Expand Down
Loading