diff --git a/camel/Extraction/Parameter.php b/camel/Extraction/Parameter.php
index 0d78f6bb..7e1f275a 100644
--- a/camel/Extraction/Parameter.php
+++ b/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 = [])
{
diff --git a/src/Attributes/GenericParam.php b/src/Attributes/GenericParam.php
index 55e13703..21dd83a1 100644
--- a/src/Attributes/GenericParam.php
+++ b/src/Attributes/GenericParam.php
@@ -14,6 +14,7 @@ public function __construct(
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 @@ public function toArray()
"required" => $this->required,
"example" => $this->example,
"enumValues" => $this->getEnumValues(),
+ 'nullable' => $this->nullable,
];
}
diff --git a/src/Attributes/ResponseField.php b/src/Attributes/ResponseField.php
index a11c790e..dd7fa643 100644
--- a/src/Attributes/ResponseField.php
+++ b/src/Attributes/ResponseField.php
@@ -15,7 +15,8 @@ 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
+ public mixed $enum = null, // Can pass a list of values, or a native PHP enum,
+ public ?bool $nullable = false,
) {
}
}
diff --git a/src/Extracting/ParsesValidationRules.php b/src/Extracting/ParsesValidationRules.php
index 0dd9038c..58a2c9ad 100644
--- a/src/Extracting/ParsesValidationRules.php
+++ b/src/Extracting/ParsesValidationRules.php
@@ -50,6 +50,7 @@ public function getParametersFromValidationRules(array $validationRules, array $
'type' => null,
'example' => self::$MISSING_VALUE,
'description' => $description,
+ 'nullable' => false,
];
$dependentRules[$parameter] = [];
@@ -69,6 +70,11 @@ public function getParametersFromValidationRules(array $validationRules, array $
}
$parameterData['name'] = $parameter;
+
+ if ($parameterData['required'] === true){
+ $parameterData['nullable'] = false;
+ }
+
$parameters[$parameter] = $parameterData;
} catch (Throwable $e) {
if ($e instanceof ScribeException) {
@@ -531,6 +537,9 @@ protected function parseRule($rule, array &$parameterData, bool $independentOnly
case 'different':
$parameterData['description'] .= " The value and {$arguments[0]} must be different.";
break;
+ case 'nullable':
+ $parameterData['nullable'] = true;
+ break;
case 'exists':
$parameterData['description'] .= " The {$arguments[1]} of an existing record in the {$arguments[0]} table.";
break;
diff --git a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
index d6e1264d..a5924d83 100644
--- a/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
+++ b/src/Extracting/Strategies/GetParamsFromAttributeStrategy.php
@@ -38,6 +38,10 @@ protected function normalizeParameterData(array $data): array
$data['example'] = null;
}
+ if ($data['required']){
+ $data['nullable'] = false;
+ }
+
$data['description'] = trim($data['description'] ?? '');
return $data;
}
diff --git a/src/Writing/OpenAPISpecWriter.php b/src/Writing/OpenAPISpecWriter.php
index 11a6a84d..52708b2a 100644
--- a/src/Writing/OpenAPISpecWriter.php
+++ b/src/Writing/OpenAPISpecWriter.php
@@ -488,6 +488,7 @@ public function generateFieldData($field): array
'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 @@ public function generateFieldData($field): array
$baseItem['enum'] = $field->enumValues;
}
+ if ($field->nullable) {
+ $baseItem['nullable'] = true;
+ }
+
$fieldData = [
'type' => 'array',
'description' => $field->description ?: '',
@@ -509,6 +514,7 @@ public function generateFieldData($field): array
'name' => '',
'type' => $baseType,
'example' => ($field->example ?: [null])[0],
+ 'nullable' => $field->nullable,
])
: $baseItem,
];
@@ -535,6 +541,7 @@ public function generateFieldData($field): array
'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 @@ public function generateFieldData($field): array
'type' => static::normalizeTypeName($field->type),
'description' => $field->description ?: '',
'example' => $field->example,
+ 'nullable' => $field->nullable,
];
if (!empty($field->enumValues)) {
$schema['enum'] = $field->enumValues;
diff --git a/tests/Fixtures/openapi.yaml b/tests/Fixtures/openapi.yaml
index 9ea3a135..97e8b4ca 100644
--- a/tests/Fixtures/openapi.yaml
+++ b/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
diff --git a/tests/GenerateDocumentation/OutputTest.php b/tests/GenerateDocumentation/OutputTest.php
index 763d789e..448bf8a5 100644
--- a/tests/GenerateDocumentation/OutputTest.php
+++ b/tests/GenerateDocumentation/OutputTest.php
@@ -475,6 +475,7 @@ public function will_not_overwrite_manually_modified_content_unless_force_flag_i
'enumValues' => [],
'custom' => [],
'exampleWasSpecified' => false,
+ 'nullable' => false,
];
$group['endpoints'][0]['urlParameters']['a_param'] = $extraParam;
file_put_contents($firstGroupFilePath, Yaml::dump(
diff --git a/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php b/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
index 93c90be0..cc3470f0 100644
--- a/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
+++ b/tests/Strategies/BodyParameters/GetFromBodyParamAttributeTest.php
@@ -32,85 +32,115 @@ public function can_fetch_from_bodyparam_attribute()
'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()
{
diff --git a/tests/Unit/OpenAPISpecWriterTest.php b/tests/Unit/OpenAPISpecWriterTest.php
index d906dc6d..019415c8 100644
--- a/tests/Unit/OpenAPISpecWriterTest.php
+++ b/tests/Unit/OpenAPISpecWriterTest.php
@@ -210,6 +210,7 @@ public function adds_query_parameters_correctly_as_parameters_on_operation_objec
'example' => 'hahoho',
'type' => 'string',
'name' => 'param',
+ 'nullable' => false
],
],
]);
@@ -231,6 +232,7 @@ public function adds_query_parameters_correctly_as_parameters_on_operation_objec
'type' => 'string',
'description' => 'A query param',
'example' => 'hahoho',
+ 'nullable' => false
],
], $results['paths']['/path1']['get']['parameters'][0]);
}
@@ -248,6 +250,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'required' => false,
'example' => 'hahoho',
'type' => 'string',
+ 'nullable' => false,
],
'integerParam' => [
'name' => 'integerParam',
@@ -255,6 +258,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'required' => true,
'example' => 99,
'type' => 'integer',
+ 'nullable' => false,
],
'booleanParam' => [
'name' => 'booleanParam',
@@ -262,6 +266,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'required' => true,
'example' => false,
'type' => 'boolean',
+ 'nullable' => false,
],
'objectParam' => [
'name' => 'objectParam',
@@ -269,6 +274,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'required' => false,
'example' => [],
'type' => 'object',
+ 'nullable' => false,
],
'objectParam.field' => [
'name' => 'objectParam.field',
@@ -276,6 +282,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'required' => false,
'example' => 119.0,
'type' => 'number',
+ 'nullable' => false,
],
],
]);
@@ -338,26 +345,31 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'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 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'description' => 'File param',
'type' => 'string',
'format' => 'binary',
+ 'nullable' => false,
],
'numberArrayParam' => [
'description' => 'Number array param',
@@ -410,6 +423,7 @@ public function adds_body_parameters_correctly_as_requestBody_on_operation_objec
'type' => 'string',
'description' => '',
'example' => "hi",
+ 'nullable' => false,
],
],
],
diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php
index 5a877ac9..0bfda300 100644
--- a/tests/Unit/ValidationRuleParsingTest.php
+++ b/tests/Unit/ValidationRuleParsingTest.php
@@ -639,6 +639,57 @@ public function can_translate_validation_rules_with_types_with_translator_withou
$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