Skip to content

Commit 39e02c8

Browse files
committed
feat: implement metadata support
1 parent 5b10c20 commit 39e02c8

5 files changed

Lines changed: 561 additions & 5 deletions

File tree

.junie/guidelines.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ final class ExampleTest extends TestCase
100100

101101
### Adding New Tests
102102

103-
1. Create test file in `tests/` mirroring the `src/` structure
103+
1. Create a test file in `tests/` mirroring the `src/` structure
104104
2. Use `final class {Name}Test extends TestCase`
105105
3. Import required classes and use stub classes from `Constructo\Test\Stub\`
106106
4. Use `Set::createFrom()` for test data and `Target::createFrom()` for reflection
@@ -117,6 +117,14 @@ final class ExampleTest extends TestCase
117117
- **Support:** Utility classes and reflection helpers (`src/Support/`)
118118
- **Types:** Type-specific implementations (`src/Type/`)
119119

120+
## Coding Standards and Practices
121+
122+
This project adheres to strict coding standards and best practices:
123+
- **Strict Types:** All files use `declare(strict_types=1);`
124+
- **PSR-12:** Follow PSR-12 coding standards for consistency
125+
- **Type Hints:** Just use type hints if the typing is not clear or is ambiguous
126+
- **Comments:** Do not use comments for code explanations, prefer clear code structure
127+
120128
### Key Classes
121129

122130
- `Builder`: Main deserialization class using chain of responsibility pattern

src/Core/Metadata/Schema/Registry/Spec.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
use Constructo\Contract\Formatter;
88
use Constructo\Support\Set;
99

10-
class Spec
10+
readonly class Spec
1111
{
1212
public function __construct(
13-
public readonly string $name,
14-
public readonly Set $properties,
15-
public readonly ?Formatter $formatter,
13+
public string $name,
14+
public Set $properties,
15+
public ?Formatter $formatter,
1616
) {
1717
}
1818
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Core\Metadata\Schema\Field;
6+
7+
use Constructo\Core\Metadata\Schema\Field;
8+
use Constructo\Core\Metadata\Schema\Field\Fieldset;
9+
use Constructo\Core\Metadata\Schema\Field\Rules;
10+
use Constructo\Core\Metadata\Schema\Registry;
11+
use PHPUnit\Framework\TestCase;
12+
13+
final class FieldsetTest extends TestCase
14+
{
15+
private Fieldset $fieldset;
16+
private Field $field1;
17+
private Field $field2;
18+
19+
protected function setUp(): void
20+
{
21+
$this->fieldset = new Fieldset();
22+
23+
$registry = $this->createMock(Registry::class);
24+
25+
$rules1 = new Rules();
26+
$rules2 = new Rules();
27+
28+
$this->field1 = new Field($registry, $rules1, 'test_field_1');
29+
$this->field2 = new Field($registry, $rules2, 'test_field_2');
30+
}
31+
32+
public function testCanAddFieldToFieldset(): void
33+
{
34+
$this->fieldset->add('field1', $this->field1);
35+
36+
$this->assertTrue($this->fieldset->has('field1'));
37+
}
38+
39+
public function testCanGetExistingField(): void
40+
{
41+
$this->fieldset->add('field1', $this->field1);
42+
43+
$retrievedField = $this->fieldset->get('field1');
44+
45+
$this->assertSame($this->field1, $retrievedField);
46+
}
47+
48+
public function testGetNonExistentFieldReturnsNull(): void
49+
{
50+
$result = $this->fieldset->get('nonexistent');
51+
52+
$this->assertNull($result);
53+
}
54+
55+
public function testHasReturnsTrueForExistingField(): void
56+
{
57+
$this->fieldset->add('field1', $this->field1);
58+
59+
$this->assertTrue($this->fieldset->has('field1'));
60+
}
61+
62+
public function testHasReturnsFalseForNonExistentField(): void
63+
{
64+
$this->assertFalse($this->fieldset->has('nonexistent'));
65+
}
66+
67+
public function testCanAddMultipleFields(): void
68+
{
69+
$this->fieldset->add('field1', $this->field1);
70+
$this->fieldset->add('field2', $this->field2);
71+
72+
$this->assertTrue($this->fieldset->has('field1'));
73+
$this->assertTrue($this->fieldset->has('field2'));
74+
$this->assertSame($this->field1, $this->fieldset->get('field1'));
75+
$this->assertSame($this->field2, $this->fieldset->get('field2'));
76+
}
77+
78+
public function testAddingFieldWithSameNameOverwritesPrevious(): void
79+
{
80+
$this->fieldset->add('field1', $this->field1);
81+
$this->fieldset->add('field1', $this->field2);
82+
83+
$retrievedField = $this->fieldset->get('field1');
84+
85+
$this->assertSame($this->field2, $retrievedField);
86+
$this->assertNotSame($this->field1, $retrievedField);
87+
}
88+
89+
public function testFilterReturnsMatchingFields(): void
90+
{
91+
$this->fieldset->add('field1', $this->field1);
92+
$this->fieldset->add('field2', $this->field2);
93+
94+
$filtered = $this->fieldset->filter(fn (Field $field) => $field->name === 'test_field_1');
95+
96+
$this->assertCount(1, $filtered);
97+
$this->assertContains($this->field1, $filtered);
98+
$this->assertNotContains($this->field2, $filtered);
99+
}
100+
101+
public function testFilterReturnsEmptyArrayWhenNoMatches(): void
102+
{
103+
$this->fieldset->add('field1', $this->field1);
104+
$this->fieldset->add('field2', $this->field2);
105+
106+
$filtered = $this->fieldset->filter(fn (Field $field) => $field->name === 'nonexistent');
107+
108+
$this->assertEmpty($filtered);
109+
}
110+
111+
public function testFilterReturnsAllFieldsWhenAllMatch(): void
112+
{
113+
$this->fieldset->add('field1', $this->field1);
114+
$this->fieldset->add('field2', $this->field2);
115+
116+
$filtered = $this->fieldset->filter(fn (Field $field) => true);
117+
118+
$this->assertCount(2, $filtered);
119+
$this->assertContains($this->field1, $filtered);
120+
$this->assertContains($this->field2, $filtered);
121+
}
122+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Core\Metadata\Schema\Field;
6+
7+
use Constructo\Contract\Formatter;
8+
use Constructo\Core\Metadata\Schema\Field\Rule;
9+
use Constructo\Core\Metadata\Schema\Registry\Spec;
10+
use Constructo\Support\Set;
11+
use PHPUnit\Framework\TestCase;
12+
13+
final class RuleTest extends TestCase
14+
{
15+
public function testRuleConstructionWithBasicSpec(): void
16+
{
17+
$properties = Set::createFrom([]);
18+
$spec = new Spec('required', $properties, null);
19+
20+
$rule = new Rule($spec, []);
21+
22+
$this->assertSame($spec, $rule->spec);
23+
$this->assertSame([], $rule->arguments);
24+
$this->assertSame('required', $rule->key);
25+
}
26+
27+
public function testRuleConstructionWithKindProperty(): void
28+
{
29+
$properties = Set::createFrom(['kind' => 'type']);
30+
$spec = new Spec('string', $properties, null);
31+
32+
$rule = new Rule($spec, []);
33+
34+
$this->assertSame('type', $rule->key);
35+
}
36+
37+
public function testRuleConstructionWithArguments(): void
38+
{
39+
$properties = Set::createFrom([]);
40+
$spec = new Spec('min', $properties, null);
41+
$arguments = ['value' => 10];
42+
43+
$rule = new Rule($spec, $arguments);
44+
45+
$this->assertSame($arguments, $rule->arguments);
46+
}
47+
48+
public function testToStringWithoutArguments(): void
49+
{
50+
$properties = Set::createFrom([]);
51+
$spec = new Spec('required', $properties, null);
52+
$rule = new Rule($spec, []);
53+
54+
$result = $rule->__toString();
55+
56+
$this->assertSame('required', $result);
57+
}
58+
59+
public function testToStringWithSimpleArguments(): void
60+
{
61+
$properties = Set::createFrom([]);
62+
$spec = new Spec('min', $properties, null);
63+
$rule = new Rule($spec, [10]);
64+
65+
$result = $rule->__toString();
66+
67+
$this->assertSame('min:10', $result);
68+
}
69+
70+
public function testToStringWithMultipleArguments(): void
71+
{
72+
$properties = Set::createFrom([]);
73+
$spec = new Spec('between', $properties, null);
74+
$rule = new Rule($spec, [10, 20]);
75+
76+
$result = $rule->__toString();
77+
78+
$this->assertSame('between:10,20', $result);
79+
}
80+
81+
public function testToStringWithStringArguments(): void
82+
{
83+
$properties = Set::createFrom([]);
84+
$spec = new Spec('regex', $properties, null);
85+
$rule = new Rule($spec, ['/^[a-z]+$/']);
86+
87+
$result = $rule->__toString();
88+
89+
$this->assertSame('regex:/^[a-z]+$/', $result);
90+
}
91+
92+
public function testToStringWithArrayArguments(): void
93+
{
94+
$properties = Set::createFrom([]);
95+
$spec = new Spec('in', $properties, null);
96+
$rule = new Rule($spec, [['apple', 'banana', 'cherry']]);
97+
98+
$result = $rule->__toString();
99+
100+
$this->assertSame('in:apple,banana,cherry', $result);
101+
}
102+
103+
public function testToStringWithNestedArrayArguments(): void
104+
{
105+
$properties = Set::createFrom([]);
106+
$spec = new Spec('complex', $properties, null);
107+
$rule = new Rule($spec, [['a', ['b', 'c']], 'd']);
108+
109+
$result = $rule->__toString();
110+
111+
$this->assertSame('complex:a,b,c,d', $result);
112+
}
113+
114+
public function testToStringWithMixedArguments(): void
115+
{
116+
$properties = Set::createFrom([]);
117+
$spec = new Spec('mixed', $properties, null);
118+
$rule = new Rule($spec, ['string', 42, true, ['array', 'values']]);
119+
120+
$result = $rule->__toString();
121+
122+
$this->assertSame('mixed:string,42,1,array,values', $result);
123+
}
124+
125+
public function testToStringWithFormatter(): void
126+
{
127+
$formatter = $this->createMock(Formatter::class);
128+
$formatter->expects($this->once())
129+
->method('format')
130+
->with(['value1', 'value2'])
131+
->willReturn(['formatted1', 'formatted2']);
132+
133+
$properties = Set::createFrom([]);
134+
$spec = new Spec('custom', $properties, $formatter);
135+
$rule = new Rule($spec, ['value1', 'value2']);
136+
137+
$result = $rule->__toString();
138+
139+
$this->assertSame('custom:formatted1,formatted2', $result);
140+
}
141+
142+
public function testJsonSerialize(): void
143+
{
144+
$properties = Set::createFrom([]);
145+
$spec = new Spec('required', $properties, null);
146+
$rule = new Rule($spec, []);
147+
148+
$result = $rule->jsonSerialize();
149+
150+
$this->assertSame('required', $result);
151+
}
152+
153+
public function testJsonSerializeWithArguments(): void
154+
{
155+
$properties = Set::createFrom([]);
156+
$spec = new Spec('min', $properties, null);
157+
$rule = new Rule($spec, [10]);
158+
159+
$result = $rule->jsonSerialize();
160+
161+
$this->assertSame('min:10', $result);
162+
}
163+
164+
public function testJsonEncoding(): void
165+
{
166+
$properties = Set::createFrom([]);
167+
$spec = new Spec('between', $properties, null);
168+
$rule = new Rule($spec, [5, 15]);
169+
170+
$json = json_encode($rule);
171+
172+
$this->assertSame('"between:5,15"', $json);
173+
}
174+
175+
public function testRuleKeyFallsBackToSpecName(): void
176+
{
177+
$properties = Set::createFrom(['other' => 'value']);
178+
$spec = new Spec('email', $properties, null);
179+
$rule = new Rule($spec, []);
180+
181+
$this->assertSame('email', $rule->key);
182+
}
183+
184+
public function testEnforceMethodWithBooleanValues(): void
185+
{
186+
$properties = Set::createFrom([]);
187+
$spec = new Spec('test', $properties, null);
188+
$rule = new Rule($spec, [true, false]);
189+
190+
$result = $rule->__toString();
191+
192+
$this->assertSame('test:1,', $result);
193+
}
194+
195+
public function testEnforceMethodWithNullValue(): void
196+
{
197+
$properties = Set::createFrom([]);
198+
$spec = new Spec('test', $properties, null);
199+
$rule = new Rule($spec, [null]);
200+
201+
$result = $rule->__toString();
202+
203+
$this->assertSame('test:', $result);
204+
}
205+
}

0 commit comments

Comments
 (0)