Skip to content

Commit f6119cf

Browse files
authored
Merge pull request #11 from ensi-platform/task-81781
#81781
2 parents 5279808 + fb6cb06 commit f6119cf

26 files changed

+884
-14
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Controller class IS meant to be modified after generation.
6767

6868
Generates Laravel Form Requests for DELETE, PATCH, POST, PUT paths
6969
Destination must be configured with array as namespace instead of string.
70-
E.g
70+
E.g.
7171

7272
```php
7373
'requests' => [
@@ -82,6 +82,11 @@ If the file already exists it IS NOT overriden with each generation.
8282
Form Request class name is `ucFirst(operationId)`. You can override it with `x-lg-request-class-name`
8383
You can skip generating form request for a give route with `x-lg-skip-request-generation: true` directive.
8484

85+
When generating a request, the Laravel Validation rules for request fields are automatically generated.
86+
Validation rules generate based on the field type and required field.
87+
For fields whose values should only take the values of some enum, you must specify the `x-lg-enum-class option`.
88+
E.g.: `x-lg-enum-class: 'CustomerGenderEnum'`.
89+
8590
### 'enums' => EnumsGenerator::class
8691

8792
Generates Enum class only for enums listed in `oas3->components->schemas`.

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"autoload": {
1717
"psr-4": {
1818
"Ensi\\LaravelOpenApiServerGenerator\\": "src/"
19-
}
19+
},
20+
"files": [
21+
"src/helpers.php"
22+
]
2023
},
2124
"autoload-dev": {
2225
"psr-4": {

src/Commands/GenerateServer.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use cebe\openapi\Reader;
66
use cebe\openapi\SpecObjectInterface;
7+
use Ensi\LaravelOpenApiServerGenerator\Exceptions\EnumsNamespaceMissingException;
78
use Illuminate\Console\Command;
89
use LogicException;
910

@@ -66,7 +67,14 @@ public function handleMapping(string $sourcePath, array $optionsPerEntity)
6667
}
6768

6869
$this->infoIfVerbose("Generating files for entity \"$entity\" using generator \"$generatorClass\"");
69-
resolve($generatorClass)->setOptions($optionsPerEntity[$entity])->generate($specObject);
70+
71+
try {
72+
resolve($generatorClass)->setOptions($optionsPerEntity)->generate($specObject);
73+
} catch (EnumsNamespaceMissingException) {
74+
$this->error("Option \"enums_namespace\" for entity \"$entity\" are not set in \"api_docs_mappings\" config for source \"$sourcePath\"");
75+
76+
return self::FAILURE;
77+
}
7078
}
7179

7280
return self::SUCCESS;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Data\OpenApi3;
4+
5+
use Ensi\LaravelOpenApiServerGenerator\Enums\OpenApi3PropertyTypeEnum;
6+
use Ensi\LaravelOpenApiServerGenerator\Exceptions\EnumsNamespaceMissingException;
7+
use Illuminate\Support\Collection;
8+
use stdClass;
9+
10+
class OpenApi3Object
11+
{
12+
/** @var Collection|OpenApi3ObjectProperty[] */
13+
public Collection $properties;
14+
15+
public function __construct()
16+
{
17+
$this->properties = collect();
18+
}
19+
20+
public function fillFromStdObject(stdClass $object): void
21+
{
22+
if (std_object_has($object, 'properties')) {
23+
foreach (get_object_vars($object->properties) as $propertyName => $property) {
24+
/** @var OpenApi3ObjectProperty $objectProperty */
25+
$objectProperty = $this->properties->get($propertyName);
26+
if (!$objectProperty) {
27+
$objectProperty = new OpenApi3ObjectProperty(type: $property->type, name: $propertyName);
28+
$this->properties->put($propertyName, $objectProperty);
29+
}
30+
$objectProperty->fillFromStdProperty($propertyName, $property);
31+
}
32+
}
33+
if (std_object_has($object, 'required') && is_array($object->required)) {
34+
foreach ($object->required as $requiredProperty) {
35+
$objectProperty = $this->properties->get($requiredProperty);
36+
if (!$objectProperty) {
37+
$objectProperty = new OpenApi3ObjectProperty(
38+
type: OpenApi3PropertyTypeEnum::OBJECT->value,
39+
name: $requiredProperty,
40+
);
41+
$this->properties->put($requiredProperty, $objectProperty);
42+
}
43+
44+
$objectProperty->required = true;
45+
}
46+
}
47+
}
48+
49+
public function toLaravelValidationRules(array $options): array
50+
{
51+
$validations = [];
52+
$enums = [];
53+
foreach ($this->properties as $property) {
54+
[$propertyValidations, $propertyEnums] = $property->getLaravelValidationsAndEnums($options);
55+
56+
$validations = array_merge($propertyValidations, $validations);
57+
$enums = array_merge($propertyEnums, $enums);
58+
}
59+
60+
if ($validations) {
61+
$validationStrings = [];
62+
foreach ($validations as $propertyName => $validation) {
63+
$validationString = implode(', ', $validation);
64+
$validationStrings[] = "'{$propertyName}' => [{$validationString}],";
65+
}
66+
$validationsString = implode("\n ", $validationStrings);
67+
} else {
68+
$validationsString = '';
69+
}
70+
71+
if ($enums) {
72+
throw_unless(isset($options['enums']['namespace']), EnumsNamespaceMissingException::class);
73+
74+
$enumStrings = [];
75+
foreach ($enums as $enumClass => $value) {
76+
$enumStrings[] = 'use ' . $options['enums']['namespace'] . "{$enumClass};";
77+
}
78+
if ($enumStrings) {
79+
$enumStrings[] = 'use Illuminate\Validation\Rules\Enum;';
80+
}
81+
sort($enumStrings);
82+
$enumsString = implode("\n", $enumStrings);
83+
} else {
84+
$enumsString = '';
85+
}
86+
87+
return [$validationsString, $enumsString];
88+
}
89+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Data\OpenApi3;
4+
5+
use Ensi\LaravelOpenApiServerGenerator\Enums\LaravelValidationRuleEnum;
6+
use Ensi\LaravelOpenApiServerGenerator\Enums\OpenApi3PropertyFormatEnum;
7+
use Ensi\LaravelOpenApiServerGenerator\Enums\OpenApi3PropertyTypeEnum;
8+
use stdClass;
9+
10+
class OpenApi3ObjectProperty
11+
{
12+
public function __construct(
13+
public string $type,
14+
public ?string $name = null,
15+
public ?string $format = null,
16+
public bool $required = false,
17+
public bool $nullable = false,
18+
public ?string $enumClass = null,
19+
public ?OpenApi3Object $object = null,
20+
public ?OpenApi3ObjectProperty $items = null,
21+
) {
22+
//
23+
}
24+
25+
public function fillFromStdProperty(string $propertyName, stdClass $stdProperty): void
26+
{
27+
if (std_object_has($stdProperty, 'nullable')) {
28+
$this->nullable = true;
29+
}
30+
if (std_object_has($stdProperty, 'format')) {
31+
$this->format = $stdProperty->format;
32+
}
33+
if (std_object_has($stdProperty, 'x-lg-enum-class')) {
34+
$this->enumClass = $stdProperty->{'x-lg-enum-class'};
35+
}
36+
37+
switch (OpenApi3PropertyTypeEnum::from($stdProperty->type)) {
38+
case OpenApi3PropertyTypeEnum::OBJECT:
39+
$this->object = new OpenApi3Object();
40+
$this->object->fillFromStdObject($stdProperty);
41+
42+
break;
43+
case OpenApi3PropertyTypeEnum::ARRAY:
44+
$this->items = new OpenApi3ObjectProperty(type: $stdProperty->items->type);
45+
$this->items->fillFromStdProperty("{$propertyName}.*", $stdProperty->items);
46+
47+
break;
48+
default:
49+
}
50+
}
51+
52+
public function getLaravelValidationsAndEnums(array $options, array &$validations = [], array &$enums = [], string $namePrefix = null): array
53+
{
54+
$name = "{$namePrefix}{$this->name}";
55+
56+
if ($this->required) {
57+
$validations[$name][] = "'required'";
58+
}
59+
if ($this->nullable) {
60+
$validations[$name][] = "'nullable'";
61+
}
62+
if ($this->enumClass) {
63+
$validations[$name][] = "new Enum({$this->enumClass}::class)";
64+
$enums[$this->enumClass] = true;
65+
} else {
66+
[$currentValidations, $currentEnums] = $this->getValidationsAndEnumsByTypeAndFormat($options, $validations, $enums, $name);
67+
$validations = array_merge($validations, $currentValidations);
68+
$enums = array_merge($enums, $currentEnums);
69+
}
70+
71+
return [$validations, $enums];
72+
}
73+
74+
protected function getValidationsAndEnumsByTypeAndFormat(array $options, array &$validations, array &$enums, string $name): array
75+
{
76+
$type = OpenApi3PropertyTypeEnum::from($this->type);
77+
$format = OpenApi3PropertyFormatEnum::tryFrom($this->format);
78+
switch ($type) {
79+
case OpenApi3PropertyTypeEnum::INTEGER:
80+
case OpenApi3PropertyTypeEnum::BOOLEAN:
81+
case OpenApi3PropertyTypeEnum::NUMBER:
82+
$validations[$name][] = "'{$type->toLaravelValidationRule()->value}'";
83+
84+
break;
85+
case OpenApi3PropertyTypeEnum::STRING:
86+
switch ($format) {
87+
case OpenApi3PropertyFormatEnum::DATE:
88+
case OpenApi3PropertyFormatEnum::DATE_TIME:
89+
case OpenApi3PropertyFormatEnum::PASSWORD:
90+
case OpenApi3PropertyFormatEnum::EMAIL:
91+
case OpenApi3PropertyFormatEnum::IPV4:
92+
case OpenApi3PropertyFormatEnum::IPV6:
93+
case OpenApi3PropertyFormatEnum::TIMEZONE:
94+
case OpenApi3PropertyFormatEnum::PHONE:
95+
case OpenApi3PropertyFormatEnum::URL:
96+
case OpenApi3PropertyFormatEnum::UUID:
97+
$validations[$name][] = "'{$format->toLaravelValidationRule()->value}'";
98+
99+
break;
100+
case OpenApi3PropertyFormatEnum::BINARY:
101+
$validations[$name][] = "'". LaravelValidationRuleEnum::FILE->value . "'";
102+
103+
break;
104+
default:
105+
$validations[$name][] = "'{$type->toLaravelValidationRule()->value}'";
106+
107+
break;
108+
}
109+
110+
break;
111+
case OpenApi3PropertyTypeEnum::OBJECT:
112+
foreach ($this->object->properties ?? [] as $property) {
113+
[$currentValidations, $currentEnums] = $property->getLaravelValidationsAndEnums($options, $validations, $enums, "{$name}.");
114+
$validations = array_merge($validations, $currentValidations);
115+
$enums = array_merge($enums, $currentEnums);
116+
}
117+
118+
break;
119+
case OpenApi3PropertyTypeEnum::ARRAY:
120+
$validations[$name][] = "'{$type->toLaravelValidationRule()->value}'";
121+
[$currentValidations, $currentEnums] = $this->items->getLaravelValidationsAndEnums($options, $validations, $enums, "{$name}.*");
122+
$validations = array_merge($validations, $currentValidations);
123+
$enums = array_merge($enums, $currentEnums);
124+
}
125+
126+
return [$validations, $enums];
127+
}
128+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Data\OpenApi3;
4+
5+
use Ensi\LaravelOpenApiServerGenerator\Enums\OpenApi3ContentTypeEnum;
6+
use stdClass;
7+
8+
class OpenApi3Schema
9+
{
10+
public OpenApi3ContentTypeEnum $contentType;
11+
public OpenApi3Object $object;
12+
13+
public function __construct()
14+
{
15+
$this->object = new OpenApi3Object();
16+
}
17+
18+
public function fillFromStdRequestBody(OpenApi3ContentTypeEnum $contentType, stdClass $requestBody): void
19+
{
20+
switch ($contentType) {
21+
case OpenApi3ContentTypeEnum::APPLICATION_JSON:
22+
$schema = $requestBody->content->{OpenApi3ContentTypeEnum::APPLICATION_JSON->value}->schema;
23+
if (std_object_has($schema, 'allOf')) {
24+
foreach ($schema->allOf as $object) {
25+
$this->object->fillFromStdObject($object);
26+
}
27+
} else {
28+
$this->object->fillFromStdObject($schema);
29+
}
30+
31+
break;
32+
case OpenApi3ContentTypeEnum::MULTIPART_FROM_DATA:
33+
$this->object->fillFromStdObject(
34+
$requestBody->content
35+
->{OpenApi3ContentTypeEnum::MULTIPART_FROM_DATA->value}
36+
->schema
37+
);
38+
39+
break;
40+
}
41+
}
42+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Enums;
4+
5+
enum LaravelValidationRuleEnum: string
6+
{
7+
// types
8+
case INTEGER = 'integer';
9+
case STRING = 'string';
10+
case BOOLEAN = 'boolean';
11+
case NUMERIC = 'numeric';
12+
case ARRAY = 'array';
13+
case FILE = 'file';
14+
15+
// formats
16+
case DATE = 'date';
17+
case DATE_TIME = 'date_format:Y-m-d\TH:i:s.u\Z';
18+
case PASSWORD = 'password';
19+
case EMAIL = 'email';
20+
case IPV4 = 'ipv4';
21+
case IPV6 = 'ipv6';
22+
case TIMEZONE = 'timezone';
23+
case PHONE = 'regex:/^\+7\d{10}$/';
24+
case URL = 'url';
25+
case UUID = 'uuid';
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Enums;
4+
5+
enum OpenApi3ContentTypeEnum: string
6+
{
7+
case APPLICATION_JSON = 'application/json';
8+
case MULTIPART_FROM_DATA = 'multipart/form-data';
9+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Ensi\LaravelOpenApiServerGenerator\Enums;
4+
5+
enum OpenApi3PropertyFormatEnum: string
6+
{
7+
// build-in
8+
case DATE = 'date';
9+
case DATE_TIME = 'date-time';
10+
case PASSWORD = 'password';
11+
case BYTE = 'byte';
12+
case BINARY = 'binary';
13+
14+
// custom
15+
case EMAIL = 'email';
16+
case IPV4 = 'ipv4';
17+
case IPV6 = 'ipv6';
18+
case TIMEZONE = 'timezone';
19+
case PHONE = 'phone';
20+
case URL = 'url';
21+
case UUID = 'uuid';
22+
23+
public function toLaravelValidationRule(): LaravelValidationRuleEnum
24+
{
25+
return match ($this) {
26+
OpenApi3PropertyFormatEnum::DATE => LaravelValidationRuleEnum::DATE,
27+
OpenApi3PropertyFormatEnum::DATE_TIME => LaravelValidationRuleEnum::DATE_TIME,
28+
OpenApi3PropertyFormatEnum::PASSWORD => LaravelValidationRuleEnum::PASSWORD,
29+
OpenApi3PropertyFormatEnum::BINARY => LaravelValidationRuleEnum::FILE,
30+
OpenApi3PropertyFormatEnum::EMAIL => LaravelValidationRuleEnum::EMAIL,
31+
OpenApi3PropertyFormatEnum::IPV4 => LaravelValidationRuleEnum::IPV4,
32+
OpenApi3PropertyFormatEnum::IPV6 => LaravelValidationRuleEnum::IPV6,
33+
OpenApi3PropertyFormatEnum::TIMEZONE => LaravelValidationRuleEnum::TIMEZONE,
34+
OpenApi3PropertyFormatEnum::PHONE => LaravelValidationRuleEnum::PHONE,
35+
OpenApi3PropertyFormatEnum::URL => LaravelValidationRuleEnum::URL,
36+
OpenApi3PropertyFormatEnum::UUID => LaravelValidationRuleEnum::UUID,
37+
};
38+
}
39+
}

0 commit comments

Comments
 (0)