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

Support limitng parameter values (enums) #713

Merged
merged 4 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions camel/Extraction/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Parameter extends BaseDTO
public bool $required = false;
public mixed $example = null;
public string $type = 'string';
public array $enumValues = [];

public function __construct(array $parameters = [])
{
Expand Down
5 changes: 5 additions & 0 deletions camel/Extraction/ResponseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

class ResponseField extends BaseDTO
{
// TODO make this extend Parameter, so we can have strong types and a unified API
// but first we need to normalize incoming data

/** @var string */
public $name;

Expand All @@ -15,4 +18,6 @@ class ResponseField extends BaseDTO

/** @var string */
public $type;

public array $enumValues = [];
}
4 changes: 4 additions & 0 deletions resources/views/components/field-details.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@
}
@endphp
{!! Parsedown::instance()->text(trim($description)) !!}
@if(!empty($enumValues))
Must be one of:
<ul style="list-style-type: square;">{!! implode(" ", array_map(fn($val) => "<li><code>$val</code></li>", $enumValues)) !!}</ul>
@endif
3 changes: 3 additions & 0 deletions resources/views/components/nested-fields.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'required' => $subfield['required'] ?? false,
'description' => $subfield['description'] ?? '',
'example' => $subfield['example'] ?? '',
'enumValues' => $subfield['enumValues'] ?? null,
'endpointId' => $endpointId,
'hasChildren' => false,
'component' => 'body',
Expand Down Expand Up @@ -66,6 +67,7 @@
'required' => $subfield['required'] ?? false,
'description' => $subfield['description'] ?? '',
'example' => $subfield['example'] ?? '',
'enumValues' => $field['enumValues'] ?? null,
'endpointId' => $endpointId,
'hasChildren' => false,
'component' => 'body',
Expand All @@ -86,6 +88,7 @@
'required' => $field['required'] ?? false,
'description' => $field['description'] ?? '',
'example' => $field['example'] ?? '',
'enumValues' => $field['enumValues'] ?? null,
'endpointId' => $endpointId,
'hasChildren' => false,
'component' => 'body',
Expand Down
2 changes: 2 additions & 0 deletions resources/views/themes/default/endpoint.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
'required' => $parameter->required,
'description' => $parameter->description,
'example' => $parameter->example ?? '',
'enumValues' => $parameter->enumValues,
'endpointId' => $endpoint->endpointId(),
'component' => 'url',
'isInput' => true,
Expand All @@ -156,6 +157,7 @@
'required' => $parameter->required,
'description' => $parameter->description,
'example' => $parameter->example ?? '',
'enumValues' => $parameter->enumValues,
'endpointId' => $endpoint->endpointId(),
'component' => 'query',
'isInput' => true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class="svg-inline--fa fa-chevron-right fa-fw fa-sm sl-icon" role="img"
{!! Parsedown::instance()->text($description) !!}
</div>
@endif
@if(!empty($enumValues))
Must be one of:
<ul style="list-style-position: inside; list-style-type: square;">{!! implode(" ", array_map(fn($val) => "<li><code>$val</code></li>", $enumValues)) !!}</ul>
@endif
@if($isArrayBody)
<div class="sl-flex sl-items-baseline sl-text-base">
<div class="sl-font-mono sl-font-semibold sl-mr-2">array of:</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'required' => $field['required'] ?? false,
'description' => $field['description'] ?? '',
'example' => $field['example'] ?? '',
'enumValues' => $field['enumValues'] ?? null,
'endpointId' => $endpointId,
'hasChildren' => !empty($field['__fields']),
'component' => 'body',
Expand Down
2 changes: 2 additions & 0 deletions resources/views/themes/elements/endpoint.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class="sl-overflow-x-hidden sl-truncate sl-text-muted">{{ rtrim($baseUrl, '/') }
'required' => $parameter->required,
'description' => $parameter->description,
'example' => $parameter->example ?? '',
'enumValues' => $parameter->enumValues,
'endpointId' => $endpoint->endpointId(),
'component' => 'url',
'isInput' => true,
Expand All @@ -104,6 +105,7 @@ class="sl-overflow-x-hidden sl-truncate sl-text-muted">{{ rtrim($baseUrl, '/') }
'required' => $parameter->required,
'description' => $parameter->description,
'example' => $parameter->example ?? '',
'enumValues' => $parameter->enumValues,
'endpointId' => $endpoint->endpointId(),
'component' => 'query',
'isInput' => true,
Expand Down
24 changes: 24 additions & 0 deletions src/Attributes/GenericParam.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public function __construct(
public ?string $description = '',
public ?bool $required = true,
public mixed $example = null, /* Pass 'No-example' to omit the example */
public mixed $enum = null, // Can pass a list of values, or a native PHP enum
) {
}

Expand All @@ -24,6 +25,29 @@ public function toArray()
"type" => $this->type,
"required" => $this->required,
"example" => $this->example,
"enumValues" => $this->getEnumValues(),
];
}

