@ioka-technologies/asyncapi-ts-client-template 0.0.22 → 0.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ioka-technologies/asyncapi-ts-client-template",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "TypeScript AsyncAPI client generator template compatible with rust-asyncapi patterns",
5
5
  "main": "template/index.js",
6
6
  "scripts": {
@@ -8,8 +8,8 @@
8
8
  "build:dev": "webpack --config ../webpack.config.js --env template=ts-client --mode development",
9
9
  "prepublishOnly": "npm run build",
10
10
  "test": "npm run test:generate",
11
- "test:generate": "asyncapi generate fromTemplate examples/auth-retry/asyncapi.yaml . -o test-output-auth-retry --force-write && cd test-output-auth-retry && npm install && npm run build",
12
- "test:realworld": "asyncapi generate fromTemplate ../examples/realworld/asyncapi.yaml . -o test-output-realworld --force-write",
11
+ "test:generate": "asyncapi generate fromTemplate examples/auth-retry/asyncapi.yaml ./ -o test-output-auth-retry --force-write && cd test-output-auth-retry && npm install && npm run build",
12
+ "test:realworld": "asyncapi generate fromTemplate ../examples/realworld/asyncapi.yaml ./ -o test-output-realworld --force-write",
13
13
  "test:simple": "asyncapi generate fromTemplate ../examples/simple/asyncapi.yaml ./ -o test-output-simple --force-write",
14
14
  "clean": "rm -rf test-output-* dist"
15
15
  },
@@ -7,7 +7,7 @@ import {
7
7
  analyzeOperationSecurity,
8
8
  operationHasSecurity,
9
9
  hasSecuritySchemes
10
- } from '../dist/common/index.js';
10
+ } from "../dist/common/index.js";
11
11
 
