refactor(schemas): use standardized schema shapes (#33)
* refactor(schemas): use standardized schema shapes * refactor: apply rector * chore: fix types * test(schemas): add missing tests * chore(schemas): drop null schema * feat(schemas): cover json rule
This commit is contained in:
@@ -7,14 +7,12 @@ use Illuminate\Support\Str;
|
||||
use Sunchayn\Nimbus\Modules\Routes\DataTransferObjects\ExtractedRoute;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type SchemaShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\Schema
|
||||
*
|
||||
* @phpstan-type RouteDefinitionShape array{
|
||||
* uri: string,
|
||||
* shortUri: string,
|
||||
* methods: string[],
|
||||
* schema: SchemaShape,
|
||||
* extractionError: string,
|
||||
* schema: array<string, mixed>,
|
||||
* extractionError: string|null,
|
||||
* }
|
||||
*
|
||||
* @extends Collection<array-key, ExtractedRoute>
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\Builders;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\StringFormat;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\RulesMapper\RuleToSchemaMapper;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\ArraySchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\BooleanSchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\IntegerSchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\NumberSchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\ObjectSchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\Schema;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\StringSchemaProperty;
|
||||
|
||||
/**
|
||||
* Converts Laravel validation rules into individual schema properties.
|
||||
@@ -16,7 +25,6 @@ use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty;
|
||||
* Output: SchemaProperty with type="string", format="email", required=true
|
||||
*
|
||||
* @phpstan-import-type NormalizedRulesShape from \Sunchayn\Nimbus\Modules\Schemas\Collections\Ruleset
|
||||
* @phpstan-import-type SchemaPropertyFormatsShape from SchemaProperty
|
||||
*/
|
||||
class PropertyBuilder
|
||||
{
|
||||
@@ -27,26 +35,53 @@ class PropertyBuilder
|
||||
/**
|
||||
* @param NormalizedRulesShape $rules
|
||||
*/
|
||||
public function buildPropertyFromRules(string $field, array $rules): SchemaProperty
|
||||
public function buildPropertyFromRules(string $field, array $rules): SchemaPropertyInterface
|
||||
{
|
||||
$schemaMetadata = $this->ruleToSchemaMapper->convertRulesToBaseSchemaPropertyMetadata($rules);
|
||||
|
||||
return new SchemaProperty(
|
||||
name: $field,
|
||||
type: $schemaMetadata['type'],
|
||||
required: $schemaMetadata['required'],
|
||||
format: $this->extractFormat($rules),
|
||||
enum: $schemaMetadata['enum'] ?? null,
|
||||
minimum: $schemaMetadata['minimum'],
|
||||
maximum: $schemaMetadata['maximum'],
|
||||
);
|
||||
return match ($schemaMetadata['type']) {
|
||||
SchemaPropertyType::STRING => new StringSchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
stringFormat: $this->extractFormat($rules),
|
||||
enum: $schemaMetadata['enum'] ?? null,
|
||||
minLength: $schemaMetadata['minimum'],
|
||||
maxLength: $schemaMetadata['maximum'],
|
||||
),
|
||||
SchemaPropertyType::INTEGER => new IntegerSchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
minimum: $schemaMetadata['minimum'],
|
||||
maximum: $schemaMetadata['maximum'],
|
||||
enum: $schemaMetadata['enum'] ?? null,
|
||||
),
|
||||
SchemaPropertyType::NUMBER => new NumberSchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
minimum: $schemaMetadata['minimum'],
|
||||
maximum: $schemaMetadata['maximum'],
|
||||
),
|
||||
SchemaPropertyType::BOOLEAN => new BooleanSchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
),
|
||||
SchemaPropertyType::ARRAY => new ArraySchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
schemaProperty: null, // <- Items will be set later by SchemaBuilder if needed.
|
||||
),
|
||||
SchemaPropertyType::OBJECT => new ObjectSchemaProperty(
|
||||
name: $field,
|
||||
required: $schemaMetadata['required'],
|
||||
schema: new Schema([]), // <- Properties will be set later by SchemaBuilder if needed.
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NormalizedRulesShape $rules
|
||||
* @return SchemaPropertyFormatsShape|null
|
||||
*/
|
||||
private function extractFormat(array $rules): ?string
|
||||
private function extractFormat(array $rules): ?StringFormat
|
||||
{
|
||||
foreach ($rules as $rule) {
|
||||
if (! is_string($rule)) {
|
||||
@@ -55,7 +90,7 @@ class PropertyBuilder
|
||||
|
||||
$format = $this->detectFormatFromRule($rule);
|
||||
|
||||
if ($format !== null) {
|
||||
if ($format instanceof \Sunchayn\Nimbus\Modules\Schemas\Enums\StringFormat) {
|
||||
return $format;
|
||||
}
|
||||
}
|
||||
@@ -63,16 +98,8 @@ class PropertyBuilder
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SchemaPropertyFormatsShape|null
|
||||
*/
|
||||
private function detectFormatFromRule(string $rule): ?string
|
||||
private function detectFormatFromRule(string $rule): ?StringFormat
|
||||
{
|
||||
return match ($rule) {
|
||||
'email' => 'email',
|
||||
'uuid' => 'uuid',
|
||||
'date' => 'date-time',
|
||||
default => null,
|
||||
};
|
||||
return StringFormat::fromRule($rule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Sunchayn\Nimbus\Modules\Routes\ValueObjects\RulesExtractionError;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Collections\Ruleset;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\RulesFieldType;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\ArraySchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\FieldPath;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\ObjectSchemaProperty;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\PathSegment;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\Schema;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty;
|
||||
|
||||
/**
|
||||
* Converts Laravel validation rules into JSON Schema structures.
|
||||
@@ -45,7 +47,7 @@ class SchemaBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, SchemaProperty>
|
||||
* @return array<string, SchemaPropertyInterface>
|
||||
*/
|
||||
private function buildProperties(Ruleset $ruleset): array
|
||||
{
|
||||
@@ -96,9 +98,9 @@ class SchemaBuilder
|
||||
/**
|
||||
* Adds a simple array property (e.g., "tags.*" => "string").
|
||||
*
|
||||
* @param array<string, SchemaProperty> $properties
|
||||
* @param array<string, SchemaPropertyInterface> $properties
|
||||
* @param NormalizedRulesShape $rules
|
||||
* @return array<string, SchemaProperty>
|
||||
* @return array<string, SchemaPropertyInterface>
|
||||
*/
|
||||
private function addSimpleArrayProperty(FieldPath $fieldPath, array $rules, array $properties): array
|
||||
{
|
||||
@@ -116,11 +118,10 @@ class SchemaBuilder
|
||||
rules: $rules,
|
||||
);
|
||||
|
||||
$properties[$arrayName] = new SchemaProperty(
|
||||
$properties[$arrayName] = new ArraySchemaProperty(
|
||||
name: $arrayName,
|
||||
type: 'array',
|
||||
required: $existingProperty->required ?? false,
|
||||
itemsSchema: $schemaProperty,
|
||||
required: $existingProperty?->isRequired() ?? false,
|
||||
schemaProperty: $schemaProperty,
|
||||
);
|
||||
|
||||
return $properties;
|
||||
@@ -134,9 +135,9 @@ class SchemaBuilder
|
||||
* - "users.*.email" → array of objects with email property
|
||||
* - "company.teams.*.members.*.name" → deeply nested arrays
|
||||
*
|
||||
* @param array<string, SchemaProperty> $properties
|
||||
* @param array<string, SchemaPropertyInterface> $properties
|
||||
* @param NormalizedRulesShape $rules
|
||||
* @return array<string, SchemaProperty>
|
||||
* @return array<string, SchemaPropertyInterface>
|
||||
*/
|
||||
private function addDotNotationStructure(FieldPath $fieldPath, array $rules, array $properties): array
|
||||
{
|
||||
@@ -190,10 +191,10 @@ class SchemaBuilder
|
||||
* @param PathSegment[] $segments
|
||||
*/
|
||||
private function buildNestedStructure(
|
||||
SchemaProperty $schemaProperty,
|
||||
SchemaPropertyInterface $schemaProperty,
|
||||
array $segments,
|
||||
array $rules
|
||||
): SchemaProperty {
|
||||
): SchemaPropertyInterface {
|
||||
if ($segments === []) {
|
||||
return $schemaProperty;
|
||||
}
|
||||
@@ -214,20 +215,22 @@ class SchemaBuilder
|
||||
* @param PathSegment[] $segments
|
||||
*/
|
||||
private function convertPropertyToArray(
|
||||
SchemaProperty $schemaProperty,
|
||||
SchemaPropertyInterface $schemaProperty,
|
||||
array $segments,
|
||||
array $rules
|
||||
): SchemaProperty {
|
||||
$itemObject = $schemaProperty->itemsSchema ?? $this->createEmptyObject(name: 'item');
|
||||
): SchemaPropertyInterface {
|
||||
// Get existing item schema if this is already an array, otherwise create new empty object
|
||||
$itemObject = ($schemaProperty instanceof ArraySchemaProperty)
|
||||
? $schemaProperty->getItemsSchema() ?? $this->createEmptyObject(name: 'item')
|
||||
: $this->createEmptyObject(name: 'item');
|
||||
|
||||
// Build the item structure from remaining segments.
|
||||
$itemSchema = $this->buildNestedStructure($itemObject, $segments, $rules);
|
||||
|
||||
return new SchemaProperty(
|
||||
name: $schemaProperty->name,
|
||||
type: 'array',
|
||||
required: $schemaProperty->required,
|
||||
itemsSchema: $itemSchema,
|
||||
return new ArraySchemaProperty(
|
||||
name: $schemaProperty->getName(),
|
||||
required: $schemaProperty->isRequired(),
|
||||
schemaProperty: $itemSchema,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -238,25 +241,28 @@ class SchemaBuilder
|
||||
* @param PathSegment[] $remainingSegments
|
||||
*/
|
||||
private function addPropertyToStructure(
|
||||
SchemaProperty $schemaProperty,
|
||||
SchemaPropertyInterface $schemaProperty,
|
||||
PathSegment $pathSegment,
|
||||
array $remainingSegments,
|
||||
array $rules
|
||||
): SchemaProperty {
|
||||
): SchemaPropertyInterface {
|
||||
$propertyName = $pathSegment->value;
|
||||
|
||||
$existingSchema = $schemaProperty->propertiesSchema ?? new Schema([]);
|
||||
// Get existing schema from object property
|
||||
$existingSchema = ($schemaProperty instanceof ObjectSchemaProperty)
|
||||
? $schemaProperty->getPropertiesSchema() ?? new Schema([])
|
||||
: new Schema([]);
|
||||
|
||||
/** @var Collection<string, SchemaProperty> $properties */
|
||||
$properties = Collection::make($existingSchema->properties)->keyBy('name');
|
||||
/** @var Collection<string, SchemaPropertyInterface> $properties */
|
||||
$properties = Collection::make($existingSchema->properties)->keyBy(fn ($p): string => $p->getName());
|
||||
|
||||
// If this is a leaf, build the final property with rules.
|
||||
if ($pathSegment->isLeaf) {
|
||||
$newProperty = $this->propertyBuilder->buildPropertyFromRules($propertyName, $rules);
|
||||
|
||||
$properties->put($newProperty->name, $newProperty);
|
||||
$properties->put($newProperty->getName(), $newProperty);
|
||||
|
||||
return $this->rebuildObjectPropertyWithNewSchema($schemaProperty, $properties->all());
|
||||
return $this->rebuildObjectPropertyWithNewSchema($schemaProperty, $properties->values()->all());
|
||||
}
|
||||
|
||||
// Otherwise, create/get intermediate object and recurse.
|
||||
@@ -265,40 +271,36 @@ class SchemaBuilder
|
||||
|
||||
$updatedProperty = $this->buildNestedStructure($intermediateProperty, $remainingSegments, $rules);
|
||||
|
||||
$properties->put($updatedProperty->name, $updatedProperty);
|
||||
$properties->put($updatedProperty->getName(), $updatedProperty);
|
||||
|
||||
return $this->rebuildObjectPropertyWithNewSchema($schemaProperty, $properties->all());
|
||||
return $this->rebuildObjectPropertyWithNewSchema($schemaProperty, $properties->values()->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds a property with updated child properties.
|
||||
*
|
||||
* @param SchemaProperty[] $properties
|
||||
* @param SchemaPropertyInterface[] $properties
|
||||
*/
|
||||
private function rebuildObjectPropertyWithNewSchema(
|
||||
SchemaProperty $schemaProperty,
|
||||
SchemaPropertyInterface $schemaProperty,
|
||||
array $properties
|
||||
): SchemaProperty {
|
||||
return new SchemaProperty(
|
||||
name: $schemaProperty->name,
|
||||
type: 'object',
|
||||
required: $schemaProperty->required,
|
||||
format: $schemaProperty->format,
|
||||
enum: $schemaProperty->enum,
|
||||
propertiesSchema: new Schema($properties),
|
||||
): SchemaPropertyInterface {
|
||||
return new ObjectSchemaProperty(
|
||||
name: $schemaProperty->getName(),
|
||||
required: $schemaProperty->isRequired(),
|
||||
schema: new Schema($properties),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty object property.
|
||||
*/
|
||||
private function createEmptyObject(string $name): SchemaProperty
|
||||
private function createEmptyObject(string $name): SchemaPropertyInterface
|
||||
{
|
||||
return new SchemaProperty(
|
||||
return new ObjectSchemaProperty(
|
||||
name: $name,
|
||||
type: 'object',
|
||||
required: false,
|
||||
propertiesSchema: new Schema([])
|
||||
schema: new Schema([])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Modules/Schemas/Contracts/SchemaPropertyInterface.php
Normal file
40
src/Modules/Schemas/Contracts/SchemaPropertyInterface.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\Contracts;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Interface for all schema property types.
|
||||
*
|
||||
* Defines the contract that all schema properties must implement,
|
||||
* ensuring consistent serialization to both internal format (with custom properties)
|
||||
* and standard JSON Schema format.
|
||||
*/
|
||||
interface SchemaPropertyInterface
|
||||
{
|
||||
/**
|
||||
* Get the property name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Check if the property is required.
|
||||
*/
|
||||
public function isRequired(): bool;
|
||||
|
||||
/**
|
||||
* Get the JSON Schema type.
|
||||
*/
|
||||
public function getType(): SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Convert to standard JSON Schema format.
|
||||
*
|
||||
* This produces a pure JSON Schema Draft 2020-12 compliant structure
|
||||
* without custom extensions.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toJsonSchema(): array;
|
||||
}
|
||||
25
src/Modules/Schemas/Enums/SchemaPropertyType.php
Normal file
25
src/Modules/Schemas/Enums/SchemaPropertyType.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\Enums;
|
||||
|
||||
/**
|
||||
* JSON Schema property types.
|
||||
*
|
||||
* Represents the standard JSON Schema primitive and complex types.
|
||||
*
|
||||
* @see https://json-schema.org/understanding-json-schema/reference/type
|
||||
*/
|
||||
enum SchemaPropertyType: string
|
||||
{
|
||||
case STRING = 'string';
|
||||
|
||||
case INTEGER = 'integer';
|
||||
|
||||
case NUMBER = 'number';
|
||||
|
||||
case BOOLEAN = 'boolean';
|
||||
|
||||
case ARRAY = 'array';
|
||||
|
||||
case OBJECT = 'object';
|
||||
}
|
||||
39
src/Modules/Schemas/Enums/StringFormat.php
Normal file
39
src/Modules/Schemas/Enums/StringFormat.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\Enums;
|
||||
|
||||
/**
|
||||
* Supported JSON Schema string formats.
|
||||
*
|
||||
* @see https://json-schema.org/draft/2020-12/json-schema-validation#section-7.3
|
||||
*/
|
||||
enum StringFormat: string
|
||||
{
|
||||
case UUID = 'uuid';
|
||||
|
||||
case EMAIL = 'email';
|
||||
|
||||
case DATE_TIME = 'date-time';
|
||||
|
||||
case URL = 'url';
|
||||
|
||||
case URI = 'uri';
|
||||
|
||||
case DATE = 'date';
|
||||
|
||||
case TIME = 'time';
|
||||
|
||||
/**
|
||||
* Map common validation rule names to their corresponding format.
|
||||
*/
|
||||
public static function fromRule(string $rule): ?self
|
||||
{
|
||||
return match ($rule) {
|
||||
'uuid' => self::UUID,
|
||||
'email' => self::EMAIL,
|
||||
'date' => self::DATE_TIME, // Laravel's 'date' rule often maps to date-time in schema context
|
||||
'url' => self::URL,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,16 @@ namespace Sunchayn\Nimbus\Modules\Schemas\RulesMapper\Processors;
|
||||
|
||||
use BackedEnum;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
use UnitEnum;
|
||||
|
||||
/**
|
||||
* Processes `Enum` validation rules to extract enum values for schema generation.
|
||||
*
|
||||
* @phpstan-import-type SchemaPropertyEnumShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
*/
|
||||
class EnumRuleProcessor
|
||||
{
|
||||
/**
|
||||
* @return array{type: 'string', enum: SchemaPropertyEnumShape|null}
|
||||
* @return array{type: SchemaPropertyType, enum: ?non-empty-array<array-key, scalar>}
|
||||
*/
|
||||
public static function process(Enum $rule): array
|
||||
{
|
||||
@@ -22,7 +21,7 @@ class EnumRuleProcessor
|
||||
$enumClass = invade($rule)->type; // @phpstan-ignore-line
|
||||
|
||||
if (! enum_exists($enumClass)) {
|
||||
return ['type' => 'string', 'enum' => null];
|
||||
return ['type' => SchemaPropertyType::STRING, 'enum' => null];
|
||||
}
|
||||
|
||||
$values = array_map(
|
||||
@@ -31,9 +30,9 @@ class EnumRuleProcessor
|
||||
);
|
||||
|
||||
if ($values === []) {
|
||||
return ['type' => 'string', 'enum' => null];
|
||||
return ['type' => SchemaPropertyType::STRING, 'enum' => null];
|
||||
}
|
||||
|
||||
return ['type' => 'string', 'enum' => $values];
|
||||
return ['type' => SchemaPropertyType::STRING, 'enum' => $values];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,16 @@ namespace Sunchayn\Nimbus\Modules\Schemas\RulesMapper\Processors;
|
||||
|
||||
use BackedEnum;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
use UnitEnum;
|
||||
|
||||
/**
|
||||
* Processes `In` validation rules to extract allowed values for schema generation.
|
||||
*
|
||||
* @phpstan-import-type SchemaPropertyTypesShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
* @phpstan-import-type SchemaPropertyEnumShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
*/
|
||||
class InRuleProcessor
|
||||
{
|
||||
/**
|
||||
* @return array{type: 'string'|'integer', enum: SchemaPropertyEnumShape|null}
|
||||
* @return array{type: SchemaPropertyType::STRING | SchemaPropertyType::INTEGER, enum: ?non-empty-array<array-key, scalar>}
|
||||
*/
|
||||
public static function process(In $in): array
|
||||
{
|
||||
@@ -33,20 +31,20 @@ class InRuleProcessor
|
||||
$rawValues,
|
||||
);
|
||||
|
||||
/** @var array<array-key, scalar> $values */
|
||||
/** @var array<array-key, scalar>|array{} $values */
|
||||
$values = array_values(
|
||||
array_filter($values), // <- Removes null values.
|
||||
);
|
||||
|
||||
if (empty($values)) {
|
||||
return ['type' => 'string', 'enum' => null];
|
||||
return ['type' => SchemaPropertyType::STRING, 'enum' => null];
|
||||
}
|
||||
|
||||
$identityValue = $values[0];
|
||||
|
||||
$type = match (true) {
|
||||
is_int($identityValue) => 'integer',
|
||||
default => 'string',
|
||||
is_int($identityValue) => SchemaPropertyType::INTEGER,
|
||||
default => SchemaPropertyType::STRING,
|
||||
};
|
||||
|
||||
return ['type' => $type, 'enum' => $values];
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Sunchayn\Nimbus\Modules\Schemas\RulesMapper;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use Illuminate\Validation\ValidationRuleParser;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\RulesMapper\Processors\EnumRuleProcessor;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\RulesMapper\Processors\InRuleProcessor;
|
||||
|
||||
@@ -12,24 +13,18 @@ use Sunchayn\Nimbus\Modules\Schemas\RulesMapper\Processors\InRuleProcessor;
|
||||
* Converts Laravel validation rules into JSON Schema property definitions.
|
||||
*
|
||||
* @phpstan-import-type NormalizedRulesShape from \Sunchayn\Nimbus\Modules\Schemas\Collections\Ruleset
|
||||
* @phpstan-import-type SchemaPropertyTypesShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
* @phpstan-import-type SchemaPropertyFormatsShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
* @phpstan-import-type SchemaPropertyEnumShape from \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty
|
||||
*/
|
||||
class RuleToSchemaMapper
|
||||
{
|
||||
/**
|
||||
* Converts an array of Laravel validation rules into schema property data.
|
||||
*
|
||||
* Laravel's validation rules are processed sequentially, with later rules
|
||||
* potentially overriding earlier ones (e.g., 'string' then 'email').
|
||||
*
|
||||
* @param NormalizedRulesShape $rules
|
||||
* @return array{
|
||||
* type: SchemaPropertyTypesShape,
|
||||
* type: SchemaPropertyType,
|
||||
* required: bool,
|
||||
* format: SchemaPropertyFormatsShape|null,
|
||||
* enum: SchemaPropertyEnumShape|null,
|
||||
* format: string|null,
|
||||
* enum: ?non-empty-array<array-key, scalar>,
|
||||
* minimum: ?int,
|
||||
* maximum: ?int,
|
||||
* }
|
||||
@@ -37,7 +32,7 @@ class RuleToSchemaMapper
|
||||
public function convertRulesToBaseSchemaPropertyMetadata(array $rules): array
|
||||
{
|
||||
$shape = [
|
||||
'type' => 'string',
|
||||
'type' => SchemaPropertyType::STRING,
|
||||
'required' => false,
|
||||
'format' => null,
|
||||
'enum' => null,
|
||||
@@ -58,13 +53,14 @@ class RuleToSchemaMapper
|
||||
/**
|
||||
* Processes individual validation rules and returns the changes to apply.
|
||||
*
|
||||
* @return array{}|array{
|
||||
* type?: SchemaPropertyTypesShape,
|
||||
* format?: SchemaPropertyFormatsShape,
|
||||
* enum?: SchemaPropertyEnumShape|null,
|
||||
* @return array{
|
||||
* type?: SchemaPropertyType,
|
||||
* format?: string,
|
||||
* enum?: ?non-empty-array<array-key, scalar>,
|
||||
* minimum?: int,
|
||||
* maximum?: int,
|
||||
* }
|
||||
* required?: bool,
|
||||
* }|array{}
|
||||
*/
|
||||
private function processRule(mixed $rule): array
|
||||
{
|
||||
@@ -82,11 +78,12 @@ class RuleToSchemaMapper
|
||||
|
||||
return match ($ruleName) {
|
||||
'required' => ['required' => true],
|
||||
'string' => ['type' => 'string'],
|
||||
'integer' => ['type' => 'integer'],
|
||||
'numeric' => ['type' => 'number'],
|
||||
'boolean' => ['type' => 'boolean'],
|
||||
'array' => ['type' => 'array'],
|
||||
'string' => ['type' => SchemaPropertyType::STRING],
|
||||
'integer' => ['type' => SchemaPropertyType::INTEGER],
|
||||
'numeric' => ['type' => SchemaPropertyType::NUMBER],
|
||||
'boolean' => ['type' => SchemaPropertyType::BOOLEAN],
|
||||
'array' => ['type' => SchemaPropertyType::ARRAY],
|
||||
'json' => ['type' => SchemaPropertyType::OBJECT],
|
||||
'email' => $this->setFormat('email'),
|
||||
'uuid' => $this->setFormat('uuid'),
|
||||
'date' => $this->setFormat('date-time'),
|
||||
@@ -99,27 +96,21 @@ class RuleToSchemaMapper
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles custom validation rule objects.
|
||||
*
|
||||
* Analyzes specific rule types like Enum and In to extract constraint
|
||||
* information, falling back to string type for unknown rules.
|
||||
*
|
||||
* @return array{type: 'integer'|'string', enum?: SchemaPropertyEnumShape|null}
|
||||
* @return array{type: SchemaPropertyType, enum?: ?non-empty-array<array-key, scalar>}
|
||||
*/
|
||||
private function processObjectRule(object $rule): array
|
||||
{
|
||||
return match (true) {
|
||||
$rule instanceof Enum => EnumRuleProcessor::process($rule),
|
||||
$rule instanceof In => InRuleProcessor::process($rule),
|
||||
default => ['type' => 'string'],
|
||||
default => ['type' => SchemaPropertyType::STRING],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the format specification for the property.
|
||||
*
|
||||
* @param SchemaPropertyFormatsShape $format
|
||||
* @return array{format: SchemaPropertyFormatsShape, type?: 'string'}
|
||||
* @return array{format: string, type?: SchemaPropertyType}
|
||||
*/
|
||||
private function setFormat(string $format): array
|
||||
{
|
||||
@@ -127,20 +118,15 @@ class RuleToSchemaMapper
|
||||
|
||||
// Email, UUID, and date-time are all string-based formats in JSON Schema
|
||||
if (in_array($format, ['email', 'uuid', 'date-time'], true)) {
|
||||
$result['type'] = 'string';
|
||||
$result['type'] = SchemaPropertyType::STRING;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets enum constraints from validation rule parameters.
|
||||
*
|
||||
* The 'in' rule provides explicit allowed values that must be preserved
|
||||
* in the schema for proper validation.
|
||||
*
|
||||
* @param array<int, mixed> $params
|
||||
* @return array{enum: SchemaPropertyEnumShape}|array{}
|
||||
* @param array<array-key, scalar> $params
|
||||
* @return array{enum: non-empty-array<array-key, scalar>}|array{}
|
||||
*/
|
||||
private function setEnum(array $params): array
|
||||
{
|
||||
|
||||
73
src/Modules/Schemas/ValueObjects/ArraySchemaProperty.php
Normal file
73
src/Modules/Schemas/ValueObjects/ArraySchemaProperty.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Schema property for array types.
|
||||
*
|
||||
* Supports JSON Schema array validation including:
|
||||
* - Item schema definition (can be any property type)
|
||||
* - Size constraints (minItems, maxItems)
|
||||
*
|
||||
* Arrays can contain:
|
||||
* - Primitives (string, integer, boolean)
|
||||
* - Objects
|
||||
* - Nested arrays
|
||||
*/
|
||||
class ArraySchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
private readonly ?SchemaPropertyInterface $schemaProperty = null,
|
||||
private readonly ?int $minItems = null,
|
||||
private readonly ?int $maxItems = null,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the items schema (for nested structure building).
|
||||
*/
|
||||
public function getItemsSchema(): ?SchemaPropertyInterface
|
||||
{
|
||||
return $this->schemaProperty;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::ARRAY;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
$properties = [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
|
||||
if ($this->schemaProperty instanceof \Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface) {
|
||||
$properties['items'] = $this->schemaProperty->toJsonSchema();
|
||||
}
|
||||
|
||||
if ($this->minItems !== null) {
|
||||
$properties['minItems'] = $this->minItems;
|
||||
}
|
||||
|
||||
if ($this->maxItems !== null) {
|
||||
$properties['maxItems'] = $this->maxItems;
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
41
src/Modules/Schemas/ValueObjects/BooleanSchemaProperty.php
Normal file
41
src/Modules/Schemas/ValueObjects/BooleanSchemaProperty.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Schema property for boolean types.
|
||||
*
|
||||
* Simple boolean type with no additional constraints.
|
||||
*/
|
||||
class BooleanSchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::BOOLEAN;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
63
src/Modules/Schemas/ValueObjects/IntegerSchemaProperty.php
Normal file
63
src/Modules/Schemas/ValueObjects/IntegerSchemaProperty.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Schema property for integer types.
|
||||
*
|
||||
* Supports JSON Schema integer validation including:
|
||||
* - Range constraints (minimum, maximum)
|
||||
* - Enum constraints
|
||||
*/
|
||||
class IntegerSchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, scalar>|null $enum
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
private readonly ?int $minimum = null,
|
||||
private readonly ?int $maximum = null,
|
||||
private readonly ?array $enum = null,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::INTEGER;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
$properties = [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$properties['minimum'] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$properties['maximum'] = $this->maximum;
|
||||
}
|
||||
|
||||
if ($this->enum !== null && $this->enum !== []) {
|
||||
$properties['enum'] = $this->enum;
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
54
src/Modules/Schemas/ValueObjects/NumberSchemaProperty.php
Normal file
54
src/Modules/Schemas/ValueObjects/NumberSchemaProperty.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Schema property for number types (floating point).
|
||||
*
|
||||
* Supports JSON Schema number validation including:
|
||||
* - Range constraints (minimum, maximum)
|
||||
*/
|
||||
class NumberSchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
private readonly ?float $minimum = null,
|
||||
private readonly ?float $maximum = null,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::NUMBER;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
$properties = [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$properties['minimum'] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$properties['maximum'] = $this->maximum;
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
66
src/Modules/Schemas/ValueObjects/ObjectSchemaProperty.php
Normal file
66
src/Modules/Schemas/ValueObjects/ObjectSchemaProperty.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
|
||||
/**
|
||||
* Schema property for object types.
|
||||
*
|
||||
* Supports JSON Schema object validation including:
|
||||
* - Nested properties (defined via Schema)
|
||||
* - Required properties list
|
||||
* - Additional properties control
|
||||
*
|
||||
* Objects can contain any combination of property types,
|
||||
* including nested objects and arrays.
|
||||
*/
|
||||
class ObjectSchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
private readonly ?Schema $schema = null,
|
||||
private readonly bool $additionalProperties = false,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the properties schema (for nested structure building).
|
||||
*/
|
||||
public function getPropertiesSchema(): ?Schema
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::OBJECT;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
$result = [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
|
||||
if ($this->schema instanceof \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\Schema && ! $this->schema->isEmpty()) {
|
||||
$result['properties'] = $this->schema->toPropertiesArray();
|
||||
$result['required'] = $this->schema->getRequiredProperties();
|
||||
}
|
||||
|
||||
$result['additionalProperties'] = $this->additionalProperties;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -5,27 +5,26 @@ namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Sunchayn\Nimbus\Modules\Routes\ValueObjects\RulesExtractionError;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type SchemaPropertyShape from SchemaProperty
|
||||
*
|
||||
* @phpstan-type SchemaShape array{
|
||||
* '$schema': 'https://json-schema.org/draft/2020-12/schema',
|
||||
* type: 'object',
|
||||
* properties: array<string, SchemaPropertyShape>,
|
||||
* properties: array<string, array<string, mixed>>,
|
||||
* required: string[],
|
||||
* additionalProperties: false,
|
||||
* }
|
||||
*
|
||||
* @implements Arrayable<string, SchemaPropertyShape>
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class Schema implements Arrayable
|
||||
{
|
||||
/** @var SchemaProperty[] */
|
||||
/** @var SchemaPropertyInterface[] */
|
||||
public readonly array $properties;
|
||||
|
||||
/**
|
||||
* @param SchemaProperty[] $properties
|
||||
* @param SchemaPropertyInterface[] $properties
|
||||
*/
|
||||
public function __construct(
|
||||
array $properties,
|
||||
@@ -52,36 +51,49 @@ class Schema implements Arrayable
|
||||
public function getRequiredProperties(): array
|
||||
{
|
||||
return collect($this->properties)
|
||||
->filter(fn (SchemaProperty $schemaProperty): bool => $schemaProperty->required)
|
||||
->map(fn (SchemaProperty $schemaProperty): string => $schemaProperty->name)
|
||||
->filter(fn (SchemaPropertyInterface $schemaProperty): bool => $schemaProperty->isRequired())
|
||||
->map(fn (SchemaPropertyInterface $schemaProperty): string => $schemaProperty->getName())
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
/**
|
||||
* Convert properties to JSON Schema format (for nested objects).
|
||||
*
|
||||
* This method is used when serializing object properties to JSON Schema.
|
||||
* It produces a map of property names to their JSON Schema representations.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function toPropertiesArray(): array
|
||||
{
|
||||
return Arr::mapWithKeys(
|
||||
$this->properties,
|
||||
fn (SchemaProperty $schemaProperty): array => [
|
||||
$schemaProperty->name => $schemaProperty->toArray(),
|
||||
fn (SchemaPropertyInterface $schemaProperty): array => [
|
||||
$schemaProperty->getName() => $schemaProperty->toJsonSchema(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->toJsonSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this schema to proper JSON Schema format.
|
||||
*
|
||||
* Generates a complete JSON Schema object with all necessary metadata
|
||||
* that can be used directly by JSON Schema validators and editors.
|
||||
*
|
||||
* @return SchemaShape
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
return [
|
||||
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
||||
'type' => 'object',
|
||||
'properties' => $this->toArray(),
|
||||
'properties' => $this->toPropertiesArray(),
|
||||
'required' => $this->getRequiredProperties(),
|
||||
'additionalProperties' => false,
|
||||
];
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
// TODO [Refactor] Refactor this into specialized classes with proper support for JSONSchema.
|
||||
// E.g. IntegerSchemaProperty, ObjectSchemaProperty, etc.
|
||||
// Most likely its own package.
|
||||
|
||||
/**
|
||||
* @phpstan-type SchemaPropertyTypesShape 'number'|'integer'|'array'|'object'|'string'|'boolean'
|
||||
* @phpstan-type SchemaPropertyFormatsShape 'uuid'|'email'|'date-time'
|
||||
* @phpstan-type SchemaPropertyEnumShape array<array-key, scalar>
|
||||
* TODO [Documentation] Figure out how to annotate the `items` and `properties` recursively.`
|
||||
* @phpstan-type SchemaPropertyShape array{
|
||||
* type: SchemaPropertyTypesShape,
|
||||
* x-name: string,
|
||||
* x-required: bool,
|
||||
* format?: SchemaPropertyFormatsShape,
|
||||
* enum?: SchemaPropertyEnumShape,
|
||||
* items?: array<array-key, mixed>,
|
||||
* properties?: array<string, array<array-key, mixed>>,
|
||||
* required?: bool,
|
||||
* minLength?: int,
|
||||
* minimum?: int,
|
||||
* maxLength?: int,
|
||||
* maximum?: int,
|
||||
* }
|
||||
*/
|
||||
class SchemaProperty
|
||||
{
|
||||
/**
|
||||
* @param SchemaPropertyTypesShape $type
|
||||
* @param SchemaPropertyFormatsShape|null $format
|
||||
* @param SchemaPropertyEnumShape|null $enum Allowed enum values.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $type = 'string', // <- Make this an enum.
|
||||
public readonly bool $required = false,
|
||||
public readonly ?string $format = null, // <- Make this an enum.
|
||||
public readonly ?array $enum = null,
|
||||
public readonly ?SchemaProperty $itemsSchema = null, // <- For arrays
|
||||
public readonly ?Schema $propertiesSchema = null, // <- For objects
|
||||
public readonly ?int $minimum = null,
|
||||
public readonly ?int $maximum = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return SchemaPropertyShape
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [
|
||||
'type' => $this->type,
|
||||
];
|
||||
|
||||
if ($this->format !== null) {
|
||||
$result['format'] = $this->format;
|
||||
}
|
||||
|
||||
if ($this->enum !== null && $this->enum !== []) {
|
||||
$result['enum'] = $this->enum;
|
||||
}
|
||||
|
||||
if ($this->itemsSchema instanceof \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\SchemaProperty) {
|
||||
$result['items'] = $this->itemsSchema->toArray();
|
||||
}
|
||||
|
||||
if ($this->propertiesSchema instanceof \Sunchayn\Nimbus\Modules\Schemas\ValueObjects\Schema) {
|
||||
$result['properties'] = Arr::mapWithKeys(
|
||||
$this->propertiesSchema->properties,
|
||||
fn (SchemaProperty $schemaProperty): array => [$schemaProperty->name => $schemaProperty->toArray()]
|
||||
);
|
||||
|
||||
$result['required'] = $this->propertiesSchema->getRequiredProperties();
|
||||
}
|
||||
|
||||
if ($this->minimum !== null && in_array($this->type, ['string', 'integer', 'number'])) {
|
||||
$minPropertyName = $this->type === 'string' ? 'minLength' : 'minimum';
|
||||
|
||||
$result[$minPropertyName] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null && in_array($this->type, ['string', 'integer', 'number'])) {
|
||||
$minPropertyName = $this->type === 'string' ? 'maxLength' : 'maximum';
|
||||
|
||||
$result[$minPropertyName] = $this->maximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var SchemaPropertyShape $final PHPStan couldn't infer the correct type when building the array incrementally.
|
||||
*/
|
||||
$final = array_merge(
|
||||
$result,
|
||||
// To make dealing with props easier, for instance, for payload generation.
|
||||
// We also add a couple of custom properties to the schema.
|
||||
$this->getCustomProperties(),
|
||||
);
|
||||
|
||||
return $final;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* x-name: string,
|
||||
* x-required: bool,
|
||||
* }
|
||||
*/
|
||||
private function getCustomProperties(): array
|
||||
{
|
||||
return [
|
||||
// Keep in mind: the `required` property is reserved for objects to tell what properties are required in the object.
|
||||
// this custom property, on the other hand, is to tell if the current property is required or not.
|
||||
'x-required' => $this->required,
|
||||
'x-name' => $this->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
80
src/Modules/Schemas/ValueObjects/StringSchemaProperty.php
Normal file
80
src/Modules/Schemas/ValueObjects/StringSchemaProperty.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Schemas\ValueObjects;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Contracts\SchemaPropertyInterface;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\SchemaPropertyType;
|
||||
use Sunchayn\Nimbus\Modules\Schemas\Enums\StringFormat;
|
||||
|
||||
/**
|
||||
* Schema property for string types.
|
||||
*
|
||||
* Supports JSON Schema string validation including:
|
||||
* - Format validation (email, uuid, date-time, etc.)
|
||||
* - Enum constraints
|
||||
* - Length constraints (minLength, maxLength)
|
||||
*/
|
||||
class StringSchemaProperty implements SchemaPropertyInterface
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, scalar>|null $enum
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly bool $required = false,
|
||||
private readonly ?StringFormat $stringFormat = null,
|
||||
private readonly ?array $enum = null,
|
||||
private readonly ?int $minLength = null,
|
||||
private readonly ?int $maxLength = null,
|
||||
private readonly ?string $pattern = null,
|
||||
private readonly mixed $const = null,
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function getType(): SchemaPropertyType
|
||||
{
|
||||
return SchemaPropertyType::STRING;
|
||||
}
|
||||
|
||||
public function toJsonSchema(): array
|
||||
{
|
||||
$properties = [
|
||||
'type' => $this->getType()->value,
|
||||
];
|
||||
|
||||
if ($this->stringFormat instanceof \Sunchayn\Nimbus\Modules\Schemas\Enums\StringFormat) {
|
||||
$properties['format'] = $this->stringFormat->value;
|
||||
}
|
||||
|
||||
if ($this->enum !== null && $this->enum !== []) {
|
||||
$properties['enum'] = $this->enum;
|
||||
}
|
||||
|
||||
if ($this->minLength !== null) {
|
||||
$properties['minLength'] = $this->minLength;
|
||||
}
|
||||
|
||||
if ($this->maxLength !== null) {
|
||||
$properties['maxLength'] = $this->maxLength;
|
||||
}
|
||||
|
||||
if ($this->pattern !== null) {
|
||||
$properties['pattern'] = $this->pattern;
|
||||
}
|
||||
|
||||
if ($this->const !== null) {
|
||||
$properties['const'] = $this->const;
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user