Skip to content

Commit

Permalink
add closure converter and union schema
Browse files Browse the repository at this point in the history
  • Loading branch information
tymondesigns committed Jan 15, 2025
1 parent 344a4df commit aea1bb6
Show file tree
Hide file tree
Showing 16 changed files with 1,213 additions and 385 deletions.
82 changes: 65 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,35 @@ $schema = SchemaFactory::object('user')
SchemaFactory::object('settings')
->additionalProperties(false)
->properties([
SchemaFactory::string('theme')->enum(['light', 'dark']),
SchemaFactory::boolean('notifications')
SchemaFactory::string('theme')
->enum(['light', 'dark']),
]),
);
```

Or you can use the objects directly
You can also use the objects directly instead of the factory methods.
```php
$schema = new ObjectSchema('user')
$schema = (new ObjectSchema('user'))
->description('User schema')
->properties(
new StringSchema('name')
(new StringSchema('name'))
->minLength(2)
->maxLength(100)
->required(),
new StringSchema('email')
(new StringSchema('email'))
->format(SchemaFormat::Email)
->required(),
(new IntegerSchema('age'))
->minimum(18)
->maximum(150),
(new BooleanSchema('active'))
->default(true),
(new ObjectSchema('settings'))
->additionalProperties(false)
->properties(
(new StringSchema('theme'))
->enum(['light', 'dark']),
),
);
```

Expand All @@ -68,23 +79,24 @@ $schema->toArray();
// Convert to JSON string
$schema->toJson();

$data = [
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 16,
'active' => true,
'settings' => [
'theme' => 'dark',
],
];

// Validate data against the schema
try {
$schema->validate([
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 16,
'active' => true,
'settings' => [
'theme' => 'dark',
'notifications' => true,
],
]);
$schema->validate($data);
} catch (SchemaException $e) {
echo $e->getMessage(); // "The data must match the 'email' format"
}

// Validate data against the schema
// Or just get a boolean
$schema->isValid($data);
```

