Bladeren bron

added feature/nullable (#834)

Co-authored-by: Shalvah <shalvah@users.noreply.github.com>
Mertcan Dinler 7 maanden geleden
bovenliggende
commit
4d4a44e48a

+ 1 - 0
camel/Extraction/Parameter.php

@@ -14,6 +14,7 @@ class Parameter extends BaseDTO
     public string $type = 'string';
     public array $enumValues = [];
     public bool $exampleWasSpecified = false;
+    public bool $nullable = false;
 
     public function __construct(array $parameters = [])
     {

+ 2 - 0
src/Attributes/GenericParam.php

@@ -14,6 +14,7 @@ class GenericParam
         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 ?bool $nullable = false,
     ) {
     }
 
@@ -26,6 +27,7 @@ class GenericParam
             "required" => $this->required,
             "example" => $this->example,
             "enumValues" => $this->getEnumValues(),
+            'nullable' => $this->nullable,
         ];
     }
 

+ 2 - 1
src/Attributes/ResponseField.php

@@ -15,7 +15,8 @@ class ResponseField extends GenericParam
         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 mixed $enum = null, // Can pass a list of values, or a native PHP enum,
+        public ?bool $nullable = false,
     ) {
     }
 }

+ 9 - 0
src/Extracting/ParsesValidationRules.php

@@ -50,6 +50,7 @@ trait ParsesValidationRules
                     'type' => null,
                     'example' => self::$MISSING_VALUE,
                     'description' => $description,
+                    'nullable' => false,
                 ];
                 $dependentRules[$parameter] = [];
 
@@ -69,6 +70,11 @@ trait ParsesValidationRules
                 }
 
                 $parameterData['name'] = $parameter;
+
+                if ($parameterData['required'] === true){
+                    $parameterData['nullable'] = false;
+                }
+
                 $parameters[$parameter] = $parameterData;
             } catch (Throwable $e) {
                 if ($e instanceof ScribeException) {
@@ -531,6 +537,9 @@ trait ParsesValidationRules
                 case 'different':
                     $parameterData['description'] .= " The value and <code>{$arguments[0]}</code> must be different.";
                     break;
+                case 'nullable':
+                    $parameterData['nullable'] = true;
+                    break;
                 case 'exists':
                     $parameterData['description'] .= " The <code>{$arguments[1]}</code> of an existing record in the {$arguments[0]} table.";
                     break;

+ 4 - 0
src/Extracting/Strategies/GetParamsFromAttributeStrategy.php

@@ -38,6 +38,10 @@ class GetParamsFromAttributeStrategy extends PhpAttributeStrategy
             $data['example'] = null;
         }
 
+        if ($data['required']){
+            $data['nullable'] = false;
+        }
+
         $data['description'] = trim($data['description'] ?? '');
         return $data;
     }

+ 8 - 0
src/Writing/OpenAPISpecWriter.php

@@ -488,6 +488,7 @@ class OpenAPISpecWriter
                 'type' => 'string',
                 'format' => 'binary',
                 'description' => $field->description ?: '',
+                'nullable' => $field->nullable,
             ];
         } else if (Utils::isArrayType($field->type)) {
             $baseType = Utils::getBaseTypeFromArrayType($field->type);
@@ -500,6 +501,10 @@ class OpenAPISpecWriter
                 $baseItem['enum'] = $field->enumValues;
             }
 
+            if ($field->nullable) {
+                $baseItem['nullable'] = true;
+            }
+
             $fieldData = [
                 'type' => 'array',
                 'description' => $field->description ?: '',
@@ -509,6 +514,7 @@ class OpenAPISpecWriter
                         'name' => '',
                         'type' => $baseType,
                         'example' => ($field->example ?: [null])[0],
+                        'nullable' => $field->nullable,
                     ])
                     : $baseItem,
             ];
@@ -535,6 +541,7 @@ class OpenAPISpecWriter
                 'type' => 'object',
                 'description' => $field->description ?: '',
                 'example' => $field->example,
+                'nullable'=> $field->nullable,
                 'properties' => $this->objectIfEmpty(collect($field->__fields)->mapWithKeys(function ($subfield, $subfieldName) {
                     return [$subfieldName => $this->generateFieldData($subfield)];
                 })->all()),
@@ -544,6 +551,7 @@ class OpenAPISpecWriter
                 'type' => static::normalizeTypeName($field->type),
                 'description' => $field->description ?: '',
                 'example' => $field->example,
