@pylonts/dsl 1.0.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.
@@ -0,0 +1,143 @@
1
+ import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportRef } from './dto';
2
+ import { Field } from './dsl';
3
+
4
+ // TypeBox driver: renders a DtoMessage into TypeBox TypeScript source.
5
+ // Shape matches the codegen product consumed by fastify v5 TypeBoxTypeProvider:
6
+ //
7
+ // export const RegisterUserInput = Type.Object({...});
8
+ // export type RegisterUserInput = Static<typeof RegisterUserInput>;
9
+ //
10
+ // ENUM fields reference a generated enum (see enum-driver) by its import
11
+ // location — the resolver maps a jsName to its product import.
12
+ // DTO bases (.include()) render as Type.Intersect([...bases, Type.Object({...})]).
13
+
14
+ export type EnumResolver = (enumName: string) => ImportRef | undefined;
15
+
16
+ function renderString(s: string): string {
17
+ return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
18
+ }
19
+
20
+ function renderBasic(field: Field, pattern: string | undefined, resolver: EnumResolver | undefined): string {
21
+ if (pattern !== undefined && field.type !== 'string') {
22
+ throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
23
+ }
24
+ switch (field.type) {
25
+ case 'string': {
26
+ const opts: string[] = [];
27
+ if (field.minLength !== undefined) opts.push(`minLength: ${field.minLength}`);
28
+ if (field.maxLength !== undefined) opts.push(`maxLength: ${field.maxLength}`);
29
+ if (pattern !== undefined) opts.push(`pattern: ${renderString(pattern)}`);
30
+ return opts.length > 0 ? `Type.String({ ${opts.join(', ')} })` : 'Type.String()';
31
+ }
32
+ case 'text':
33
+ return 'Type.String()';
34
+ case 'integer': {
35
+ const opts: string[] = [];
36
+ if (field.min !== undefined) opts.push(`minimum: ${field.min}`);
37
+ if (field.max !== undefined) opts.push(`maximum: ${field.max}`);
38
+ return opts.length > 0 ? `Type.Integer({ ${opts.join(', ')} })` : 'Type.Integer()';
39
+ }
40
+ case 'bigint':
41
+ case 'decimal':
42
+ case 'time':
43
+ case 'date':
44
+ case 'datetime':
45
+ // Transmitted as string over HTTP: bigint/decimal keep full precision,
46
+ // date/time serialize to string.
47
+ return 'Type.String()';
48
+ case 'boolean':
49
+ return 'Type.Boolean()';
50
+ case 'json':
51
+ return 'Type.Unknown()';
52
+ case 'enum': {
53
+ if (!field.jsName) throw new Error(`enum field ${field.name} requires jsName to render`);
54
+ const ref = resolver?.(field.jsName);
55
+ if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.jsName} — pass an EnumResolver`);
56
+ return `Type.Enum(${ref.name})`;
57
+ }
58
+ default:
59
+ // Field union is exhaustive; this branch is unreachable at runtime.
60
+ throw new Error(`unsupported field type: ${String((field as Field).type)}`);
61
+ }
62
+ }
63
+
64
+ function renderObject(fields: Record<string, DtoField>, indent: number, resolver: EnumResolver | undefined): string {
65
+ const pad = ' '.repeat(indent);
66
+ const entries = Object.entries(fields).map(([name, f]) => `${pad}${name}: ${renderField(f, indent, resolver)}`);
67
+ return `Type.Object({\n${entries.join(',\n')}\n${' '.repeat(indent - 1)}})`;
68
+ }
69
+
70
+ function renderField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
71
+ const base = renderValue(f, indent, resolver);
72
+ return f.isOptional() ? `Type.Optional(${base})` : base;
73
+ }
74
+
75
+ function renderValue(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
76
+ if (f instanceof DtoArrayField) {
77
+ return `Type.Array(${renderField(f.items(), indent + 1, resolver)})`;
78
+ }
79
+ if (f instanceof DtoObjectField) {
80
+ return renderObject(f.properties(), indent + 1, resolver);
81
+ }
82
+ // DtoField only wraps a database Field; array/object defs live in the subclasses.
83
+ return renderBasic(f.field as Field, f.pattern, resolver);
84
+ }
85
+
86
+ function collectEnumImports(
87
+ f: DtoField,
88
+ resolver: EnumResolver | undefined,
89
+ out: Map<string, ImportRef>,
90
+ ): void {
91
+ if (f instanceof DtoArrayField) {
92
+ collectEnumImports(f.items(), resolver, out);
93
+ return;
94
+ }
95
+ if (f instanceof DtoObjectField) {
96
+ for (const child of Object.values(f.properties())) collectEnumImports(child, resolver, out);
97
+ return;
98
+ }
99
+ if (f.field.type === 'enum') {
100
+ if (!f.field.jsName) throw new Error(`enum field ${f.field.name} requires jsName to render`);
101
+ const ref = resolver?.(f.field.jsName);
102
+ if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.jsName} — pass an EnumResolver`);
103
+ out.set(`${ref.from}#${ref.name}`, ref);
104
+ }
105
+ }
106
+
107
+ export function renderDtoMessage(
108
+ schema: DtoMessage,
109
+ options: { resolver?: EnumResolver; source?: string } = {},
110
+ ): string {
111
+ const { resolver, source } = options;
112
+ const imports = new Map<string, ImportRef>();
113
+ for (const base of schema.bases ?? []) imports.set(`${base.from}#${base.name}`, base);
114
+ for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, imports);
115
+
116
+ const header = [
117
+ '// AUTO-GENERATED by typebox-driver — DO NOT EDIT',
118
+ ...(source !== undefined ? [`// Source: ${source}`] : []),
119
+ "import { Type, Static } from '@sinclair/typebox';",
120
+ ...[...imports.values()].map((r) => `import { ${r.name} } from '${r.from}';`),
121
+ ];
122
+
123
+ const object = renderObject(schema.fields, 1, resolver);
124
+ const bases = schema.bases ?? [];
125
+ const body =
126
+ bases.length > 0
127
+ ? `Type.Intersect([${bases.map(renderBase).join(', ')}, ${object}])`
128
+ : object;
129
+
130
+ return [
131
+ ...header,
132
+ '',
133
+ `export const ${schema.name} = ${body};`,
134
+ `export type ${schema.name} = Static<typeof ${schema.name}>;`,
135
+ '',
136
+ ].join('\n');
137
+ }
138
+
139
+ function renderBase(base: ImportRef): string {
140
+ return base.args !== undefined && base.args.length > 0
141
+ ? `${base.name}(${base.args.join(', ')})`
142
+ : base.name;
143
+ }