@genesislcap/event-type-codegen 15.19.0 → 15.19.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/src/render.ts DELETED
@@ -1,149 +0,0 @@
1
- import {
2
- kotlinPropertyToWireField,
3
- handlerInputToDetailsInterfaceName,
4
- eventNameToEventClassName,
5
- } from './naming';
6
- import { kotlinEnumTypeRef, type KotlinEnumMap } from './parse-enums';
7
- import { kotlinTypeToTs } from './parse-kotlin';
8
- import type { KotlinDto, MappedHandler, UnmappedHandler } from './types';
9
-
10
- const kotlinTypeRoot = (kotlinType: string): string => {
11
- const listMatch = kotlinType.match(/^(?:List|MutableList|Set|MutableSet)<(.+)>$/);
12
- if (listMatch) {
13
- return kotlinTypeRoot(listMatch[1].trim());
14
- }
15
- return kotlinType.replace(/\?$/, '').trim();
16
- };
17
-
18
- /** Handler inputs plus any nested custom DTOs referenced from their properties. */
19
- const expandUsedDtoNames = (mapped: MappedHandler[], dtos: Map<string, KotlinDto>): Set<string> => {
20
- const used = new Set<string>();
21
-
22
- const visit = (dtoName: string) => {
23
- if (used.has(dtoName) || !dtos.has(dtoName)) return;
24
- used.add(dtoName);
25
- const dto = dtos.get(dtoName)!;
26
- for (const prop of dto.properties) {
27
- const nested = kotlinTypeRoot(prop.kotlinType);
28
- if (dtos.has(nested)) {
29
- visit(nested);
30
- }
31
- }
32
- };
33
-
34
- for (const handler of mapped) {
35
- visit(handler.inputType);
36
- }
37
-
38
- return used;
39
- };
40
-
41
- const renderInterface = (
42
- dto: KotlinDto,
43
- dtos: Map<string, KotlinDto>,
44
- enums: KotlinEnumMap,
45
- ): string => {
46
- const interfaceName = handlerInputToDetailsInterfaceName(dto.name);
47
- const lines = dto.properties.map((prop) => {
48
- const wireName = kotlinPropertyToWireField(prop.name);
49
- const tsType = kotlinTypeToTs(prop.kotlinType, dtos, enums);
50
- const optional = prop.optional ? '?' : '';
51
- return ` ${wireName}${optional}: ${tsType};`;
52
- });
53
-
54
- return `export interface ${interfaceName} {\n${lines.join('\n')}\n}`;
55
- };
56
-
57
- const renderEventClasses = (mapped: MappedHandler[]): string => {
58
- if (mapped.length === 0) return '';
59
-
60
- const sorted = [...mapped].sort((a, b) => a.eventName.localeCompare(b.eventName));
61
-
62
- return sorted
63
- .map((handler) => {
64
- const className = eventNameToEventClassName(handler.eventName);
65
- return `export class ${className} extends GenesisEvent<'${handler.eventName}', ${handler.detailsInterface}> {
66
- readonly MESSAGE_TYPE = '${handler.eventName}' as const;
67
-
68
- constructor(details: ${handler.detailsInterface}) {
69
- super(details);
70
- }
71
- }`;
72
- })
73
- .join('\n\n');
74
- };
75
-
76
- const renderUnmappedComment = (unmapped: UnmappedHandler[]): string => {
77
- if (unmapped.length === 0) return '';
78
-
79
- const lines = unmapped.map(
80
- (entry) =>
81
- ` * - ${entry.handlerName} (${entry.inputType}): ${entry.reason} [${entry.sourceFile.split(/[/\\]/).pop()}]`,
82
- );
83
-
84
- return `/**\n * Unmapped handlers (not included in EventDetailsMap):\n${lines.join('\n')}\n */\n\n`;
85
- };
86
-
87
- const renderEnumExports = (enums: KotlinEnumMap, usedEnumNames: Set<string>): string => {
88
- const blocks: string[] = [];
89
- for (const name of [...usedEnumNames].sort()) {
90
- const values = enums.get(name);
91
- if (!values?.length) continue;
92
- const entries = values.map((value) => ` ${value}: '${value}',`).join('\n');
93
- blocks.push(
94
- `export const ${name} = {\n${entries}\n} as const;\n\nexport type ${name} = (typeof ${name})[keyof typeof ${name}];`,
95
- );
96
- }
97
- return blocks.length > 0 ? `${blocks.join('\n\n')}\n\n` : '';
98
- };
99
-
100
- const collectUsedEnumNames = (
101
- usedDtoNames: Set<string>,
102
- dtos: Map<string, KotlinDto>,
103
- enums: KotlinEnumMap,
104
- ): Set<string> => {
105
- const used = new Set<string>();
106
- for (const dtoName of usedDtoNames) {
107
- const dto = dtos.get(dtoName);
108
- if (!dto) continue;
109
- for (const prop of dto.properties) {
110
- const ref = kotlinEnumTypeRef(kotlinTypeRoot(prop.kotlinType), enums);
111
- if (ref) used.add(ref);
112
- }
113
- }
114
- return used;
115
- };
116
-
117
- export const renderEventTypesFile = (
118
- mapped: MappedHandler[],
119
- unmapped: UnmappedHandler[],
120
- dtos: Map<string, KotlinDto>,
121
- enums: KotlinEnumMap = new Map(),
122
- ): string => {
123
- const usedDtos = expandUsedDtoNames(mapped, dtos);
124
- const interfaces = [...usedDtos]
125
- .map((name) => dtos.get(name))
126
- .filter((dto): dto is KotlinDto => dto != null)
127
- .sort((a, b) => a.name.localeCompare(b.name))
128
- .map((dto) => renderInterface(dto, dtos, enums));
129
-
130
- const usedEnumNames = collectUsedEnumNames(usedDtos, dtos, enums);
131
- const enumExports = renderEnumExports(enums, usedEnumNames);
132
-
133
- const mapEntries = mapped.map((m) => ` ${m.eventName}: ${m.detailsInterface};`).join('\n');
134
-
135
- const eventClasses = renderEventClasses(mapped);
136
- const foundationCommsImport =
137
- mapped.length > 0 ? `import { GenesisEvent } from '@genesislcap/foundation-comms';\n\n` : '';
138
-
139
- return `// AUTO-GENERATED — do not edit
140
-
141
- ${renderUnmappedComment(unmapped)}${foundationCommsImport}${enumExports}${interfaces.join('\n\n')}
142
-
143
- export interface EventDetailsMap {
144
- ${mapEntries}
145
- }
146
-
147
- export type GenesisEventName = keyof EventDetailsMap;
148
- ${eventClasses ? `\n${eventClasses}\n` : ''}`;
149
- };
package/src/types.ts DELETED
@@ -1,56 +0,0 @@
1
- export type EventTypesConfig = {
2
- enabled?: boolean;
3
- serverModule?: string;
4
- kotlinSources?: string[];
5
- /**
6
- * Glob patterns (relative to the parent of `serverModule`, typically `../server`)
7
- * for Genesis generated table DAO Kotlin sources (`…/gen/dao/Trade.kt`).
8
- */
9
- generatedDaoSources?: string[];
10
- handlerScan?: string[];
11
- output?: string;
12
- runOnBuild?: boolean;
13
- };
14
-
15
- export type EventTypesResolvedConfig = {
16
- enabled: boolean;
17
- serverModule: string;
18
- kotlinSources: string[];
19
- generatedDaoSources: string[];
20
- handlerScan: string[];
21
- output: string;
22
- runOnBuild: boolean;
23
- };
24
-
25
- export type KotlinProperty = {
26
- name: string;
27
- kotlinType: string;
28
- optional: boolean;
29
- };
30
-
31
- export type KotlinDto = {
32
- name: string;
33
- properties: KotlinProperty[];
34
- filePath: string;
35
- };
36
-
37
- export type MappedHandler = {
38
- eventName: string;
39
- inputType: string;
40
- detailsInterface: string;
41
- sourceFile: string;
42
- };
43
-
44
- export type UnmappedHandler = {
45
- handlerName: string;
46
- inputType: string;
47
- reason: string;
48
- sourceFile: string;
49
- };
50
-
51
- export type GenerateResult = {
52
- outputPath: string;
53
- eventCount: number;
54
- dtoCount: number;
55
- unmappedCount: number;
56
- };
@@ -1,23 +0,0 @@
1
- package com.example
2
-
3
- import com.example.dto.AnnotatedInput
4
- import com.example.dto.BulkSideInput
5
- import com.example.dto.ChargeAmendInput
6
- import com.example.dto.FooInput
7
- import com.example.dto.NestedInput
8
-
9
- fun register() {
10
- // eventHandler<FooInput>("GHOST")
11
- eventHandler<FooInput>("FOO")
12
- eventHandler<NestedInput>("BAR_BAZ")
13
- eventHandler<ChargeAmendInput>("CHARGE_AMEND")
14
- eventHandler<AnnotatedInput>("ANNOTATED")
15
- eventHandler<BulkSideInput>("BULK_SIDE")
16
- contextEventHandler<FooInput, String>(name = "CONTEXT_FOO")
17
- // Full table DAO — resolved from generated-dao sources
18
- eventHandler<Trade>("TRADE_INSERT", transactional = true)
19
- eventHandler<Trade.ById>("TRADE_DELETE", transactional = true)
20
- // Index type — resolved from generated DAO nested data class
21
- eventHandler<Instrument.ById>("DAO_HANDLER", transactional = true)
22
- eventHandler<Unit>("NOOP")
23
- }
@@ -1,30 +0,0 @@
1
- package com.example.dto
2
-
3
- data class FooInput(
4
- @NotNull val fooId: String, // unique id, required
5
- /* optional bar, with comma */ val barId: String? = null,
6
- )
7
-
8
- data class AnnotatedInput(
9
- @field:JsonProperty("x") val counterpartyId: String,
10
- @param:NotNull val name: String,
11
- @get:JsonIgnore val ignoredHint: String? = null,
12
- )
13
-
14
- data class BulkSideInput(
15
- val priorities: List<Priority>,
16
- )
17
-
18
- data class AmendCellInput(
19
- val maxSize: Int,
20
- val maxInstrumentScore: Int,
21
- )
22
-
23
- data class ChargeAmendInput(
24
- val chargeAdditionId: String,
25
- val cells: List<AmendCellInput>,
26
- )
27
-
28
- data class NestedInput(
29
- val items: List<FooInput>,
30
- )
@@ -1,115 +0,0 @@
1
- import { readFileSync, rmSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
4
-
5
- import { generateEventTypes } from '../src/index';
6
-
7
- const fixtureRoot = resolve(__dirname, 'fixtures');
8
- const serverModule = join(fixtureRoot, 'mini-server');
9
- const goldenPath = join(fixtureRoot, 'golden', 'genesis-event-types.ts');
10
-
11
- const suite = createLogicSuite('event-type-codegen');
12
-
13
- suite('generates golden genesis-event-types.ts from fixture Kotlin module', async () => {
14
- const clientRoot = join(fixtureRoot, 'client-out');
15
- const output = 'genesis-event-types.ts';
16
- const outputPath = join(clientRoot, output);
17
-
18
- rmSync(clientRoot, { recursive: true, force: true });
19
-
20
- const result = await generateEventTypes({
21
- clientRoot,
22
- config: {
23
- enabled: true,
24
- serverModule,
25
- kotlinSources: ['src/main/kotlin/**/dto/**/*.kt'],
26
- generatedDaoSources: ['generated-dao/**/*.kt'],
27
- handlerScan: ['src/main/kotlin/**/*.kt'],
28
- output,
29
- runOnBuild: true,
30
- },
31
- });
32
-
33
- assert.ok(result);
34
- assert.equal(result?.eventCount, 9);
35
-
36
- const actual = readFileSync(outputPath, 'utf8').trim();
37
- const expected = readFileSync(goldenPath, 'utf8').trim();
38
- assert.equal(actual, expected);
39
- });
40
-
41
- suite('maps handler name FOO to EVENT_FOO', async () => {
42
- const actual = readFileSync(goldenPath, 'utf8');
43
- assert.is(actual.includes('EVENT_FOO: FooInputDetails'), true);
44
- });
45
-
46
- suite('ignores line-commented eventHandler registrations', async () => {
47
- const actual = readFileSync(goldenPath, 'utf8');
48
- assert.is(actual.includes('EVENT_GHOST'), false);
49
- });
50
-
51
- suite('keeps properties with Kotlin use-site annotations', async () => {
52
- const actual = readFileSync(goldenPath, 'utf8');
53
- assert.is(actual.includes('export interface AnnotatedInputDetails'), true);
54
- assert.is(actual.includes('COUNTERPARTY_ID: string'), true);
55
- assert.is(actual.includes('NAME: string'), true);
56
- });
57
-
58
- suite('exports enums referenced only from List/Set properties', async () => {
59
- const actual = readFileSync(goldenPath, 'utf8');
60
- assert.is(actual.includes('PRIORITIES: Priority[]'), true);
61
- assert.is(actual.includes('export const Priority = {'), true);
62
- // Priority is not used as a bare enum field on any DTO/DAO — only via List<>.
63
- assert.is(/PRIORITY\??: Priority\b/.test(actual), false);
64
- });
65
-
66
- suite('maps generated DAO Trade handler to EVENT_TRADE_INSERT', async () => {
67
- const actual = readFileSync(goldenPath, 'utf8');
68
- assert.is(actual.includes('EVENT_TRADE_INSERT: TradeDetails'), true);
69
- assert.is(actual.includes('INSTRUMENT_ID: string'), true);
70
- assert.is(
71
- actual.includes("export class EventTradeInsert extends GenesisEvent<'EVENT_TRADE_INSERT'"),
72
- true,
73
- );
74
- });
75
-
76
- suite('emits GenesisEvent subclasses with message type', async () => {
77
- const actual = readFileSync(goldenPath, 'utf8');
78
- assert.is(actual.includes("export class EventFoo extends GenesisEvent<'EVENT_FOO'"), true);
79
- assert.is(actual.includes("readonly MESSAGE_TYPE = 'EVENT_FOO' as const"), true);
80
- });
81
-
82
- suite('maps generated DAO Trade.ById handler to EVENT_TRADE_DELETE', async () => {
83
- const actual = readFileSync(goldenPath, 'utf8');
84
- assert.is(actual.includes('EVENT_TRADE_DELETE: TradeByIdDetails'), true);
85
- assert.is(actual.includes('export interface TradeByIdDetails'), true);
86
- assert.is(
87
- actual.includes("export class EventTradeDelete extends GenesisEvent<'EVENT_TRADE_DELETE'"),
88
- true,
89
- );
90
- });
91
-
92
- suite('maps Instrument.ById index handler', async () => {
93
- const actual = readFileSync(goldenPath, 'utf8');
94
- assert.is(actual.includes('EVENT_DAO_HANDLER: InstrumentByIdDetails'), true);
95
- });
96
-
97
- suite('maps Trade SIDE enum to string union', async () => {
98
- const actual = readFileSync(goldenPath, 'utf8');
99
- assert.is(actual.includes('export const Side = {'), true);
100
- assert.is(actual.includes('SIDE?: Side'), true);
101
- });
102
-
103
- suite('parses handlers with trailing named args (transactional)', async () => {
104
- const actual = readFileSync(goldenPath, 'utf8');
105
- assert.is(actual.includes('EVENT_TRADE_INSERT: TradeDetails'), true);
106
- });
107
-
108
- suite('emits nested DTO interfaces not used as handler inputs', async () => {
109
- const actual = readFileSync(goldenPath, 'utf8');
110
- assert.is(actual.includes('export interface AmendCellInputDetails'), true);
111
- assert.is(actual.includes('CELLS: AmendCellInputDetails[]'), true);
112
- assert.is(actual.includes('EVENT_CHARGE_AMEND: ChargeAmendInputDetails'), true);
113
- });
114
-
115
- suite.run();
@@ -1,42 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
-
3
- import { parseKotlinProperty } from '../src/kotlin-parse-utils';
4
- import { kotlinPropertyToWireField } from '../src/naming';
5
-
6
- const suite = createLogicSuite('parseKotlinProperty');
7
-
8
- suite('parses ordinary camelCase properties', () => {
9
- const prop = parseKotlinProperty('public var instrumentId: String');
10
- assert.ok(prop);
11
- assert.equal(prop!.name, 'instrumentId');
12
- assert.equal(prop!.kotlinType, 'String');
13
- assert.equal(prop!.optional, false);
14
- assert.equal(kotlinPropertyToWireField(prop!.name), 'INSTRUMENT_ID');
15
- });
16
-
17
- suite('parses backtick-escaped reserved identifiers (Position.VALUE)', () => {
18
- const prop = parseKotlinProperty(
19
- '@Title("Value") @GenesisType(Field.Type.DOUBLE) public var `value`: Double? = null',
20
- );
21
- assert.ok(prop);
22
- assert.equal(prop!.name, 'value');
23
- assert.equal(prop!.kotlinType, 'Double');
24
- assert.equal(prop!.optional, true);
25
- assert.equal(kotlinPropertyToWireField(prop!.name), 'VALUE');
26
- });
27
-
28
- suite('parses bare backtick params when allowBareParams is set', () => {
29
- const prop = parseKotlinProperty('`object`: String? = null', { allowBareParams: true });
30
- assert.ok(prop);
31
- assert.equal(prop!.name, 'object');
32
- assert.equal(kotlinPropertyToWireField(prop!.name), 'OBJECT');
33
- });
34
-
35
- suite('passes through backticked UPPER_SNAKE properties unchanged on the wire', () => {
36
- const prop = parseKotlinProperty('public var `ORDER_ID`: String');
37
- assert.ok(prop);
38
- assert.equal(prop!.name, 'ORDER_ID');
39
- assert.equal(kotlinPropertyToWireField(prop!.name), 'ORDER_ID');
40
- });
41
-
42
- suite.run();
@@ -1,49 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
-
3
- import { kotlinPropertyToWireField, toUpperUnderscoreWithAcronyms } from '../src/naming';
4
-
5
- const suite = createLogicSuite('toUpperUnderscoreWithAcronyms');
6
-
7
- /** Cases mirrored from genesis-server ToUpperUnderscoreWithAcronymsTest. */
8
- suite('matches genesis-server simple cases', () => {
9
- assert.equal(toUpperUnderscoreWithAcronyms('orderIds'), 'ORDER_IDS');
10
- assert.equal(toUpperUnderscoreWithAcronyms('orderIdsTest'), 'ORDER_IDS_TEST');
11
- });
12
-
13
- suite('matches genesis-server acronym cases', () => {
14
- assert.equal(toUpperUnderscoreWithAcronyms('orderI'), 'ORDER_I');
15
- assert.equal(toUpperUnderscoreWithAcronyms('orderID'), 'ORDER_ID');
16
- assert.equal(toUpperUnderscoreWithAcronyms('orderIDS'), 'ORDER_IDS');
17
- assert.equal(toUpperUnderscoreWithAcronyms('orderIa'), 'ORDER_IA');
18
- assert.equal(toUpperUnderscoreWithAcronyms('orderIDNotional'), 'ORDER_ID_NOTIONAL');
19
- assert.equal(toUpperUnderscoreWithAcronyms('orderIDSInstrument'), 'ORDER_IDS_INSTRUMENT');
20
- });
21
-
22
- suite('matches genesis-server number cases', () => {
23
- assert.equal(toUpperUnderscoreWithAcronyms('order1'), 'ORDER_1');
24
- assert.equal(toUpperUnderscoreWithAcronyms('order12'), 'ORDER_12');
25
- assert.equal(toUpperUnderscoreWithAcronyms('order123'), 'ORDER_123');
26
- assert.equal(toUpperUnderscoreWithAcronyms('order1a'), 'ORDER_1A');
27
- assert.equal(toUpperUnderscoreWithAcronyms('order12a'), 'ORDER_12A');
28
- assert.equal(toUpperUnderscoreWithAcronyms('order123a'), 'ORDER_123A');
29
- assert.equal(toUpperUnderscoreWithAcronyms('order1A'), 'ORDER_1_A');
30
- assert.equal(toUpperUnderscoreWithAcronyms('order12A'), 'ORDER_12_A');
31
- assert.equal(toUpperUnderscoreWithAcronyms('order123A'), 'ORDER_123_A');
32
- });
33
-
34
- suite('fixes FUI-2606 field-name examples', () => {
35
- assert.equal(kotlinPropertyToWireField('addressLine1'), 'ADDRESS_LINE_1');
36
- assert.equal(kotlinPropertyToWireField('addressLine2'), 'ADDRESS_LINE_2');
37
- assert.equal(kotlinPropertyToWireField('brokerCtmBicM2i'), 'BROKER_CTM_BIC_M_2I');
38
- assert.equal(kotlinPropertyToWireField('dfaNotes1'), 'DFA_NOTES_1');
39
- assert.equal(kotlinPropertyToWireField('tradeType1'), 'TRADE_TYPE_1');
40
- assert.equal(kotlinPropertyToWireField('createdAt'), 'CREATED_AT');
41
- });
42
-
43
- suite('passes through names that are already UPPER_SNAKE', () => {
44
- assert.equal(toUpperUnderscoreWithAcronyms('ORDER_ID'), 'ORDER_ID');
45
- assert.equal(toUpperUnderscoreWithAcronyms('ADDRESS_LINE_1'), 'ADDRESS_LINE_1');
46
- assert.equal(toUpperUnderscoreWithAcronyms('ALREADY_UPPER'), 'ALREADY_UPPER');
47
- });
48
-
49
- suite.run();
@@ -1,208 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
-
3
- import {
4
- eventNameToDetailsInterfaceName,
5
- parseEventMetadataDump,
6
- renderEventTypesFromMetadataContracts,
7
- type EventMetadataDump,
8
- } from '../src/parse-metadata';
9
-
10
- const suite = createLogicSuite('parse-metadata');
11
-
12
- const sampleDump: EventMetadataDump = {
13
- capturedAt: '2026-08-24T10:00:00.000Z',
14
- events: {
15
- EVENT_BROKER_INSERT: {
16
- schema: {
17
- MESSAGE_TYPE: 'EVENT_BROKER_INSERT',
18
- INBOUND: {
19
- properties: {
20
- DETAILS: {
21
- type: 'object',
22
- required: ['BROKER_NAME'],
23
- properties: {
24
- BROKER_ID: {
25
- oneOf: [{ type: 'null' }, { type: 'string' }],
26
- genesisType: 'STRING',
27
- },
28
- BROKER_NAME: { type: 'string', genesisType: 'STRING' },
29
- ADDRESS_LINE_1: {
30
- oneOf: [{ type: 'null' }, { type: 'string' }],
31
- },
32
- UPDATE_DATE: { type: 'integer', genesisType: 'DATETIME' },
33
- },
34
- },
35
- },
36
- },
37
- },
38
- },
39
- EVENT_BROKER_AMEND: {
40
- schema: {
41
- INBOUND: {
42
- properties: {
43
- DETAILS: {
44
- type: 'object',
45
- required: ['BROKER_ID', 'BROKER_NAME'],
46
- properties: {
47
- BROKER_ID: { type: 'string' },
48
- BROKER_NAME: { type: 'string' },
49
- },
50
- },
51
- },
52
- },
53
- },
54
- },
55
- },
56
- };
57
-
58
- suite('derives per-event details interface names', () => {
59
- assert.equal(eventNameToDetailsInterfaceName('EVENT_BROKER_INSERT'), 'BrokerInsertDetails');
60
- assert.equal(eventNameToDetailsInterfaceName('EVENT_TRADE_MODIFY'), 'TradeModifyDetails');
61
- });
62
-
63
- suite('parses schema DETAILS with correct optionality and integer dates', () => {
64
- const { contracts, unmapped } = parseEventMetadataDump(sampleDump);
65
- assert.equal(unmapped.length, 0);
66
- assert.equal(contracts.length, 2);
67
-
68
- const insert = contracts.find((c) => c.eventName === 'EVENT_BROKER_INSERT')!;
69
- const brokerId = insert.fields.find((f) => f.name === 'BROKER_ID')!;
70
- const brokerName = insert.fields.find((f) => f.name === 'BROKER_NAME')!;
71
- const address = insert.fields.find((f) => f.name === 'ADDRESS_LINE_1')!;
72
- const updateDate = insert.fields.find((f) => f.name === 'UPDATE_DATE')!;
73
-
74
- assert.equal(brokerId.optional, true);
75
- assert.equal(brokerName.optional, false);
76
- assert.equal(address.name, 'ADDRESS_LINE_1');
77
- assert.equal(updateDate.tsType, 'number');
78
-
79
- const amend = contracts.find((c) => c.eventName === 'EVENT_BROKER_AMEND')!;
80
- assert.equal(amend.fields.find((f) => f.name === 'BROKER_ID')!.optional, false);
81
- });
82
-
83
- suite('renders separate insert/amend interfaces from metadata', () => {
84
- const { contracts, enums, unmapped } = parseEventMetadataDump(sampleDump);
85
- const rendered = renderEventTypesFromMetadataContracts(contracts, enums, unmapped);
86
- assert.is(rendered.includes('export interface BrokerInsertDetails'), true);
87
- assert.is(rendered.includes('export interface BrokerAmendDetails'), true);
88
- assert.is(rendered.includes('ADDRESS_LINE_1?: string'), true);
89
- assert.is(rendered.includes('UPDATE_DATE?: number'), true);
90
- assert.is(rendered.includes('EVENT_BROKER_INSERT: BrokerInsertDetails'), true);
91
- });
92
-
93
- suite('scopes enum names per event to avoid cross-event collisions', () => {
94
- const enumDump: EventMetadataDump = {
95
- events: {
96
- EVENT_TRADE_INSERT: {
97
- schema: {
98
- INBOUND: {
99
- properties: {
100
- DETAILS: {
101
- type: 'object',
102
- properties: {
103
- SIDE: { type: 'string', enum: ['BUY', 'SELL'] },
104
- status: { type: 'string', enum: ['OPEN', 'CLOSED'] },
105
- SETTLEMENT: { type: 'string', enum: ['T+1', 'N/A', "O'Brien"] },
106
- 'MY-FIELD': { type: 'string', enum: ['A', 'B'] },
107
- },
108
- },
109
- },
110
- },
111
- },
112
- },
113
- EVENT_ORDER_INSERT: {
114
- schema: {
115
- INBOUND: {
116
- properties: {
117
- DETAILS: {
118
- type: 'object',
119
- properties: {
120
- SIDE: { type: 'string', enum: ['LONG', 'SHORT'] },
121
- },
122
- },
123
- },
124
- },
125
- },
126
- },
127
- // Hashes to the same PascalCase as EVENT_TRADE_INSERT.SIDE → TradeInsertSide
128
- EVENT_TRADE: {
129
- schema: {
130
- INBOUND: {
131
- properties: {
132
- DETAILS: {
133
- type: 'object',
134
- properties: {
135
- INSERT_SIDE: { type: 'string', enum: ['IN', 'OUT'] },
136
- },
137
- },
138
- },
139
- },
140
- },
141
- },
142
- },
143
- };
144
-
145
- const { contracts, enums, unmapped } = parseEventMetadataDump(enumDump);
146
-
147
- const tradeSide = enums.find((e) => e.name === 'TradeInsertSide');
148
- const orderSide = enums.find((e) => e.name === 'OrderInsertSide');
149
- const tradeStatus = enums.find((e) => e.name === 'TradeInsertStatus');
150
- const settlement = enums.find((e) => e.name === 'TradeInsertSettlement');
151
- const myField = enums.find((e) => e.name === 'TradeInsertMyField');
152
- const collision = enums.find((e) => e.name === 'TradeInsertSide2');
153
-
154
- assert.ok(tradeSide);
155
- assert.ok(orderSide);
156
- assert.ok(tradeStatus);
157
- assert.ok(settlement);
158
- assert.ok(myField);
159
- assert.ok(collision);
160
- // EVENT_TRADE sorts before EVENT_TRADE_INSERT, so INSERT_SIDE wins TradeInsertSide
161
- assert.equal(tradeSide!.values.join(','), 'IN,OUT');
162
- assert.equal(orderSide!.values.join(','), 'LONG,SHORT');
163
- assert.equal(tradeStatus!.values.join(','), 'OPEN,CLOSED');
164
- assert.equal(settlement!.values.join(','), "T+1,N/A,O'Brien");
165
- assert.equal(collision!.values.join(','), 'BUY,SELL');
166
- assert.ok(unmapped.some((line) => line.includes('enum name collision for TradeInsertSide')));
167
-
168
- const trade = contracts.find((c) => c.eventName === 'EVENT_TRADE_INSERT')!;
169
- assert.equal(trade.fields.find((f) => f.name === 'SIDE')!.tsType, 'TradeInsertSide2');
170
- assert.equal(trade.fields.find((f) => f.name === 'status')!.tsType, 'TradeInsertStatus');
171
- assert.equal(trade.fields.find((f) => f.name === 'MY-FIELD')!.tsType, 'TradeInsertMyField');
172
-
173
- const tradeOnly = contracts.find((c) => c.eventName === 'EVENT_TRADE')!;
174
- assert.equal(tradeOnly.fields.find((f) => f.name === 'INSERT_SIDE')!.tsType, 'TradeInsertSide');
175
-
176
- const rendered = renderEventTypesFromMetadataContracts(contracts, enums, unmapped);
177
- assert.is(rendered.includes('export const TradeInsertSide'), true);
178
- assert.is(rendered.includes('export const OrderInsertSide'), true);
179
- assert.is(rendered.includes('SIDE?: TradeInsertSide2'), true);
180
- assert.is(rendered.includes("'MY-FIELD'?: TradeInsertMyField"), true);
181
- // Non-identifier / quote-bearing values must be quoted on both sides
182
- assert.is(rendered.includes("'T+1': 'T+1'"), true);
183
- assert.is(rendered.includes("'N/A': 'N/A'"), true);
184
- assert.is(rendered.includes("'O\\'Brien': 'O\\'Brien'"), true);
185
- // Emitted file must be syntactically valid JS for the enum object
186
- const enumBlock = rendered.slice(
187
- rendered.indexOf('export const TradeInsertSettlement'),
188
- rendered.indexOf('export type TradeInsertSettlement'),
189
- );
190
- assert.ok(enumBlock.includes('as const'));
191
- const runnable = enumBlock.replace(/^export\s+/m, '').replace(/\s+as const/, '');
192
- // eslint-disable-next-line no-new-func -- assert generated enum object parses
193
- const parsed = new Function(`${runnable}; return TradeInsertSettlement;`)();
194
- assert.equal(parsed['T+1'], 'T+1');
195
- assert.equal(parsed['N/A'], 'N/A');
196
- assert.equal(parsed["O'Brien"], "O'Brien");
197
-
198
- const ifaceStart = rendered.indexOf('export interface TradeInsertDetails {');
199
- const ifaceEnd = rendered.indexOf('}', ifaceStart);
200
- const ifaceBody = rendered.slice(rendered.indexOf('{', ifaceStart) + 1, ifaceEnd);
201
- const objectLiteral = `{${ifaceBody.replace(/\?:/g, ':').replace(/: [^;]+;/g, ': 1,')}}`;
202
- // eslint-disable-next-line no-new-func -- assert generated interface keys parse
203
- const keys = new Function(`return ${objectLiteral};`)() as Record<string, number>;
204
- assert.equal(keys['MY-FIELD'], 1);
205
- assert.equal(keys.SIDE, 1);
206
- });
207
-
208
- suite.run();