tangled
alpha
login
or
join now
socialde.pt
/
atp-schema
1
fork
atom
Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1
fork
atom
overview
issues
pulls
pipelines
Implement PHPDoc generation
Miguel Batres
4 months ago
e59a41d6
c19826c4
+648
-17
3 changed files
expand all
collapse all
unified
split
src
Generator
ClassGenerator.php
DocBlockGenerator.php
tests
Unit
Generator
DocBlockGeneratorTest.php
+17
-17
src/Generator/ClassGenerator.php
···
28
protected MethodGenerator $methodGenerator;
29
30
/**
0
0
0
0
0
31
* Create a new ClassGenerator.
32
*/
33
public function __construct(
34
?NamingConverter $naming = null,
35
?TypeMapper $typeMapper = null,
36
?StubRenderer $renderer = null,
37
-
?MethodGenerator $methodGenerator = null
0
38
) {
39
$this->naming = $naming ?? new NamingConverter;
40
$this->typeMapper = $typeMapper ?? new TypeMapper($this->naming);
41
$this->renderer = $renderer ?? new StubRenderer;
42
$this->methodGenerator = $methodGenerator ?? new MethodGenerator($this->naming, $this->typeMapper, $this->renderer);
0
43
}
44
45
/**
···
174
*/
175
protected function generateClassDocBlock(LexiconDocument $document, array $definition): string
176
{
177
-
$lines = ['/**'];
178
-
179
-
if ($document->description) {
180
-
$lines[] = ' * '.$document->description;
181
-
$lines[] = ' *';
182
-
}
183
-
184
-
$lines[] = ' * Lexicon: '.$document->getNsid();
185
-
186
-
if (isset($definition['type'])) {
187
-
$lines[] = ' * Type: '.$definition['type'];
188
-
}
189
-
190
-
$lines[] = ' */';
191
-
192
-
return implode("\n", $lines);
193
}
194
195
/**
···
270
public function getMethodGenerator(): MethodGenerator
271
{
272
return $this->methodGenerator;
0
0
0
0
0
0
0
0
273
}
274
}
···
28
protected MethodGenerator $methodGenerator;
29
30
/**
31
+
* DocBlock generator instance.
32
+
*/
33
+
protected DocBlockGenerator $docBlockGenerator;
34
+
35
+
/**
36
* Create a new ClassGenerator.
37
*/
38
public function __construct(
39
?NamingConverter $naming = null,
40
?TypeMapper $typeMapper = null,
41
?StubRenderer $renderer = null,
42
+
?MethodGenerator $methodGenerator = null,
43
+
?DocBlockGenerator $docBlockGenerator = null
44
) {
45
$this->naming = $naming ?? new NamingConverter;
46
$this->typeMapper = $typeMapper ?? new TypeMapper($this->naming);
47
$this->renderer = $renderer ?? new StubRenderer;
48
$this->methodGenerator = $methodGenerator ?? new MethodGenerator($this->naming, $this->typeMapper, $this->renderer);
49
+
$this->docBlockGenerator = $docBlockGenerator ?? new DocBlockGenerator($this->typeMapper);
50
}
51
52
/**
···
181
*/
182
protected function generateClassDocBlock(LexiconDocument $document, array $definition): string
183
{
184
+
return $this->docBlockGenerator->generateClassDocBlock($document, $definition);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
185
}
186
187
/**
···
262
public function getMethodGenerator(): MethodGenerator
263
{
264
return $this->methodGenerator;
265
+
}
266
+
267
+
/**
268
+
* Get the docblock generator.
269
+
*/
270
+
public function getDocBlockGenerator(): DocBlockGenerator
271
+
{
272
+
return $this->docBlockGenerator;
273
}
274
}
+303
src/Generator/DocBlockGenerator.php
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Generator;
4
+
5
+
use SocialDept\Schema\Data\LexiconDocument;
6
+
7
+
class DocBlockGenerator
8
+
{
9
+
/**
10
+
* Type mapper instance.
11
+
*/
12
+
protected TypeMapper $typeMapper;
13
+
14
+
/**
15
+
* Create a new DocBlockGenerator.
16
+
*/
17
+
public function __construct(?TypeMapper $typeMapper = null)
18
+
{
19
+
$this->typeMapper = $typeMapper ?? new TypeMapper;
20
+
}
21
+
22
+
/**
23
+
* Generate a class-level docblock with rich annotations.
24
+
*
25
+
* @param array<string, mixed> $definition
26
+
*/
27
+
public function generateClassDocBlock(
28
+
LexiconDocument $document,
29
+
array $definition
30
+
): string {
31
+
$lines = ['/**'];
32
+
33
+
// Add description
34
+
if ($document->description) {
35
+
$lines = array_merge($lines, $this->wrapDescription($document->description));
36
+
$lines[] = ' *';
37
+
}
38
+
39
+
// Add lexicon metadata
40
+
$lines[] = ' * Lexicon: '.$document->getNsid();
41
+
42
+
if (isset($definition['type'])) {
43
+
$lines[] = ' * Type: '.$definition['type'];
44
+
}
45
+
46
+
// Add @property tags for magic access
47
+
$properties = $definition['properties'] ?? [];
48
+
$required = $definition['required'] ?? [];
49
+
50
+
if (! empty($properties)) {
51
+
$lines[] = ' *';
52
+
foreach ($properties as $name => $propDef) {
53
+
$isRequired = in_array($name, $required);
54
+
$docType = $this->typeMapper->toPhpDocType($propDef, ! $isRequired);
55
+
$desc = $propDef['description'] ?? '';
56
+
57
+
if ($desc) {
58
+
$lines[] = ' * @property '.$docType.' $'.$name.' '.$desc;
59
+
} else {
60
+
$lines[] = ' * @property '.$docType.' $'.$name;
61
+
}
62
+
}
63
+
}
64
+
65
+
// Add validation constraints as annotations
66
+
if (! empty($properties)) {
67
+
$constraints = $this->extractConstraints($properties, $required);
68
+
if (! empty($constraints)) {
69
+
$lines[] = ' *';
70
+
$lines[] = ' * Constraints:';
71
+
foreach ($constraints as $constraint) {
72
+
$lines[] = ' * - '.$constraint;
73
+
}
74
+
}
75
+
}
76
+
77
+
$lines[] = ' */';
78
+
79
+
return implode("\n", $lines);
80
+
}
81
+
82
+
/**
83
+
* Generate a property-level docblock.
84
+
*
85
+
* @param array<string, mixed> $definition
86
+
*/
87
+
public function generatePropertyDocBlock(
88
+
string $name,
89
+
array $definition,
90
+
bool $isRequired
91
+
): string {
92
+
$lines = [' /**'];
93
+
94
+
// Add description
95
+
if (isset($definition['description'])) {
96
+
$lines = array_merge($lines, $this->wrapDescription($definition['description'], ' * '));
97
+
$lines[] = ' *';
98
+
}
99
+
100
+
// Add type annotation
101
+
$docType = $this->typeMapper->toPhpDocType($definition, ! $isRequired);
102
+
$lines[] = ' * @var '.$docType;
103
+
104
+
// Add validation constraints
105
+
$constraints = $this->extractPropertyConstraints($definition);
106
+
if (! empty($constraints)) {
107
+
$lines[] = ' *';
108
+
foreach ($constraints as $constraint) {
109
+
$lines[] = ' * '.$constraint;
110
+
}
111
+
}
112
+
113
+
$lines[] = ' */';
114
+
115
+
return implode("\n", $lines);
116
+
}
117
+
118
+
/**
119
+
* Generate a method-level docblock.
120
+
*
121
+
* @param array<array{name: string, type: string, description?: string}> $params
122
+
*/
123
+
public function generateMethodDocBlock(
124
+
?string $description,
125
+
?string $returnType,
126
+
array $params = [],
127
+
?string $throws = null
128
+
): string {
129
+
$lines = [' /**'];
130
+
131
+
// Add description
132
+
if ($description) {
133
+
$lines = array_merge($lines, $this->wrapDescription($description, ' * '));
134
+
}
135
+
136
+
// Add blank line if we have params or return
137
+
if ((! empty($params) || $returnType) && $description) {
138
+
$lines[] = ' *';
139
+
}
140
+
141
+
// Add parameters
142
+
foreach ($params as $param) {
143
+
$desc = $param['description'] ?? '';
144
+
if ($desc) {
145
+
$lines[] = ' * @param '.$param['type'].' $'.$param['name'].' '.$desc;
146
+
} else {
147
+
$lines[] = ' * @param '.$param['type'].' $'.$param['name'];
148
+
}
149
+
}
150
+
151
+
// Add return type
152
+
if ($returnType && $returnType !== 'void') {
153
+
$lines[] = ' * @return '.$returnType;
154
+
}
155
+
156
+
// Add throws
157
+
if ($throws) {
158
+
$lines[] = ' * @throws '.$throws;
159
+
}
160
+
161
+
$lines[] = ' */';
162
+
163
+
return implode("\n", $lines);
164
+
}
165
+
166
+
/**
167
+
* Wrap a long description into multiple lines.
168
+
*
169
+
* @return array<string>
170
+
*/
171
+
protected function wrapDescription(string $description, string $prefix = ' * '): array
172
+
{
173
+
$maxWidth = 80 - strlen($prefix);
174
+
$words = explode(' ', $description);
175
+
$lines = [];
176
+
$currentLine = '';
177
+
178
+
foreach ($words as $word) {
179
+
if (empty($currentLine)) {
180
+
$currentLine = $word;
181
+
} elseif (strlen($currentLine.' '.$word) <= $maxWidth) {
182
+
$currentLine .= ' '.$word;
183
+
} else {
184
+
$lines[] = $prefix.$currentLine;
185
+
$currentLine = $word;
186
+
}
187
+
}
188
+
189
+
if (! empty($currentLine)) {
190
+
$lines[] = $prefix.$currentLine;
191
+
}
192
+
193
+
return $lines;
194
+
}
195
+
196
+
/**
197
+
* Extract validation constraints from properties.
198
+
*
199
+
* @param array<string, array<string, mixed>> $properties
200
+
* @param array<string> $required
201
+
* @return array<string>
202
+
*/
203
+
protected function extractConstraints(array $properties, array $required): array
204
+
{
205
+
$constraints = [];
206
+
207
+
// Required fields
208
+
if (! empty($required)) {
209
+
$constraints[] = 'Required: '.implode(', ', $required);
210
+
}
211
+
212
+
// Property-specific constraints
213
+
foreach ($properties as $name => $definition) {
214
+
$propConstraints = $this->extractPropertyConstraints($definition);
215
+
foreach ($propConstraints as $constraint) {
216
+
$constraints[] = $name.': '.trim(str_replace('@constraint', '', $constraint));
217
+
}
218
+
}
219
+
220
+
return $constraints;
221
+
}
222
+
223
+
/**
224
+
* Extract validation constraints for a single property.
225
+
*
226
+
* @param array<string, mixed> $definition
227
+
* @return array<string>
228
+
*/
229
+
protected function extractPropertyConstraints(array $definition): array
230
+
{
231
+
$constraints = [];
232
+
233
+
// String constraints
234
+
if (isset($definition['maxLength'])) {
235
+
$constraints[] = '@constraint Max length: '.$definition['maxLength'];
236
+
}
237
+
238
+
if (isset($definition['minLength'])) {
239
+
$constraints[] = '@constraint Min length: '.$definition['minLength'];
240
+
}
241
+
242
+
if (isset($definition['maxGraphemes'])) {
243
+
$constraints[] = '@constraint Max graphemes: '.$definition['maxGraphemes'];
244
+
}
245
+
246
+
if (isset($definition['minGraphemes'])) {
247
+
$constraints[] = '@constraint Min graphemes: '.$definition['minGraphemes'];
248
+
}
249
+
250
+
// Number constraints
251
+
if (isset($definition['maximum'])) {
252
+
$constraints[] = '@constraint Maximum: '.$definition['maximum'];
253
+
}
254
+
255
+
if (isset($definition['minimum'])) {
256
+
$constraints[] = '@constraint Minimum: '.$definition['minimum'];
257
+
}
258
+
259
+
// Array constraints
260
+
if (isset($definition['maxItems'])) {
261
+
$constraints[] = '@constraint Max items: '.$definition['maxItems'];
262
+
}
263
+
264
+
if (isset($definition['minItems'])) {
265
+
$constraints[] = '@constraint Min items: '.$definition['minItems'];
266
+
}
267
+
268
+
// Enum constraints
269
+
if (isset($definition['enum'])) {
270
+
$values = is_array($definition['enum']) ? implode(', ', $definition['enum']) : $definition['enum'];
271
+
$constraints[] = '@constraint Enum: '.$values;
272
+
}
273
+
274
+
// Format constraints
275
+
if (isset($definition['format'])) {
276
+
$constraints[] = '@constraint Format: '.$definition['format'];
277
+
}
278
+
279
+
// Const constraint
280
+
if (isset($definition['const'])) {
281
+
$value = is_bool($definition['const']) ? ($definition['const'] ? 'true' : 'false') : $definition['const'];
282
+
$constraints[] = '@constraint Const: '.$value;
283
+
}
284
+
285
+
return $constraints;
286
+
}
287
+
288
+
/**
289
+
* Generate a simple docblock.
290
+
*/
291
+
public function generateSimple(string $description): string
292
+
{
293
+
return " /**\n * {$description}\n */";
294
+
}
295
+
296
+
/**
297
+
* Generate a one-line docblock.
298
+
*/
299
+
public function generateOneLine(string $text): string
300
+
{
301
+
return " /** {$text} */";
302
+
}
303
+
}
+328
tests/Unit/Generator/DocBlockGeneratorTest.php
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Tests\Unit\Generator;
4
+
5
+
use Orchestra\Testbench\TestCase;
6
+
use SocialDept\Schema\Data\LexiconDocument;
7
+
use SocialDept\Schema\Generator\DocBlockGenerator;
8
+
use SocialDept\Schema\Parser\Nsid;
9
+
10
+
class DocBlockGeneratorTest extends TestCase
11
+
{
12
+
protected DocBlockGenerator $generator;
13
+
14
+
protected function setUp(): void
15
+
{
16
+
parent::setUp();
17
+
18
+
$this->generator = new DocBlockGenerator;
19
+
}
20
+
21
+
public function test_it_generates_class_docblock_with_description(): void
22
+
{
23
+
$document = $this->createDocument('app.test.post', [
24
+
'type' => 'record',
25
+
'description' => 'A social media post',
26
+
'properties' => [],
27
+
], 'A social media post');
28
+
29
+
$docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition());
30
+
31
+
$this->assertStringContainsString('/**', $docBlock);
32
+
$this->assertStringContainsString('* A social media post', $docBlock);
33
+
$this->assertStringContainsString('* Lexicon: app.test.post', $docBlock);
34
+
$this->assertStringContainsString('* Type: record', $docBlock);
35
+
}
36
+
37
+
public function test_it_generates_class_docblock_with_property_tags(): void
38
+
{
39
+
$document = $this->createDocument('app.test.user', [
40
+
'type' => 'record',
41
+
'properties' => [
42
+
'name' => [
43
+
'type' => 'string',
44
+
'description' => 'User name',
45
+
],
46
+
'age' => [
47
+
'type' => 'integer',
48
+
],
49
+
],
50
+
'required' => ['name'],
51
+
]);
52
+
53
+
$docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition());
54
+
55
+
$this->assertStringContainsString('@property string $name User name', $docBlock);
56
+
$this->assertStringContainsString('@property int|null $age', $docBlock);
57
+
}
58
+
59
+
public function test_it_includes_validation_constraints_in_class_docblock(): void
60
+
{
61
+
$document = $this->createDocument('app.test.post', [
62
+
'type' => 'record',
63
+
'properties' => [
64
+
'text' => [
65
+
'type' => 'string',
66
+
'maxLength' => 280,
67
+
],
68
+
],
69
+
'required' => ['text'],
70
+
]);
71
+
72
+
$docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition());
73
+
74
+
$this->assertStringContainsString('Constraints:', $docBlock);
75
+
$this->assertStringContainsString('Required: text', $docBlock);
76
+
$this->assertStringContainsString('text: Max length: 280', $docBlock);
77
+
}
78
+
79
+
public function test_it_generates_property_docblock(): void
80
+
{
81
+
$docBlock = $this->generator->generatePropertyDocBlock(
82
+
'title',
83
+
[
84
+
'type' => 'string',
85
+
'description' => 'The post title',
86
+
],
87
+
true
88
+
);
89
+
90
+
$this->assertStringContainsString('/**', $docBlock);
91
+
$this->assertStringContainsString('* The post title', $docBlock);
92
+
$this->assertStringContainsString('* @var string', $docBlock);
93
+
$this->assertStringContainsString('*/', $docBlock);
94
+
}
95
+
96
+
public function test_it_includes_constraints_in_property_docblock(): void
97
+
{
98
+
$docBlock = $this->generator->generatePropertyDocBlock(
99
+
'text',
100
+
[
101
+
'type' => 'string',
102
+
'maxLength' => 280,
103
+
'minLength' => 1,
104
+
],
105
+
true
106
+
);
107
+
108
+
$this->assertStringContainsString('@constraint Max length: 280', $docBlock);
109
+
$this->assertStringContainsString('@constraint Min length: 1', $docBlock);
110
+
}
111
+
112
+
public function test_it_generates_method_docblock(): void
113
+
{
114
+
$docBlock = $this->generator->generateMethodDocBlock(
115
+
'Create a new post',
116
+
'static',
117
+
[
118
+
['name' => 'text', 'type' => 'string', 'description' => 'Post text'],
119
+
['name' => 'author', 'type' => 'string'],
120
+
]
121
+
);
122
+
123
+
$this->assertStringContainsString('* Create a new post', $docBlock);
124
+
$this->assertStringContainsString('* @param string $text Post text', $docBlock);
125
+
$this->assertStringContainsString('* @param string $author', $docBlock);
126
+
$this->assertStringContainsString('* @return static', $docBlock);
127
+
}
128
+
129
+
public function test_it_handles_void_return_type(): void
130
+
{
131
+
$docBlock = $this->generator->generateMethodDocBlock(
132
+
'Process data',
133
+
'void',
134
+
[]
135
+
);
136
+
137
+
$this->assertStringNotContainsString('@return', $docBlock);
138
+
}
139
+
140
+
public function test_it_includes_throws_annotation(): void
141
+
{
142
+
$docBlock = $this->generator->generateMethodDocBlock(
143
+
'Validate data',
144
+
'bool',
145
+
[],
146
+
'\\InvalidArgumentException'
147
+
);
148
+
149
+
$this->assertStringContainsString('@throws \\InvalidArgumentException', $docBlock);
150
+
}
151
+
152
+
public function test_it_wraps_long_descriptions(): void
153
+
{
154
+
$longDescription = 'This is a very long description that should be wrapped across multiple lines when it exceeds the maximum line width of eighty characters including the comment prefix and should definitely span more than one line';
155
+
156
+
$docBlock = $this->generator->generatePropertyDocBlock(
157
+
'description',
158
+
[
159
+
'type' => 'string',
160
+
'description' => $longDescription,
161
+
],
162
+
true
163
+
);
164
+
165
+
// Just verify the long description is present in the docblock
166
+
$this->assertStringContainsString('This is a very long description', $docBlock);
167
+
168
+
// And that it doesn't exceed reasonable line lengths
169
+
$lines = explode("\n", $docBlock);
170
+
foreach ($lines as $line) {
171
+
$this->assertLessThan(120, strlen($line), 'Line too long: '.$line);
172
+
}
173
+
}
174
+
175
+
public function test_it_extracts_max_length_constraint(): void
176
+
{
177
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
178
+
['maxLength' => 100],
179
+
]);
180
+
181
+
$this->assertContains('@constraint Max length: 100', $constraints);
182
+
}
183
+
184
+
public function test_it_extracts_min_length_constraint(): void
185
+
{
186
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
187
+
['minLength' => 5],
188
+
]);
189
+
190
+
$this->assertContains('@constraint Min length: 5', $constraints);
191
+
}
192
+
193
+
public function test_it_extracts_grapheme_constraints(): void
194
+
{
195
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
196
+
['maxGraphemes' => 280, 'minGraphemes' => 1],
197
+
]);
198
+
199
+
$this->assertContains('@constraint Max graphemes: 280', $constraints);
200
+
$this->assertContains('@constraint Min graphemes: 1', $constraints);
201
+
}
202
+
203
+
public function test_it_extracts_number_constraints(): void
204
+
{
205
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
206
+
['maximum' => 100, 'minimum' => 0],
207
+
]);
208
+
209
+
$this->assertContains('@constraint Maximum: 100', $constraints);
210
+
$this->assertContains('@constraint Minimum: 0', $constraints);
211
+
}
212
+
213
+
public function test_it_extracts_array_constraints(): void
214
+
{
215
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
216
+
['maxItems' => 10, 'minItems' => 1],
217
+
]);
218
+
219
+
$this->assertContains('@constraint Max items: 10', $constraints);
220
+
$this->assertContains('@constraint Min items: 1', $constraints);
221
+
}
222
+
223
+
public function test_it_extracts_enum_constraint(): void
224
+
{
225
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
226
+
['enum' => ['active', 'inactive', 'pending']],
227
+
]);
228
+
229
+
$this->assertContains('@constraint Enum: active, inactive, pending', $constraints);
230
+
}
231
+
232
+
public function test_it_extracts_format_constraint(): void
233
+
{
234
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
235
+
['format' => 'datetime'],
236
+
]);
237
+
238
+
$this->assertContains('@constraint Format: datetime', $constraints);
239
+
}
240
+
241
+
public function test_it_extracts_const_constraint(): void
242
+
{
243
+
$constraints = $this->invokeMethod('extractPropertyConstraints', [
244
+
['const' => true],
245
+
]);
246
+
247
+
$this->assertContains('@constraint Const: true', $constraints);
248
+
}
249
+
250
+
public function test_it_generates_simple_docblock(): void
251
+
{
252
+
$docBlock = $this->generator->generateSimple('A simple description');
253
+
254
+
$this->assertSame(" /**\n * A simple description\n */", $docBlock);
255
+
}
256
+
257
+
public function test_it_generates_one_line_docblock(): void
258
+
{
259
+
$docBlock = $this->generator->generateOneLine('Quick note');
260
+
261
+
$this->assertSame(' /** Quick note */', $docBlock);
262
+
}
263
+
264
+
public function test_it_handles_empty_properties(): void
265
+
{
266
+
$document = $this->createDocument('app.test.empty', [
267
+
'type' => 'record',
268
+
'properties' => [],
269
+
'required' => [],
270
+
]);
271
+
272
+
$docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition());
273
+
274
+
$this->assertStringContainsString('Lexicon: app.test.empty', $docBlock);
275
+
$this->assertStringNotContainsString('@property', $docBlock);
276
+
}
277
+
278
+
public function test_it_handles_nullable_properties(): void
279
+
{
280
+
$document = $this->createDocument('app.test.post', [
281
+
'type' => 'record',
282
+
'properties' => [
283
+
'subtitle' => ['type' => 'string'],
284
+
],
285
+
'required' => [],
286
+
]);
287
+
288
+
$docBlock = $this->generator->generateClassDocBlock($document, $document->getMainDefinition());
289
+
290
+
$this->assertStringContainsString('@property string|null $subtitle', $docBlock);
291
+
}
292
+
293
+
/**
294
+
* Helper to create a test document.
295
+
*
296
+
* @param array<string, mixed> $mainDef
297
+
*/
298
+
protected function createDocument(string $nsid, array $mainDef, ?string $description = null): LexiconDocument
299
+
{
300
+
return new LexiconDocument(
301
+
lexicon: 1,
302
+
id: Nsid::parse($nsid),
303
+
defs: ['main' => $mainDef],
304
+
description: $description,
305
+
source: null,
306
+
raw: [
307
+
'lexicon' => 1,
308
+
'id' => $nsid,
309
+
'defs' => ['main' => $mainDef],
310
+
]
311
+
);
312
+
}
313
+
314
+
/**
315
+
* Helper to invoke protected method.
316
+
*
317
+
* @param array<mixed> $args
318
+
* @return mixed
319
+
*/
320
+
protected function invokeMethod(string $methodName, array $args)
321
+
{
322
+
$reflection = new \ReflectionClass($this->generator);
323
+
$method = $reflection->getMethod($methodName);
324
+
$method->setAccessible(true);
325
+
326
+
return $method->invokeArgs($this->generator, $args);
327
+
}
328
+
}