Вход Регистрация
Файл: vendor/laravel/framework/src/Illuminate/JsonSchema/Deserializer.php
Строк: 603
<?php

namespace IlluminateJsonSchema;

use 
InvalidArgumentException;

class 
Deserializer
{
    
/**
     * The root schema being deserialized (used to resolve local $refs).
     *
     * @var array<string, mixed>
     */
    
protected array $root;

    
/**
     * Create a new deserializer instance.
     *
     * @param  array<string, mixed>  $root
     */
    
protected function __construct(array $root)
    {
        
$this->root $root;
    }

    
/**
     * Deserialize the Laravel-supported JSON Schema subset into a type.
     *
     * @param  array<string, mixed>  $schema
     *
     * @throws InvalidArgumentException
     */
    
public static function deserialize(array $schema): TypesType
    
{
        return (new static(
$schema))->build($schema);
    }

    
/**
     * Build a type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     * @param  array<int, string>  $refs
     *
     * @throws InvalidArgumentException
     */
    
protected function build(array $schema, array $refs = []): TypesType
    
{
        [
$schema$refs] = $this->resolveRef($schema$refs);

        [
$schema$nullableFromUnion$refs] = $this->normalizeUnions($schema$refs);

        [
$name$nullableFromType] = $this->resolveType($schema);

        if (
is_array($name)) {
            
$this->ensureUnionConstraintsAreSupported($schema);

            
$type = new TypesUnionType($name);
        } else {
            
$type match ($name) {
                
'object' => $this->buildObject($schema$refs),
                
'array' => $this->buildArray($schema$refs),
                
'string' => $this->buildString($schema),
                
'integer' => $this->buildInteger($schema),
                
'number' => $this->buildNumber($schema),
                
'boolean' => new TypesBooleanType,
                default => throw new 
InvalidArgumentException("Unsupported JSON Schema type [{$name}]."),
            };
        }

        
$this->applyCommon($type$schema);

        if (
$nullableFromUnion || $nullableFromType) {
            
$type->nullable();
        }

        return 
$type;
    }

    
/**
     * Build an object type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     * @param  array<int, string>  $refs
     *
     * @throws InvalidArgumentException
     */
    
protected function buildObject(array $schema, array $refs = []): TypesObjectType
    
{
        
$properties = [];

        if (isset(
$schema['properties']) && is_array($schema['properties'])) {
            
$required is_array($schema['required'] ?? null)
                ? 
array_map('strval'$schema['required'])
                : [];

            foreach (
$schema['properties'] as $key => $definition) {
                if (! 
is_array($definition)) {
                    throw new 
InvalidArgumentException(
                        
"Unable to represent the schema for property [{$key}]; boolean schemas are not supported."
                    
);
                }

                
$property $this->build($definition$refs);

                if (
in_array((string) $key$requiredtrue)) {
                    
$property->required();
                }

                
$properties[$key] = $property;
            }
        }

        
$type = new TypesObjectType($properties);

        if ((
$schema['additionalProperties'] ?? null) === false) {
            
$type->withoutAdditionalProperties();
        }

        return 
$type;
    }

    
/**
     * Build an array type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     * @param  array<int, string>  $refs
     *
     * @throws InvalidArgumentException
     */
    
protected function buildArray(array $schema, array $refs = []): TypesArrayType
    
{
        
$type = new TypesArrayType;

        if (isset(
$schema['items']) && $schema['items'] !== []) {
            if (! 
is_array($schema['items']) || array_is_list($schema['items'])) {
                throw new 
InvalidArgumentException('Tuple and boolean JSON Schema "items" are not supported.');
            }

            
$type->items($this->build($schema['items'], $refs));
        }

        if (isset(
$schema['minItems'])) {
            
$type->min((int) $schema['minItems']);
        }

        if (isset(
$schema['maxItems'])) {
            
$type->max((int) $schema['maxItems']);
        }

        if (isset(
$schema['uniqueItems'])) {
            
$type->unique((bool) $schema['uniqueItems']);
        }

        return 
$type;
    }

    
/**
     * Build a string type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     */
    
protected function buildString(array $schema): TypesStringType
    
{
        
$type = new TypesStringType;

        if (isset(
$schema['minLength'])) {
            
$type->min((int) $schema['minLength']);
        }

        if (isset(
$schema['maxLength'])) {
            
$type->max((int) $schema['maxLength']);
        }

        if (isset(
$schema['pattern'])) {
            
$type->pattern((string) $schema['pattern']);
        }

        if (isset(
$schema['format'])) {
            
$type->format((string) $schema['format']);
        }

        return 
$type;
    }

    
/**
     * Build an integer type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     */
    
protected function buildInteger(array $schema): TypesIntegerType
    
{
        return 
$this->applyNumericBounds(new TypesIntegerType$schema$this->toInteger(...));
    }

    
/**
     * Build a number type from the given schema fragment.
     *
     * @param  array<string, mixed>  $schema
     */
    
protected function buildNumber(array $schema): TypesNumberType
    
{
        return 
$this->applyNumericBounds(new TypesNumberType$schema);
    }

    
/**
     * Apply the numeric bound keywords to the given integer or number type.
     *
     * @template TType of TypesIntegerType|TypesNumberType
     *
     * @param  TType  $type
     * @param  array<string, mixed>  $schema
     * @param  (callable(int|float): (int|float))|null  $cast
     * @return TType
     *
     * @throws InvalidArgumentException
     */
    
protected function applyNumericBounds(TypesIntegerType|TypesNumberType $type, array $schema, ?callable $cast null)
    {
        
$cast ??= static fn (int|float $value) => $value;

        foreach ([
'minimum' => 'min''maximum' => 'max''multipleOf' => 'multipleOf'] as $keyword => $method) {
            if (! isset(
$schema[$keyword])) {
                continue;
            }

            if ((
$value $this->toNumber($schema[$keyword])) === null) {
                throw new 
InvalidArgumentException("The JSON Schema [{$keyword}] constraint must be a number.");
            }

            
$type->{$method}($cast($value));
        }

        return 
$type;
    }

    
/**
     * Apply the keywords shared by every type to the given instance.
     *
     * @param  array<string, mixed>  $schema
     *
     * @throws InvalidArgumentException
     */
    
protected function applyCommon(TypesType $type, array $schema): void
    
{
        if (isset(
$schema['title'])) {
            
$type->title((string) $schema['title']);
        }

        if (isset(
$schema['description'])) {
            
$type->description((string) $schema['description']);
        }

        if (isset(
$schema['enum']) && is_array($schema['enum'])) {
            
$type->enum($schema['enum']);
        }

        if (
array_key_exists('default'$schema)) {
            if (
$schema['default'] === null) {
                throw new 
InvalidArgumentException('A null JSON Schema [default] is not supported.');
            }

            
// The "default" setter is typed per concrete type, so assign it directly...
            
(fn () => $this->default $schema['default'])->call($type);
        }
    }

    
/**
     * Resolve the base type name and whether the schema is nullable.
     *
     * @param  array<string, mixed>  $schema
     * @return array{0: string|array<int, string>, 1: bool}
     *
     * @throws InvalidArgumentException
     */
    
protected function resolveType(array $schema): array
    {
        
$type $schema['type'] ?? null;
        
$nullable false;

        if (
is_array($type)) {
            
$nullable in_array('null'$typetrue);

            
$names array_values(array_unique(array_map('strval'array_filter(
                
$type,
                static 
fn ($value) => $value !== 'null',
            ))));

            if (
count($names) > 1) {
                return [
$names$nullable];
            }

            
$type $names[0] ?? null;
        }

        
$type ??= $this->inferType($schema);

        if (! 
is_string($type)) {
            throw new 
InvalidArgumentException('Unable to determine the JSON Schema type for the given schema.');
        }

        return [
$type$nullable];
    }

    
/**
     * Infer the type name when "type" is absent but the shape is unambiguous.
     *
     * @param  array<string, mixed>  $schema
     */
    
protected function inferType(array $schema): ?string
    
{
        return 
match (true) {
            isset(
$schema['properties']), isset($schema['additionalProperties']), isset($schema['required']) => 'object',
            isset(
$schema['items']), isset($schema['minItems']), isset($schema['maxItems']), isset($schema['uniqueItems']) => 'array',
            isset(
$schema['enum']) && is_array($schema['enum']) => $this->inferEnumType($schema['enum']),
            isset(
$schema['minLength']), isset($schema['maxLength']), isset($schema['pattern']), isset($schema['format']) => 'string',
            isset(
$schema['minimum']), isset($schema['maximum']), isset($schema['multipleOf']) => 'number',
            default => 
null,
        };
    }

    
/**
     * Infer the scalar type shared by a homogeneous enum of scalars.
     *
     * @param  array<int, mixed>  $enum
     */
    
protected function inferEnumType(array $enum): ?string
    
{
        
$resolved null;

        foreach (
$enum as $value) {
            
$current match (true) {
                
is_bool($value) => 'boolean',
                
is_int($value) => 'integer',
                
is_float($value) => 'number',
                
is_string($value) => 'string',
                default => 
null,
            };

            if (
$current === null) {
                return 
null;
            }

            if (
$resolved === null || $resolved === $current) {
                
$resolved $current;

                continue;
            }

            
// A mix of integers and floats is still numeric; anything else is ambiguous...
            
if (in_array($resolved, ['integer''number'], true) && in_array($current, ['integer''number'], true)) {
                
$resolved 'number';

                continue;
            }

            return 
null;
        }

        return 
$resolved;
    }

    
/**
     * Ensure a multi-type union carries no type-specific constraint keywords.
     *
     * @param  array<string, mixed>  $schema
     *
     * @throws InvalidArgumentException
     */
    
protected function ensureUnionConstraintsAreSupported(array $schema): void
    
{
        
$keywords = [
            
'minLength''maxLength''pattern''format',
            
'minimum''maximum''multipleOf',
            
'items''minItems''maxItems''uniqueItems',
            
'properties''required''additionalProperties',
        ];

        
$unsupported array_values(array_intersect($keywordsarray_keys($schema)));

        if (
$unsupported !== []) {
            throw new 
InvalidArgumentException(
                
'Type-specific keywords ['.implode(', '$unsupported).'] are not supported on a multi-type JSON Schema union.'
            
);
        }
    }

    
/**
     * Collapse "anyOf" / "oneOf" null branches into a single effective schema.
     *
     * @param  array<string, mixed>  $schema
     * @param  array<int, string>  $refs
     * @return array{0: array<string, mixed>, 1: bool, 2: array<int, string>}
     *
     * @throws InvalidArgumentException
     */
    
protected function normalizeUnions(array $schema, array $refs = []): array
    {
        foreach ([
'anyOf''oneOf'] as $key) {
            if (! isset(
$schema[$key]) || ! is_array($schema[$key])) {
                continue;
            }

            
$nullable false;
            
$branches = [];

            foreach (
$schema[$key] as $branch) {
                if (! 
is_array($branch)) {
                    continue;
                }

                [
$branch$branchRefs] = $this->resolveRef($branch$refs);

                if (
$this->isNullBranch($branch)) {
                    
$nullable true;
                } else {
                    
$branches[] = [$branch$branchRefs];
                }
            }

            if (! 
$nullable || count($branches) !== 1) {
                throw new 
InvalidArgumentException(
                    
"Only a nullable "{$key}" (a single schema plus a "null" branch) is supported."
                
);
            }

            [
$branch$branchRefs] = $branches[0];

            
$siblings $schema;
            unset(
$siblings[$key]);

            foreach (
$siblings as $siblingKey => $value) {
                if (
array_key_exists($siblingKey$branch) && $branch[$siblingKey] !== $value) {
                    throw new 
InvalidArgumentException(
                        
"Conflicting [{$siblingKey}] between a "{$key}" branch and its sibling keys."
                    
);
                }
            }

            return [
array_merge($siblings$branch), true$branchRefs];
        }

        return [
$schemafalse$refs];
    }

    
/**
     * Determine if the given schema branch describes only the "null" type.
     *
     * @param  array<string, mixed>  $branch
     */
    
protected function isNullBranch(array $branch): bool
    
{
        
$type $branch['type'] ?? null;

        return 
$type === 'null' || $type === ['null'];
    }

    
/**
     * Resolve a local "$ref" against the root schema, merging sibling keys.
     *
     * @param  array<string, mixed>  $schema
     * @param  array<int, string>  $refs
     * @return array{0: array<string, mixed>, 1: array<int, string>}
     *
     * @throws InvalidArgumentException
     */
    
protected function resolveRef(array $schema, array $refs = []): array
    {
        if (! isset(
$schema['$ref']) || ! is_string($schema['$ref'])) {
            return [
$schema$refs];
        }

        
$ref $schema['$ref'];

        if (
in_array($ref$refstrue)) {
            throw new 
InvalidArgumentException("Circular JSON Schema $ref [{$ref}] detected.");
        }

        
$refs[] = $ref;

        
$resolved $this->lookupRef($ref);

        
$siblings $schema;
        unset(
$siblings['$ref']);

        return 
$this->resolveRef(array_merge($resolved$siblings), $refs);
    }

    
/**
     * Look up a local JSON pointer reference within the root schema.
     *
     * @return array<string, mixed>
     *
     * @throws InvalidArgumentException
     */
    
protected function lookupRef(string $ref): array
    {
        if (
$ref === '#') {
            return 
$this->root;
        }

        if (! 
str_starts_with($ref'#/')) {
            throw new 
InvalidArgumentException("Unable to resolve non-local JSON Schema $ref [{$ref}].");
        }

        
$target $this->root;

        foreach (
explode('/'substr($ref2)) as $segment) {
            
$segment str_replace(['~1''~0'], ['/''~'], rawurldecode($segment));

            if (! 
is_array($target) || ! array_key_exists($segment$target)) {
                throw new 
InvalidArgumentException("Unable to resolve JSON Schema $ref [{$ref}].");
            }

            
$target $target[$segment];
        }

        if (! 
is_array($target)) {
            throw new 
InvalidArgumentException("The JSON Schema $ref [{$ref}] does not point to a schema.");
        }

        return 
$target;
    }

    
/**
     * Normalize the given value to an integer or float, or null when not numeric.
     */
    
protected function toNumber(mixed $value): int|float|null
    
{
        if (
is_int($value) || is_float($value)) {
            return 
$value;
        }

        if (
is_string($value) && is_numeric($value)) {
            return 
$value 0;
        }

        return 
null;
    }

    
/**
     * Cast the given number to an integer, rejecting non-integer values.
     *
     * @throws InvalidArgumentException
     */
    
protected function toInteger(int|float $value): int
    
{
        if (
is_float($value) && floor($value) !== $value) {
            throw new 
InvalidArgumentException("The JSON Schema integer constraint [{$value}] must be an integer.");
        }

        return (int) 
$value;
    }
}
Онлайн: 1
Реклама