12
12
  /**
13
13
  * Get security scheme type from AsyncAPI security scheme definition
@@ -1,378 +1,21 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
  import { File } from '@asyncapi/generator-react-sdk';
3
+ import { generateTypeScriptModels } from '../../../common/src/index.js';
3
4
 
4
- function generateModels(asyncapi) {
5
- let content = '// Generated TypeScript models from AsyncAPI specification\n\n';
6
-
7
- // Helper functions for TypeScript identifier generation
8
- function toTypeScriptIdentifier(str) {
9
- if (!str) return 'unknown';
10
- let identifier = str
11
- .replace(/[^a-zA-Z0-9_]/g, '_')
12
- .replace(/^[0-9]/, '_$&')
13
- .replace(/_+/g, '_')
14
- .replace(/^_+|_+$/g, '');
15
- if (/^[0-9]/.test(identifier)) {
16
- identifier = 'item_' + identifier;
17
- }
18
- if (!identifier) {
19
- identifier = 'unknown';
20
- }
21
- return identifier;
22
- }
23
-
24
-
25
- function toTypeScriptTypeName(str) {
26
- if (!str) return 'Unknown';
27
- // Handle camelCase and PascalCase properly
28
- const identifier = str
29
- .replace(/[^a-zA-Z0-9]/g, '_')
30
- .replace(/^[0-9]/, '_$&')
31
- .replace(/_+/g, '_')
32
- .replace(/^_+|_+$/g, '');
33
-
34
- // Split on underscores and camelCase boundaries
35
- const parts = identifier.split(/[_\s]+|(?=[A-Z])/);
36
-
37
- return parts
38
- .filter(part => part.length > 0)
39
- .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
40
- .join('');
41
- }
42
-
43
- // Extract message schemas and build channel mapping
44
- const components = asyncapi.components();
45
- const messageSchemas = [];
46
- const componentSchemas = [];
47
- const messageToChannels = new Map();
48
- const generatedTypes = new Set();
49
- const schemaRegistry = new Map();
50
-
51
- // Build schema registry from components.schemas
52
- // Try to access the raw AsyncAPI document
53
- let rawDoc = null;
54
- try {
55
- if (asyncapi.json && typeof asyncapi.json === 'function') {
56
- rawDoc = asyncapi.json();
57
- } else if (asyncapi._json) {
58
- rawDoc = asyncapi._json;
59
- }
60
- } catch (e) {
61
- // Ignore
62
- }
63
-
64
- // Extract schemas from raw document if available
65
- if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
66
- Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
67
- if (name && typeof name === 'string' && schema && typeof schema === 'object') {
68
- schemaRegistry.set(name, schema);
69
- componentSchemas.push({
70
- name,
71
- typeName: toTypeScriptTypeName(name),
72
- schema: schema,
73
- description: schema.description
74
- });
75
- }
76
- });
77
- }
78
-
79
- // Fallback: try the components.schemas() method
80
- if (componentSchemas.length === 0 && components && components.schemas) {
81
- try {
82
- const schemas = components.schemas();
83
- if (schemas) {
84
- // Try different ways to access schemas
85
- let schemaEntries = [];
86
-
87
- if (schemas instanceof Map) {
88
- schemaEntries = Array.from(schemas.entries());
89
- } else if (typeof schemas === 'object') {
90
- schemaEntries = Object.entries(schemas);
91
- } else if (schemas.all && typeof schemas.all === 'function') {
92
- // AsyncAPI parser might have an all() method
93
- const allSchemas = schemas.all();
94
- if (Array.isArray(allSchemas)) {
95
- schemaEntries = allSchemas.map(schema => {
96
- const name = schema.uid ? schema.uid() : (schema.id ? schema.id() : null);
97
- return [name, schema];
98
- }).filter(([name]) => name);
99
- }
100
- }
101
-
102
- schemaEntries.forEach(([name, schema]) => {
103
- // Skip internal AsyncAPI parser objects and numeric keys
104
- if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
105
- return;
106
- }
107
-
108
- let schemaData = null;
109
- let description = null;
110
-
111
- try {
112
- // Handle different schema object types
113
- if (schema && typeof schema.json === 'function') {
114
- schemaData = schema.json();
115
- } else if (schema && typeof schema === 'object') {
116
- schemaData = schema;
117
- }
118
-
119
- if (schema && typeof schema.description === 'function') {
120
- description = schema.description();
121
- } else if (schema && schema.description) {
122
- description = schema.description;
123
- }
124
- } catch (e) {
125
- // Ignore schema extraction errors
126
- console.warn(`Failed to extract schema for ${name}:`, e.message);
127
- }
128
-
129
- if (schemaData && typeof name === 'string' && name.length > 0) {
130
- schemaRegistry.set(name, schemaData);
131
- componentSchemas.push({
132
- name,
133
- typeName: toTypeScriptTypeName(name),
134
- schema: schemaData,
135
- description
136
- });
137
- }
138
- });
139
- }
140
- } catch (e) {
141
- console.warn('Failed to extract component schemas:', e.message);
142
- }
143
- }
144
-
145
- // First, build channel to message mapping
146
- if (asyncapi.channels) {
147
- const channels = asyncapi.channels();
148
- if (channels) {
149
- Object.entries(channels).forEach(([channelName, channel]) => {
150
- try {
151
- // Handle AsyncAPI 3.x format
152
- if (channel.messages) {
153
- const messages = channel.messages();
154
- if (messages) {
155
- Object.entries(messages).forEach(([msgKey, message]) => {
156
- if (message) {
157
- let messageName = null;
158
- if (message.$ref) {
159
- messageName = message.$ref.split('/').pop();
160
- } else if (message.name) {
161
- messageName = typeof message.name === 'function' ? message.name() : message.name;
162
- }
163
-
164
- if (messageName) {
165
- if (!messageToChannels.has(messageName)) {
166
- messageToChannels.set(messageName, []);
167
- }
168
- messageToChannels.get(messageName).push(channelName);
169
- }
170
- }
171
- });
172
- }
173
- }
174
- } catch (e) {
175
- // Ignore channel processing errors
176
- }
177
- });
178
- }
179
- }
180
-
181
- // Extract messages from components
182
- if (components && components.messages) {
183
- const messages = components.messages();
184
- if (messages) {
185
- Object.entries(messages).forEach(([name, message]) => {
186
- // Skip internal AsyncAPI parser objects
187
- if (name === 'collections' || name === '_meta' || name.startsWith('_')) {
188
- return;
189
- }
190
- let payload = null;
191
- let description = null;
192
- let title = null;
193
- let messageName = name;
194
-
195
- try {
196
- if (message.payload && typeof message.payload === 'function') {
197
- const payloadSchema = message.payload();
198
- payload = payloadSchema && payloadSchema.json ? payloadSchema.json() : payloadSchema;
199
- }
200
- description = message.description && typeof message.description === 'function' ? message.description() : message.description;
201
- title = message.title && typeof message.title === 'function' ? message.title() : message.title;
202
-
203
- // Try to get the actual message name
204
- if (message.name && typeof message.name === 'function') {
205
- messageName = message.name();
206
- } else if (message.name) {
207
- messageName = message.name;
208
- }
209
- } catch (e) {
210
- // Ignore payload extraction errors
211
- }
212
-
213
- const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
214
- messageSchemas.push({
215
- name: messageName,
216
- typeName: toTypeScriptTypeName(messageName),
217
- payload,
218
- description: description || title,
219
- channels
220
- });
221
- });
222
- }
223
- }
224
-
225
- // Helper function to convert JSON schema to TypeScript type
226
- function jsonSchemaToTypeScriptType(schema, fieldName = '') {
227
- if (!schema) return 'any';
228
-
229
- // Handle $ref - resolve from schema registry
230
- if (schema.$ref) {
231
- const refName = schema.$ref.split('/').pop();
232
- // Always return the type name for $ref, since we generate all component schemas
233
- const typeName = toTypeScriptTypeName(refName);
234
- return typeName;
235
- }
236
-
237
- // Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
238
- if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
239
- const schemaId = schema['x-parser-schema-id'];
240
- // Check if this matches a known component schema
241
- if (schemaRegistry.has(schemaId)) {
242
- const typeName = toTypeScriptTypeName(schemaId);
243
- return typeName;
244
- }
245
- }
246
-
247
- if (!schema.type) {
248
- // If no type specified, check for properties (object) or items (array)
249
- if (schema.properties) {
250
- schema.type = 'object';
251
- } else if (schema.items) {
252
- schema.type = 'array';
253
- } else {
254
- return 'any';
255
- }
256
- }
257
-
258
- switch (schema.type) {
259
- case 'string':
260
- if (schema.enum && schema.enum.length > 0) {
261
- return schema.enum.map(val => `'${val}'`).join(' | ');
262
- }
263
- return 'string';
264
- case 'integer':
265
- case 'number':
266
- return 'number';
267
- case 'boolean':
268
- return 'boolean';
269
- case 'array': {
270
- if (schema.items) {
271
- const itemType = jsonSchemaToTypeScriptType(schema.items, fieldName);
272
- return `${itemType}[]`;
273
- }
274
- return 'any[]';
275
- }
276
- case 'object':
277
- // For objects with properties, we should generate inline types or check if it's a known schema
278
- if (schema.properties) {
279
- // This is a complex object - for now return Record<string, any>
280
- // In a more sophisticated implementation, we could generate inline types
281
- return 'Record<string, any>';
282
- }
283
- return 'Record<string, any>';
284
- default:
285
- return 'any';
286
- }
287
- }
288
-
289
- // Generate message interfaces
290
- function generateMessageInterface(schema, messageName) {
291
- if (!schema || !schema.properties) {
292
- return ' [key: string]: any;';
293
- }
294
-
295
- const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
296
- const tsType = jsonSchemaToTypeScriptType(fieldSchema, fieldName);
297
- const optional = !schema.required || !schema.required.includes(fieldName);
298
- const optionalMarker = optional ? '?' : '';
299
-
300
- let fieldDoc = '';
301
- if (fieldSchema.description) {
302
- fieldDoc = ` /** ${fieldSchema.description} */\n`;
303
- }
304
-
305
- return `${fieldDoc} ${fieldName}${optionalMarker}: ${tsType};`;
306
- }).join('\n');
307
-
308
- return fields;
309
- }
310
-
311
- // Generate interfaces for component schemas first (so they can be referenced by messages)
312
- componentSchemas.forEach(schema => {
313
- const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} */\n`;
314
-
315
- // Check if this is a standalone enum schema
316
- if (schema.schema.type === 'string' && schema.schema.enum && Array.isArray(schema.schema.enum)) {
317
- // Generate union type for enum
318
- const enumValues = schema.schema.enum.map(val => `'${val}'`).join(' | ');
319
- content += `${doc}export type ${schema.typeName} = ${enumValues};\n\n`;
320
- } else {
321
- // Generate interface for object schema
322
- content += `${doc}export interface ${schema.typeName} {\n`;
323
- content += generateMessageInterface(schema.schema, schema.typeName);
324
- content += '\n}\n\n';
325
- }
326
-
327
- // Track generated types to avoid duplicates
328
- generatedTypes.add(schema.typeName);
329
- });
330
-
331
- // Generate interfaces for each message (only if not already generated as component schema)
332
- messageSchemas.forEach(schema => {
333
- const interfaceName = `${schema.typeName}Payload`;
334
-
335
- // Check if this is a duplicate of a component schema
336
- // For message payloads that match component schema names, skip the payload version
337
- if (generatedTypes.has(schema.typeName) || generatedTypes.has(interfaceName)) {
338
- // Skip generating the payload version if we already have the component schema
339
- return;
340
- }
341
-
342
- const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} message payload */\n`;
343
-
344
- content += `${doc}export interface ${interfaceName} {\n`;
345
- content += generateMessageInterface(schema.payload, schema.typeName);
346
- content += '\n}\n\n';
347
-
348
- generatedTypes.add(interfaceName);
5
+ export default function ({ asyncapi, params }) {
6
+ // Generate models using the common helper
7
+ const models = generateTypeScriptModels(asyncapi, {
8
+ includeMessageTypes: true // TypeScript client includes message type constants
349
9
  });
350
10
 
351
- // Generate a union type for all message payloads
352
- if (messageSchemas.length > 0) {
353
- const payloadTypes = messageSchemas.map(schema => `${schema.typeName}Payload`).join(' | ');
354
- content += '/** Union type for all message payloads */\n';
355
- content += `export type MessagePayload = ${payloadTypes};\n\n`;
356
-
357
- // Generate message type constants
358
- content += '/** Message type constants */\n';
359
- content += 'export const MessageTypes = {\n';
360
- messageSchemas.forEach(schema => {
361
- content += ` ${schema.typeName.toUpperCase()}: '${schema.name}',\n`;
362
- });
363
- content += '} as const;\n\n';
364
-
365
- content += '/** Message type union */\n';
366
- content += 'export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n';
367
- }
368
-
369
- return content;
370
- }
371
-
372
- export default function ({ asyncapi, params }) {
373
11
  return (
374
12
  <File name="models.ts">
375
- {generateModels(asyncapi)}
13
+ {`// Generated TypeScript models from AsyncAPI specification
14
+
15
+ ${models.generateComponentSchemas()}
16
+ ${models.generateMessageSchemas()}
17
+ ${models.generateMessageTypes()}
18
+ `}
376
19
  </File>
377
20
  );
378
21
  }
@@ -1,142 +0,0 @@
1
- # yaml-language-server: $schema=https://raw.githubusercontent.com/asyncapi/spec-json-schemas/refs/heads/master/schemas/all.schema-store.json
2
- asyncapi: 3.0.0
3
- info:
4
- title: Method Name Sanitization Test API
5
- version: 1.0.0
6
- description: Test API for validating method name sanitization
7
-
8
- channels:
9
- deviceList:
10
- address: device/list
11
- messages:
12
- DeviceListRequest:
13
- name: DeviceListRequest
14
- title: Device List Request
15
- payload:
16
- type: object
17
- properties:
18
- filter:
19
- type: string
20
- DeviceListResponse:
21
- name: DeviceListResponse
22
- title: Device List Response
23
- payload:
24
- type: object
25
- properties:
26
- devices:
27
- type: array
28
- items:
29
- type: object
30
-
31
- userProfile:
32
- address: user-profile
33
- messages:
34
- UserProfileRequest:
35
- name: UserProfileRequest
36
- title: User Profile Request
37
- payload:
38
- type: object
39
- properties:
40
- userId:
41
- type: string
42
- UserProfileResponse:
43
- name: UserProfileResponse
44
- title: User Profile Response
45
- payload:
46
- type: object
47
- properties:
48
- profile:
49
- type: object
50
-
51
- apiStatus:
52
- address: api.status
53
- messages:
54
- StatusRequest:
55
- name: StatusRequest
56
- title: Status Request
57
- payload:
58
- type: object
59
- StatusResponse:
60
- name: StatusResponse
61
- title: Status Response
62
- payload:
63
- type: object
64
- properties:
65
- status:
66
- type: string
67
-
68
- eventStream:
69
- address: event_stream
70
- messages:
71
- EventMessage:
72
- name: EventMessage
73
- title: Event Message
74
- payload:
75
- type: object
76
- properties:
77
- eventType:
78
- type: string
79
-
80
- operations:
81
- # Test operation with dots
82
- device.list:
83
- action: send
84
- channel:
85
- $ref: '#/channels/deviceList'
86
- messages:
87
- - $ref: '#/channels/deviceList/messages/DeviceListRequest'
88
- reply:
89
- channel:
90
- $ref: '#/channels/deviceList'
91
- messages:
92
- - $ref: '#/channels/deviceList/messages/DeviceListResponse'
93
-
94
- # Test operation with hyphens
95
- user-profile:
96
- action: send
97
- channel:
98
- $ref: '#/channels/userProfile'
99
- messages:
100
- - $ref: '#/channels/userProfile/messages/UserProfileRequest'
101
- reply:
102
- channel:
103
- $ref: '#/channels/userProfile'
104
- messages:
105
- - $ref: '#/channels/userProfile/messages/UserProfileResponse'
106
-
107
- # Test operation with forward slashes
108
- api/status:
109
- action: send
110
- channel:
111
- $ref: '#/channels/apiStatus'
112
- messages:
113
- - $ref: '#/channels/apiStatus/messages/StatusRequest'
114
- reply:
115
- channel:
116
- $ref: '#/channels/apiStatus'
117
- messages:
118
- - $ref: '#/channels/apiStatus/messages/StatusResponse'
119
-
120
- # Test operation with underscores
121
- event_stream:
122
- action: receive
123
- channel:
124
- $ref: '#/channels/eventStream'
125
- messages:
126
- - $ref: '#/channels/eventStream/messages/EventMessage'
127
-
128
- # Test operation that conflicts with class method
129
- connect:
130
- action: send
131
- channel:
132
- $ref: '#/channels/apiStatus'
133
- messages:
134
- - $ref: '#/channels/apiStatus/messages/StatusRequest'
135
-
136
- # Test operation with reserved word
137
- return:
138
- action: send
139
- channel:
140
- $ref: '#/channels/apiStatus'
141
- messages:
142
- - $ref: '#/channels/apiStatus/messages/StatusRequest'