diff --git a/src/Support/Generator/Combined/AnyOf.php b/src/Support/Generator/Combined/AnyOf.php index 16f57b546..682909614 100644 --- a/src/Support/Generator/Combined/AnyOf.php +++ b/src/Support/Generator/Combined/AnyOf.php @@ -2,12 +2,18 @@ namespace Dedoc\Scramble\Support\Generator\Combined; +use Dedoc\Scramble\Support\Generator\Example; +use Dedoc\Scramble\Support\Generator\MissingValue; use Dedoc\Scramble\Support\Generator\Types\StringType; use Dedoc\Scramble\Support\Generator\Types\Type; use InvalidArgumentException; class AnyOf extends Type { + use HasCombinedItems; + + public string $combinedOperator = 'anyOf'; + /** @var Type[] */ public $items; @@ -28,21 +34,6 @@ public function clone(): static return $clone; } - public function toArray() - { - $parentArray = parent::toArray(); - - unset($parentArray['type']); - - return [ - ...$parentArray, - 'anyOf' => array_map( - fn ($item) => $item->toArray(), - $this->items, - ), - ]; - } - public function setItems($items) { if (collect($items)->contains(fn ($item) => ! $item instanceof Type)) { diff --git a/src/Support/Generator/Combined/HasCombinedItems.php b/src/Support/Generator/Combined/HasCombinedItems.php new file mode 100644 index 000000000..ed20edbb1 --- /dev/null +++ b/src/Support/Generator/Combined/HasCombinedItems.php @@ -0,0 +1,120 @@ + $item->clone(), $this->items); + + $distributedCount = [ + 'example' => 0, + 'examples' => [], + 'default' => 0, + ]; + + if (! ($this->example instanceof MissingValue)) { + foreach ($items as $item) { + if ($this->itemMatchesExample($item, $this->example)) { + if ($this->example instanceof Example) { + $item->example($this->example->value); + if (! $item->description && $this->example->summary) { + $item->setDescription($this->example->summary); + } + } else { + $item->example($this->example); + } + $distributedCount['example']++; + } + } + } + + if (count($this->examples)) { + $itemExamples = []; + foreach ($this->examples as $i => $example) { + $matched = false; + foreach ($items as $itemIndex => $item) { + if ($this->itemMatchesExample($item, $example)) { + $itemExamples[$itemIndex][] = $example; + $matched = true; + } + } + if ($matched) { + $distributedCount['examples'][] = $i; + } + } + + foreach ($itemExamples as $itemIndex => $examples) { + if (count($examples) === 1) { + $example = $examples[0]; + if ($example instanceof Example) { + $items[$itemIndex]->example($example->value); + if (! $items[$itemIndex]->description && $example->summary) { + $items[$itemIndex]->setDescription($example->summary); + } + } else { + $items[$itemIndex]->example($example); + } + } else { + $items[$itemIndex]->examples($examples); + } + } + } + + if (! ($this->default instanceof MissingValue)) { + foreach ($items as $item) { + if ($item->matches($this->default)) { + $item->default($this->default); + $distributedCount['default']++; + } + } + } + + if ($distributedCount['example'] > 0) { + unset($parentArray['example']); + } + + if (count($distributedCount['examples']) === count($this->examples) && count($this->examples) > 0) { + unset($parentArray['examples']); + } elseif (count($distributedCount['examples']) > 0) { + $parentArray['examples'] = collect($this->examples) + ->filter(fn ($e, $i) => ! in_array($i, $distributedCount['examples'])) + ->values() + ->toArray(); + } + + if ($distributedCount['default'] > 0) { + unset($parentArray['default']); + } + + return [ + ...$parentArray, + $this->combinedOperator => array_map( + fn ($item) => $item->toArray(), + $items, + ), + ]; + } + + private function itemMatchesExample(Type $item, $example): bool + { + if ($example instanceof Example) { + if ($example->type) { + return $item->type === $example->type; + } + + return $item->matches($example->value); + } + + return $item->matches($example); + } +} diff --git a/src/Support/Generator/Combined/OneOf.php b/src/Support/Generator/Combined/OneOf.php new file mode 100644 index 000000000..2b4e76cf0 --- /dev/null +++ b/src/Support/Generator/Combined/OneOf.php @@ -0,0 +1,47 @@ +items = [new StringType]; + } + + public function clone(): static + { + $clone = parent::clone(); + $clone->items = array_map( + fn (Type $item) => $item->clone(), + $clone->items, + ); + + return $clone; + } + + public function setItems($items) + { + if (collect($items)->contains(fn ($item) => ! $item instanceof Type)) { + throw new InvalidArgumentException('All items should be instances of '.Type::class); + } + + $this->items = $items; + + return $this; + } +} diff --git a/src/Support/Generator/Example.php b/src/Support/Generator/Example.php index 0659a4c15..aa3ee4e72 100644 --- a/src/Support/Generator/Example.php +++ b/src/Support/Generator/Example.php @@ -9,7 +9,15 @@ public function __construct( public ?string $summary = null, public ?string $description = null, public ?string $externalValue = null, - ) {} + public ?string $type = null, + ) { + if ($this->type === 'int') { + $this->type = 'integer'; + } + if ($this->type === 'bool') { + $this->type = 'boolean'; + } + } public function toArray() { diff --git a/src/Support/Generator/Parameter.php b/src/Support/Generator/Parameter.php index 1e67ffcf6..6de5aa765 100644 --- a/src/Support/Generator/Parameter.php +++ b/src/Support/Generator/Parameter.php @@ -86,7 +86,7 @@ public function toArray(): array return array_merge( $result, - $this->example instanceof MissingValue ? [] : ['example' => $this->example], + $this->example instanceof MissingValue ? [] : ['example' => $this->example instanceof Example ? $this->example->toArray() : $this->example], ! is_null($this->explode) ? [ 'explode' => $this->explode, ] : [], @@ -133,6 +133,13 @@ public function example($example) return $this; } + public function examples(array $examples) + { + $this->examples = $examples; + + return $this; + } + public function setExplode(bool $explode): self { $this->explode = $explode; diff --git a/src/Support/Generator/Reference.php b/src/Support/Generator/Reference.php index bd586e615..54ea09021 100644 --- a/src/Support/Generator/Reference.php +++ b/src/Support/Generator/Reference.php @@ -74,6 +74,20 @@ private function getEnumReferenceCasesDescription(): ?string return $casesDescription; } + public function matches($value): bool + { + if (parent::matches($value)) { + return true; + } + + $resolved = $this->resolve(); + if ($resolved instanceof Schema) { + return $resolved->type->matches($value); + } + + return false; + } + public function toArray() { if ($this->nullable) { diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php index 8bd826b33..990a4948f 100644 --- a/src/Support/Generator/TypeTransformer.php +++ b/src/Support/Generator/TypeTransformer.php @@ -10,6 +10,7 @@ use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper; use Dedoc\Scramble\Support\Generator\Combined\AllOf; use Dedoc\Scramble\Support\Generator\Combined\AnyOf; +use Dedoc\Scramble\Support\Generator\Combined\OneOf; use Dedoc\Scramble\Support\Generator\Types\ArrayType; use Dedoc\Scramble\Support\Generator\Types\BooleanType; use Dedoc\Scramble\Support\Generator\Types\IntegerType; @@ -267,6 +268,10 @@ private function transformUncached(Type $type): OpenApiType } if ($examples = ExamplesExtractor::make($docNode)->extract(preferString: $openApiType instanceof StringType)) { + if (count($examples) > 1 && $openApiType instanceof AnyOf) { + $openApiType = (new OneOf)->setItems($openApiType->items)->addProperties($openApiType); + } + $openApiType->examples($examples); } diff --git a/src/Support/Generator/Types/ArrayType.php b/src/Support/Generator/Types/ArrayType.php index 107f8d950..545d55a64 100644 --- a/src/Support/Generator/Types/ArrayType.php +++ b/src/Support/Generator/Types/ArrayType.php @@ -73,6 +73,11 @@ public function setAdditionalItems($additionalItems) return $this; } + public function matches($value): bool + { + return is_array($value) && (empty($value) || array_keys($value) === range(0, count($value) - 1)); + } + public function toArray() { $shouldOmitItems = $this->items->getAttribute('missing') diff --git a/src/Support/Generator/Types/BooleanType.php b/src/Support/Generator/Types/BooleanType.php index a4ffb6c85..d9ebc0015 100644 --- a/src/Support/Generator/Types/BooleanType.php +++ b/src/Support/Generator/Types/BooleanType.php @@ -8,4 +8,9 @@ public function __construct() { parent::__construct('boolean'); } + + public function matches($value): bool + { + return is_bool($value); + } } diff --git a/src/Support/Generator/Types/IntegerType.php b/src/Support/Generator/Types/IntegerType.php index 976cefdb3..f320624c5 100644 --- a/src/Support/Generator/Types/IntegerType.php +++ b/src/Support/Generator/Types/IntegerType.php @@ -8,4 +8,9 @@ public function __construct() { parent::__construct('integer'); } + + public function matches($value): bool + { + return is_int($value); + } } diff --git a/src/Support/Generator/Types/MixedType.php b/src/Support/Generator/Types/MixedType.php index bc9a02231..b89e4e615 100644 --- a/src/Support/Generator/Types/MixedType.php +++ b/src/Support/Generator/Types/MixedType.php @@ -9,6 +9,11 @@ public function __construct() parent::__construct('mixed'); } + public function matches($value): bool + { + return true; + } + public function toArray() { // Yes. It is not an array. I live with it. diff --git a/src/Support/Generator/Types/NullType.php b/src/Support/Generator/Types/NullType.php index 4e896473d..65a327042 100644 --- a/src/Support/Generator/Types/NullType.php +++ b/src/Support/Generator/Types/NullType.php @@ -8,4 +8,9 @@ public function __construct() { parent::__construct('null'); } + + public function matches($value): bool + { + return is_null($value); + } } diff --git a/src/Support/Generator/Types/NumberType.php b/src/Support/Generator/Types/NumberType.php index fd11bf563..ea06302d8 100644 --- a/src/Support/Generator/Types/NumberType.php +++ b/src/Support/Generator/Types/NumberType.php @@ -27,6 +27,11 @@ public function setMax($max) return $this; } + public function matches($value): bool + { + return is_numeric($value); + } + public function toArray() { return array_merge(parent::toArray(), array_filter([ diff --git a/src/Support/Generator/Types/ObjectType.php b/src/Support/Generator/Types/ObjectType.php index 2ba86cd64..11cfe6a5d 100644 --- a/src/Support/Generator/Types/ObjectType.php +++ b/src/Support/Generator/Types/ObjectType.php @@ -66,6 +66,11 @@ public function addRequired(array $keys) return $this; } + public function matches($value): bool + { + return is_array($value) || is_object($value); + } + public function toArray() { $result = parent::toArray(); diff --git a/src/Support/Generator/Types/StringType.php b/src/Support/Generator/Types/StringType.php index ee863ca35..b394719e4 100644 --- a/src/Support/Generator/Types/StringType.php +++ b/src/Support/Generator/Types/StringType.php @@ -27,6 +27,11 @@ public function setMax($max) return $this; } + public function matches($value): bool + { + return is_string($value); + } + public function toArray() { return array_merge(parent::toArray(), array_filter([ diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php index 86fc65813..a736c85ff 100644 --- a/src/Support/Generator/Types/Type.php +++ b/src/Support/Generator/Types/Type.php @@ -115,6 +115,15 @@ public function addProperties(Type $fromType): self return $this; } + public function matches($value): bool + { + if ($value === null && $this->nullable) { + return true; + } + + return false; + } + public function toArray() { return array_merge( @@ -129,11 +138,12 @@ public function toArray() 'enum' => count($this->enum) ? $this->enum : null, 'const' => ! is_null($this->const) ? $this->const : null, ]), - $this->example instanceof MissingValue ? [] : ['example' => $this->example], + $this->example instanceof MissingValue ? [] : ['example' => $this->example instanceof \Dedoc\Scramble\Support\Generator\Example ? $this->example->toArray() : $this->example], $this->default instanceof MissingValue ? [] : ['default' => $this->default], count( $examples = collect($this->examples) ->reject(fn ($example) => $example instanceof MissingValue) + ->map(fn ($example) => $example instanceof \Dedoc\Scramble\Support\Generator\Example ? $example->toArray() : $example) ->values() ->toArray() ) ? ['examples' => $examples] : [], diff --git a/src/Support/Helpers/ExamplesExtractor.php b/src/Support/Helpers/ExamplesExtractor.php index 57c8ecf37..1ed09c6d8 100644 --- a/src/Support/Helpers/ExamplesExtractor.php +++ b/src/Support/Helpers/ExamplesExtractor.php @@ -41,6 +41,16 @@ private function getTypedExampleValue($exampleValue, bool $preferString = false) if (function_exists('json_decode')) { $json = json_decode($exampleValue, true); + if (is_array($json) && (array_key_exists('value', $json) || array_key_exists('externalValue', $json)) && (array_key_exists('summary', $json) || array_key_exists('description', $json) || array_key_exists('type', $json))) { + return new \Dedoc\Scramble\Support\Generator\Example( + value: array_key_exists('value', $json) ? $json['value'] : new \Dedoc\Scramble\Support\Generator\MissingValue, + summary: $json['summary'] ?? null, + description: $json['description'] ?? null, + externalValue: $json['externalValue'] ?? null, + type: $json['type'] ?? null, + ); + } + $exampleValue = $json === null || $json == $exampleValue ? $exampleValue : $json; diff --git a/src/Support/OperationExtensions/RulesExtractor/PhpDocSchemaTransformer.php b/src/Support/OperationExtensions/RulesExtractor/PhpDocSchemaTransformer.php index 08a35e4fa..2dc719f62 100644 --- a/src/Support/OperationExtensions/RulesExtractor/PhpDocSchemaTransformer.php +++ b/src/Support/OperationExtensions/RulesExtractor/PhpDocSchemaTransformer.php @@ -3,6 +3,8 @@ namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor; use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper; +use Dedoc\Scramble\Support\Generator\Example; +use Dedoc\Scramble\Support\Generator\MissingValue; use Dedoc\Scramble\Support\Generator\Types\StringType; use Dedoc\Scramble\Support\Generator\Types\Type as Schema; use Dedoc\Scramble\Support\Generator\TypeTransformer; @@ -10,6 +12,7 @@ use Illuminate\Support\Str; use PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; class PhpDocSchemaTransformer { @@ -36,7 +39,33 @@ public function transform(Schema $type, PhpDocNode $docNode): Schema } if ($examples = ExamplesExtractor::make($docNode)->extract(preferString: $type instanceof StringType)) { - $type->example($examples[0]); + $exampleTypes = collect($examples) + ->map(fn ($e) => $this->getExampleType($e)) + ->filter() + ->unique() + ->values(); + + $allMatch = collect($examples)->every(fn ($e) => $this->typeMatchesExample($type, $e)); + + if (! $allMatch && $exampleTypes->count() > 1 && ! $type instanceof \Dedoc\Scramble\Support\Generator\Combined\AnyOf && ! $type instanceof \Dedoc\Scramble\Support\Generator\Combined\OneOf) { + $items = $exampleTypes->map(function ($typeName) { + return $this->openApiTransformer->transform( + PhpDocTypeHelper::toType(new IdentifierTypeNode($typeName)) + ); + })->all(); + + $type = (new \Dedoc\Scramble\Support\Generator\Combined\OneOf)->setItems($items)->addProperties($type); + } + + if (count($examples) > 1 && $type instanceof \Dedoc\Scramble\Support\Generator\Combined\AnyOf) { + $type = (new \Dedoc\Scramble\Support\Generator\Combined\OneOf)->setItems($type->items)->addProperties($type); + } + + if (count($examples) === 1) { + $type->example($examples[0]); + } else { + $type->examples($examples); + } } if ($default = ExamplesExtractor::make($docNode, '@default')->extract(preferString: $type instanceof StringType)) { @@ -65,4 +94,46 @@ public function transform(Schema $type, PhpDocNode $docNode): Schema return $type; } + + private function getExampleType(mixed $example): ?string + { + if ($example instanceof Example && $example->type) { + return $example->type; + } + $val = $example instanceof Example ? $example->value : $example; + if ($val instanceof MissingValue) { + return null; + } + + if (is_int($val)) { + return 'integer'; + } + if (is_bool($val)) { + return 'boolean'; + } + if (is_float($val)) { + return 'number'; + } + if (is_array($val)) { + return 'array'; + } + if (is_null($val)) { + return 'null'; + } + if (is_string($val)) { + return 'string'; + } + + return null; + } + + private function typeMatchesExample(Schema $type, mixed $example): bool + { + if ($example instanceof Example && $example->type) { + return $type->type === $example->type; + } + $val = $example instanceof Example ? $example->value : $example; + + return $type->matches($val); + } } diff --git a/src/Support/OperationExtensions/RulesExtractor/SchemaBagToParametersTransformer.php b/src/Support/OperationExtensions/RulesExtractor/SchemaBagToParametersTransformer.php index 4ea98a30f..abe3ba6ec 100644 --- a/src/Support/OperationExtensions/RulesExtractor/SchemaBagToParametersTransformer.php +++ b/src/Support/OperationExtensions/RulesExtractor/SchemaBagToParametersTransformer.php @@ -2,6 +2,8 @@ namespace Dedoc\Scramble\Support\OperationExtensions\RulesExtractor; +use Dedoc\Scramble\Support\Generator\Combined\AnyOf; +use Dedoc\Scramble\Support\Generator\Combined\OneOf; use Dedoc\Scramble\Support\Generator\MissingValue; use Dedoc\Scramble\Support\Generator\Parameter; use Dedoc\Scramble\Support\Generator\Schema; @@ -59,13 +61,26 @@ protected function makeParameterFromSchema(OpenApiSchema $schema, string $name): { $description = $schema->description; $example = $schema->example; + $examples = $schema->examples; - $schema->setDescription('')->example(new MissingValue); + $schema->setDescription(''); + if (! ($schema instanceof AnyOf || $schema instanceof OneOf)) { + $schema->example(new MissingValue)->examples([]); + } else { + $example = new MissingValue; + $examples = []; + } - return Parameter::make($name, $schema->getAttribute('isInQuery') ? 'query' : $this->in) + $parameter = Parameter::make($name, $schema->getAttribute('isInQuery') ? 'query' : $this->in) ->setSchema(Schema::fromType($schema)) ->example($example) ->required((bool) $schema->getAttribute('required', false)) ->description($description); + + if (count($examples)) { + $parameter->examples($examples); + } + + return $parameter; } } diff --git a/tests/Generator/AnyOfTest.php b/tests/Generator/AnyOfTest.php new file mode 100644 index 000000000..8710d187e --- /dev/null +++ b/tests/Generator/AnyOfTest.php @@ -0,0 +1,170 @@ +context = new OpenApiContext(new OpenApi('3.1.0'), new GeneratorConfig); +}); + +it('distributes examples to oneOf items when multiple examples provided', function () { + $transformer = new TypeTransformer(app(Infer::class), $this->context, []); + + $union = new Union([ + new StringType(), + new IntegerType(), + ]); + + $docNode = new PhpDocNode([ + new PhpDocTagNode('@example', new GenericTagValueNode('123456-1234-4321-123456')), + new PhpDocTagNode('@example', new GenericTagValueNode('1234')), + ]); + + $property = new ArrayItemType_('external_id', $union); + $property->setAttribute('docNode', $docNode); + + $openApiType = $transformer->transform($property); + + expect($openApiType->toArray())->toMatchArray([ + 'oneOf' => [ + [ + 'type' => 'string', + 'example' => '123456-1234-4321-123456', + ], + [ + 'type' => 'integer', + 'example' => 1234, + ], + ], + ]); +}); + +it('distributes rich examples to oneOf items using type when multiple examples provided', function () { + $transformer = new TypeTransformer(app(Infer::class), $this->context, []); + + $union = new Union([ + new StringType(), + new IntegerType(), + ]); + + $docNode = new PhpDocNode([ + new PhpDocTagNode('@example', new GenericTagValueNode(json_encode([ + 'type' => 'string', + 'summary' => 'UUID Example', + 'description' => 'This is a UUID', + 'value' => '123456-1234-4321-123456', + ]))), + new PhpDocTagNode('@example', new GenericTagValueNode(json_encode([ + 'type' => 'integer', + 'summary' => 'Integer Example', + 'value' => 1234, + ]))), + ]); + + $property = new ArrayItemType_('external_id', $union); + $property->setAttribute('docNode', $docNode); + + $openApiType = $transformer->transform($property); + + expect($openApiType->toArray())->toMatchArray([ + 'oneOf' => [ + [ + 'type' => 'string', + 'description' => 'UUID Example', + 'example' => '123456-1234-4321-123456', + ], + [ + 'type' => 'integer', + 'description' => 'Integer Example', + 'example' => 1234, + ], + ], + ]); +}); + +it('keeps anyOf when only one example provided', function () { + $transformer = new TypeTransformer(app(Infer::class), $this->context, []); + + $union = new Union([ + new StringType(), + new IntegerType(), + ]); + + $docNode = new PhpDocNode([ + new PhpDocTagNode('@example', new GenericTagValueNode('"123456-1234-4321-123456"')), + ]); + + $property = new ArrayItemType_('external_id', $union); + $property->setAttribute('docNode', $docNode); + + $openApiType = $transformer->transform($property); + + expect($openApiType->toArray())->toMatchArray([ + 'anyOf' => [ + [ + 'type' => 'string', + 'example' => '123456-1234-4321-123456', + ], + [ + 'type' => 'integer', + ], + ], + ]); +}); + +it('broadens type based on examples when @var is missing', function () { + $context = new \Dedoc\Scramble\OpenApiContext( + new \Dedoc\Scramble\Support\Generator\OpenApi('3.1.0'), + new \Dedoc\Scramble\GeneratorConfig + ); + $transformer = new \Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\PhpDocSchemaTransformer( + app()->make(\Dedoc\Scramble\Support\Generator\TypeTransformer::class, ['context' => $context]) + ); + + $type = new \Dedoc\Scramble\Support\Generator\Types\StringType; + $docNode = new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode([ + new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode('@example', new \PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode('{"type": "int", "value": 123}')), + new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode('@example', new \PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode('{"type": "string", "value": "string"}')), + ]); + + $transformedType = $transformer->transform($type, $docNode); + + expect($transformedType)->toBeInstanceOf(\Dedoc\Scramble\Support\Generator\Combined\OneOf::class); + expect($transformedType->toArray()['oneOf'])->toBe([ + ['type' => 'integer', 'example' => 123], + ['type' => 'string', 'example' => 'string'], + ]); +}); + +it('supports bool and int as aliases in examples', function () { + $context = new \Dedoc\Scramble\OpenApiContext( + new \Dedoc\Scramble\Support\Generator\OpenApi('3.1.0'), + new \Dedoc\Scramble\GeneratorConfig + ); + $transformer = new \Dedoc\Scramble\Support\OperationExtensions\RulesExtractor\PhpDocSchemaTransformer( + app()->make(\Dedoc\Scramble\Support\Generator\TypeTransformer::class, ['context' => $context]) + ); + + $type = new \Dedoc\Scramble\Support\Generator\Types\StringType; + $docNode = new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode([ + new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode('@example', new \PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode('{"type": "int", "value": 123}')), + new \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode('@example', new \PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode('{"type": "bool", "value": true}')), + ]); + + $transformedType = $transformer->transform($type, $docNode); + + expect($transformedType->toArray()['oneOf'])->toBe([ + ['type' => 'integer', 'example' => 123], + ['type' => 'boolean', 'example' => true], + ]); +}); diff --git a/tests/ValidationRulesDocumentingTest.php b/tests/ValidationRulesDocumentingTest.php index ef7b2bc8d..13141c3c5 100644 --- a/tests/ValidationRulesDocumentingTest.php +++ b/tests/ValidationRulesDocumentingTest.php @@ -1172,29 +1172,65 @@ public function index() {} it('extracts manual documentation for rules from request->validate call when rules are defined in a different method', function () { $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', ValidateCallDifferentMethodsRules_ValidationRulesDocumentingTest::class)); - expect($openApiDocument['paths']['/test']['get']['parameters'][0]) + expect($openApiDocument['paths']['/test']['get']['parameters']) ->toBe([ - 'name' => 'foo', - 'in' => 'query', - 'required' => true, - 'description' => 'Nice parameter', - 'schema' => ['type' => 'string'], + [ + 'name' => 'foo', + 'in' => 'query', + 'required' => true, + 'description' => 'Nice parameter', + 'schema' => ['type' => 'string'], + ], + [ + 'name' => 'bar', + 'in' => 'query', + 'description' => 'Another parameter', + 'schema' => [ + 'oneOf' => [ + [ + 'type' => 'string', + 'description' => 'First Format', + 'example' => '123456-1234-4321-123456', + ], + [ + 'type' => 'integer', + 'description' => 'Second Format', + 'example' => 1234, + ], + ], + ], + ], ]); }); class ValidateCallDifferentMethodsRules_ValidationRulesDocumentingTest { public function __invoke(Request $request) { - $request->validate((new ValidateCallDifferentMethodsRules_ValidationRulesDocumentingTest)->getRules()); + $request->validate([ + ...(new ValidateCallDifferentMethodsRules_ValidationRulesDocumentingTest)->getFooRules(), + ...(new ValidateCallDifferentMethodsRules_ValidationRulesDocumentingTest)->getBarRules(), + ]); } - public function getRules() + public function getFooRules(): array { return [ // Nice parameter 'foo' => ['required', 'string'], ]; } + + public function getBarRules(): array + { + return [ + /** + * Another parameter + * @example {"type": "string", "value": "123456-1234-4321-123456", "summary": "First Format"} + * @example {"type": "int", "value": 1234, "summary": "Second Format"} + */ + 'bar' => ['alpha_num'], + ]; + } } it('extracts manual documentation for merged rules from request->validate call when rules are defined in a different method', function () {