Skip to content

Commit 984af7f

Browse files
authored
Merge pull request #20 from spiral/issue/10
feat: add Additional Properties Extractor for dynamic object support
2 parents 93cfbdf + d23bb51 commit 984af7f

16 files changed

+1075
-84
lines changed

README.md

Lines changed: 212 additions & 80 deletions
Large diffs are not rendered by default.

context.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,3 @@ documents:
2222
- type: file
2323
sourcePaths:
2424
- src
25-
- tests/Unit
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Attribute;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
8+
final readonly class AdditionalProperties
9+
{
10+
/**
11+
* @param string $valueType The type of values for additional properties (e.g., 'string', 'int', 'number', 'boolean', 'mixed')
12+
* @param class-string|null $valueClass Optional class reference for object-typed additional properties
13+
*/
14+
public function __construct(
15+
public string $valueType,
16+
public ?string $valueClass = null,
17+
) {}
18+
}

src/Schema/Property.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ public function jsonSerialize(): array
4444

4545
// Check if we have an array shape constraint that should override the type
4646
if (isset($this->validationRules['type']) && $this->validationRules['type'] === 'object') {
47-
// Array shape overrides normal type processing
47+
// Array shape or additional properties overrides normal type processing
4848
$property = \array_merge($property, $this->validationRules);
49+
50+
// Clean up internal metadata keys
51+
unset($property['_additionalPropertiesClass']);
52+
4953
return $property;
5054
}
5155

@@ -61,7 +65,7 @@ public function jsonSerialize(): array
6165
// Apply validation rules from PHPDoc constraints (except type overrides)
6266
$filteredValidationRules = $this->validationRules;
6367
if (isset($filteredValidationRules['type'])) {
64-
unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties']);
68+
unset($filteredValidationRules['type'], $filteredValidationRules['properties'], $filteredValidationRules['required'], $filteredValidationRules['additionalProperties'], $filteredValidationRules['_additionalPropertiesClass']);
6569
}
6670
$property = \array_merge($property, $filteredValidationRules);
6771

@@ -84,6 +88,11 @@ public function getDependencies(): array
8488
}
8589
}
8690

91+
// Extract dependencies from additional properties references
92+
if (isset($this->validationRules['_additionalPropertiesClass'])) {
93+
$dependencies[] = $this->validationRules['_additionalPropertiesClass'];
94+
}
95+
8796
return $dependencies;
8897
}
8998

@@ -107,7 +116,11 @@ protected function propertyTypeToDefinition(PropertyType $propertyType): array
107116
}
108117
$property['items']['anyOf'][] = $schemaType;
109118
} else {
110-
$property['items']['anyOf'][] = ['$ref' => (new Reference($collectionType->type))->jsonSerialize()];
119+
$property['items']['anyOf'][] = [
120+
'$ref' => (new Reference(
121+
$collectionType->type,
122+
))->jsonSerialize(),
123+
];
111124
}
112125
}
113126
} elseif ($collectionTypeCount === 1) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\JsonSchemaGenerator\Validation;
6+
7+
use Spiral\JsonSchemaGenerator\Attribute\AdditionalProperties;
8+
use Spiral\JsonSchemaGenerator\Parser\PropertyInterface;
9+
use Spiral\JsonSchemaGenerator\Schema\Reference;
10+
use Spiral\JsonSchemaGenerator\Schema\Type;
11+
12+
final readonly class AdditionalPropertiesExtractor implements PropertyDataExtractorInterface
13+
{
14+
public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array
15+
{
16+
$validationRules = [];
17+
18+
// Only process array types for additional properties
19+
if ($jsonSchemaType !== Type::Array) {
20+
return $validationRules;
21+
}
22+
23+
$additionalProperties = $property->findAttribute(AdditionalProperties::class);
24+
if (!$additionalProperties instanceof AdditionalProperties) {
25+
return $validationRules;
26+
}
27+
28+
// Override array type to object type for additional properties
29+
$validationRules['type'] = 'object';
30+
31+
// Process the additional properties value type
32+
$additionalPropertiesSchema = $this->processValueType(
33+
$additionalProperties->valueType,
34+
$additionalProperties->valueClass,
35+
);
36+
37+
$validationRules['additionalProperties'] = $additionalPropertiesSchema;
38+
39+
// Store class dependencies for later extraction
40+
if ($additionalProperties->valueType === 'object' && $additionalProperties->valueClass !== null) {
41+
$validationRules['_additionalPropertiesClass'] = $additionalProperties->valueClass;
42+
}
43+
44+
return $validationRules;
45+
}
46+
47+
/**
48+
* Process the value type and return appropriate JSON schema structure.
49+
*
50+
* @param class-string|null $valueClass
51+
*/
52+
private function processValueType(string $valueType, ?string $valueClass): array|bool
53+
{
54+
return match ($valueType) {
55+
'int', 'integer' => ['type' => 'integer'],
56+
'number', 'float' => ['type' => 'number'],
57+
'boolean', 'bool' => ['type' => 'boolean'],
58+
'mixed' => true, // Allow any type
59+
'object' => $this->processObjectType($valueClass),
60+
default => ['type' => 'string'], // fallback to string
61+
};
62+
}
63+
64+
/**
65+
* Process object type with class reference.
66+
*
67+
* @param class-string|null $valueClass
68+
*/
69+
private function processObjectType(?string $valueClass): array
70+
{
71+
if ($valueClass === null) {
72+
return ['type' => 'object'];
73+
}
74+
75+
// Create reference to the class definition
76+
return ['$ref' => (new Reference($valueClass))->jsonSerialize()];
77+
}
78+
}

src/Validation/CompositePropertyDataExtractor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static function createDefault(): self
2424
return new self([
2525
new PhpDocValidationConstraintExtractor(),
2626
new AttributeConstraintExtractor(),
27+
new AdditionalPropertiesExtractor(), // Add the new extractor
2728
]);
2829
}
2930

0 commit comments

Comments
 (0)