Skip to content

Commit d13dd5f

Browse files
author
Alexander Miertsch
authored
Merge pull request #2 from event-engine/feature/improve_schema_detection
Improve schema detection and customizing
2 parents 493397f + e1747b4 commit d13dd5f

25 files changed

+1499
-16
lines changed

src/JsonSchemaAwareCollection.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EventEngine\JsonSchema;
5+
6+
use EventEngine\Schema\TypeSchema;
7+
8+
interface JsonSchemaAwareCollection
9+
{
10+
public static function __itemSchema(): TypeSchema;
11+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EventEngine\JsonSchema;
5+
6+
use EventEngine\Data\ImmutableRecord;
7+
use EventEngine\JsonSchema\RecordLogic\TypeDetector;
8+
use EventEngine\Schema\TypeSchema;
9+
10+
trait JsonSchemaAwareCollectionLogic
11+
{
12+
private static function __itemType(): ?string
13+
{
14+
return null;
15+
}
16+
17+
public static function __itemSchema(): TypeSchema
18+
{
19+
if(null === self::$__itemSchema) {
20+
$itemType = self::__itemType();
21+
22+
if(null === $itemType) {
23+
return JsonSchema::any();
24+
}
25+
26+
if(self::isScalarType($itemType)) {
27+
return JsonSchema::schemaFromScalarPhpType($itemType, false);
28+
}
29+
30+
self::$__itemSchema = TypeDetector::getTypeFromClass($itemType, self::__allowNestedSchema());
31+
}
32+
33+
return self::$__itemSchema;
34+
}
35+
36+
private static function isScalarType(string $type): bool
37+
{
38+
switch ($type) {
39+
case ImmutableRecord::PHP_TYPE_STRING:
40+
case ImmutableRecord::PHP_TYPE_INT:
41+
case ImmutableRecord::PHP_TYPE_FLOAT:
42+
case ImmutableRecord::PHP_TYPE_BOOL:
43+
return true;
44+
default:
45+
return false;
46+
}
47+
}
48+
49+
/**
50+
* If item type is a class and that class implements JsonSchemaAwareRecord the resulting JsonSchema
51+
* for that type can either be a TypeRef (no nested schema allowed - default logic) or an object schema derived
52+
* from JsonSchemaAwareRecord::__schema (enabled by returning true from the method)
53+
*
54+
* @return bool
55+
*/
56+
private static function __allowNestedSchema(): bool
57+
{
58+
return false;
59+
}
60+
61+
/**
62+
* Static item schema cache
63+
*
64+
* @var TypeSchema
65+
*/
66+
private static $__itemSchema;
67+
}

src/JsonSchemaAwareRecord.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ public static function __type(): string;
2727
* @return TypeSchema JSON Schema of the type
2828
*/
2929
public static function __schema(): TypeSchema;
30-
}
30+
}

src/JsonSchemaAwareRecordLogic.php

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use EventEngine\Data\ImmutableRecord;
1515
use EventEngine\Data\ImmutableRecordLogic;
1616
use EventEngine\JsonSchema\Exception\InvalidArgumentException;
17+
use EventEngine\JsonSchema\RecordLogic\TypeDetector;
1718
use EventEngine\Schema\TypeSchema;
1819

1920
trait JsonSchemaAwareRecordLogic
@@ -30,6 +31,28 @@ public static function __schema(): TypeSchema
3031
return self::generateSchemaFromPropTypeMap();
3132
}
3233

