Skip to content

Commit 36d1927

Browse files
author
Daniel Held
committed
feat: enhance schema generation with request body examples and flexible constraints
1 parent 7b0ea90 commit 36d1927

File tree

4 files changed

+129
-8
lines changed

4 files changed

+129
-8
lines changed

src/Services/CustomValidationRuleAnalyzer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ private function extractConstraintsFromRule(object $rule, ReflectionClass $refle
178178
$value = $property->getValue($rule);
179179

180180
match ($name) {
181+
// Store min/max as temporary values - they will be converted based on type later
181182
'min', 'minimum' => $constraints['minimum'] = (int) $value,
182183
'max', 'maximum' => $constraints['maximum'] = (int) $value,
183184
'minLength', 'min_length' => $constraints['minLength'] = (int) $value,

src/Services/EnhancedValidationRuleAnalyzer.php

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,60 @@ public function parseValidationRules(array $rules): array
9393
}
9494
}
9595

96+
// Handle min/max based on the final determined type
97+
// For strings, use minLength/maxLength; for arrays, use minItems/maxItems; for numbers, use minimum/maximum
98+
if ($type === 'string') {
99+
// Convert minimum/maximum to minLength/maxLength for strings
100+
if ($minimum !== null) {
101+
$minLength = $minimum;
102+
$minimum = null;
103+
// Update description to reflect string length constraint
104+
$description = array_map(function ($desc) use ($minLength) {
105+
return str_replace("Minimum constraint: {$minLength}.", "Minimum length: {$minLength}.", $desc);
106+
}, $description);
107+
}
108+
if ($maximum !== null) {
109+
$maxLength = $maximum;
110+
$maximum = null;
111+
// Update description to reflect string length constraint
112+
$description = array_map(function ($desc) use ($maxLength) {
113+
return str_replace("Maximum constraint: {$maxLength}.", "Maximum length: {$maxLength}.", $desc);
114+
}, $description);
115+
}
116+
} elseif ($type === 'array') {
117+
// For arrays, convert to minItems/maxItems
118+
$minItems = null;
119+
$maxItems = null;
120+
if ($minimum !== null) {
121+
$minItems = $minimum;
122+
$minimum = null;
123+
// Update description to reflect array items constraint
124+
$description = array_map(function ($desc) use ($minItems) {
125+
return str_replace("Minimum constraint: {$minItems}.", "Minimum items: {$minItems}.", $desc);
126+
}, $description);
127+
}
128+
if ($maximum !== null) {
129+
$maxItems = $maximum;
130+
$maximum = null;
131+
// Update description to reflect array items constraint
132+
$description = array_map(function ($desc) use ($maxItems) {
133+
return str_replace("Maximum constraint: {$maxItems}.", "Maximum items: {$maxItems}.", $desc);
134+
}, $description);
135+
}
136+
} else {
137+
// For numeric types, update descriptions to reflect value constraints
138+
if ($minimum !== null) {
139+
$description = array_map(function ($desc) use ($minimum) {
140+
return str_replace("Minimum constraint: {$minimum}.", "Minimum value: {$minimum}.", $desc);
141+
}, $description);
142+
}
143+
if ($maximum !== null) {
144+
$description = array_map(function ($desc) use ($maximum) {
145+
return str_replace("Maximum constraint: {$maximum}.", "Maximum value: {$maximum}.", $desc);
146+
}, $description);
147+
}
148+
}
149+
96150
// Build the result
97151
$result = [
98152
'type' => $type,
@@ -131,6 +185,14 @@ public function parseValidationRules(array $rules): array
131185
$result['maxLength'] = $maxLength;
132186
}
133187

188+
if (isset($minItems) && $minItems !== null) {
189+
$result['minItems'] = $minItems;
190+
}
191+
192+
if (isset($maxItems) && $maxItems !== null) {
193+
$result['maxItems'] = $maxItems;
194+
}
195+
134196
if ($pattern !== null) {
135197
$result['pattern'] = $pattern;
136198
}
@@ -145,6 +207,9 @@ public function parseValidationRules(array $rules): array
145207

146208
if ($type === 'array' && $items !== null) {
147209
$result['items'] = $items;
210+
} elseif ($type === 'array' && $items === null) {
211+
// If items is not set but type is array, set a default
212+
$result['items'] = ['type' => 'string'];
148213
}
149214

