@syncular/typegen 0.15.13 → 0.15.15
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/README.md +41 -7
- package/dist/emit-queries-rust.d.ts +2 -0
- package/dist/emit-queries-rust.js +748 -0
- package/dist/emit-queries.js +47 -3
- package/dist/generate.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lsp.js +2 -0
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +24 -3
- package/dist/naming.d.ts +11 -1
- package/dist/naming.js +126 -2
- package/package.json +3 -3
- package/src/emit-queries-rust.ts +975 -0
- package/src/emit-queries.ts +57 -2
- package/src/generate.ts +16 -1
- package/src/index.ts +1 -0
- package/src/lsp.ts +1 -0
- package/src/manifest.ts +37 -4
- package/src/naming.ts +153 -3
package/src/emit-queries.ts
CHANGED
|
@@ -49,6 +49,29 @@ function propertyKey(name: string): string {
|
|
|
49
49
|
return IDENTIFIER_RE.test(name) ? name : quote(name);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function emitResultMapper(query: AnalyzedQuery, Row: string): string[] {
|
|
53
|
+
const booleans = query.columns.filter((column) => column.type === 'boolean');
|
|
54
|
+
const lines = [
|
|
55
|
+
`/** Decode one storage-shaped result row for ${quote(query.name)}. */`,
|
|
56
|
+
`export function ${query.name}MapRow(row: Readonly<Record<string, unknown>>): ${Row} {`,
|
|
57
|
+
];
|
|
58
|
+
if (booleans.length === 0) {
|
|
59
|
+
lines.push(` return row as unknown as ${Row};`);
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(' return {', ' ...row,');
|
|
62
|
+
for (const column of booleans) {
|
|
63
|
+
const access = `row[${quote(column.langName)}]`;
|
|
64
|
+
const decoded = `decodeQueryBoolean(${access}, ${quote(query.name)}, ${quote(column.langName)})`;
|
|
65
|
+
lines.push(
|
|
66
|
+
` ${propertyKey(column.langName)}: ${column.nullable ? `${access} === null ? null : ${decoded}` : decoded},`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
lines.push(` } as unknown as ${Row};`);
|
|
70
|
+
}
|
|
71
|
+
lines.push('}');
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
/** A named-query param value is the SqlValue subset its type maps to. */
|
|
53
76
|
const PARAM_TS_TYPE: Readonly<Record<IrColumnType, string>> = TS_TYPE;
|
|
54
77
|
const SYQL_PARAM_TS_TYPE: Readonly<Record<IrColumnType, string>> = {
|
|
@@ -235,6 +258,7 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
235
258
|
);
|
|
236
259
|
}
|
|
237
260
|
lines.push('}', '');
|
|
261
|
+
lines.push(...emitResultMapper(query, Row), '');
|
|
238
262
|
|
|
239
263
|
for (const input of inputs) {
|
|
240
264
|
if (input.kind === 'group') {
|
|
@@ -349,7 +373,7 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
349
373
|
`export async function ${query.name}(client: QueryClient${runnerParam}): Promise<${Row}[]> {`,
|
|
350
374
|
` const selected = ${query.name}Select(${hasParams ? 'params' : ''});`,
|
|
351
375
|
' const rows = await client.query(selected.sql, selected.bind);',
|
|
352
|
-
` return rows
|
|
376
|
+
` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`,
|
|
353
377
|
'}',
|
|
354
378
|
'',
|
|
355
379
|
);
|
|
@@ -367,6 +391,7 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
367
391
|
` id: ${quote(`${hash}/${query.name}`)},`,
|
|
368
392
|
` hasParams: ${hasParams},`,
|
|
369
393
|
` sql: ${quote(defaultStatement?.positionalSql ?? query.positionalSql)},`,
|
|
394
|
+
` mapRow: ${query.name}MapRow,`,
|
|
370
395
|
);
|
|
371
396
|
if (hasParams) {
|
|
372
397
|
lines.push(
|
|
@@ -452,6 +477,8 @@ function emitQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
452
477
|
}
|
|
453
478
|
lines.push('}');
|
|
454
479
|
lines.push('');
|
|
480
|
+
lines.push(...emitResultMapper(query, Row));
|
|
481
|
+
lines.push('');
|
|
455
482
|
|
|
456
483
|
const hasParams = query.params.length > 0;
|
|
457
484
|
if (hasParams) {
|
|
@@ -492,7 +519,9 @@ function emitQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
492
519
|
} else {
|
|
493
520
|
lines.push(` const rows = await client.query(${sqlConst});`);
|
|
494
521
|
}
|
|
495
|
-
lines.push(
|
|
522
|
+
lines.push(
|
|
523
|
+
` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`,
|
|
524
|
+
);
|
|
496
525
|
lines.push('}');
|
|
497
526
|
lines.push('');
|
|
498
527
|
|
|
@@ -506,6 +535,7 @@ function emitQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
506
535
|
lines.push(` id: ${quote(`${hash}/${query.name}`)},`);
|
|
507
536
|
lines.push(` hasParams: ${hasParams},`);
|
|
508
537
|
lines.push(` sql: ${sqlConst},`);
|
|
538
|
+
lines.push(` mapRow: ${query.name}MapRow,`);
|
|
509
539
|
lines.push(` tables: ${query.name}Tables,`);
|
|
510
540
|
const reactiveUsesParams = query.reactive.dependencies.some((dependency) =>
|
|
511
541
|
dependency.scopes.some((scope) => scope.params.length > 0),
|
|
@@ -597,6 +627,30 @@ export function emitQueriesModule(
|
|
|
597
627
|
].join('\n'),
|
|
598
628
|
);
|
|
599
629
|
}
|
|
630
|
+
if (
|
|
631
|
+
queries.some((query) =>
|
|
632
|
+
query.columns.some((column) => column.type === 'boolean'),
|
|
633
|
+
)
|
|
634
|
+
) {
|
|
635
|
+
parts.push(
|
|
636
|
+
[
|
|
637
|
+
'/** A generated named-query row did not match its analyzed result type. */',
|
|
638
|
+
'export class QueryResultDecodeError extends TypeError {',
|
|
639
|
+
" readonly name = 'QueryResultDecodeError';",
|
|
640
|
+
' constructor(readonly query: string, readonly column: string) {',
|
|
641
|
+
" super(query + ': result column ' + column + ' is not a SQLite boolean');",
|
|
642
|
+
' }',
|
|
643
|
+
'}',
|
|
644
|
+
'',
|
|
645
|
+
'/** Lift a SQLite boolean: preserve booleans, map 0 to false and finite non-zero numbers to true. */',
|
|
646
|
+
'function decodeQueryBoolean(value: unknown, query: string, column: string): boolean {',
|
|
647
|
+
" if (typeof value === 'boolean') return value;",
|
|
648
|
+
" if (typeof value === 'number' && Number.isFinite(value)) return value !== 0;",
|
|
649
|
+
' throw new QueryResultDecodeError(query, column);',
|
|
650
|
+
'}',
|
|
651
|
+
].join('\n'),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
600
654
|
parts.push(
|
|
601
655
|
[
|
|
602
656
|
"/** A bindable SQL param/row value (the wrapper's SqlValue subset). */",
|
|
@@ -627,6 +681,7 @@ export function emitQueriesModule(
|
|
|
627
681
|
' readonly id: string;',
|
|
628
682
|
' readonly hasParams: boolean;',
|
|
629
683
|
' readonly sql: string;',
|
|
684
|
+
' readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;',
|
|
630
685
|
' readonly tables: readonly string[];',
|
|
631
686
|
' readonly bind: (params: Params) => readonly QueryValue[];',
|
|
632
687
|
' readonly sqlFor?: (params: Params) => string;',
|
package/src/generate.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { emitKotlinModule } from './emit-kotlin';
|
|
|
18
18
|
import { emitQueriesModule } from './emit-queries';
|
|
19
19
|
import { emitQueriesDartModule } from './emit-queries-dart';
|
|
20
20
|
import { emitQueriesKotlinModule } from './emit-queries-kotlin';
|
|
21
|
+
import { emitQueriesRustModule } from './emit-queries-rust';
|
|
21
22
|
import { emitQueriesSwiftModule } from './emit-queries-swift';
|
|
22
23
|
import { emitSwiftModule } from './emit-swift';
|
|
23
24
|
import { TypegenError } from './errors';
|
|
@@ -555,6 +556,7 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
555
556
|
if (manifest.output.swift !== undefined) targets.push('swift');
|
|
556
557
|
if (manifest.output.kotlin !== undefined) targets.push('kotlin');
|
|
557
558
|
if (manifest.output.dart !== undefined) targets.push('dart');
|
|
559
|
+
if (manifest.output.rust !== undefined) targets.push('rust');
|
|
558
560
|
const naming: QueryNamingOptions = {
|
|
559
561
|
naming: manifest.naming,
|
|
560
562
|
targets,
|
|
@@ -586,6 +588,7 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
586
588
|
swift,
|
|
587
589
|
kotlin,
|
|
588
590
|
dart,
|
|
591
|
+
rust,
|
|
589
592
|
} = manifest.output;
|
|
590
593
|
|
|
591
594
|
// Named queries: analyzed once (SELECT-only, typed against the IR via
|
|
@@ -596,7 +599,8 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
596
599
|
tsQueriesPath !== undefined ||
|
|
597
600
|
swift?.queriesPath !== undefined ||
|
|
598
601
|
kotlin?.queriesPath !== undefined ||
|
|
599
|
-
dart?.queriesPath !== undefined
|
|
602
|
+
dart?.queriesPath !== undefined ||
|
|
603
|
+
rust?.queriesPath !== undefined;
|
|
600
604
|
const analyzedQueries = wantsQueries
|
|
601
605
|
? analyzeQueries(
|
|
602
606
|
ir,
|
|
@@ -678,6 +682,17 @@ export function generate(manifestDir: string): GenerateResult {
|
|
|
678
682
|
});
|
|
679
683
|
}
|
|
680
684
|
}
|
|
685
|
+
if (rust !== undefined) {
|
|
686
|
+
outputs.push({
|
|
687
|
+
path: resolve(manifestDir, rust.queriesPath),
|
|
688
|
+
content: emitQueriesRustModule(
|
|
689
|
+
analyzedQueries,
|
|
690
|
+
queryHash,
|
|
691
|
+
ir.irVersion,
|
|
692
|
+
rust.clientCrate,
|
|
693
|
+
),
|
|
694
|
+
});
|
|
695
|
+
}
|
|
681
696
|
return {
|
|
682
697
|
ir,
|
|
683
698
|
irJson,
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './emit-kotlin';
|
|
|
9
9
|
export * from './emit-queries';
|
|
10
10
|
export * from './emit-queries-dart';
|
|
11
11
|
export * from './emit-queries-kotlin';
|
|
12
|
+
export * from './emit-queries-rust';
|
|
12
13
|
export * from './emit-queries-swift';
|
|
13
14
|
export * from './emit-swift';
|
|
14
15
|
export * from './errors';
|
package/src/lsp.ts
CHANGED
|
@@ -269,6 +269,7 @@ export class SyqlLanguageServer {
|
|
|
269
269
|
if (manifest.output.swift !== undefined) targets.push('swift');
|
|
270
270
|
if (manifest.output.kotlin !== undefined) targets.push('kotlin');
|
|
271
271
|
if (manifest.output.dart !== undefined) targets.push('dart');
|
|
272
|
+
if (manifest.output.rust !== undefined) targets.push('rust');
|
|
272
273
|
const lookup: ProjectLookup = {
|
|
273
274
|
kind: 'ready',
|
|
274
275
|
context: {
|
package/src/manifest.ts
CHANGED
|
@@ -28,20 +28,21 @@
|
|
|
28
28
|
* placeholders (partial templates are unsupported).
|
|
29
29
|
* - `output.ir` / `output.module` are the always-emitted TS defaults;
|
|
30
30
|
* `output.queryIr` opts into the deterministic analyzed-query document.
|
|
31
|
-
* `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native
|
|
31
|
+
* `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native schema
|
|
32
32
|
* emitters — a bare string is the output path; an object carries
|
|
33
33
|
* language-appropriate options (Kotlin `package`/`objectName`, Swift
|
|
34
34
|
* `enumName`; Dart takes `path` only). This is additive within the `output`
|
|
35
35
|
* object (the same forward-extension shape `output.ir`/`output.module`
|
|
36
36
|
* already use); no `manifestVersion` bump — new *recognized* keys, not
|
|
37
37
|
* tolerated-unknown ones. Absent → that language is not generated (TS
|
|
38
|
-
* stays the default).
|
|
38
|
+
* stays the default). `output.rust.queriesPath` opts into Rust named-query
|
|
39
|
+
* source over the neutral schema IR consumed by `syncular-client`.
|
|
39
40
|
* - Unknown keys are hard errors everywhere (fail loud; growth happens by
|
|
40
41
|
* bumping `manifestVersion`), except inside `extensions`, the reserved
|
|
41
42
|
* WP-49 passthrough slot copied verbatim into the IR.
|
|
42
43
|
*/
|
|
43
44
|
import { TypegenError } from './errors';
|
|
44
|
-
import type
|
|
45
|
+
import { isRustKeyword, type NamingMode } from './naming';
|
|
45
46
|
|
|
46
47
|
/** §7/§8 `.syql` conditional-lowering backend selection. */
|
|
47
48
|
export type ManifestQueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
@@ -103,6 +104,14 @@ export interface DartOutput {
|
|
|
103
104
|
readonly queriesPath?: string;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/** Rust named-query output. Rust consumes neutral schema IR at runtime, so
|
|
108
|
+
* revision 1 has no separate generated schema source path. */
|
|
109
|
+
export interface RustOutput {
|
|
110
|
+
readonly queriesPath: string;
|
|
111
|
+
/** Rust module identifier for the `syncular-client` Cargo dependency. */
|
|
112
|
+
readonly clientCrate: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
export interface ManifestOutput {
|
|
107
116
|
readonly ir: string;
|
|
108
117
|
readonly module: string;
|
|
@@ -116,6 +125,7 @@ export interface ManifestOutput {
|
|
|
116
125
|
readonly swift?: SwiftOutput;
|
|
117
126
|
readonly kotlin?: KotlinOutput;
|
|
118
127
|
readonly dart?: DartOutput;
|
|
128
|
+
readonly rust?: RustOutput;
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
export interface Manifest {
|
|
@@ -254,6 +264,26 @@ function parseDartOutput(value: unknown): DartOutput {
|
|
|
254
264
|
};
|
|
255
265
|
}
|
|
256
266
|
|
|
267
|
+
const RUST_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
268
|
+
|
|
269
|
+
function parseRustOutput(value: unknown): RustOutput {
|
|
270
|
+
const obj = asObject(value, 'output.rust');
|
|
271
|
+
rejectUnknownKeys(obj, ['queriesPath', 'clientCrate'], 'output.rust');
|
|
272
|
+
const clientCrate =
|
|
273
|
+
obj.clientCrate === undefined
|
|
274
|
+
? 'syncular_client'
|
|
275
|
+
: asString(obj.clientCrate, 'output.rust.clientCrate');
|
|
276
|
+
if (!RUST_IDENTIFIER_RE.test(clientCrate) || isRustKeyword(clientCrate)) {
|
|
277
|
+
fail(
|
|
278
|
+
`output.rust.clientCrate must be one Rust identifier, got ${JSON.stringify(clientCrate)}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
queriesPath: asString(obj.queriesPath, 'output.rust.queriesPath'),
|
|
283
|
+
clientCrate,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
257
287
|
function parseTable(value: unknown, index: number): ManifestTable {
|
|
258
288
|
const context = `tables[${index}]`;
|
|
259
289
|
const obj = asObject(value, context);
|
|
@@ -407,11 +437,12 @@ export function parseManifest(raw: unknown): Manifest {
|
|
|
407
437
|
let swift: SwiftOutput | undefined;
|
|
408
438
|
let kotlin: KotlinOutput | undefined;
|
|
409
439
|
let dart: DartOutput | undefined;
|
|
440
|
+
let rust: RustOutput | undefined;
|
|
410
441
|
if (obj.output !== undefined) {
|
|
411
442
|
const output = asObject(obj.output, 'output');
|
|
412
443
|
rejectUnknownKeys(
|
|
413
444
|
output,
|
|
414
|
-
['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart'],
|
|
445
|
+
['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart', 'rust'],
|
|
415
446
|
'output',
|
|
416
447
|
);
|
|
417
448
|
if (output.ir !== undefined) ir = asString(output.ir, 'output.ir');
|
|
@@ -427,6 +458,7 @@ export function parseManifest(raw: unknown): Manifest {
|
|
|
427
458
|
if (output.swift !== undefined) swift = parseSwiftOutput(output.swift);
|
|
428
459
|
if (output.kotlin !== undefined) kotlin = parseKotlinOutput(output.kotlin);
|
|
429
460
|
if (output.dart !== undefined) dart = parseDartOutput(output.dart);
|
|
461
|
+
if (output.rust !== undefined) rust = parseRustOutput(output.rust);
|
|
430
462
|
}
|
|
431
463
|
// Build `output` with only the keys that are set — `exactOptionalPropertyTypes`
|
|
432
464
|
// forbids explicit `undefined` on optional properties.
|
|
@@ -438,6 +470,7 @@ export function parseManifest(raw: unknown): Manifest {
|
|
|
438
470
|
...(swift !== undefined ? { swift } : {}),
|
|
439
471
|
...(kotlin !== undefined ? { kotlin } : {}),
|
|
440
472
|
...(dart !== undefined ? { dart } : {}),
|
|
473
|
+
...(rust !== undefined ? { rust } : {}),
|
|
441
474
|
};
|
|
442
475
|
if (!Array.isArray(obj.schemaVersions) || obj.schemaVersions.length === 0) {
|
|
443
476
|
fail('schemaVersions must be a non-empty array (§1.5 version history)');
|
package/src/naming.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function snakeToCamel(name: string): string {
|
|
|
47
47
|
/** The emitter targets whose keyword sets we police. `ts` is included for
|
|
48
48
|
* completeness (its emitters can quote any property, so its list is the
|
|
49
49
|
* small set that breaks generated FUNCTION/const identifiers). */
|
|
50
|
-
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart';
|
|
50
|
+
export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart' | 'rust';
|
|
51
51
|
|
|
52
52
|
/** Reserved words that cannot be a generated field/property name on each
|
|
53
53
|
* target. Deliberately the core keyword lists — mechanical and predictable
|
|
@@ -211,8 +211,157 @@ const TARGET_KEYWORDS: Readonly<Record<NamingTarget, ReadonlySet<string>>> = {
|
|
|
211
211
|
'while',
|
|
212
212
|
'with',
|
|
213
213
|
]),
|
|
214
|
+
rust: new Set([
|
|
215
|
+
'abstract',
|
|
216
|
+
'as',
|
|
217
|
+
'async',
|
|
218
|
+
'await',
|
|
219
|
+
'become',
|
|
220
|
+
'box',
|
|
221
|
+
'break',
|
|
222
|
+
'const',
|
|
223
|
+
'continue',
|
|
224
|
+
'crate',
|
|
225
|
+
'do',
|
|
226
|
+
'dyn',
|
|
227
|
+
'else',
|
|
228
|
+
'enum',
|
|
229
|
+
'extern',
|
|
230
|
+
'false',
|
|
231
|
+
'final',
|
|
232
|
+
'fn',
|
|
233
|
+
'for',
|
|
234
|
+
'gen',
|
|
235
|
+
'if',
|
|
236
|
+
'impl',
|
|
237
|
+
'in',
|
|
238
|
+
'let',
|
|
239
|
+
'loop',
|
|
240
|
+
'macro',
|
|
241
|
+
'match',
|
|
242
|
+
'mod',
|
|
243
|
+
'move',
|
|
244
|
+
'mut',
|
|
245
|
+
'override',
|
|
246
|
+
'priv',
|
|
247
|
+
'pub',
|
|
248
|
+
'ref',
|
|
249
|
+
'return',
|
|
250
|
+
'self',
|
|
251
|
+
'static',
|
|
252
|
+
'struct',
|
|
253
|
+
'super',
|
|
254
|
+
'trait',
|
|
255
|
+
'true',
|
|
256
|
+
'try',
|
|
257
|
+
'type',
|
|
258
|
+
'typeof',
|
|
259
|
+
'unsafe',
|
|
260
|
+
'unsized',
|
|
261
|
+
'use',
|
|
262
|
+
'virtual',
|
|
263
|
+
'where',
|
|
264
|
+
'while',
|
|
265
|
+
'yield',
|
|
266
|
+
]),
|
|
214
267
|
};
|
|
215
268
|
|
|
269
|
+
export function isRustKeyword(name: string): boolean {
|
|
270
|
+
return TARGET_KEYWORDS.rust.has(name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Pinned Rust identifier conversion from RFC 0006. */
|
|
274
|
+
export function rustSnakeCase(name: string): string {
|
|
275
|
+
if (!/^_*[A-Za-z][A-Za-z0-9_]*$/.test(name)) return name;
|
|
276
|
+
const lead = /^_*/.exec(name)?.[0] ?? '';
|
|
277
|
+
const withoutLead = name.slice(lead.length);
|
|
278
|
+
const trail = /_*$/.exec(withoutLead)?.[0] ?? '';
|
|
279
|
+
const middle = withoutLead.slice(0, withoutLead.length - trail.length);
|
|
280
|
+
const out: string[] = [];
|
|
281
|
+
for (let index = 0; index < middle.length; index++) {
|
|
282
|
+
const char = middle[index] as string;
|
|
283
|
+
if (char === '_') {
|
|
284
|
+
if (out.length > 0 && out[out.length - 1] !== '_') out.push('_');
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const previous = index > 0 ? middle[index - 1] : undefined;
|
|
288
|
+
const next = index + 1 < middle.length ? middle[index + 1] : undefined;
|
|
289
|
+
const uppercase = char >= 'A' && char <= 'Z';
|
|
290
|
+
const previousLowerOrDigit =
|
|
291
|
+
previous !== undefined && /[a-z0-9]/.test(previous);
|
|
292
|
+
const acronymBoundary =
|
|
293
|
+
uppercase &&
|
|
294
|
+
previous !== undefined &&
|
|
295
|
+
/[A-Z]/.test(previous) &&
|
|
296
|
+
next !== undefined &&
|
|
297
|
+
/[a-z]/.test(next);
|
|
298
|
+
if (
|
|
299
|
+
uppercase &&
|
|
300
|
+
(previousLowerOrDigit || acronymBoundary) &&
|
|
301
|
+
out.length > 0 &&
|
|
302
|
+
out[out.length - 1] !== '_'
|
|
303
|
+
) {
|
|
304
|
+
out.push('_');
|
|
305
|
+
}
|
|
306
|
+
out.push(char.toLowerCase());
|
|
307
|
+
}
|
|
308
|
+
return lead + out.join('') + trail;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function rustPascalCase(name: string): string {
|
|
312
|
+
const snake = rustSnakeCase(name);
|
|
313
|
+
const lead = /^_*/.exec(snake)?.[0] ?? '';
|
|
314
|
+
const bare = snake.slice(lead.length).replace(/_*$/, '');
|
|
315
|
+
const suffix = snake.slice(lead.length + bare.length);
|
|
316
|
+
return (
|
|
317
|
+
lead +
|
|
318
|
+
bare
|
|
319
|
+
.split('_')
|
|
320
|
+
.filter((part) => part.length > 0)
|
|
321
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
322
|
+
.join('') +
|
|
323
|
+
suffix
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface RustNameMapping {
|
|
328
|
+
readonly langName: string;
|
|
329
|
+
readonly rustName: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Validate and map QueryIR runtime names to emitted Rust identifiers. */
|
|
333
|
+
export function buildRustNamingMap(
|
|
334
|
+
langNames: readonly string[],
|
|
335
|
+
context: string,
|
|
336
|
+
scope: string,
|
|
337
|
+
): RustNameMapping[] {
|
|
338
|
+
const seen = new Map<string, string>();
|
|
339
|
+
return langNames.map((langName) => {
|
|
340
|
+
const rustName = rustSnakeCase(langName);
|
|
341
|
+
if (!/^_*[a-z][a-z0-9_]*$/.test(rustName)) {
|
|
342
|
+
throw new TypegenError(
|
|
343
|
+
context,
|
|
344
|
+
`${scope}: ${JSON.stringify(langName)} cannot be emitted as a Rust identifier — alias it in SQL (AS)`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (TARGET_KEYWORDS.rust.has(rustName)) {
|
|
348
|
+
throw new TypegenError(
|
|
349
|
+
context,
|
|
350
|
+
`${scope}: ${JSON.stringify(langName)} maps to ${JSON.stringify(rustName)}, a reserved word on the rust target — alias it in SQL (AS)`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const clash = seen.get(rustName);
|
|
354
|
+
if (clash !== undefined && clash !== langName) {
|
|
355
|
+
throw new TypegenError(
|
|
356
|
+
context,
|
|
357
|
+
`${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(langName)} both map to ${JSON.stringify(rustName)} on the rust target — alias one in SQL (AS)`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
seen.set(rustName, langName);
|
|
361
|
+
return { langName, rustName };
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
216
365
|
/** One naming-map entry: the SQL-truth name and its language-facing name. */
|
|
217
366
|
export interface NameMapping {
|
|
218
367
|
readonly sqlName: string;
|
|
@@ -248,10 +397,11 @@ export function buildNamingMap(
|
|
|
248
397
|
);
|
|
249
398
|
}
|
|
250
399
|
for (const target of targets) {
|
|
251
|
-
|
|
400
|
+
const targetName = target === 'rust' ? rustSnakeCase(langName) : langName;
|
|
401
|
+
if (TARGET_KEYWORDS[target].has(targetName)) {
|
|
252
402
|
throw new TypegenError(
|
|
253
403
|
context,
|
|
254
|
-
`${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(
|
|
404
|
+
`${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(targetName)}, a reserved word on the ${target} target — rename it, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`,
|
|
255
405
|
);
|
|
256
406
|
}
|
|
257
407
|
if (target === 'dart' && langName.startsWith('_')) {
|