34+
/**
35+
* Override method to return a list property keys that are optional
36+
*
37+
* @return array
38+
*/
39+
private static function __optionalProperties(): array
40+
{
41+
return [];
42+
}
43+
44+
/**
45+
* If a property type is a class and that class implements JsonSchemaAwareRecord the resulting JsonSchema
46+
* for that type can either be a TypeRef (no nested schema allowed - default logic) or an object schema derived
47+
* from JsonSchemaAwareRecord::__schema (enabled by returning true from the method)
48+
*
49+
* @return bool
50+
*/
51+
private static function __allowNestedSchema(): bool
52+
{
53+
return false;
54+
}
55+
3356
/**
3457
* @param array $arrayPropTypeMap Map of array property name to array item type
3558
* @return Type
@@ -70,20 +93,26 @@ private static function generateSchemaFromPropTypeMap(array $arrayPropTypeMap =
7093
} elseif ($arrayItemType === ImmutableRecord::PHP_TYPE_ARRAY) {
7194
throw new InvalidArgumentException("Array item type of property $prop must not be 'array', only a scalar type or an existing class can be used as array item type.");
7295
} else {
73-
$arrayItemSchema = JsonSchema::typeRef(self::getTypeFromClass($arrayItemType));
96+
$arrayItemSchema = self::getTypeFromClass($arrayItemType);
7497
}
7598

7699
$props[$prop] = JsonSchema::array($arrayItemSchema);
77100
} else {
78-
$props[$prop] = JsonSchema::typeRef(self::getTypeFromClass($type));
101+
$props[$prop] = self::getTypeFromClass($type);
79102
}
80103

81104
if ($isNullable) {
82105
$props[$prop] = JsonSchema::nullOr($props[$prop]);
83106
}
84107
}
85108

86-
self::$__schema = JsonSchema::object($props);
109+
$optionalProps = [];
110+
foreach (self::__optionalProperties() as $optProp) {
111+
$optionalProps[$optProp] = $props[$optProp];
112+
unset($props[$optProp]);
113+
}
114+
115+
self::$__schema = JsonSchema::object($props, $optionalProps);
87116
}
88117

89118
return self::$__schema;
@@ -94,19 +123,9 @@ private static function convertClassToTypeName(string $class): string
94123
return \substr(\strrchr($class, '\\'), 1);
95124
}
96125

97-
private static function getTypeFromClass(string $classOrType): string
126+
private static function getTypeFromClass(string $classOrType): Type
98127
{
99-
if (! \class_exists($classOrType)) {
100-
return $classOrType;
101-
}
102-
103-
$refObj = new \ReflectionClass($classOrType);
104-
105-
if ($refObj->implementsInterface(ImmutableRecord::class)) {
106-
return \call_user_func([$classOrType, '__type']);
107-
}
108-
109-
return self::convertClassToTypeName($classOrType);
128+
return TypeDetector::getTypeFromClass($classOrType, self::__allowNestedSchema());
110129
}
111130

112131
/**

src/RecordLogic/TypeDetector.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EventEngine\JsonSchema\RecordLogic;
5+
6+
use EventEngine\JsonSchema\JsonSchema;
7+
use EventEngine\JsonSchema\JsonSchemaAwareCollection;
8+
use EventEngine\JsonSchema\JsonSchemaAwareRecord;
9+
use EventEngine\JsonSchema\Type;
10+
11+
final class TypeDetector
12+
{
13+
public static function getTypeFromClass(string $classOrType, bool $allowNestedSchema = true): Type
14+
{
15+
if (! \class_exists($classOrType)) {
16+
return JsonSchema::typeRef($classOrType);
17+
}
18+
19+
$refObj = new \ReflectionClass($classOrType);
20+
21+
if ($refObj->implementsInterface(JsonSchemaAwareRecord::class)) {
22+
23+
if($allowNestedSchema) {
24+
return \call_user_func([$classOrType, '__schema']);
25+
}
26+
27+
return new Type\TypeRef(\call_user_func([$classOrType, '__type']));
28+
}
29+
30+
if($refObj->implementsInterface(JsonSchemaAwareCollection::class)) {
31+
return JsonSchema::array(\call_user_func([$classOrType, '__itemSchema']));
32+
}
33+
34+
if($scalarSchemaType = self::determineScalarTypeIfPossible($classOrType)) {
35+
return $scalarSchemaType;
36+
}
37+
38+
return self::convertClassToType($classOrType);
39+
}
40+
41+
private static function determineScalarTypeIfPossible(string $class): ?Type
42+
{
43+
if(is_callable([$class, 'fromString'])) {
44+
return JsonSchema::string();
45+
}
46+
47+
if(is_callable([$class, 'fromInt'])) {
48+
return JsonSchema::integer();
49+
}
50+
51+
if(is_callable([$class, 'fromFloat'])) {
52+
return JsonSchema::float();
53+
}
54+
55+
if(is_callable([$class, 'fromBool'])) {
56+
return JsonSchema::boolean();
57+
}
58+
59+
return null;
60+
}
61+
62+
private static function convertClassToType(string $class): Type
63+
{
64+
return new Type\TypeRef(\substr(\strrchr($class, '\\'), 1));
65+
}
66+
}

tests/BasicTestCase.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace EventEngineTest\JsonSchema;
5+
6+
use PHPUnit\Framework\TestCase;
7+
8+
class BasicTestCase extends TestCase
9+
{
10+
11+
}

0 commit comments

Comments
 (0)