skir-php-generator 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/generator.js CHANGED
@@ -1,968 +1,12 @@
1
+ import { generatePhp, PHP_FILE_HEADER, } from "@php-skir/generator-core";
2
+ import { GeneratorConfig, } from "./config.js";
3
+ import { StandardPhpTarget } from "./target.js";
4
+ export { PHP_FILE_HEADER };
1
5
  export function generatePhpFiles(input) {
2
- const namespace = input.config?.namespace ?? "App\\Skir";
3
- const classNames = buildClassNameRegistry(namespace, input.modules);
4
- const methodGroups = new Map();
5
- const recordFiles = [];
6
- for (const module of input.modules) {
7
- const context = outputContextForModule(namespace, module, input.recordMap, classNames);
8
- recordFiles.push(...(module.records ?? [])
9
- .map((record, index) => normalizeRecord(record, module, index, classNames))
10
- .filter((record) => isStruct(record) || isEnum(record))
11
- .map((record) => isEnum(record)
12
- ? generateEnumFile(context, record)
13
- : generateStructFile(context, record)));
14
- const methods = module.methods ?? [];
15
- if (methods.length > 0) {
16
- const groupKey = `${context.namespace}\n${context.pathPrefix}`;
17
- const existingGroup = methodGroups.get(groupKey);
18
- if (existingGroup !== undefined) {
19
- existingGroup.methods.push(...methods);
20
- }
21
- else {
22
- methodGroups.set(groupKey, {
23
- context,
24
- methods: [...methods],
25
- });
26
- }
27
- }
28
- }
29
- return [
30
- ...recordFiles,
31
- ...Array.from(methodGroups.values()).flatMap((group) => [
32
- generateMethodsFile(group.context, group.methods),
33
- generateMethodEnumFile(group.context, group.methods),
34
- generateClientFile(group.context, group.methods),
35
- generateProceduresFile(group.context, group.methods),
36
- generateAbstractProceduresFile(group.context, group.methods),
37
- generateProcedureProviderFile(group.context, group.methods),
38
- ]),
39
- ];
40
- }
41
- function generateStructFile(context, record) {
42
- const className = classNameForRecord(record);
43
- const fields = collectStructFields(record);
44
- const runtimeImports = [
45
- "Skir\\Runtime\\DenseJson",
46
- "Skir\\Runtime\\Field",
47
- "Skir\\Runtime\\Type",
48
- ];
49
- const fileContext = fileOutputContext(context, className, runtimeImports);
50
- const constructor = generateConstructor(fields, fileContext);
51
- const skirType = generateSkirType(record, fileContext);
52
- const fromArray = generateFromArray(className, fields, fileContext);
53
- const classMembers = [
54
- constructor,
55
- skirType,
56
- generateToArray(fields, fileContext),
57
- fromArray,
58
- generateToDenseJson(),
59
- generateFromDenseJson(className),
60
- ].filter((member) => member !== null);
61
- return {
62
- path: outputPath(context, `${className}.php`),
63
- code: [
64
- "<?php",
65
- "",
66
- "declare(strict_types=1);",
67
- "",
68
- `namespace ${context.namespace};`,
69
- "",
70
- ...generateUseStatements(fileContext, runtimeImports),
71
- "",
72
- `final readonly class ${className}`,
73
- "{",
74
- ...classMembers.flatMap((member, index) => index === 0
75
- ? [indent(member)]
76
- : ["", indent(member)]),
77
- "}",
78
- "",
79
- ].join("\n"),
80
- };
81
- }
82
- function generateConstructor(fields, context) {
83
- if (fields.length === 0) {
84
- return null;
85
- }
86
- return [
87
- "public function __construct(",
88
- ...fields.map((field) => ` public ${phpType(field.type, context)} $${toPropertyName(field.name)},`),
89
- ") {}",
90
- ].join("\n");
91
- }
92
- function generateSkirType(record, context) {
93
- const entries = collectDeclarations(record, "string")
94
- .map((declaration) => {
95
- if (isRemovedDeclaration(declaration)) {
96
- return ` Field::removed(${declaration.number}),`;
97
- }
98
- return ` Field::value('${declaration.name}', ${declaration.number}, ${runtimeTypeExpression(declaration.type ?? "string", context)}),`;
99
- })
100
- .join("\n");
101
- return [
102
- "public static function skirType(): Type",
103
- "{",
104
- " return Type::struct([",
105
- entries,
106
- " ]);",
107
- "}",
108
- ].join("\n");
109
- }
110
- function generateToArray(fields, context) {
111
- return [
112
- "/** @return array<string, mixed> */",
113
- "public function toArray(): array",
114
- "{",
115
- " return [",
116
- ...fields.map((field) => {
117
- const property = toPropertyName(field.name);
118
- return ` '${field.name}' => ${valueToArrayExpression(field.type, `$this->${property}`, context)},`;
119
- }),
120
- " ];",
121
- "}",
122
- ].join("\n");
123
- }
124
- function generateFromArray(className, fields, context) {
125
- return [
126
- "/** @param array<string, mixed> $data */",
127
- `public static function fromArray(array $data): ${className}`,
128
- "{",
129
- " return new self(",
130
- ...fields.map((field) => ` ${toPropertyName(field.name)}: ${valueFromArrayExpression(field.type, `$data['${field.name}']`, context)},`),
131
- " );",
132
- "}",
133
- ].join("\n");
134
- }
135
- function generateToDenseJson() {
136
- return [
137
- "public function toDenseJson(): string",
138
- "{",
139
- " return DenseJson::toJson(self::skirType(), $this->toArray());",
140
- "}",
141
- ].join("\n");
142
- }
143
- function generateFromDenseJson(className) {
144
- return [
145
- `public static function fromDenseJson(string $json): ${className}`,
146
- "{",
147
- " return self::fromArray(DenseJson::fromJson(self::skirType(), $json));",
148
- "}",
149
- ].join("\n");
150
- }
151
- function generateEnumFile(context, record) {
152
- const className = classNameForRecord(record);
153
- const variants = collectDeclarations(record);
154
- const runtimeImports = [
155
- "Skir\\Runtime\\DenseJson",
156
- "Skir\\Runtime\\EnumValue",
157
- "Skir\\Runtime\\Type",
158
- "Skir\\Runtime\\Variant",
159
- ];
160
- const fileContext = fileOutputContext(context, className, runtimeImports);
161
- const constructors = generateEnumConstructors(variants, fileContext);
162
- const skirType = generateEnumSkirType(record, fileContext);
163
- return {
164
- path: outputPath(context, `${className}.php`),
165
- code: [
166
- "<?php",
167
- "",
168
- "declare(strict_types=1);",
169
- "",
170
- `namespace ${context.namespace};`,
171
- "",
172
- ...generateUseStatements(fileContext, runtimeImports),
173
- "",
174
- `final readonly class ${className}`,
175
- "{",
176
- indent("private function __construct(private EnumValue $value) {}"),
177
- "",
178
- indent(constructors),
179
- "",
180
- indent(skirType),
181
- "",
182
- indent(generateEnumAccessors()),
183
- "",
184
- indent(generateEnumToSkirValue()),
185
- "",
186
- indent(generateEnumFromSkirValue(className)),
187
- "",
188
- indent(generateEnumToDenseValue()),
189
- "",
190
- indent(generateEnumFromDenseValue(className)),
191
- "",
192
- indent(generateEnumToDenseJson()),
193
- "",
194
- indent(generateEnumFromDenseJson(className)),
195
- "}",
196
- "",
197
- ].join("\n"),
198
- };
199
- }
200
- function generateEnumConstructors(variants, context) {
201
- return variants
202
- .filter((variant) => !isRemovedDeclaration(variant))
203
- .map((variant) => {
204
- if (variant.type === undefined) {
205
- return [
206
- `public static function ${toPropertyName(variant.name)}(): self`,
207
- "{",
208
- ` return new self(EnumValue::constant('${variant.name}'));`,
209
- "}",
210
- ].join("\n");
211
- }
212
- return [
213
- `public static function ${toPropertyName(variant.name)}(${phpType(variant.type, context)} $value): self`,
214
- "{",
215
- ` return new self(EnumValue::wrapper('${variant.name}', $value));`,
216
- "}",
217
- ].join("\n");
218
- })
219
- .join("\n\n");
220
- }
221
- function generateEnumSkirType(record, context) {
222
- const entries = collectDeclarations(record)
223
- .map((declaration) => {
224
- if (isRemovedDeclaration(declaration)) {
225
- return null;
226
- }
227
- if (declaration.type === undefined) {
228
- return ` Variant::constant('${declaration.name}', ${declaration.number}),`;
229
- }
230
- return ` Variant::wrapper('${declaration.name}', ${declaration.number}, ${runtimeTypeExpression(declaration.type, context)}),`;
231
- })
232
- .filter((entry) => entry !== null)
233
- .join("\n");
234
- return [
235
- "public static function skirType(): Type",
236
- "{",
237
- " return Type::enum([",
238
- entries,
239
- " ]);",
240
- "}",
241
- ].join("\n");
242
- }
243
- function generateEnumAccessors() {
244
- return [
245
- "public function name(): string",
246
- "{",
247
- " return $this->value->name;",
248
- "}",
249
- "",
250
- "public function payload(): mixed",
251
- "{",
252
- " return $this->value->value;",
253
- "}",
254
- ].join("\n");
255
- }
256
- function generateEnumToSkirValue() {
257
- return [
258
- "public function toSkirValue(): EnumValue",
259
- "{",
260
- " return $this->value;",
261
- "}",
262
- ].join("\n");
263
- }
264
- function generateEnumFromSkirValue(className) {
265
- return [
266
- `public static function fromSkirValue(EnumValue $value): ${className}`,
267
- "{",
268
- " return new self($value);",
269
- "}",
270
- ].join("\n");
271
- }
272
- function generateEnumToDenseValue() {
273
- return [
274
- "/** @return int|array<int, mixed> */",
275
- "public function toDenseValue(): int|array",
276
- "{",
277
- " return DenseJson::encode(self::skirType(), $this->value);",
278
- "}",
279
- ].join("\n");
280
- }
281
- function generateEnumFromDenseValue(className) {
282
- return [
283
- "/** @param int|array<int, mixed> $value */",
284
- `public static function fromDenseValue(int|array $value): ${className}`,
285
- "{",
286
- " return new self(DenseJson::decode(self::skirType(), $value));",
287
- "}",
288
- ].join("\n");
289
- }
290
- function generateEnumToDenseJson() {
291
- return [
292
- "public function toDenseJson(): string",
293
- "{",
294
- " return DenseJson::toJson(self::skirType(), $this->value);",
295
- "}",
296
- ].join("\n");
297
- }
298
- function generateEnumFromDenseJson(className) {
299
- return [
300
- `public static function fromDenseJson(string $json): ${className}`,
301
- "{",
302
- " return new self(DenseJson::fromJson(self::skirType(), $json));",
303
- "}",
304
- ].join("\n");
305
- }
306
- function generateMethodsFile(context, methods) {
307
- const runtimeImports = [
308
- "Skir\\Runtime\\MethodDescriptor",
309
- ];
310
- const fileContext = fileOutputContext(context, "SkirMethods", runtimeImports);
311
- const descriptors = methods.map((method) => generateMethodDescriptor(method, fileContext)).join("\n\n");
312
- return {
313
- path: outputPath(context, "SkirMethods.php"),
314
- code: [
315
- "<?php",
316
- "",
317
- "declare(strict_types=1);",
318
- "",
319
- `namespace ${context.namespace};`,
320
- "",
321
- ...generateUseStatements(fileContext, runtimeImports),
322
- "",
323
- "final readonly class SkirMethods",
324
- "{",
325
- indent(generateAllMethods(methods)),
326
- "",
327
- indent(descriptors),
328
- "}",
329
- "",
330
- ].join("\n"),
331
- };
332
- }
333
- function generateMethodEnumFile(context, methods) {
334
- const runtimeImports = [
335
- "Skir\\Runtime\\MethodDescriptor",
336
- "Skir\\Server\\Contracts\\SkirMethodReference",
337
- ];
338
- const className = methodEnumClassName(context);
339
- const fileContext = fileOutputContext(context, className, runtimeImports);
340
- const cases = methods.map((method) => `case ${toClassName(tokenText(method.name))};`).join("\n");
341
- const matches = methods
342
- .map((method) => `self::${toClassName(tokenText(method.name))} => SkirMethods::${toPropertyName(tokenText(method.name))}(),`)
343
- .join("\n");
344
- return {
345
- path: outputPath(context, `${className}.php`),
346
- code: [
347
- "<?php",
348
- "",
349
- "declare(strict_types=1);",
350
- "",
351
- `namespace ${context.namespace};`,
352
- "",
353
- ...generateUseStatements(fileContext, runtimeImports),
354
- "",
355
- `enum ${className} implements SkirMethodReference`,
356
- "{",
357
- indent(cases),
358
- "",
359
- indent([
360
- "public function descriptor(): MethodDescriptor",
361
- "{",
362
- " return match ($this) {",
363
- indent(indent(matches)),
364
- " };",
365
- "}",
366
- ].join("\n")),
367
- "}",
368
- "",
369
- ].join("\n"),
370
- };
371
- }
372
- function generateAllMethods(methods) {
373
- return [
374
- "/** @return array<string, MethodDescriptor> */",
375
- "public static function all(): array",
376
- "{",
377
- " return [",
378
- ...methods.map((method) => ` '${tokenText(method.name)}' => self::${toPropertyName(tokenText(method.name))}(),`),
379
- " ];",
380
- "}",
381
- ].join("\n");
382
- }
383
- function generateMethodDescriptor(method, context) {
384
- return [
385
- `public static function ${toPropertyName(tokenText(method.name))}(): MethodDescriptor`,
386
- "{",
387
- " return new MethodDescriptor(",
388
- ` name: '${tokenText(method.name)}',`,
389
- ` number: ${method.number},`,
390
- ` requestType: ${runtimeTypeExpression(method.requestType ?? "string", context)},`,
391
- ` responseType: ${runtimeTypeExpression(method.responseType ?? "string", context)},`,
392
- " );",
393
- "}",
394
- ].join("\n");
395
- }
396
- function generateClientFile(context, methods) {
397
- const runtimeImports = [
398
- "Skir\\Client\\SkirClient",
399
- ];
400
- const fileContext = fileOutputContext(context, "SkirRpcClient", runtimeImports);
401
- const clientMethods = methods.map((method) => generateClientMethod(method, fileContext)).join("\n\n");
402
- return {
403
- path: outputPath(context, "SkirRpcClient.php"),
404
- code: [
405
- "<?php",
406
- "",
407
- "declare(strict_types=1);",
408
- "",
409
- `namespace ${context.namespace};`,
410
- "",
411
- ...generateUseStatements(fileContext, runtimeImports),
412
- "",
413
- "final readonly class SkirRpcClient",
414
- "{",
415
- indent(generateClientConstructor()),
416
- "",
417
- indent(clientMethods),
418
- "}",
419
- "",
420
- ].join("\n"),
421
- };
422
- }
423
- function generateClientConstructor() {
424
- return [
425
- "public function __construct(",
426
- " private SkirClient $client,",
427
- ") {}",
428
- ].join("\n");
429
- }
430
- function generateClientMethod(method, context) {
431
- const methodName = toPropertyName(tokenText(method.name));
432
- const requestType = method.requestType ?? "string";
433
- const responseType = method.responseType ?? "string";
434
- const request = valueToArrayExpression(requestType, "$request", context);
435
- const response = valueFromArrayExpression(responseType, "$response", context);
436
- return [
437
- `public function ${methodName}(${phpType(requestType, context)} $request): ${phpType(responseType, context)}`,
438
- "{",
439
- ` $response = $this->client->invoke(SkirMethods::${methodName}(), ${request});`,
440
- "",
441
- ` return ${response};`,
442
- "}",
443
- ].join("\n");
444
- }
445
- function generateProceduresFile(context, methods) {
446
- const runtimeImports = [
447
- "Skir\\Server\\SkirContext",
448
- ];
449
- const fileContext = fileOutputContext(context, "SkirProcedures", runtimeImports);
450
- const procedureMethods = methods.map((method) => generateProcedureMethod(method, fileContext)).join("\n\n");
451
- return {
452
- path: outputPath(context, "SkirProcedures.php"),
453
- code: [
454
- "<?php",
455
- "",
456
- "declare(strict_types=1);",
457
- "",
458
- `namespace ${context.namespace};`,
459
- "",
460
- ...generateUseStatements(fileContext, runtimeImports),
461
- "",
462
- "interface SkirProcedures",
463
- "{",
464
- indent(procedureMethods),
465
- "}",
466
- "",
467
- ].join("\n"),
468
- };
469
- }
470
- function generateProcedureMethod(method, context) {
471
- const methodName = toPropertyName(tokenText(method.name));
472
- const requestType = method.requestType ?? "string";
473
- const responseType = method.responseType ?? "string";
474
- return `public function ${methodName}(${phpType(requestType, context)} $request, SkirContext $context): ${phpType(responseType, context)};`;
475
- }
476
- function generateAbstractProceduresFile(context, methods) {
477
- const runtimeImports = [
478
- "Skir\\Server\\ProcedureProvider",
479
- "Skir\\Server\\SkirContext",
480
- "Skir\\Server\\SkirServer",
481
- ];
482
- const fileContext = fileOutputContext(context, "AbstractSkirProcedures", runtimeImports);
483
- const procedureMethods = methods.map((method) => generateAbstractProcedureMethod(method, fileContext)).join("\n\n");
484
- const registrations = methods.map((method) => generateProcedureRegistration(method, fileContext, "$this")).join("\n\n");
485
- return {
486
- path: outputPath(context, "AbstractSkirProcedures.php"),
487
- code: [
488
- "<?php",
489
- "",
490
- "declare(strict_types=1);",
491
- "",
492
- `namespace ${context.namespace};`,
493
- "",
494
- ...generateUseStatements(fileContext, runtimeImports),
495
- "",
496
- "abstract class AbstractSkirProcedures implements ProcedureProvider",
497
- "{",
498
- indent(procedureMethods),
499
- "",
500
- indent([
501
- "public function register(SkirServer $server): void",
502
- "{",
503
- indent(registrations),
504
- "}",
505
- ].join("\n")),
506
- "}",
507
- "",
508
- ].join("\n"),
509
- };
510
- }
511
- function generateAbstractProcedureMethod(method, context) {
512
- const methodName = toPropertyName(tokenText(method.name));
513
- const requestType = method.requestType ?? "string";
514
- const responseType = method.responseType ?? "string";
515
- return `abstract public function ${methodName}(${phpType(requestType, context)} $request, SkirContext $context): ${phpType(responseType, context)};`;
516
- }
517
- function generateProcedureProviderFile(context, methods) {
518
- const runtimeImports = [
519
- "Skir\\Server\\ProcedureProvider",
520
- "Skir\\Server\\SkirContext",
521
- "Skir\\Server\\SkirServer",
522
- ];
523
- const fileContext = fileOutputContext(context, "SkirProcedureProvider", runtimeImports);
524
- const registrations = methods.map((method) => generateProcedureRegistration(method, fileContext)).join("\n\n");
525
- return {
526
- path: outputPath(context, "SkirProcedureProvider.php"),
527
- code: [
528
- "<?php",
529
- "",
530
- "declare(strict_types=1);",
531
- "",
532
- `namespace ${context.namespace};`,
533
- "",
534
- ...generateUseStatements(fileContext, runtimeImports),
535
- "",
536
- "final readonly class SkirProcedureProvider implements ProcedureProvider",
537
- "{",
538
- indent(generateProcedureProviderConstructor()),
539
- "",
540
- indent([
541
- "public function register(SkirServer $server): void",
542
- "{",
543
- indent(registrations),
544
- "}",
545
- ].join("\n")),
546
- "}",
547
- "",
548
- ].join("\n"),
549
- };
550
- }
551
- function generateProcedureProviderConstructor() {
552
- return [
553
- "public function __construct(",
554
- " private SkirProcedures $procedures,",
555
- ") {}",
556
- ].join("\n");
557
- }
558
- function generateProcedureRegistration(method, context, handlerTarget = "$this->procedures") {
559
- const methodName = toPropertyName(tokenText(method.name));
560
- const requestType = method.requestType ?? "string";
561
- const responseType = method.responseType ?? "string";
562
- const request = valueFromArrayExpression(requestType, "$request", context);
563
- const response = valueToArrayExpression(responseType, "$response", context);
564
- return [
565
- `$server->addMethod(SkirMethods::${methodName}(), function (mixed $request, SkirContext $context): mixed {`,
566
- ` $response = ${handlerTarget}->${methodName}(${request}, $context);`,
567
- "",
568
- ` return ${response};`,
569
- "});",
570
- ].join("\n");
571
- }
572
- function collectStructFields(record) {
573
- return collectDeclarations(record, "string")
574
- .filter((field) => !isRemovedDeclaration(field))
575
- .map((field) => ({
576
- name: field.name,
577
- number: field.number,
578
- type: field.type ?? "string",
579
- }));
580
- }
581
- function collectDeclarations(record, defaultMissingType) {
582
- const declarations = [];
583
- for (const field of record.fields ?? []) {
584
- if (field.kind === "removed") {
585
- declarations.push({ kind: "removed", number: field.number });
586
- continue;
587
- }
588
- declarations.push({
589
- name: tokenText(field.name),
590
- number: field.number,
591
- type: field.type ?? defaultMissingType,
592
- });
593
- }
594
- for (const removedNumber of record.removedNumbers ?? []) {
595
- if (!declarations.some((declaration) => declaration.number === removedNumber)) {
596
- declarations.push({ kind: "removed", number: removedNumber });
597
- }
598
- }
599
- return declarations.sort((left, right) => left.number - right.number);
600
- }
601
- function isStruct(record) {
602
- return record.recordType === "struct" || record.kind === "struct";
603
- }
604
- function isEnum(record) {
605
- return record.recordType === "enum" || record.kind === "enum";
606
- }
607
- function buildClassNameRegistry(rootNamespace, modules) {
608
- const candidates = modules.flatMap((module) => (module.records ?? []).map((record, index) => {
609
- const modulePath = modulePathForRecord(module, record);
610
- const baseClassName = "record" in record
611
- ? classNameForRecordLocation(record)
612
- : classNameForRecord(record);
613
- return {
614
- identity: recordIdentity(module, record, index),
615
- recordKey: recordKeyForRecord(record),
616
- namespace: outputContextForModulePath(rootNamespace, modulePath).namespace,
617
- modulePath,
618
- baseClassName,
619
- };
620
- }));
621
- const candidatesByClass = new Map();
622
- for (const candidate of candidates) {
623
- const groupKey = `${candidate.namespace}\\${candidate.baseClassName}`;
624
- const existingCandidates = candidatesByClass.get(groupKey);
625
- if (existingCandidates !== undefined) {
626
- existingCandidates.push(candidate);
627
- }
628
- else {
629
- candidatesByClass.set(groupKey, [candidate]);
630
- }
631
- }
632
- const namesByIdentity = new Map();
633
- const namesByRecordKey = new Map();
634
- for (const groupedCandidates of candidatesByClass.values()) {
635
- for (const candidate of groupedCandidates) {
636
- const className = groupedCandidates.length > 1
637
- ? `${moduleClassPrefix(candidate.modulePath)}${candidate.baseClassName}`
638
- : candidate.baseClassName;
639
- namesByIdentity.set(candidate.identity, className);
640
- if (candidate.recordKey !== undefined) {
641
- namesByRecordKey.set(candidate.recordKey, className);
642
- }
643
- }
644
- }
645
- return {
646
- namesByIdentity,
647
- namesByRecordKey,
648
- };
649
- }
650
- function modulePathForRecord(module, record) {
651
- if ("record" in record) {
652
- return record.modulePath ?? module.path;
653
- }
654
- return module.path;
655
- }
656
- function recordIdentity(module, record, index) {
657
- return recordKeyForRecord(record) ?? `${module.path}:${index}`;
658
- }
659
- function recordKeyForRecord(record) {
660
- const recordKey = "record" in record
661
- ? record.record.key
662
- : record.key;
663
- return typeof recordKey === "string" ? recordKey : undefined;
664
- }
665
- function classNameFromRegistry(classNames, identity, recordKey) {
666
- if (classNames === undefined) {
667
- return undefined;
668
- }
669
- if (typeof recordKey === "string") {
670
- const className = classNames.namesByRecordKey.get(recordKey);
671
- if (className !== undefined) {
672
- return className;
673
- }
674
- }
675
- if (identity !== undefined) {
676
- return classNames.namesByIdentity.get(identity);
677
- }
678
- return undefined;
679
- }
680
- function moduleClassPrefix(modulePath) {
681
- const moduleFileName = modulePath.split("/").at(-1) ?? modulePath;
682
- const moduleName = moduleFileName.replace(/\.skir$/, "");
683
- return toClassName(moduleName);
684
- }
685
- function normalizeRecord(record, module, index, classNames) {
686
- if ("record" in record) {
687
- return {
688
- ...record.record,
689
- phpClassName: classNameFromRegistry(classNames, recordIdentity(module, record, index), record.record.key) ?? classNameForRecordLocation(record),
690
- };
691
- }
692
- return {
693
- ...record,
694
- phpClassName: classNameFromRegistry(classNames, recordIdentity(module, record, index), record.key) ?? record.phpClassName,
695
- };
696
- }
697
- function isRemovedDeclaration(declaration) {
698
- return "kind" in declaration && declaration.kind === "removed";
699
- }
700
- function phpType(type, context) {
701
- const kind = typeKind(type);
702
- if (kind === "bool") {
703
- return "bool";
704
- }
705
- if (kind === "int32" || kind === "timestamp") {
706
- return "int";
707
- }
708
- if (kind === "int64" || kind === "hash64") {
709
- return "int|string";
710
- }
711
- if (kind === "float32" || kind === "float64") {
712
- return "float";
713
- }
714
- if (kind === "string" || kind === "bytes") {
715
- return "string";
716
- }
717
- if (kind === "array") {
718
- return "array";
719
- }
720
- if (kind === "optional") {
721
- return "?".concat(phpType(optionalInnerType(type), context));
722
- }
723
- if (kind === "record") {
724
- return recordTypeClassName(type, context);
725
- }
726
- return "mixed";
727
- }
728
- function valueToArrayExpression(type, expression, context) {
729
- const kind = typeKind(type);
730
- if (kind === "record") {
731
- if (isEnumRecordReference(type, context)) {
732
- return `${expression}->toSkirValue()`;
733
- }
734
- return `${expression}->toArray()`;
735
- }
736
- if (kind === "optional") {
737
- const innerType = optionalInnerType(type);
738
- if (typeKind(innerType) === "record" || typeKind(innerType) === "array") {
739
- return `${expression} === null ? null : ${valueToArrayExpression(innerType, expression, context)}`;
740
- }
741
- return expression;
742
- }
743
- if (kind === "array") {
744
- const itemType = arrayItemType(type);
745
- if (typeKind(itemType) === "record" || typeKind(itemType) === "optional" || typeKind(itemType) === "array") {
746
- return `array_map(fn (mixed $item): mixed => ${valueToArrayExpression(itemType, "$item", context)}, ${expression})`;
747
- }
748
- }
749
- return expression;
750
- }
751
- function valueFromArrayExpression(type, expression, context) {
752
- const kind = typeKind(type);
753
- if (kind === "record") {
754
- if (isEnumRecordReference(type, context)) {
755
- return `${recordTypeClassName(type, context)}::fromSkirValue(${expression})`;
756
- }
757
- return `${recordTypeClassName(type, context)}::fromArray(${expression})`;
758
- }
759
- if (kind === "optional") {
760
- const innerType = optionalInnerType(type);
761
- if (typeKind(innerType) === "record" || typeKind(innerType) === "array") {
762
- return `${expression} === null ? null : ${valueFromArrayExpression(innerType, expression, context)}`;
763
- }
764
- return expression;
765
- }
766
- if (kind === "array") {
767
- const itemType = arrayItemType(type);
768
- if (typeKind(itemType) === "record" || typeKind(itemType) === "optional" || typeKind(itemType) === "array") {
769
- return `array_map(fn (mixed $item): mixed => ${valueFromArrayExpression(itemType, "$item", context)}, ${expression})`;
770
- }
771
- }
772
- return expression;
773
- }
774
- function runtimeTypeExpression(type, context) {
775
- const kind = typeKind(type);
776
- if (kind === "array") {
777
- return `Type::array(${runtimeTypeExpression(arrayItemType(type), context)})`;
778
- }
779
- if (kind === "optional") {
780
- return `Type::optional(${runtimeTypeExpression(optionalInnerType(type), context)})`;
781
- }
782
- if (kind === "record") {
783
- return `${recordTypeClassName(type, context)}::skirType()`;
784
- }
785
- return `Type::${kind}()`;
786
- }
787
- function typeKind(type) {
788
- if (typeof type === "string") {
789
- return type;
790
- }
791
- if (type.kind === "primitive") {
792
- return type.primitive ?? "string";
793
- }
794
- return type.kind;
795
- }
796
- function arrayItemType(type) {
797
- if (typeof type !== "string" && type.kind === "array" && type.item !== undefined) {
798
- return type.item;
799
- }
800
- return "string";
801
- }
802
- function optionalInnerType(type) {
803
- if (typeof type !== "string" && type.kind === "optional" && type.other !== undefined) {
804
- return type.other;
805
- }
806
- return "string";
807
- }
808
- function recordTypeClassName(type, context) {
809
- if (typeof type === "string") {
810
- throw new Error("String primitive types cannot be used as record references.");
811
- }
812
- if (typeof type.key === "string" && context.recordMap !== undefined) {
813
- const recordLocation = context.recordMap.get(type.key);
814
- if (recordLocation !== undefined) {
815
- return classNameForRecordReference(context, recordLocation);
816
- }
817
- }
818
- if (type.name !== undefined) {
819
- return toClassName(tokenText(type.name));
820
- }
821
- if (type.nameParts !== undefined && type.nameParts.length > 0) {
822
- return classNameFromParts(type.nameParts.map((part) => recordNamePartText(part)));
823
- }
824
- throw new Error("Skir record reference is missing a name.");
825
- }
826
- function isEnumRecordReference(type, context) {
827
- if (typeof type === "string") {
828
- return false;
829
- }
830
- if (type.kind !== "record") {
831
- return false;
832
- }
833
- if (type.recordType === "enum") {
834
- return true;
835
- }
836
- if (typeof type.key !== "string") {
837
- return false;
838
- }
839
- return context.recordMap?.get(type.key)?.record.recordType === "enum";
840
- }
841
- function classNameForRecord(record) {
842
- return record.phpClassName ?? toClassName(tokenText(record.name));
843
- }
844
- function classNameForRecordReference(context, recordLocation) {
845
- const className = classNameFromRegistry(context.classNames, undefined, recordLocation.record.key) ?? classNameForRecordLocation(recordLocation);
846
- const recordContext = outputContextForModulePath(context.rootNamespace, recordLocation.modulePath ?? "", context.recordMap, context.classNames);
847
- if (recordContext.namespace === context.namespace) {
848
- return className;
849
- }
850
- return importedClassName(context, `${recordContext.namespace}\\${className}`);
851
- }
852
- function outputContextForModule(rootNamespace, module, recordMap, classNames) {
853
- return outputContextForModulePath(rootNamespace, module.path, recordMap, classNames);
854
- }
855
- function outputContextForModulePath(rootNamespace, modulePath, recordMap, classNames) {
856
- const directoryParts = modulePath
857
- .split("/")
858
- .slice(0, -1)
859
- .map((part) => toClassName(part))
860
- .filter((part) => part !== "");
861
- if (directoryParts.length === 0) {
862
- return {
863
- rootNamespace,
864
- namespace: rootNamespace,
865
- pathPrefix: "",
866
- recordMap,
867
- classNames,
868
- };
869
- }
870
- return {
871
- rootNamespace,
872
- namespace: [rootNamespace, ...directoryParts].join("\\"),
873
- pathPrefix: directoryParts.join("/"),
874
- recordMap,
875
- classNames,
876
- };
877
- }
878
- function outputPath(context, fileName) {
879
- if (context.pathPrefix === "") {
880
- return fileName;
881
- }
882
- return `${context.pathPrefix}/${fileName}`;
883
- }
884
- function methodEnumClassName(context) {
885
- if (context.pathPrefix === "") {
886
- return "SkirMethod";
887
- }
888
- return `${context.pathPrefix.split("/").map((part) => toClassName(part)).join("")}SkirMethod`;
889
- }
890
- function fileOutputContext(context, className, runtimeImports) {
891
- const reservedNames = new Set([
892
- className,
893
- ...runtimeImports.map((importName) => shortClassName(importName)),
894
- ]);
895
- return {
896
- ...context,
897
- imports: {
898
- reservedNames,
899
- imports: new Map(),
900
- },
901
- };
902
- }
903
- function generateUseStatements(context, runtimeImports) {
904
- const generatedImports = Array.from(context.imports?.imports.values() ?? []).sort();
905
- return [
906
- ...runtimeImports,
907
- ...generatedImports,
908
- ].map((importName) => `use ${importName};`);
909
- }
910
- function importedClassName(context, fullyQualifiedClassName) {
911
- const shortName = shortClassName(fullyQualifiedClassName);
912
- if (context.imports === undefined) {
913
- return `\\${fullyQualifiedClassName}`;
914
- }
915
- if (context.imports.reservedNames.has(shortName)) {
916
- return `\\${fullyQualifiedClassName}`;
917
- }
918
- const existingImport = context.imports.imports.get(shortName);
919
- if (existingImport === undefined) {
920
- context.imports.imports.set(shortName, fullyQualifiedClassName);
921
- return shortName;
922
- }
923
- if (existingImport === fullyQualifiedClassName) {
924
- return shortName;
925
- }
926
- return `\\${fullyQualifiedClassName}`;
927
- }
928
- function shortClassName(fullyQualifiedClassName) {
929
- return fullyQualifiedClassName.split("\\").at(-1) ?? fullyQualifiedClassName;
930
- }
931
- function classNameForRecordLocation(record) {
932
- if (record.recordAncestors !== undefined && record.recordAncestors.length > 0) {
933
- return classNameFromParts(record.recordAncestors.map((ancestor) => tokenText(ancestor.name)));
934
- }
935
- return toClassName(tokenText(record.record.name));
936
- }
937
- function classNameFromParts(parts) {
938
- return parts.map((part) => toClassName(part)).join("");
939
- }
940
- function recordNamePartText(part) {
941
- if (typeof part === "string") {
942
- return part;
943
- }
944
- if ("token" in part) {
945
- return tokenText(part.token);
946
- }
947
- return tokenText(part);
948
- }
949
- function toClassName(name) {
950
- return name
951
- .split(/[_\-\s]+/)
952
- .filter(Boolean)
953
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
954
- .join("");
955
- }
956
- function toPropertyName(name) {
957
- const className = toClassName(name);
958
- return className.charAt(0).toLowerCase() + className.slice(1);
959
- }
960
- function tokenText(token) {
961
- return typeof token === "string" ? token : token.text;
962
- }
963
- function indent(code) {
964
- return code
965
- .split("\n")
966
- .map((line) => (line === "" ? line : ` ${line}`))
967
- .join("\n");
6
+ const { namespace } = GeneratorConfig.parse(input.config ?? {});
7
+ return generatePhp({
8
+ ...input,
9
+ namespace,
10
+ adapter: new StandardPhpTarget(),
11
+ });
968
12
  }