Expand Down Expand Up @@ -374,6 +386,42 @@ $schema->isValid([

---

### Union Schema

```php
use Cortex\JsonSchema\SchemaFactory;
use Cortex\JsonSchema\Enums\SchemaType;

$schema = SchemaFactory::union([SchemaType::String, SchemaType::Integer], 'id')
->description('ID can be either a string or an integer')
->enum(['abc123', 'def456', 1, 2, 3])
->nullable();
```

```php
$schema->isValid('abc123'); // true
$schema->isValid(1); // true
$schema->isValid(null); // true (because it's nullable)
$schema->isValid(true); // false (not a string or integer)
$schema->isValid('invalid'); // false (not in enum)
```

<details>
<summary>View JSON Schema</summary>

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": ["string", "integer", "null"],
"title": "id",
"description": "ID can be either a string or an integer",
"enum": ["abc123", "def456", 1, 2, 3]
}
```
</details>

---

## Validation

The library throws a `SchemaException` when validation fails:
Expand Down
22 changes: 22 additions & 0 deletions src/Contracts/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ public function getType(): SchemaType|array;
*/
public function isRequired(): bool;

/**
* Add null type to schema.
*/
public function nullable(): static;

/**
* Set the default value
*/
public function default(mixed $value): static;

/**
* Set the allowed enum values.
*
* @param non-empty-array<int|string|bool|float|null> $values
*/
public function enum(array $values): static;

/**
* Set the schema as required.
*/
public function required(): static;

/**
* Convert to array.
*
Expand Down
119 changes: 119 additions & 0 deletions src/Converters/FromClosure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

namespace Cortex\JsonSchema\Converters;

use Closure;
use ReflectionEnum;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use ReflectionIntersectionType;
use Cortex\JsonSchema\Contracts\Schema;
use Cortex\JsonSchema\Enums\SchemaType;
use Cortex\JsonSchema\Types\UnionSchema;
use Cortex\JsonSchema\Types\ObjectSchema;
use Cortex\JsonSchema\Exceptions\SchemaException;

class FromClosure
{
public static function convert(Closure $closure): ObjectSchema
{
$reflection = new ReflectionFunction($closure);
$schema = new ObjectSchema();

// TODO: handle descriptions
// $doc = $reflection->getDocComment();

foreach ($reflection->getParameters() as $parameter) {
$propertySchema = self::getPropertySchema($parameter);

// No type hint, skip
if ($propertySchema === null) {
continue;
}

$schema->properties($propertySchema);
}

return $schema;
}

/**
* Create a schema from a given type.
*/
protected static function getPropertySchema(ReflectionParameter $parameter): ?Schema
{
$type = $parameter->getType();

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

$matchedTypes = match (true) {
$type instanceof ReflectionUnionType, $type instanceof ReflectionIntersectionType => array_map(
fn(ReflectionNamedType $t): SchemaType => self::resolveSchemaType($t),

Check failure on line 57 in src/Converters/FromClosure.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $callback of function array_map expects (callable(ReflectionType): mixed)|null, Closure(ReflectionNamedType): Cortex\JsonSchema\Enums\SchemaType given.
$type->getTypes(),
),
$type instanceof ReflectionNamedType => [self::resolveSchemaType($type)],
default => throw new SchemaException('Unknown type: ' . $type),
};

// TODO: handle mixed type

$schema = count($matchedTypes) === 1
? $matchedTypes[0]->instance()
: new UnionSchema($matchedTypes);

$schema->title($parameter->getName());

if ($type->allowsNull()) {
$schema->nullable();
}

if ($parameter->isDefaultValueAvailable() && ! $parameter->isDefaultValueConstant()) {
$schema->default($parameter->getDefaultValue());
}

if (! $parameter->isOptional()) {
$schema->required();
}

// If it's an enum, add the possible values
if ($type instanceof ReflectionNamedType) {
$typeName = $type->getName();

if (enum_exists($typeName)) {
$reflection = new ReflectionEnum($typeName);

if ($reflection->isBacked()) {
$cases = $typeName::cases();
$schema->enum(array_map(fn($case): int|string => $case->value, $cases));

Check failure on line 93 in src/Converters/FromClosure.php

View workflow job for this annotation

GitHub Actions / phpstan

Access to an undefined property UnitEnum::$value.

Check failure on line 93 in src/Converters/FromClosure.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $values of method Cortex\JsonSchema\Contracts\Schema::enum() expects non-empty-array<bool|float|int|string|null>, list<int|string> given.
}
}
}

return $schema;
}

/**
* Resolve the schema type from the given reflection type.
*/
protected static function resolveSchemaType(ReflectionNamedType $type): SchemaType
{
$typeName = $type->getName();

if (enum_exists($typeName)) {
$reflection = new ReflectionEnum($typeName);
$typeName = $reflection->getBackingType()?->getName();

if ($typeName === null) {
throw new SchemaException('Enum type has no backing type: ' . $typeName);
}
}

return SchemaType::fromScalar($typeName);
}
}
18 changes: 18 additions & 0 deletions src/Enums/SchemaType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Cortex\JsonSchema\Types\StringSchema;
use Cortex\JsonSchema\Types\BooleanSchema;
use Cortex\JsonSchema\Types\IntegerSchema;
use Cortex\JsonSchema\Exceptions\SchemaException;

enum SchemaType: string
{
Expand All @@ -38,4 +39,21 @@ public function instance(?string $title = null): Schema
self::Null => new NullSchema($title),
};
}

/**
* Create a new schema instance from a given scalar type.
*/
public static function fromScalar(string $type): self
{
return match ($type) {
'int' => self::Integer,
'float' => self::Number,
'string' => self::String,
'array' => self::Array,
'bool' => self::Boolean,
'object' => self::Object,
'null' => self::Null,
default => throw new SchemaException('Unknown type: ' . $type),
};
}
}
9 changes: 9 additions & 0 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Cortex\JsonSchema\Types\NullSchema;
use Cortex\JsonSchema\Types\ArraySchema;
use Cortex\JsonSchema\Types\UnionSchema;
use Cortex\JsonSchema\Types\NumberSchema;
use Cortex\JsonSchema\Types\ObjectSchema;
use Cortex\JsonSchema\Types\StringSchema;
Expand Down Expand Up @@ -48,4 +49,12 @@ public static function null(?string $title = null): NullSchema
{
return new NullSchema($title);
}

/**
* @param array<int, \Cortex\JsonSchema\Enums\SchemaType> $types
*/
public static function union(array $types, ?string $title = null): UnionSchema
{
return new UnionSchema($types, $title);
}
}
Loading

0 comments on commit aea1bb6

Please sign in to comment.