Skip to content

Commit 08bf834

Browse files
committed
feat: implement metadata support
1 parent 21369ea commit 08bf834

8 files changed

Lines changed: 408 additions & 57 deletions

File tree

src/Core/Reflect/Reflector.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ public function reflect(string $source): Schema
4848

4949
$parameters = $this->getParameters($source);
5050
$schema = $this->factory->make();
51-
$this->extractFields($parameters, $schema);
51+
$this->introspect($parameters, $schema);
5252

5353
return $schema;
5454
}
5555

5656
/**
5757
* @throws ReflectionException
5858
*/
59-
protected function extractFields(array $parameters, Schema $schema, ?Field $parent = null, array $path = []): void
59+
protected function introspect(array $parameters, Schema $schema, ?Field $parent = null, array $path = []): void
6060
{
6161
$chain = (new TypeChain($this->types))
6262
->then(new RequirementChain($parent))
@@ -82,22 +82,22 @@ protected function extractFields(array $parameters, Schema $schema, ?Field $pare
8282
continue;
8383
}
8484

85-
$this->extractFieldsNestedSource($source, $schema, $field, $nestedPath);
85+
$this->introspectSource($source, $schema, $field, $nestedPath);
8686
}
8787
}
8888

8989
/**
9090
* @throws ReflectionException
9191
*/
92-
private function extractFieldsNestedSource(string $source, Schema $schema, Field $parent, array $path): void
92+
private function introspectSource(string $source, Schema $schema, Field $parent, array $path): void
9393
{
9494
$nestedParameters = $this->getParameters($source);
9595
if (empty($nestedParameters)) {
9696
return;
9797
}
9898

9999
$this->currentPath[] = $source;
100-
$this->extractFields($nestedParameters, $schema, $parent, $path);
100+
$this->introspect($nestedParameters, $schema, $parent, $path);
101101
array_pop($this->currentPath);
102102
}
103103

src/Core/Reflect/Resolver/Type/ManagedAttributeTypeHandler.php

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/Support/Metadata/Schema/Registry/Types.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Constructo\Support\Metadata\Schema\Registry;
66

7+
use Constructo\Type\Timestamp;
8+
79
use function Constructo\Cast\stringify;
810