+                'nullable' => $field->nullable,
             ];
             if (!empty($field->enumValues)) {
                 $schema['enum'] = $field->enumValues;

+ 11 - 3
tests/Fixtures/openapi.yaml

@@ -34,10 +34,12 @@ paths:
                                     type: string
                                     description: 'Name of image.'
                                     example: cat.jpg
+                                    nullable: false
                                 image:
                                     type: string
                                     format: binary
                                     description: 'The image.'
+                                    nullable: false
                             required:
                                 - name
                                 - image
@@ -95,6 +97,7 @@ paths:
                         type: string
                         description: 'The id of the location.'
                         example: consequatur
+                        nullable: false
                 -
                     in: query
                     name: user_id
@@ -105,6 +108,7 @@ paths:
                         type: string
                         description: 'The id of the user.'
                         example: me
+                        nullable: false
                 -
                     in: query
                     name: page
@@ -115,6 +119,7 @@ paths:
                         type: string
                         description: 'The page number.'
                         example: '4'
+                        nullable: false
                 -
                     in: query
                     name: filters
@@ -125,6 +130,7 @@ paths:
                         type: string
                         description: 'The filters.'
                         example: consequatur
+                        nullable: false
                 -
                     in: query
                     name: url_encoded
@@ -135,6 +141,7 @@ paths:
                         type: string
                         description: 'Used for testing that URL parameters will be URL-encoded where needed.'
                         example: '+ []&='
+                        nullable: false
                 -
                     in: header
                     name: Custom-Header
@@ -192,6 +199,7 @@ paths:
                         type: string
                         description: ''
                         example: consequatur
+                        nullable: false
                 -
                     in: header
                     name: Custom-Header
@@ -293,9 +301,9 @@ paths:
                             items:
                                 type: object
                                 properties:
-                                    first_name: { type: string, description: 'The first name of the user.', example: John }
-                                    last_name: { type: string, description: 'The last name of the user.', example: Doe }
-                                    contacts: { type: array, description: 'Contact info', example: [ [ ] ], items: { type: object, properties: { first_name: { type: string, description: 'The first name of the contact.', example: Janelle }, last_name: { type: string, description: 'The last name of the contact.', example: Monáe } }, required: [ first_name, last_name ] } }
+                                    first_name: { type: string, description: 'The first name of the user.', example: John, nullable: false }
+                                    last_name: { type: string, description: 'The last name of the user.', example: Doe, nullable: false}
+                                    contacts: { type: array, description: 'Contact info', example: [ [ ] ], items: { type: object, properties: { first_name: { type: string, description: 'The first name of the contact.', example: Janelle, nullable: false }, last_name: { type: string, description: 'The last name of the contact.', example: Monáe, nullable: false } }, required: [ first_name, last_name ] } }
                                     roles: { type: array, description: 'The name of the role.', example: [ Admin ], items: { type: string } }
                                 required:
                                     - first_name

+ 1 - 0
tests/GenerateDocumentation/OutputTest.php

@@ -475,6 +475,7 @@ class OutputTest extends BaseLaravelTest
             'enumValues' => [],
             'custom' => [],
             'exampleWasSpecified' => false,
+            'nullable' => false,
         ];
         $group['endpoints'][0]['urlParameters']['a_param'] = $extraParam;
         file_put_contents($firstGroupFilePath, Yaml::dump(

+ 33 - 1
tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php

@@ -32,85 +32,115 @@ class GetFromBodyParamAttributeTest extends TestCase
                 'required' => true,
                 'description' => 'The id of the user.',
                 'example' => 9,
+                'nullable' => false,
             ],
             'room_id' => [
                 'type' => 'string',
                 'required' => false,
                 'description' => 'The id of the room.',
+                'nullable' => false,
             ],
             'forever' => [
                 'type' => 'boolean',
                 'required' => false,
                 'description' => 'Whether to ban the user forever.',
                 'example' => false,
+                'nullable' => false,
             ],
             'another_one' => [
                 'type' => 'number',
                 'required' => false,
                 'description' => 'Just need something here.',
+                'nullable' => false,
             ],
             'yet_another_param' => [
                 'type' => 'object',
                 'required' => true,
                 'description' => 'Some object params.',
+                'nullable' => false,
             ],
             'yet_another_param.name' => [
                 'type' => 'string',
                 'description' => '',
                 'required' => true,
+                'nullable' => false,
             ],
             'even_more_param' => [
                 'type' => 'number[]',
                 'description' => 'A list of numbers',
                 'required' => false,
+                'nullable' => false,
             ],
             'book' => [
                 'type' => 'object',
                 'description' => 'Book information',
                 'required' => false,
+                'nullable' => false,
             ],
             'book.name' => [
                 'type' => 'string',
                 'description' => '',
                 'required' => true,
+                'nullable' => false,
             ],
             'book.author_id' => [
                 'type' => 'integer',
                 'description' => '',
                 'required' => true,
+                'nullable' => false,
             ],
             'book.pages_count' => [
                 'type' => 'integer',
                 'description' => '',
                 'required' => true,
+                'nullable' => false,
             ],
             'ids' => [
                 'type' => 'integer[]',
                 'description' => '',
                 'required' => true,
+                'nullable' => false,
             ],
             'state' => [
                 'type' => 'string',
                 'description' => '',
                 'required' => true,
-                'enumValues' => ["active", "pending"]
+                'enumValues' => ["active", "pending"],
+                'nullable' => false,
             ],
             'users' => [
                 'type' => 'object[]',
                 'description' => 'Users\' details',
                 'required' => false,
+                'nullable' => false,
             ],
             'users[].first_name' => [
                 'type' => 'string',
                 'description' => 'The first name of the user.',
                 'required' => false,
                 'example' => 'John',
+                'nullable' => false,
             ],
             'users[].last_name' => [
                 'type' => 'string',
                 'description' => 'The last name of the user.',
                 'required' => false,
                 'example' => 'Doe',
+                'nullable' => false,
+            ],
+            'note' => [
+                'type' => 'string',
+                'description' => '',
+                'required' => false,
+                'example' => 'This is a note.',
+                'nullable' => true,
+            ],
+            'required_note' => [
+                'type' => 'string',
+                'description' => '',
+                'required' => true,
+                'example' => 'This is a note.',
+                'nullable' => false,
             ],
         ], $results);
     }