protected function getEnumValues()
{
if (!$this->enum) {
return null;
}

if (is_array($this->enum)) {
return $this->enum;
}

if (function_exists('enum_exists') && enum_exists($this->enum)) {
return array_map(
fn ($case) => $case->value,
$this->enum::cases()
);
}

throw new \InvalidArgumentException(
'The enum property of a parameter must be either a PHP enum or an array of values'
);
}
}
16 changes: 6 additions & 10 deletions src/Attributes/ResponseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@
use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class ResponseField
class ResponseField extends GenericParam
{
// Don't default to string; type inference is currently handled by the normalizer
// TODO change this in the future
public function __construct(
public string $name,
public ?string $type = null,
public ?string $description = '',
public ?bool $required = true,
public mixed $example = null, /* Pass 'No-example' to omit the example */
public mixed $enum = null, // Can pass a list of values, or a native PHP enum
) {
}

public function toArray()
{
return [
"name" => $this->name,
"description" => $this->description,
"type" => $this->type,
];
}
}
18 changes: 17 additions & 1 deletion src/Extracting/ParamHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Faker\Factory;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

trait ParamHelpers
Expand Down Expand Up @@ -46,6 +47,10 @@ protected function getFaker(): \Faker\Generator

protected function generateDummyValue(string $type, array $hints = [])
{
if(!empty($hints['enumValues'])) {
return Arr::random($hints['enumValues']);
}

$fakeFactory = $this->getDummyValueGenerator($type, $hints);

return $fakeFactory();
Expand Down Expand Up @@ -232,13 +237,24 @@ protected function shouldExcludeExample(string $description): bool
protected function parseExampleFromParamDescription(string $description, string $type): array
{
$example = null;
$enumValues = [];

if (preg_match('/(.*)\bExample:\s*([\s\S]+)\s*/s', $description, $content)) {
$description = trim($content[1]);

// Examples are parsed as strings by default, we need to cast them properly
$example = $this->castToType($content[2], $type);
}

return [$description, $example];
if (preg_match('/(.*)\bEnum:\s*([\s\S]+)\s*/s', $description, $content)) {
$description = trim($content[1]);

$enumValues = array_map(
fn ($value) => $this->castToType(trim($value), $type),
explode(',', rtrim(trim($content[2]), '.'))
);
}

return [$description, $example, $enumValues];
}
}
5 changes: 2 additions & 3 deletions src/Extracting/ParsesValidationRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
if (enum_exists($type) && method_exists($type, 'tryFrom')) {
$cases = array_map(fn ($case) => $case->value, $type::cases());
$parameterData['type'] = gettype($cases[0]);
$parameterData['description'] .= ' Must be one of ' . w::getListOfValuesAsFriendlyHtmlString($cases) . ' ';
$parameterData['enumValues'] = $cases;
$parameterData['setter'] = fn () => Arr::random($cases);
}

Expand Down Expand Up @@ -464,8 +464,7 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
* Other rules.
*/
case 'in':
// Not using the rule description here because it only says "The attribute is invalid"
$parameterData['description'] .= ' Must be one of ' . w::getListOfValuesAsFriendlyHtmlString($arguments) . ' ';
$parameterData['enumValues'] = $arguments;
$parameterData['setter'] = function () use ($arguments) {
return Arr::random($arguments);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public function parseTag(string $tagContent): array
}

$type = static::normalizeTypeName($type);
[$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name);
[$description, $example, $enumValues] =
$this->getDescriptionAndExample($description, $type, $tagContent, $name);

return compact('name', 'type', 'description', 'required', 'example');
return compact('name', 'type', 'description', 'required', 'example', 'enumValues');
}
}
16 changes: 10 additions & 6 deletions src/Extracting/Strategies/GetFieldsFromTagStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,21 @@ public function getFromTags(array $tagsOnMethod, array $tagsOnClass = []): array

abstract protected function parseTag(string $tagContent): array;

protected function getDescriptionAndExample(string $description, string $type, string $tagContent, string $fieldName): array
protected function getDescriptionAndExample(
string $description, string $type, string $tagContent, string $fieldName
): array
{
[$description, $example] = $this->parseExampleFromParamDescription($description, $type);
$example = $this->setExampleIfNeeded($example, $type, $tagContent, $fieldName);
return [$description, $example];
[$description, $example, $enumValues] = $this->parseExampleFromParamDescription($description, $type);
$example = $this->setExampleIfNeeded($example, $type, $tagContent, $fieldName, $enumValues);
return [$description, $example, $enumValues];
}

