Repository: dedoc/scramble (php)

Issue:
Add @example for response types
for example:
```php
* @example {
 *   "fields": [
 *     {
 *       "field": "foo_count",
 *       "label": "Foo Count",
 *       "type": "integer",
 *       "operators": ["equal", "not_equal", "less", "less_or_equal", "greater", "greater_or_equal"]
 *     },
 * ]}
 ```

Pull request:
Added `@example` support to API resources' attributes
fixes #264  

Patch:
diff --git a/src/Support/Generator/TypeTransformer.php b/src/Support/Generator/TypeTransformer.php
index b59bf93..abd4a2f 100644
--- a/src/Support/Generator/TypeTransformer.php
+++ b/src/Support/Generator/TypeTransformer.php
@@ -14,6 +14,7 @@ use Dedoc\Scramble\Support\Generator\Types\NumberType;
 use Dedoc\Scramble\Support\Generator\Types\ObjectType;
 use Dedoc\Scramble\Support\Generator\Types\StringType;
 use Dedoc\Scramble\Support\Generator\Types\UnknownType;
+use Dedoc\Scramble\Support\Helpers\ExamplesExtractor;
 use Dedoc\Scramble\Support\Type\ArrayItemType_;
 use Dedoc\Scramble\Support\Type\Literal\LiteralIntegerType;
 use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
@@ -116,6 +117,10 @@ class TypeTransformer
                 if ($varNode && $varNode->description) {
                     $openApiType->setDescription($varNode->description);
                 }
+
+                if ($examples = ExamplesExtractor::make($docNode)->extract(preferString: $openApiType instanceof StringType)) {
+                    $openApiType->examples($examples);
+                }
             }
         } elseif ($type instanceof Union) {
             if (count($type->types) === 2 && collect($type->types)->contains(fn ($t) => $t instanceof \Dedoc\Scramble\Support\Type\NullType)) {
diff --git a/src/Support/Generator/Types/Type.php b/src/Support/Generator/Types/Type.php
index 191335d..d2949bf 100644
--- a/src/Support/Generator/Types/Type.php
+++ b/src/Support/Generator/Types/Type.php
@@ -18,6 +18,9 @@ abstract class Type
     /** @var array|scalar|null|MissingExample */
     public $example;
 
+    /** @var array<array|scalar|null|MissingExample> */
+    public $examples = [];
+
     public array $enum = [];
 
     public bool $nullable = false;
@@ -56,12 +59,21 @@ abstract class Type
 
     public function toArray()
     {
-        return array_merge(array_filter([
-            'type' => $this->nullable ? [$this->type, 'null'] : $this->type,
-            'format' => $this->format,
-            'description' => $this->description,
-            'enum' => count($this->enum) ? $this->enum : null,
-        ]), $this->example instanceof MissingExample ? [] : ['example' => $this->example]);
+        return array_merge(
+            array_filter([
+                'type' => $this->nullable ? [$this->type, 'null'] : $this->type,
+                'format' => $this->format,
+                'description' => $this->description,
+                'enum' => count($this->enum) ? $this->enum : null,
+            ]),
+            $this->example instanceof MissingExample ? [] : ['example' => $this->example],
+            count(
+                $examples = collect($this->examples)
+                    ->reject(fn ($example) => $example instanceof MissingExample)
+                    ->values()
+                    ->toArray()
+            ) ? ['examples' => $examples] : [],
+        );
     }
 
     public function setDescription(string $description): Type
@@ -87,4 +99,14 @@ abstract class Type
 
         return $this;
     }
+
+    /**
+     * @param  array<array|scalar|null|MissingExample>  $examples
+     */
+    public function examples(array $examples)
+    {
+        $this->examples = $examples;
+
+        return $this;
+    }
 }