@@ -226,6 +256,8 @@ class BodyParamAttributeTestController
     #[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)]
+    #[BodyParam("note", example: "This is a note.", required: false, nullable: true)]
+    #[BodyParam("required_note", example: "This is a note.", required: true, nullable: true)]
     public function methodWithAttributes()
     {
 

+ 14 - 0
tests/Unit/OpenAPISpecWriterTest.php

@@ -210,6 +210,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'example' => 'hahoho',
                     'type' => 'string',
                     'name' => 'param',
+                    'nullable' => false
                 ],
             ],
         ]);
@@ -231,6 +232,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                 'type' => 'string',
                 'description' => 'A query param',
                 'example' => 'hahoho',
+                'nullable' => false
             ],
         ], $results['paths']['/path1']['get']['parameters'][0]);
     }
@@ -248,6 +250,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'required' => false,
                     'example' => 'hahoho',
                     'type' => 'string',
+                    'nullable' => false,
                 ],
                 'integerParam' => [
                     'name' => 'integerParam',
@@ -255,6 +258,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'required' => true,
                     'example' => 99,
                     'type' => 'integer',
+                    'nullable' => false,
                 ],
                 'booleanParam' => [
                     'name' => 'booleanParam',
@@ -262,6 +266,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'required' => true,
                     'example' => false,
                     'type' => 'boolean',
+                    'nullable' => false,
                 ],
                 'objectParam' => [
                     'name' => 'objectParam',
@@ -269,6 +274,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'required' => false,
                     'example' => [],
                     'type' => 'object',
+                    'nullable' => false,
                 ],
                 'objectParam.field' => [
                     'name' => 'objectParam.field',
@@ -276,6 +282,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                     'required' => false,
                     'example' => 119.0,
                     'type' => 'number',
+                    'nullable' => false,
                 ],
             ],
         ]);
@@ -338,26 +345,31 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                                 'description' => 'String param',
                                 'example' => 'hahoho',
                                 'type' => 'string',
+                                'nullable' => false,
                             ],
                             'booleanParam' => [
                                 'description' => 'Boolean param',
                                 'example' => false,
                                 'type' => 'boolean',
+                                'nullable' => false,
                             ],
                             'integerParam' => [
                                 'description' => 'Integer param',
                                 'example' => 99,
                                 'type' => 'integer',
+                                'nullable' => false,
                             ],
                             'objectParam' => [
                                 'description' => 'Object param',
                                 'example' => [],
                                 'type' => 'object',
+                                'nullable' => false,
                                 'properties' => [
                                     'field' => [
                                         'description' => 'Object param field',
                                         'example' => 119.0,
                                         'type' => 'number',
+                                        'nullable' => false,
                                     ],
                                 ],
                             ],
@@ -381,6 +393,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                                 'description' => 'File param',
                                 'type' => 'string',
                                 'format' => 'binary',
+                                'nullable' => false,
                             ],
                             'numberArrayParam' => [
                                 'description' => 'Number array param',
@@ -410,6 +423,7 @@ class OpenAPISpecWriterTest extends BaseUnitTest
                                             'type' => 'string',
                                             'description' => '',
                                             'example' => "hi",
+                                            'nullable' => false,
                                         ],
                                     ],
                                 ],

+ 51 - 0
tests/Unit/ValidationRuleParsingTest.php

@@ -639,6 +639,57 @@ class ValidationRuleParsingTest extends BaseLaravelTest
         $this->assertEquals('successfully translated by concatenated string.', $results['nested']['description']);
 
     }
+
+    /** @test */
+    public function can_valid_parse_nullable_rules()
+    {
+        $ruleset = [
+            'nullable_param' => 'nullable|string',
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals(true, $results['nullable_param']['nullable']);
+
+        $ruleset = [
+            'nullable_param' => 'string',
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals(false, $results['nullable_param']['nullable']);
+
+        $ruleset = [
+            'required_param' => 'required|nullable|string',
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals(false, $results['required_param']['nullable']);
+
+
+        $ruleset = [
+            'array_param' => 'array',
+            'array_param.*.field' => 'nullable|string',
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals(false, $results['array_param']['nullable']);
+        $this->assertEquals(true, $results['array_param[].field']['nullable']);
+
+        $ruleset = [
+            'object' => 'array',
+            'object.field1' => 'string',
+            'object.field2' => 'nullable|string',
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals(false, $results['object']['nullable']);
+        $this->assertEquals(false, $results['object.field1']['nullable']);
+        $this->assertEquals(true, $results['object.field2']['nullable']);
+    }
 }
 
 class DummyValidationRule implements \Illuminate\Contracts\Validation\Rule