911
readonly class Types
@@ -34,6 +36,7 @@ protected function defaults(): array
3436
'DateTime' => 'date',
3537
'DateTimeImmutable' => 'date',
3638
'DateTimeInterface' => 'date',
39+
Timestamp::class => 'date',
3740
];
3841
}
3942
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Core\Reflect;
6+
7+
use Constructo\Contract\Reflect\TypesFactory;
8+
use Constructo\Core\Reflect\Reflector;
9+
use Constructo\Factory\DefaultSpecsFactory;
10+
use Constructo\Factory\SchemaFactory;
11+
use Constructo\Support\Cache;
12+
use Constructo\Support\Metadata\Schema\Registry\Types;
13+
use Constructo\Support\Reflective\Notation;
14+
use Constructo\Test\Stub\Domain\Entity\Command\GameCommand;
15+
use Constructo\Test\Stub\Domain\Entity\Command\PersonCommand;
16+
use Constructo\Test\Stub\Reflector\Sample;
17+
use PHPUnit\Framework\TestCase;
18+
19+
use function json_decode;
20+
21+
final class ReflectorTest extends TestCase
22+
{
23+
private Reflector $reflector;
24+
25+
protected function setUp(): void
26+
{
27+
$types = new Types();
28+
$cache = new Cache();
29+
$notation = Notation::SNAKE;
30+
31+
$typesFactory = $this->createMock(TypesFactory::class);
32+
$typesFactory->method('make')
33+
->willReturn($types);
34+
35+
$specsData = [
36+
'required' => [],
37+
'present' => [],
38+
'filled' => [],
39+
'sometimes' => [],
40+
'nullable' => [],
41+
'string' => [],
42+
'integer' => [],
43+
'numeric' => [],
44+
'boolean' => [],
45+
'array' => [],
46+
'object' => [],
47+
'float' => [],
48+
'date' => [],
49+
'enum' => [],
50+
'email' => [],
51+
'url' => [],
52+
'uuid' => [],
53+
'min' => ['params' => ['min']],
54+
'max' => ['params' => ['max']],
55+
'between' => [
56+
'params' => [
57+
'min',
58+
'max',
59+
],
60+
],
61+
'in' => ['params' => ['values']],
62+
'regex' => [
63+
'params' => [
64+
'pattern',
65+
'parameters:optional',
66+
],
67+
],
68+
'bail' => [],
69+
'size' => ['params' => ['size']],
70+
];
71+
72+
$specsFactory = new DefaultSpecsFactory($specsData);
73+
$schemaFactory = new SchemaFactory($specsFactory);
74+
75+
76+
$this->reflector = new Reflector($schemaFactory, $types, $cache, $notation);
77+
}
78+
79+
public function testReflectSampleClassCreatesSchemaWithAllFields(): void
80+
{
81+
$schema = $this->reflector->reflect(Sample::class);
82+
83+
$this->assertNotNull($schema);
84+
85+
$this->assertNotNull($schema->get('required_field'));
86+
$this->assertNotNull($schema->get('required_nullable_field'));
87+
$this->assertNotNull($schema->get('required_enum_field'));
88+
$this->assertNotNull($schema->get('required_nullable_enum_field'));
89+
90+
$this->assertNotNull($schema->get('processed_field'));
91+
$this->assertNotNull($schema->get('processed_nullable_field'));
92+
93+
$this->assertNotNull($schema->get('default_string_field'));
94+
$this->assertNotNull($schema->get('default_null_field'));
95+
$this->assertNotNull($schema->get('optional_array_field'));
96+
$this->assertNotNull($schema->get('optional_object_field'));
97+
$this->assertNotNull($schema->get('default_enum_field'));
98+
}
99+
100+
public function testReflectSampleClassFieldRequirements(): void
101+
{
102+
$schema = $this->reflector->reflect(Sample::class);
103+
104+
$requiredField = $schema->get('required_field');
105+
$this->assertTrue($requiredField->hasRule('required'));
106+
107+
$requiredNullableField = $schema->get('required_nullable_field');
108+
$this->assertTrue($requiredNullableField->hasRule('present'));
109+
110+
$requiredEnumField = $schema->get('required_enum_field');
111+
$this->assertTrue($requiredEnumField->hasRule('required'));
112+
113+
$requiredNullableEnumField = $schema->get('required_nullable_enum_field');
114+
$this->assertTrue($requiredNullableEnumField->hasRule('present'));
115+
116+
$processedField = $schema->get('processed_field');
117+
$this->assertTrue($processedField->hasRule('required'));
118+
119+
$defaultStringField = $schema->get('default_string_field');
120+
$this->assertTrue($defaultStringField->hasRule('sometimes'));
121+
122+
$defaultNullField = $schema->get('default_null_field');
123+
$this->assertTrue($defaultNullField->hasRule('sometimes'));
124+
125+
$optionalArrayField = $schema->get('optional_array_field');
126+
$this->assertTrue($optionalArrayField->hasRule('sometimes'));
127+
128+
$optionalObjectField = $schema->get('optional_object_field');
129+
$this->assertTrue($optionalObjectField->hasRule('sometimes'));
130+
131+
$defaultEnumField = $schema->get('default_enum_field');
132+
$this->assertTrue($defaultEnumField->hasRule('sometimes'));
133+
134+
$processedNullableField = $schema->get('processed_nullable_field');
135+
$this->assertTrue($processedNullableField->hasRule('sometimes'));
136+
}
137+
138+
public function testReflectSampleClassFieldTypes(): void
139+
{
140+
$schema = $this->reflector->reflect(Sample::class);
141+
142+
$requiredField = $schema->get('required_field');
143+
$this->assertTrue($requiredField->hasRule('string'));
144+
145+
$requiredNullableField = $schema->get('required_nullable_field');
146+
$this->assertTrue($requiredNullableField->hasRule('string'));
147+
148+
$defaultStringField = $schema->get('default_string_field');
149+
$this->assertTrue($defaultStringField->hasRule('string'));
150+
151+
$processedField = $schema->get('processed_field');
152+
$this->assertTrue($processedField->hasRule('string'));
153+
154+
$optionalArrayField = $schema->get('optional_array_field');
155+
$this->assertTrue($optionalArrayField->hasRule('array'));
156+
157+
// Object field should have array rule
158+
$optionalObjectField = $schema->get('optional_object_field');
159+
$this->assertTrue($optionalObjectField->hasRule('array'));
160+
}
161+
162+
public function testReflectSampleClassFieldNullability(): void
163+
{
164+
$schema = $this->reflector->reflect(Sample::class);
165+
166+
// Non-nullable fields should not have nullable rule
167+
$requiredField = $schema->get('required_field');
168+
$this->assertFalse($requiredField->hasRule('nullable'));
169+
170+
$processedField = $schema->get('processed_field');
171+
$this->assertFalse($processedField->hasRule('nullable'));
172+
173+
// Nullable fields should have nullable rule
174+
$requiredNullableField = $schema->get('required_nullable_field');
175+
$this->assertTrue($requiredNullableField->hasRule('nullable'));
176+
177+
$defaultNullField = $schema->get('default_null_field');
178+
$this->assertTrue($defaultNullField->hasRule('nullable'));
179+
180+
$optionalArrayField = $schema->get('optional_array_field');
181+
$this->assertTrue($optionalArrayField->hasRule('nullable'));
182+
183+
$optionalObjectField = $schema->get('optional_object_field');
184+
$this->assertTrue($optionalObjectField->hasRule('nullable'));
185+
186+
$processedNullableField = $schema->get('processed_nullable_field');
187+
$this->assertTrue($processedNullableField->hasRule('nullable'));
188+
}
189+
190+
public function testReflectSampleClassEnumFields(): void
191+
{
192+
$schema = $this->reflector->reflect(Sample::class);
193+
194+
// Enum fields should have proper enum type handling
195+
$requiredEnumField = $schema->get('required_enum_field');
196+
$this->assertNotNull($requiredEnumField);
197+
$this->assertTrue($requiredEnumField->hasRule('required'));
198+
$this->assertFalse($requiredEnumField->hasRule('nullable'));
199+
200+
$requiredNullableEnumField = $schema->get('required_nullable_enum_field');
201+
$this->assertNotNull($requiredNullableEnumField);
202+
$this->assertTrue($requiredNullableEnumField->hasRule('present'));
203+
$this->assertTrue($requiredNullableEnumField->hasRule('nullable'));
204+
205+
$defaultEnumField = $schema->get('default_enum_field');
206+
$this->assertNotNull($defaultEnumField);
207+
$this->assertTrue($defaultEnumField->hasRule('sometimes'));
208+
$this->assertFalse($defaultEnumField->hasRule('nullable'));
209+
}
210+
211+
public function testExtractCompleteJsonSchemaStructure(): void
212+
{
213+
$schema = $this->reflector->reflect(Sample::class);
214+
$rules = $schema->rules();
215+
216+
$json = '{
217+
"required_field" : [ "required", "string" ],
218+
"required_nullable_field" : [ "present", "nullable", "string" ],
219+
"required_enum_field" : [ "required", "string", "in:first,second,third" ],
220+
"required_nullable_enum_field" : [ "present", "nullable", "string", "in:first,second,third" ],
221+
"processed_field" : [ "required", "string" ],
222+
"default_string_field" : [ "sometimes", "required", "string" ],
223+
"default_null_field" : [ "sometimes", "nullable", "string" ],
224+
"optional_array_field" : [ "sometimes", "nullable", "array" ],
225+
"optional_object_field" : [ "sometimes", "nullable", "array" ],
226+
"optional_object_field.name" : [ "sometimes", "regex:/^[a-zA-Z]{1,255}$/", "string" ],
227+
"optional_object_field.slug" : [ "sometimes", "string" ],
228+
"optional_object_field.published_at" : [ "sometimes", "date" ],
229+
"optional_object_field.data" : [ "sometimes", "array" ],
230+
"optional_object_field.features" : [ "sometimes", "array" ],
231+
"default_enum_field" : [ "sometimes", "required", "integer", "in:1,2,3" ],
232+
"processed_nullable_field" : [ "sometimes", "nullable", "string" ]
233+
}';
234+
$expected = json_decode($json, true);
235+
236+
$this->assertEquals($expected, $rules);
237+
}
238+
239+
public function testReflectorReturnsCorrectRulesForGameCommand(): void
240+
{
241+
$schema = $this->reflector->reflect(GameCommand::class);
242+
$rules = $schema->rules();
243+
$json = '{
244+
"name": [ "required", "string" ],
245+
"slug": [ "required", "string" ],
246+
"published_at": [ "required", "date" ],
247+
"data": [ "required", "array" ],
248+
"features": [ "required", "array" ]
249+
}';
250+
$expected = json_decode($json, true);
251+
$this->assertEquals($expected, $rules);
252+
}
253+
254+
public function testReflectReturnsCorrectRulesForPersonCommand(): void
255+
{
256+
$schema = $this->reflector->reflect(PersonCommand::class);
257+
$rules = $schema->rules();
258+
$json = '{
259+
"name" : [ "required", "string" ],
260+
"mom" : [ "present", "nullable", "array" ],
261+
"mom.name" : [ "required", "string" ],
262+
"mom.mom" : [ "present", "nullable", "array" ],
263+
"mom.dad" : [ "sometimes", "nullable", "array" ],
264+
"dad" : [ "sometimes", "nullable", "array" ],
265+
"dad.name" : [ "sometimes", "string" ],
266+
"dad.mom" : [ "sometimes", "nullable", "array" ],
267+
"dad.dad" : [ "sometimes", "nullable", "array" ]
268+
}';
269+
$expected = json_decode($json, true);
270+
$this->assertEquals($expected, $rules);
271+
}
272+
}

0 commit comments

Comments
 (0)