150215
if (! empty($conditionalRequired)) {
@@ -543,7 +608,7 @@ private function processRuleName(string $ruleName, array $ruleParams): array
543608
private function processBasicRule(string $ruleName, array $ruleParams): array
544609
{
545610
$result = [
546-
'type' => 'string',
611+
'type' => null, // Don't set a default type - let type-specific rules set it
547612
'format' => null,
548613
'required' => false,
549614
'nullable' => false,
@@ -638,6 +703,27 @@ private function processBasicRule(string $ruleName, array $ruleParams): array
638703
$result['description'] = 'Must be a valid date.';
639704
break;
640705

706+
case 'date_format':
707+
$result['type'] = 'string';
708+
$result['format'] = 'date-time';
709+
if (! empty($ruleParams[0])) {
710+
$result['description'] = "Must match the format: {$ruleParams[0]}.";
711+
$result['example'] = date($ruleParams[0]);
712+
} else {
713+
$result['description'] = 'Must match the specified date format.';
714+
}
715+
break;
716+
717+
case 'between':
718+
if (count($ruleParams) >= 2) {
719+
$min = (int) $ruleParams[0];
720+
$max = (int) $ruleParams[1];
721+
$result['minimum'] = $min;
722+
$result['maximum'] = $max;
723+
$result['description'] = "Value between {$min} and {$max}.";
724+
}
725+
break;
726+
641727
case 'in':
642728
if (! empty($ruleParams)) {
643729
$result['enum'] = $ruleParams;
@@ -647,16 +733,18 @@ private function processBasicRule(string $ruleName, array $ruleParams): array
647733
break;
648734

649735
case 'min':
650-
if (! empty($ruleParams[0])) {
736+
if (isset($ruleParams[0]) && $ruleParams[0] !== '') {
651737
$result['minimum'] = (int) $ruleParams[0];
652-
$result['description'] = "Minimum value: {$ruleParams[0]}.";
738+
// Description will be set correctly in post-processing based on type
739+
$result['description'] = "Minimum constraint: {$ruleParams[0]}.";
653740
}
654741
break;
655742

656743
case 'max':
657-
if (! empty($ruleParams[0])) {
744+
if (isset($ruleParams[0]) && $ruleParams[0] !== '') {
658745
$result['maximum'] = (int) $ruleParams[0];
659-
$result['description'] = "Maximum value: {$ruleParams[0]}.";
746+
// Description will be set correctly in post-processing based on type
747+
$result['description'] = "Maximum constraint: {$ruleParams[0]}.";
660748
}
661749
break;
662750
}

src/Services/OpenApi.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,12 +520,20 @@ private function processOperationRequestBody(Operation $operation, array $route,
520520
$this->enhanceSchemaWithAstAnalysis($schema, $methodReflection);
521521
}
522522

523+
$requestBodyExample = $this->generateRequestBodyExample($route['parameters']);
524+
525+
$mediaTypeConfig = [
526+
'schema' => $schema,
527+
];
528+
529+
if (!empty($requestBodyExample)) {
530+
$mediaTypeConfig['example'] = $requestBodyExample;
531+
}
532+
523533
$requestBody = new RequestBody([
524534
'required' => true,
525535
'content' => [
526-
'application/json' => new MediaType([
527-
'schema' => $schema,
528-
]),
536+
'application/json' => new MediaType($mediaTypeConfig),
529537
],
530538
]);
531539
}
@@ -822,6 +830,21 @@ private function buildResponseSchema(array $schemaData): Schema
822830
return new Schema($schema);
823831
}
824832

833+
private function generateRequestBodyExample(array $parameters): array
834+
{
835+
$example = [];
836+
837+
foreach ($parameters as $name => $param) {
838+
if (!isset($param['example']) || $param['example'] === null) {
839+
continue;
840+
}
841+
842+
$example[$name] = $param['example'];
843+
}
844+
845+
return $example;
846+
}
847+
825848
private function buildRequestBodySchema(array $parameters): Schema
826849
{
827850
$properties = [];

src/Services/RequestAnalyzer.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ public function __construct(private readonly Repository $configuration)
3939
$this->nestedArrayAnalyzer = new NestedArrayValidationAnalyzer($this->enhancedAnalyzer);
4040
}
4141

42+
/**
43+
* Parse validation rules into OpenAPI schema
44+
* This method delegates to EnhancedValidationRuleAnalyzer
45+
*/
46+
protected function parseValidationRules(array $rules): array
47+
{
48+
return $this->enhancedAnalyzer->parseValidationRules($rules);
49+
}
50+
4251
/**
4352
* Extract raw validation rules from FormRequest class for error response generation
4453
*/

0 commit comments

Comments
 (0)