diff --git a/camel/Extraction/Parameter.php b/camel/Extraction/Parameter.php
index d1538953..beb03787 100644
--- a/camel/Extraction/Parameter.php
+++ b/camel/Extraction/Parameter.php
@@ -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 = [])
{
diff --git a/camel/Extraction/ResponseField.php b/camel/Extraction/ResponseField.php
index 85d82e64..ffc92baf 100644
--- a/camel/Extraction/ResponseField.php
+++ b/camel/Extraction/ResponseField.php
@@ -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;
@@ -15,4 +18,6 @@ class ResponseField extends BaseDTO
/** @var string */
public $type;
+
+ public array $enumValues = [];
}
diff --git a/resources/views/components/field-details.blade.php b/resources/views/components/field-details.blade.php
index 57200c1c..222a094a 100644
--- a/resources/views/components/field-details.blade.php
+++ b/resources/views/components/field-details.blade.php
@@ -69,3 +69,7 @@
}
@endphp
{!! Parsedown::instance()->text(trim($description)) !!}
+@if(!empty($enumValues))
+Must be one of:
+
array of:
diff --git a/resources/views/themes/elements/components/nested-fields.blade.php b/resources/views/themes/elements/components/nested-fields.blade.php
index ec7583e7..1e2ab3de 100644
--- a/resources/views/themes/elements/components/nested-fields.blade.php
+++ b/resources/views/themes/elements/components/nested-fields.blade.php
@@ -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',
diff --git a/resources/views/themes/elements/endpoint.blade.php b/resources/views/themes/elements/endpoint.blade.php
index 61690c28..470c1617 100644
--- a/resources/views/themes/elements/endpoint.blade.php
+++ b/resources/views/themes/elements/endpoint.blade.php
@@ -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,
@@ -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,
diff --git a/src/Attributes/GenericParam.php b/src/Attributes/GenericParam.php
index df977173..2e34de38 100644
--- a/src/Attributes/GenericParam.php
+++ b/src/Attributes/GenericParam.php
@@ -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
) {
}
@@ -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'
+ );
+ }
}
diff --git a/src/Attributes/ResponseField.php b/src/Attributes/ResponseField.php
index 1ebe576b..a11c790e 100644
--- a/src/Attributes/ResponseField.php
+++ b/src/Attributes/ResponseField.php
@@ -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,
- ];
- }
}
diff --git a/src/Extracting/ParamHelpers.php b/src/Extracting/ParamHelpers.php
index 8afd230b..a8125bab 100644
--- a/src/Extracting/ParamHelpers.php
+++ b/src/Extracting/ParamHelpers.php
@@ -4,6 +4,7 @@
use Faker\Factory;
use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Arr;
use Illuminate\Support\Str;
trait ParamHelpers
@@ -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();
@@ -232,6 +237,8 @@ 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]);
@@ -239,6 +246,15 @@ protected function parseExampleFromParamDescription(string $description, string
$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];
}
}
diff --git a/src/Extracting/ParsesValidationRules.php b/src/Extracting/ParsesValidationRules.php
index c39dce40..2e1a47a8 100644
--- a/src/Extracting/ParsesValidationRules.php
+++ b/src/Extracting/ParsesValidationRules.php
@@ -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);
}
@@ -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);
};
diff --git a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php
index 33992997..1d52e579 100644
--- a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php
+++ b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php
@@ -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');
}
}
diff --git a/src/Extracting/Strategies/GetFieldsFromTagStrategy.php b/src/Extracting/Strategies/GetFieldsFromTagStrategy.php
index 60b4c0f8..1fdf59f3 100644
--- a/src/Extracting/Strategies/GetFieldsFromTagStrategy.php
+++ b/src/Extracting/Strategies/GetFieldsFromTagStrategy.php
@@ -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;
}
}
diff --git a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
index b0bd36ce..42ff07cc 100644
--- a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
+++ b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
@@ -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;
}
diff --git a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php
index 3f1cbe83..de84944c 100644
--- a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php
+++ b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php
@@ -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');
}
}
diff --git a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php
index b6194971..48cb5714 100644
--- a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php
+++ b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php
@@ -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');
}
}
diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php
index 5599ed9e..0857bfe6 100644
--- a/src/Writing/OpenAPISpecWriter.php
+++ b/src/Writing/OpenAPISpecWriter.php
@@ -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;
}
}
@@ -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);
}
diff --git a/tests/GenerateDocumentation/OutputTest.php b/tests/GenerateDocumentation/OutputTest.php
index 234defe1..28e41a34 100644
--- a/tests/GenerateDocumentation/OutputTest.php
+++ b/tests/GenerateDocumentation/OutputTest.php
@@ -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;
diff --git a/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php b/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
index 6669063f..93c90be0 100644
--- a/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
+++ b/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
@@ -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',
@@ -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)]
diff --git a/tests/Strategies/GetFromInlineValidatorTest.php b/tests/Strategies/GetFromInlineValidatorTest.php
index 8d4d9d93..4235b598 100644
--- a/tests/Strategies/GetFromInlineValidatorTest.php
+++ b/tests/Strategies/GetFromInlineValidatorTest.php
@@ -26,7 +26,7 @@ class GetFromInlineValidatorTest extends BaseLaravelTest
'room_id' => [
'type' => 'string',
'required' => false,
- 'description' => 'The id of the room. Must be one of
3
,
5
, or
6
.',
+ 'description' => 'The id of the room.',
],
'forever' => [
'type' => 'boolean',
@@ -193,12 +193,12 @@ public function can_fetch_inline_enum_rules()
$expected = [
'enum_class' => [
'type' => 'string',
- 'description' => 'Must be one of
red
,
green
, or
blue
.',
+ 'description' => '',
'required' => true,
],
'enum_string' => [
'type' => 'string',
- 'description' => 'Must be one of
1
,
2
, or
3
.',
+ 'description' => '',
'required' => true,
],
'enum_inexistent' => [
diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php
index 2380162f..10ba7e4b 100644
--- a/tests/Unit/ValidationRuleParsingTest.php
+++ b/tests/Unit/ValidationRuleParsingTest.php
@@ -68,8 +68,8 @@ public function can_parse_rule_objects()
'in_param' => ['numeric', Rule::in([3,5,6])]
]);
$this->assertEquals(
- 'Must be one of
3
,
5
, or
6
.',
- $results['in_param']['description']
+ [3, 5, 6],
+ $results['in_param']['enumValues']
);
}
@@ -228,8 +228,9 @@ public static function supportedRules()
['in_param' => 'in:3,5,6'],
['in_param' => ['description' => $description]],
[
- 'description' => "$description. Must be one of
3
,
5
, or
6
.",
+ 'description' => $description.".",
'type' => 'string',
+ 'enumValues' => [3,5,6]
],
];
yield 'not_in' => [
@@ -533,8 +534,8 @@ public function can_parse_enum_rules()
]);
$this->assertEquals('string', $results['enum']['type']);
$this->assertEquals(
- 'Must be one of
red
,
green
, or
blue
.',
- $results['enum']['description']
+ ['red', 'green', 'blue'],
+ $results['enum']['enumValues']
);
$this->assertTrue(in_array(
$results['enum']['example'],
@@ -547,8 +548,8 @@ public function can_parse_enum_rules()
]);
$this->assertEquals('integer', $results['enum']['type']);
$this->assertEquals(
- 'Must be one of
1
,
2
, or
3
.',
- $results['enum']['description']
+ [1, 2, 3],
+ $results['enum']['enumValues']
);
$this->assertTrue(in_array(
$results['enum']['example'],
@@ -562,7 +563,7 @@ public function can_parse_enum_rules()
]);
$this->assertEquals('string', $results['enum']['type']);
$this->assertEquals(
- 'A description. Must be one of
red
,
green
, or
blue
.',
+ 'A description.',
$results['enum']['description']
);
$this->assertTrue(in_array(