@genesislcap/event-type-codegen 14.489.0-canary.FUI-2574-1
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 +80 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/naming.d.ts +9 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +26 -0
- package/dist/naming.js.map +1 -0
- package/dist/parse-dao.d.ts +18 -0
- package/dist/parse-dao.d.ts.map +1 -0
- package/dist/parse-dao.js +175 -0
- package/dist/parse-dao.js.map +1 -0
- package/dist/parse-enums.d.ts +16 -0
- package/dist/parse-enums.d.ts.map +1 -0
- package/dist/parse-enums.js +98 -0
- package/dist/parse-enums.js.map +1 -0
- package/dist/parse-kotlin.d.ts +15 -0
- package/dist/parse-kotlin.d.ts.map +1 -0
- package/dist/parse-kotlin.js +190 -0
- package/dist/parse-kotlin.js.map +1 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +89 -0
- package/dist/render.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/license.txt +46 -0
- package/package.json +38 -0
- package/src/config.ts +67 -0
- package/src/index.ts +118 -0
- package/src/naming.ts +24 -0
- package/src/parse-dao.ts +186 -0
- package/src/parse-enums.ts +105 -0
- package/src/parse-kotlin.ts +216 -0
- package/src/render.ts +118 -0
- package/src/types.ts +56 -0
- package/test/fixtures/client-out/genesis-event-types.ts +102 -0
- package/test/fixtures/golden/genesis-event-types.ts +102 -0
- package/test/fixtures/mini-server/generated-dao/global/genesis/gen/dao/Instrument.kt +13 -0
- package/test/fixtures/mini-server/generated-dao/global/genesis/gen/dao/Trade.kt +51 -0
- package/test/fixtures/mini-server/src/main/kotlin/com/example/Handlers.kt +16 -0
- package/test/fixtures/mini-server/src/main/kotlin/com/example/dto/Samples.kt +10 -0
- package/test/generate.test.ts +88 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { EventTypesConfig, EventTypesResolvedConfig } from './types';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_KOTLIN_SOURCES = [
|
|
7
|
+
'src/main/kotlin/**/dto/**/*.kt',
|
|
8
|
+
'src/main/genesis/scripts/**/*.kts',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const DEFAULT_HANDLER_SCAN = ['src/main/kotlin/**/*.kt', 'src/main/genesis/scripts/**/*.kts'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Relative to the parent of `serverModule` (the Gradle `server/` tree).
|
|
15
|
+
* Covers dictionary-cache and genesis-home layouts after `./gradlew …` generateDao.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_GENERATED_DAO_SOURCES = [
|
|
18
|
+
'build/internal-modules/**/*-generated-dao/build/generated/sources/dao/global/genesis/gen/dao/**/*.kt',
|
|
19
|
+
'**/build/genesis-home/generated/**/gen/dao/**/*.kt',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_OUTPUT = 'src/generated/genesis-event-types.ts';
|
|
23
|
+
|
|
24
|
+
const discoverServerModule = (clientRoot: string): string => {
|
|
25
|
+
const serverDir = resolve(clientRoot, '../server');
|
|
26
|
+
if (!existsSync(serverDir)) {
|
|
27
|
+
return '../server';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const children = readdirSync(serverDir)
|
|
31
|
+
.map((name) => join(serverDir, name))
|
|
32
|
+
.filter((path) => {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(path).isDirectory();
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.filter((path) => existsSync(join(path, 'src', 'main', 'kotlin')));
|
|
40
|
+
|
|
41
|
+
if (children.length === 1) {
|
|
42
|
+
const relative = join('..', 'server', children[0].split(/[/\\]/).pop()!);
|
|
43
|
+
return relative.replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return '../server';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const resolveEventTypesConfig = (
|
|
50
|
+
clientRoot: string,
|
|
51
|
+
config?: Partial<EventTypesConfig> & { enabled?: boolean },
|
|
52
|
+
packageGenesis?: { eventTypes?: EventTypesConfig },
|
|
53
|
+
): EventTypesResolvedConfig => {
|
|
54
|
+
const fromGenesis = packageGenesis?.eventTypes ?? {};
|
|
55
|
+
const merged = { ...fromGenesis, ...config };
|
|
56
|
+
const enabled = merged.enabled === true;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
enabled,
|
|
60
|
+
serverModule: merged.serverModule ?? discoverServerModule(clientRoot),
|
|
61
|
+
kotlinSources: merged.kotlinSources ?? DEFAULT_KOTLIN_SOURCES,
|
|
62
|
+
generatedDaoSources: merged.generatedDaoSources ?? DEFAULT_GENERATED_DAO_SOURCES,
|
|
63
|
+
handlerScan: merged.handlerScan ?? DEFAULT_HANDLER_SCAN,
|
|
64
|
+
output: merged.output ?? DEFAULT_OUTPUT,
|
|
65
|
+
runOnBuild: merged.runOnBuild ?? true,
|
|
66
|
+
};
|
|
67
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, globSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { consola } from 'consola';
|
|
5
|
+
|
|
6
|
+
import { resolveEventTypesConfig } from './config';
|
|
7
|
+
import { parseGeneratedDaosFromSources } from './parse-dao';
|
|
8
|
+
import { parseGeneratedEnumsFromSources } from './parse-enums';
|
|
9
|
+
import { parseHandlersFromSources, parseDtosFromSources } from './parse-kotlin';
|
|
10
|
+
import { renderEventTypesFile } from './render';
|
|
11
|
+
import type { EventTypesResolvedConfig, GenerateResult, KotlinDto } from './types';
|
|
12
|
+
|
|
13
|
+
const isDtoScanPath = (filePath: string) => filePath.replace(/\\/g, '/').includes('/dto/');
|
|
14
|
+
|
|
15
|
+
const isExcludedHandlerFile = (filePath: string) => {
|
|
16
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
17
|
+
return isDtoScanPath(normalized) && normalized.endsWith('EventHandler.kt');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const collectFiles = (
|
|
21
|
+
root: string,
|
|
22
|
+
patterns: string[],
|
|
23
|
+
options?: { includeBuildDirs?: boolean },
|
|
24
|
+
) => {
|
|
25
|
+
const files = new Set<string>();
|
|
26
|
+
for (const pattern of patterns) {
|
|
27
|
+
const matches = globSync(pattern, {
|
|
28
|
+
cwd: root,
|
|
29
|
+
exclude: (name) => {
|
|
30
|
+
if (name === 'node_modules') return true;
|
|
31
|
+
if (!options?.includeBuildDirs && name === 'build') return true;
|
|
32
|
+
return false;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
for (const match of matches) {
|
|
36
|
+
files.add(join(root, match));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [...files];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const mergeTypeMaps = (
|
|
43
|
+
primary: Map<string, KotlinDto>,
|
|
44
|
+
secondary: Map<string, KotlinDto>,
|
|
45
|
+
): Map<string, KotlinDto> => {
|
|
46
|
+
const merged = new Map(secondary);
|
|
47
|
+
for (const [name, dto] of primary) {
|
|
48
|
+
merged.set(name, dto);
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type GenerateEventTypesOptions = {
|
|
54
|
+
clientRoot: string;
|
|
55
|
+
config?: EventTypesResolvedConfig;
|
|
56
|
+
packageGenesis?: { eventTypes?: unknown };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export async function generateEventTypes(
|
|
60
|
+
options: GenerateEventTypesOptions,
|
|
61
|
+
): Promise<GenerateResult | null> {
|
|
62
|
+
const resolved =
|
|
63
|
+
options.config ??
|
|
64
|
+
resolveEventTypesConfig(options.clientRoot, undefined, options.packageGenesis);
|
|
65
|
+
|
|
66
|
+
if (!resolved.enabled) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const serverRoot = resolve(options.clientRoot, resolved.serverModule);
|
|
71
|
+
if (!existsSync(serverRoot)) {
|
|
72
|
+
throw new Error(`eventTypes: server module not found at ${serverRoot}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Gradle `server/` tree — parent of the app module (e.g. …/server/showcase-app → …/server)
|
|
76
|
+
const serverWorkspaceRoot = resolve(serverRoot, '..');
|
|
77
|
+
|
|
78
|
+
const dtoFiles = collectFiles(serverRoot, resolved.kotlinSources).filter(
|
|
79
|
+
(file) => !isExcludedHandlerFile(file),
|
|
80
|
+
);
|
|
81
|
+
const handlerFiles = collectFiles(serverRoot, resolved.handlerScan).filter(
|
|
82
|
+
(file) => !isExcludedHandlerFile(file),
|
|
83
|
+
);
|
|
84
|
+
// Scan app module and Gradle server workspace (dictionary-cache lives under server/build/).
|
|
85
|
+
const daoFiles = [
|
|
86
|
+
...collectFiles(serverRoot, resolved.generatedDaoSources, { includeBuildDirs: true }),
|
|
87
|
+
...collectFiles(serverWorkspaceRoot, resolved.generatedDaoSources, {
|
|
88
|
+
includeBuildDirs: true,
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
const uniqueDaoFiles = [...new Set(daoFiles)];
|
|
92
|
+
|
|
93
|
+
const customDtos = parseDtosFromSources(dtoFiles);
|
|
94
|
+
const generatedDaos = parseGeneratedDaosFromSources(uniqueDaoFiles);
|
|
95
|
+
const generatedEnums = parseGeneratedEnumsFromSources(uniqueDaoFiles);
|
|
96
|
+
const dtos = mergeTypeMaps(customDtos, generatedDaos);
|
|
97
|
+
const { mapped, unmapped } = parseHandlersFromSources(handlerFiles, dtos);
|
|
98
|
+
|
|
99
|
+
const outputPath = resolve(options.clientRoot, resolved.output);
|
|
100
|
+
const contents = renderEventTypesFile(mapped, unmapped, dtos, generatedEnums);
|
|
101
|
+
|
|
102
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
103
|
+
await writeFile(outputPath, contents, 'utf8');
|
|
104
|
+
|
|
105
|
+
consola.info(
|
|
106
|
+
`eventTypes: wrote ${outputPath} (${mapped.length} events, ${customDtos.size} DTOs, ${generatedDaos.size} generated DAOs)`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
outputPath,
|
|
111
|
+
eventCount: mapped.length,
|
|
112
|
+
dtoCount: dtos.size,
|
|
113
|
+
unmappedCount: unmapped.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export * from './types';
|
|
118
|
+
export { resolveEventTypesConfig } from './config';
|
package/src/naming.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { constantCase, pascalCase } from 'change-case';
|
|
2
|
+
|
|
3
|
+
/** Wire field name for a Kotlin property (DETAILS uses UPPER_SNAKE_CASE). */
|
|
4
|
+
export const kotlinPropertyToWireField = (propertyName: string): string =>
|
|
5
|
+
constantCase(propertyName);
|
|
6
|
+
|
|
7
|
+
/** Generated interface name for a Kotlin DTO or handler input type. */
|
|
8
|
+
export const dtoToDetailsInterfaceName = (dtoName: string): string =>
|
|
9
|
+
dtoName.endsWith('Details') ? dtoName : `${dtoName}Details`;
|
|
10
|
+
|
|
11
|
+
/** e.g. `Trade.ById` → `TradeByIdDetails`, `FooInput` → `FooInputDetails` */
|
|
12
|
+
export const handlerInputToDetailsInterfaceName = (inputType: string): string => {
|
|
13
|
+
const indexMatch = inputType.match(/^(\w+)\.(By\w+)$/);
|
|
14
|
+
if (indexMatch) {
|
|
15
|
+
return `${indexMatch[1]}${indexMatch[2]}Details`;
|
|
16
|
+
}
|
|
17
|
+
return dtoToDetailsInterfaceName(inputType);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Per-event class carrying MESSAGE_TYPE and DETAILS (one per PAL handler). */
|
|
21
|
+
export const eventNameToEventClassName = (eventName: string): string => {
|
|
22
|
+
const base = eventName.startsWith('EVENT_') ? eventName.slice('EVENT_'.length) : eventName;
|
|
23
|
+
return `Event${pascalCase(base)}`;
|
|
24
|
+
};
|
package/src/parse-dao.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import type { KotlinDto, KotlinProperty } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generated Genesis DAO entities use a primary constructor with annotated
|
|
7
|
+
* `public var` parameters (and sometimes bare params for sequenced PKs).
|
|
8
|
+
* Only files directly under `.../gen/dao/*.kt` are entity classes.
|
|
9
|
+
*/
|
|
10
|
+
export const isGeneratedDaoEntityFile = (filePath: string): boolean => {
|
|
11
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
12
|
+
return /\/gen\/dao\/[^/]+\.kt$/i.test(normalized);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const stripKotlinAnnotations = (segment: string): string =>
|
|
16
|
+
segment
|
|
17
|
+
.replace(/@[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\([^)]*\))?/g, ' ')
|
|
18
|
+
.replace(/\s+/g, ' ')
|
|
19
|
+
.trim();
|
|
20
|
+
|
|
21
|
+
const splitTopLevelArgs = (body: string): string[] => {
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
let depth = 0;
|
|
24
|
+
let current = '';
|
|
25
|
+
for (const ch of body) {
|
|
26
|
+
if (ch === '(' || ch === '<' || ch === '[') depth += 1;
|
|
27
|
+
if (ch === ')' || ch === '>' || ch === ']') depth -= 1;
|
|
28
|
+
if (ch === ',' && depth === 0) {
|
|
29
|
+
parts.push(current.trim());
|
|
30
|
+
current = '';
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
current += ch;
|
|
34
|
+
}
|
|
35
|
+
if (current.trim()) parts.push(current.trim());
|
|
36
|
+
return parts;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseDaoConstructorProperty = (segment: string): KotlinProperty | null => {
|
|
40
|
+
const cleaned = stripKotlinAnnotations(segment);
|
|
41
|
+
// Prefer property syntax: (public) var/val name: Type
|
|
42
|
+
const propertyMatch = cleaned.match(
|
|
43
|
+
/^(?:public\s+)?(?:val|var)\s+(\w+)\s*:\s*([^=]+?)(?:\s*=\s*.+)?$/,
|
|
44
|
+
);
|
|
45
|
+
// Bare ctor param used for sequenced PKs: tradeId: String? = null
|
|
46
|
+
const bareMatch = propertyMatch ?? cleaned.match(/^(\w+)\s*:\s*([^=]+?)(?:\s*=\s*.+)?$/);
|
|
47
|
+
|
|
48
|
+
if (!bareMatch) return null;
|
|
49
|
+
|
|
50
|
+
const name = bareMatch[1];
|
|
51
|
+
let kotlinType = bareMatch[2].trim();
|
|
52
|
+
const optional = kotlinType.endsWith('?') || cleaned.includes('=');
|
|
53
|
+
kotlinType = kotlinType.replace(/\?$/, '').trim();
|
|
54
|
+
|
|
55
|
+
return { name, kotlinType, optional };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parseIndexDataClassProperty = (segment: string): KotlinProperty | null => {
|
|
59
|
+
const cleaned = stripKotlinAnnotations(segment);
|
|
60
|
+
const match = cleaned.match(/^(?:public\s+)?(?:val|var)\s+(\w+)\s*:\s*([^=]+?)(?:\s*=\s*.+)?$/);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
|
|
63
|
+
const name = match[1];
|
|
64
|
+
let kotlinType = match[2].trim();
|
|
65
|
+
const optional = kotlinType.endsWith('?') || cleaned.includes('=');
|
|
66
|
+
kotlinType = kotlinType.replace(/\?$/, '').trim();
|
|
67
|
+
|
|
68
|
+
return { name, kotlinType, optional };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const KOTLIN_FILE_EXTENSION = '.kt';
|
|
72
|
+
|
|
73
|
+
const entityNameFromDaoFile = (filePath: string): string | null => {
|
|
74
|
+
const base = filePath.replace(/\\/g, '/').split('/').pop();
|
|
75
|
+
if (!base?.endsWith(KOTLIN_FILE_EXTENSION)) return null;
|
|
76
|
+
return base.slice(0, base.length - KOTLIN_FILE_EXTENSION.length);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Nested `data class By*` index types inside generated entity Kotlin (e.g. Trade.ById).
|
|
81
|
+
*/
|
|
82
|
+
export const extractGeneratedDaoIndices = (content: string, filePath: string): KotlinDto[] => {
|
|
83
|
+
const entityName = entityNameFromDaoFile(filePath);
|
|
84
|
+
if (!entityName) return [];
|
|
85
|
+
|
|
86
|
+
const results: KotlinDto[] = [];
|
|
87
|
+
const indexRegex = /(?:^|\n)\s*public\s+data\s+class\s+(By\w+)\s*\(/gm;
|
|
88
|
+
let match: RegExpExecArray | null;
|
|
89
|
+
|
|
90
|
+
while ((match = indexRegex.exec(content)) !== null) {
|
|
91
|
+
const indexName = match[1];
|
|
92
|
+
const openParenIndex = match.index + match[0].length - 1;
|
|
93
|
+
let depth = 0;
|
|
94
|
+
let closeIndex = -1;
|
|
95
|
+
for (let i = openParenIndex; i < content.length; i += 1) {
|
|
96
|
+
const ch = content[i];
|
|
97
|
+
if (ch === '(') depth += 1;
|
|
98
|
+
if (ch === ')') {
|
|
99
|
+
depth -= 1;
|
|
100
|
+
if (depth === 0) {
|
|
101
|
+
closeIndex = i;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (closeIndex === -1) continue;
|
|
107
|
+
|
|
108
|
+
const body = content.slice(openParenIndex + 1, closeIndex);
|
|
109
|
+
const properties = splitTopLevelArgs(body)
|
|
110
|
+
.map(parseIndexDataClassProperty)
|
|
111
|
+
.filter((p): p is KotlinProperty => p != null);
|
|
112
|
+
|
|
113
|
+
if (properties.length === 0) continue;
|
|
114
|
+
|
|
115
|
+
results.push({
|
|
116
|
+
name: `${entityName}.${indexName}`,
|
|
117
|
+
properties,
|
|
118
|
+
filePath,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract table entity classes from generated DAO Kotlin sources.
|
|
127
|
+
* Prefer entities that look like Genesis table DAOs (constructor + TableEntity).
|
|
128
|
+
*/
|
|
129
|
+
export const extractGeneratedDaoEntities = (content: string, filePath: string): KotlinDto[] => {
|
|
130
|
+
const results: KotlinDto[] = [];
|
|
131
|
+
// Matches: public class Trade public constructor( ... ) :
|
|
132
|
+
const classRegex = /(?:^|\n)\s*(?:public\s+)?class\s+(\w+)\s+(?:public\s+)?constructor\s*\(/gm;
|
|
133
|
+
let match: RegExpExecArray | null;
|
|
134
|
+
|
|
135
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
136
|
+
const name = match[1];
|
|
137
|
+
const openParenIndex = match.index + match[0].length - 1;
|
|
138
|
+
let depth = 0;
|
|
139
|
+
let closeIndex = -1;
|
|
140
|
+
for (let i = openParenIndex; i < content.length; i += 1) {
|
|
141
|
+
const ch = content[i];
|
|
142
|
+
if (ch === '(') depth += 1;
|
|
143
|
+
if (ch === ')') {
|
|
144
|
+
depth -= 1;
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
closeIndex = i;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (closeIndex === -1) continue;
|
|
152
|
+
|
|
153
|
+
const body = content.slice(openParenIndex + 1, closeIndex);
|
|
154
|
+
const properties = splitTopLevelArgs(body)
|
|
155
|
+
.map(parseDaoConstructorProperty)
|
|
156
|
+
.filter((p): p is KotlinProperty => p != null);
|
|
157
|
+
|
|
158
|
+
if (properties.length > 0) {
|
|
159
|
+
results.push({ name, properties, filePath });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return results;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const parseGeneratedDaosFromSources = (files: string[]): Map<string, KotlinDto> => {
|
|
167
|
+
const daos = new Map<string, KotlinDto>();
|
|
168
|
+
|
|
169
|
+
for (const filePath of files) {
|
|
170
|
+
if (!isGeneratedDaoEntityFile(filePath)) continue;
|
|
171
|
+
const rawContent = readFileSync(filePath, 'utf8');
|
|
172
|
+
const content = rawContent.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
173
|
+
for (const entity of extractGeneratedDaoEntities(content, filePath)) {
|
|
174
|
+
if (!daos.has(entity.name)) {
|
|
175
|
+
daos.set(entity.name, entity);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const index of extractGeneratedDaoIndices(content, filePath)) {
|
|
179
|
+
if (!daos.has(index.name)) {
|
|
180
|
+
daos.set(index.name, index);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return daos;
|
|
186
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export type KotlinEnumMap = Map<string, readonly string[]>;
|
|
4
|
+
|
|
5
|
+
const KOTLIN_FILE_EXTENSION = '.kt';
|
|
6
|
+
|
|
7
|
+
/** Any generated DAO tree Kotlin (entities, enums, xref, etc.). */
|
|
8
|
+
export const isGeneratedDaoTreeFile = (filePath: string): boolean => {
|
|
9
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
10
|
+
return /\/gen\/dao\//i.test(normalized) && normalized.endsWith(KOTLIN_FILE_EXTENSION);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const parseEnumMembers = (body: string): string[] =>
|
|
14
|
+
body
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((part) => part.replace(/\/\/.*$/, '').trim())
|
|
17
|
+
.map((part) => part.split(/\s+/)[0]?.trim() ?? '')
|
|
18
|
+
.filter((name) => /^\w+$/.test(name));
|
|
19
|
+
|
|
20
|
+
/** `enum class Side { BUY, SELL }` in generated DAO sources. */
|
|
21
|
+
export const extractEnumClassesFromContent = (content: string): KotlinEnumMap => {
|
|
22
|
+
const enums: KotlinEnumMap = new Map();
|
|
23
|
+
const enumRegex = /(?:^|\n)\s*(?:public\s+)?enum\s+class\s+(\w+)\s*\{([^}]*)\}/gm;
|
|
24
|
+
let match: RegExpExecArray | null;
|
|
25
|
+
|
|
26
|
+
while ((match = enumRegex.exec(content)) !== null) {
|
|
27
|
+
const name = match[1];
|
|
28
|
+
const members = parseEnumMembers(match[2]);
|
|
29
|
+
if (members.length > 0) {
|
|
30
|
+
enums.set(name, members);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return enums;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Companion `Field` metadata on generated entities, e.g.
|
|
39
|
+
* `values = "STANDARD PREMIUM TRIAL"` on `NonNullable<Tier>(Field(… type = Field.Type.ENUM …))`.
|
|
40
|
+
*/
|
|
41
|
+
export const extractCompanionFieldEnumValues = (content: string): KotlinEnumMap => {
|
|
42
|
+
const enums: KotlinEnumMap = new Map();
|
|
43
|
+
const blockRegex =
|
|
44
|
+
/public\s+val\s+\w+:\s*NonNullable<(\w+)>\s*=\s*NonNullable<\1>\(Field\(([\s\S]*?)\)\)/g;
|
|
45
|
+
let match: RegExpExecArray | null;
|
|
46
|
+
|
|
47
|
+
while ((match = blockRegex.exec(content)) !== null) {
|
|
48
|
+
const enumTypeName = match[1];
|
|
49
|
+
const fieldBody = match[2];
|
|
50
|
+
if (!fieldBody.includes('Field.Type.ENUM')) continue;
|
|
51
|
+
|
|
52
|
+
const valuesMatch = fieldBody.match(/values\s*=\s*"([^"]+)"/);
|
|
53
|
+
if (!valuesMatch) continue;
|
|
54
|
+
|
|
55
|
+
const literals = valuesMatch[1]
|
|
56
|
+
.trim()
|
|
57
|
+
.split(/\s+/)
|
|
58
|
+
.filter((value) => value.length > 0);
|
|
59
|
+
|
|
60
|
+
if (literals.length > 0) {
|
|
61
|
+
enums.set(enumTypeName, literals);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return enums;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mergeEnumMaps = (into: KotlinEnumMap, from: KotlinEnumMap): void => {
|
|
69
|
+
for (const [name, values] of from) {
|
|
70
|
+
if (!into.has(name)) {
|
|
71
|
+
into.set(name, values);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const parseGeneratedEnumsFromSources = (files: string[]): KotlinEnumMap => {
|
|
77
|
+
const enums: KotlinEnumMap = new Map();
|
|
78
|
+
|
|
79
|
+
for (const filePath of files) {
|
|
80
|
+
if (!isGeneratedDaoTreeFile(filePath)) continue;
|
|
81
|
+
const content = readFileSync(filePath, 'utf8');
|
|
82
|
+
mergeEnumMaps(enums, extractEnumClassesFromContent(content));
|
|
83
|
+
mergeEnumMaps(enums, extractCompanionFieldEnumValues(content));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return enums;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const kotlinEnumSimpleName = (kotlinType: string): string => {
|
|
90
|
+
const bare = kotlinType.replace(/\?$/, '').trim();
|
|
91
|
+
return bare.includes('.') ? (bare.split('.').pop() ?? bare) : bare;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const kotlinEnumTypeToTs = (kotlinType: string, enums: KotlinEnumMap): string | null => {
|
|
95
|
+
const simpleName = kotlinEnumSimpleName(kotlinType);
|
|
96
|
+
const values = enums.get(simpleName);
|
|
97
|
+
if (!values || values.length === 0) return null;
|
|
98
|
+
return values.map((value) => `'${value}'`).join(' | ');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Use generated enum type name when declared (paired with `renderEnumExports`). */
|
|
102
|
+
export const kotlinEnumTypeRef = (kotlinType: string, enums: KotlinEnumMap): string | null => {
|
|
103
|
+
const simpleName = kotlinEnumSimpleName(kotlinType);
|
|
104
|
+
return enums.has(simpleName) ? simpleName : null;
|
|
105
|
+
};
|