diff --git a/src/Support/Helpers/ExamplesExtractor.php b/src/Support/Helpers/ExamplesExtractor.php
new file mode 100644
index 0000000..bc0c908
--- /dev/null
+++ b/src/Support/Helpers/ExamplesExtractor.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Dedoc\Scramble\Support\Helpers;
+
+use Dedoc\Scramble\Support\Generator\MissingExample;
+use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
+
+/**
+ * Extracts `@example` value from PHPDoc node.
+ */
+class ExamplesExtractor
+{
+    public function __construct(
+        private ?PhpDocNode $docNode
+    ) {
+    }
+
+    public static function make(?PhpDocNode $docNode)
+    {
+        return new self($docNode);
+    }
+
+    public function extract(bool $preferString = false)
+    {
+        if (! count($examples = $this->docNode->getTagsByName('@example'))) {
+            return [];
+        }
+
+        return array_map(
+            fn ($example) => $this->getTypedExampleValue($example->value->value ?? null, $preferString),
+            array_values($examples),
+        );
+    }
+
+    private function getTypedExampleValue($exampleValue, bool $preferString = false)
+    {
+        if (! is_string($exampleValue)) {
+            return new MissingExample;
+        }
+
+        if (function_exists('json_decode')) {
+            $json = json_decode($exampleValue, true);
+
+            $exampleValue = $json === null || $json == $exampleValue
+                ? $exampleValue
+                : $json;
+        }
+
+        if ($exampleValue === 'null') {
+            $exampleValue = null;
+        } elseif (in_array($exampleValue, ['true', 'false'])) {
+            $exampleValue = $exampleValue === 'true';
+        } elseif (is_numeric($exampleValue) && ! $preferString) {
+            $exampleValue = floatval($exampleValue);
+        }
+
+        return $exampleValue;
+    }
+}
diff --git a/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php b/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
index c73fddb..db6c1ca 100644
--- a/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
+++ b/src/Support/OperationExtensions/RulesExtractor/RulesToParameter.php
@@ -9,6 +9,7 @@ use Dedoc\Scramble\Support\Generator\Types\StringType;
 use Dedoc\Scramble\Support\Generator\Types\Type as OpenApiType;
 use Dedoc\Scramble\Support\Generator\Types\UnknownType;
 use Dedoc\Scramble\Support\Generator\TypeTransformer;
+use Dedoc\Scramble\Support\Helpers\ExamplesExtractor;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Str;
 use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
@@ -71,34 +72,11 @@ class RulesToParameter
         $description = (string) Str::of($this->docNode->getAttribute('summary') ?: '')
             ->append(' '.($this->docNode->getAttribute('description') ?: ''))
             ->trim();
+
         if ($description) {
             $parameter->description($description);
         }
 
-        if (count($example = $this->docNode->getTagsByName('@example'))) {
-            $exampleValue = array_values($example)[0]->value->value ?? null;
-
-            if (is_string($exampleValue)) {
-                if (function_exists('json_decode')) {
-                    $json = json_decode($exampleValue, true);
-
-                    $exampleValue = $json === null || $json == $exampleValue
-                        ? $exampleValue
-                        : $json;
-                }
-
-                if ($exampleValue === 'null') {
-                    $exampleValue = null;
-                } elseif (in_array($exampleValue, ['true', 'false'])) {
-                    $exampleValue = $exampleValue === 'true';
-                } elseif (is_numeric($exampleValue) && ! ($parameter->schema->type instanceof StringType)) {
-                    $exampleValue = floatval($exampleValue);
-                }
-
-                $parameter->example($exampleValue);
-            }
-        }
-
         if (count($varTags = $this->docNode->getVarTagValues())) {
             $varTag = $varTags[0];
 
@@ -107,6 +85,10 @@ class RulesToParameter
             ));
         }
 
+        if ($examples = ExamplesExtractor::make($this->docNode)->extract(preferString: $parameter->schema->type instanceof StringType)) {
+            $parameter->example($examples[0]);
+        }
+
         return $parameter;
     }
 


Test patch:
diff --git a/tests/TypeToSchemaTransformerTest.php b/tests/TypeToSchemaTransformerTest.php
index fcca9db..daf83d5 100644
--- a/tests/TypeToSchemaTransformerTest.php
+++ b/tests/TypeToSchemaTransformerTest.php
@@ -148,6 +148,24 @@ it('infers date column directly referenced in json as date-time', function () {
     ]);
 });
 
+it('supports @example tag in api resource', function () {
+    $transformer = new TypeTransformer($infer = app(Infer::class), $components = new Components, [JsonResourceTypeToSchema::class]);
+
+    $type = new ObjectType(ApiResourceTest_ResourceWithExamples::class);
+
+    expect($transformer->transform($type)->toArray())->toBe([
+        '$ref' => '#/components/schemas/ApiResourceTest_ResourceWithExamples',
+    ]);
+
+    expect($components->getSchema(ApiResourceTest_ResourceWithExamples::class)->toArray()['properties']['id'])->toBe([
+        'type' => 'integer',
+        'examples' => [
+            'Foo',
+            'Multiword example',
+        ],
+    ]);
+});
+
 class ComplexTypeHandlersTest_SampleType extends JsonResource
 {
     public function toArray($request)
@@ -251,6 +269,23 @@ class InferTypesTest_JsonResourceWithCarbonAttribute extends JsonResource
     }
 }
 
+/**
+ * @property SamplePostModel $resource
+ */
+class ApiResourceTest_ResourceWithExamples extends JsonResource
+{
+    public function toArray($request)
+    {
+        return [
+            /**
+             * @example Foo
+             * @example Multiword example
+             */
+            'id' => $this->id,
+        ];
+    }
+}
+
 enum StatusTwo: string
 {
     case DRAFT = 'draft';