protected function setExampleIfNeeded(mixed $currentExample, string $type, string $tagContent, string $fieldName): mixed
protected function setExampleIfNeeded(
mixed $currentExample, string $type, string $tagContent, string $fieldName, ?array $enumValues = []
): mixed
{
return (is_null($currentExample) && !$this->shouldExcludeExample($tagContent))
? $this->generateDummyValue($type, hints: ['name' => $fieldName])
? $this->generateDummyValue($type, hints: ['name' => $fieldName, 'enumValues' => $enumValues])
: $currentExample;
}
}
5 changes: 4 additions & 1 deletion src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ protected function normalizeParameterData(array $data): array
{
$data['type'] = static::normalizeTypeName($data['type']);
if (is_null($data['example'])) {
$data['example'] = $this->generateDummyValue($data['type'], ['name' => $data['name']]);
$data['example'] = $this->generateDummyValue($data['type'], [
'name' => $data['name'],
'enumValues' => $data['enumValues'],
]);
} else if ($data['example'] == 'No-example' || $data['example'] == 'No-example.') {
$data['example'] = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ public function parseTag(string $tagContent): array

}

[$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name);
[$description, $example, $enumValues] =
$this->getDescriptionAndExample($description, $type, $tagContent, $name);

return compact('name', 'description', 'required', 'example', 'type');
return compact('name', 'description', 'required', 'example', 'type', 'enumValues');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ protected function parseTag(string $tagContent): array
: static::normalizeTypeName($type);
}

[$description, $example] = $this->getDescriptionAndExample($description, $type, $tagContent, $name);
[$description, $example, $enumValues] =
$this->getDescriptionAndExample($description, $type, $tagContent, $name);

return compact('name', 'description', 'required', 'example', 'type');
return compact('name', 'description', 'required', 'example', 'type', 'enumValues');
}
}
21 changes: 13 additions & 8 deletions src/Writing/OpenAPISpecWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,11 +510,16 @@ public function generateFieldData($field): array
})->all()),
];
} else {
return [
$schema = [
'type' => static::normalizeTypeName($field->type),
'description' => $field->description ?: '',
'example' => $field->example,
];
if (!empty($field->enumValues)) {
$schema['enum'] = $field->enumValues;
}

return $schema;
}
}

Expand All @@ -523,30 +528,30 @@ protected function operationId(OutputEndpointData $endpoint): string
if ($endpoint->metadata->title) return preg_replace('/[^\w+]/', '', Str::camel($endpoint->metadata->title));

$parts = preg_split('/[^\w+]/', $endpoint->uri, -1, PREG_SPLIT_NO_EMPTY);
return Str::lower($endpoint->httpMethods[0]) . join('', array_map(fn ($part) => ucfirst($part), $parts));
return Str::lower($endpoint->httpMethods[0]) . join('', array_map(fn($part) => ucfirst($part), $parts));
}

/**
* Given an array, return an object if the array is empty. To be used with fields that are
* required by OpenAPI spec to be objects, since empty arrays get serialised as [].
*/
protected function objectIfEmpty(array $field): array | \stdClass
protected function objectIfEmpty(array $field): array|\stdClass
{
return count($field) > 0 ? $field : new \stdClass();
}

/**
* Given a value, generate the schema for it. The schema consists of: {type:, example:, properties: (if value is an object)},
* and possibly a description for each property.
* The $endpoint and $path are used for looking up response field descriptions.
* Given a value, generate the schema for it. The schema consists of: {type:, example:, properties: (if value is an
* object)}, and possibly a description for each property. The $endpoint and $path are used for looking up response
* field descriptions.
*/
public function generateSchemaForValue(mixed $value, OutputEndpointData $endpoint, string $path): array
{
if ($value instanceof \stdClass) {
$value = (array) $value;
$value = (array)$value;
$properties = [];
// Recurse into the object
foreach($value as $subField => $subValue){
foreach ($value as $subField => $subValue) {
$subFieldPath = sprintf('%s.%s', $path, $subField);
$properties[$subField] = $this->generateSchemaForValue($subValue, $endpoint, $subFieldPath);
}
Expand Down
1 change: 1 addition & 0 deletions tests/GenerateDocumentation/OutputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ public function will_not_overwrite_manually_modified_content_unless_force_flag_i
'required' => true,
'example' => 6,
'type' => 'integer',
'enumValues' => [],
'custom' => [],
];
$group['endpoints'][0]['urlParameters']['a_param'] = $extraParam;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ public function can_fetch_from_bodyparam_attribute()
'description' => '',
'required' => true,
],
'state' => [
'type' => 'string',
'description' => '',
'required' => true,
'enumValues' => ["active", "pending"]
],
'users' => [
'type' => 'object[]',
'description' => 'Users\' details',
Expand Down Expand Up @@ -216,6 +222,7 @@ class BodyParamAttributeTestController
#[BodyParam("book.author_id", type: "integer")]
#[BodyParam("book.pages_count", type: "integer")]
#[BodyParam("ids", "integer[]")]
#[BodyParam("state", enum: ["active", "pending"])]
#[BodyParam("users", "object[]", "Users' details", required: false)]
#[BodyParam("users[].first_name", "string", "The first name of the user.", example: "John", required: false)]
#[BodyParam("users[].last_name", "string", "The last name of the user.", example: "Doe", required: false)]
Expand Down
Loading
Loading