@ioka-technologies/asyncapi-rust-client-template 0.0.22 → 0.0.24
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/dist/common/index.js +1475 -1
- package/package.json +1 -1
- package/template/helpers/index.js +1 -1
- package/template/src/models.rs.js +21 -536
- package/template/dist/common/index.js +0 -2004
- package/template/package.json +0 -49
package/dist/common/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "@asyncapi/generator-react-sdk";
|
|
1
2
|
/******/ // The require scope
|
|
2
3
|
/******/ var __webpack_require__ = {};
|
|
3
4
|
/******/
|
|
@@ -1883,6 +1884,1469 @@ function generateOperationHandlerName(operation, suffix = 'Handler') {
|
|
|
1883
1884
|
const pascalCaseName = toPascalCase(operationName);
|
|
1884
1885
|
return `${pascalCaseName}${suffix}`;
|
|
1885
1886
|
}
|
|
1887
|
+
;// external "@asyncapi/generator-react-sdk"
|
|
1888
|
+
|
|
1889
|
+
;// ../common/src/models-rust.js
|
|
1890
|
+
/* eslint-disable no-unused-vars */
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
/**
|
|
1894
|
+
* Generate Rust models from AsyncAPI specification
|
|
1895
|
+
* This helper extracts the common schema processing logic used by both rust-server and rust-client templates
|
|
1896
|
+
*
|
|
1897
|
+
* @param {Object} asyncapi - AsyncAPI document
|
|
1898
|
+
* @param {Object} options - Generation options
|
|
1899
|
+
* @param {Function} options.toRustTypeName - Function to convert names to Rust type names
|
|
1900
|
+
* @param {Function} options.toRustFieldName - Function to convert names to Rust field names
|
|
1901
|
+
* @param {Function} options.toRustEnumVariantWithSerde - Function to convert enum variants with serde attributes
|
|
1902
|
+
* @param {boolean} options.includeAsyncApiTrait - Whether to include AsyncApiMessage trait implementations
|
|
1903
|
+
* @param {boolean} options.includeEnvelope - Whether to include MessageEnvelope (from envelope-rust.js)
|
|
1904
|
+
* @returns {Object} Generated models data and functions
|
|
1905
|
+
*/
|
|
1906
|
+
function generateRustModels(asyncapi, options = {}) {
|
|
1907
|
+
const {
|
|
1908
|
+
toRustTypeName,
|
|
1909
|
+
toRustFieldName,
|
|
1910
|
+
toRustEnumVariantWithSerde,
|
|
1911
|
+
includeAsyncApiTrait = false,
|
|
1912
|
+
includeEnvelope = false
|
|
1913
|
+
} = options;
|
|
1914
|
+
|
|
1915
|
+
// Extract message schemas and build channel mapping
|
|
1916
|
+
const components = asyncapi.components();
|
|
1917
|
+
const messageSchemas = [];
|
|
1918
|
+
const componentSchemas = [];
|
|
1919
|
+
const messageToChannels = new Map();
|
|
1920
|
+
const generatedTypes = new Set();
|
|
1921
|
+
const nestedSchemas = new Map();
|
|
1922
|
+
const schemaRegistry = new Map();
|
|
1923
|
+
|
|
1924
|
+
// First, build channel to message mapping and extract inline message schemas
|
|
1925
|
+
if (asyncapi.channels) {
|
|
1926
|
+
const channels = asyncapi.channels();
|
|
1927
|
+
if (channels) {
|
|
1928
|
+
// Use proper iteration for AsyncAPI collection
|
|
1929
|
+
for (const channel of channels) {
|
|
1930
|
+
try {
|
|
1931
|
+
const channelName = channel.id();
|
|
1932
|
+
|
|
1933
|
+
// Handle AsyncAPI 3.x format - extract inline messages from channels
|
|
1934
|
+
if (channel.messages) {
|
|
1935
|
+
const messages = channel.messages();
|
|
1936
|
+
if (messages) {
|
|
1937
|
+
// Check if messages is an object with message names as keys
|
|
1938
|
+
if (typeof messages === 'object' && !Array.isArray(messages)) {
|
|
1939
|
+
// Iterate through message entries (messageName -> messageObject)
|
|
1940
|
+
Object.entries(messages).forEach(([messageName, message]) => {
|
|
1941
|
+
if (message && messageName) {
|
|
1942
|
+
let payload = null;
|
|
1943
|
+
let description = null;
|
|
1944
|
+
|
|
1945
|
+
// Get payload schema
|
|
1946
|
+
if (message.payload && typeof message.payload === 'function') {
|
|
1947
|
+
payload = message.payload();
|
|
1948
|
+
if (payload && payload.json && typeof payload.json === 'function') {
|
|
1949
|
+
payload = payload.json();
|
|
1950
|
+
}
|
|
1951
|
+
} else if (message.payload) {
|
|
1952
|
+
payload = message.payload;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Get description
|
|
1956
|
+
if (message.description && typeof message.description === 'function') {
|
|
1957
|
+
description = message.description();
|
|
1958
|
+
} else if (message.description) {
|
|
1959
|
+
description = message.description;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Add to channel mapping
|
|
1963
|
+
if (!messageToChannels.has(messageName)) {
|
|
1964
|
+
messageToChannels.set(messageName, []);
|
|
1965
|
+
}
|
|
1966
|
+
messageToChannels.get(messageName).push(channelName);
|
|
1967
|
+
|
|
1968
|
+
// Add to message schemas for inline messages
|
|
1969
|
+
messageSchemas.push({
|
|
1970
|
+
name: messageName,
|
|
1971
|
+
rustName: toRustTypeName(messageName),
|
|
1972
|
+
payload,
|
|
1973
|
+
rawPayload: payload,
|
|
1974
|
+
description,
|
|
1975
|
+
channels: [channelName]
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
} else {
|
|
1980
|
+
// Try iterating as a collection
|
|
1981
|
+
for (const message of messages) {
|
|
1982
|
+
if (message) {
|
|
1983
|
+
let messageName = null;
|
|
1984
|
+
let payload = null;
|
|
1985
|
+
let description = null;
|
|
1986
|
+
|
|
1987
|
+
// Get message name - try multiple approaches
|
|
1988
|
+
if (message._meta && message._meta.id) {
|
|
1989
|
+
messageName = message._meta.id;
|
|
1990
|
+
} else if (message._json && message._json['x-parser-message-name']) {
|
|
1991
|
+
messageName = message._json['x-parser-message-name'];
|
|
1992
|
+
} else if (message._json && message._json['x-parser-unique-object-id']) {
|
|
1993
|
+
messageName = message._json['x-parser-unique-object-id'];
|
|
1994
|
+
} else if (message.name && typeof message.name === 'function') {
|
|
1995
|
+
messageName = message.name();
|
|
1996
|
+
} else if (message.name) {
|
|
1997
|
+
messageName = message.name;
|
|
1998
|
+
} else if (message.$ref) {
|
|
1999
|
+
messageName = message.$ref.split('/').pop();
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Get payload schema
|
|
2003
|
+
if (message.payload && typeof message.payload === 'function') {
|
|
2004
|
+
payload = message.payload();
|
|
2005
|
+
if (payload && payload.json && typeof payload.json === 'function') {
|
|
2006
|
+
payload = payload.json();
|
|
2007
|
+
}
|
|
2008
|
+
} else if (message.payload) {
|
|
2009
|
+
payload = message.payload;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Get description
|
|
2013
|
+
if (message.description && typeof message.description === 'function') {
|
|
2014
|
+
description = message.description();
|
|
2015
|
+
} else if (message.description) {
|
|
2016
|
+
description = message.description;
|
|
2017
|
+
}
|
|
2018
|
+
if (messageName) {
|
|
2019
|
+
// Add to channel mapping
|
|
2020
|
+
if (!messageToChannels.has(messageName)) {
|
|
2021
|
+
messageToChannels.set(messageName, []);
|
|
2022
|
+
}
|
|
2023
|
+
messageToChannels.get(messageName).push(channelName);
|
|
2024
|
+
|
|
2025
|
+
// Add to message schemas for inline messages
|
|
2026
|
+
messageSchemas.push({
|
|
2027
|
+
name: messageName,
|
|
2028
|
+
rustName: toRustTypeName(messageName),
|
|
2029
|
+
payload,
|
|
2030
|
+
rawPayload: payload,
|
|
2031
|
+
description,
|
|
2032
|
+
channels: [channelName]
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Handle AsyncAPI 2.x format
|
|
2042
|
+
if (channel.subscribe && channel.subscribe()) {
|
|
2043
|
+
const message = channel.subscribe().message();
|
|
2044
|
+
if (message) {
|
|
2045
|
+
let messageName = null;
|
|
2046
|
+
if (message.$ref) {
|
|
2047
|
+
messageName = message.$ref.split('/').pop();
|
|
2048
|
+
} else if (message.name) {
|
|
2049
|
+
messageName = typeof message.name === 'function' ? message.name() : message.name;
|
|
2050
|
+
}
|
|
2051
|
+
if (messageName) {
|
|
2052
|
+
if (!messageToChannels.has(messageName)) {
|
|
2053
|
+
messageToChannels.set(messageName, []);
|
|
2054
|
+
}
|
|
2055
|
+
messageToChannels.get(messageName).push(channelName);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if (channel.publish && channel.publish()) {
|
|
2060
|
+
const message = channel.publish().message();
|
|
2061
|
+
if (message) {
|
|
2062
|
+
let messageName = null;
|
|
2063
|
+
if (message.$ref) {
|
|
2064
|
+
messageName = message.$ref.split('/').pop();
|
|
2065
|
+
} else if (message.name) {
|
|
2066
|
+
messageName = typeof message.name === 'function' ? message.name() : message.name;
|
|
2067
|
+
}
|
|
2068
|
+
if (messageName) {
|
|
2069
|
+
if (!messageToChannels.has(messageName)) {
|
|
2070
|
+
messageToChannels.set(messageName, []);
|
|
2071
|
+
}
|
|
2072
|
+
messageToChannels.get(messageName).push(channelName);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
} catch (e) {
|
|
2077
|
+
// Ignore channel processing errors
|
|
2078
|
+
console.warn(`Error processing channel: ${e.message}`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Build schema registry from components.schemas
|
|
2085
|
+
// Try to access the raw AsyncAPI document
|
|
2086
|
+
let rawDoc = null;
|
|
2087
|
+
try {
|
|
2088
|
+
if (asyncapi.json && typeof asyncapi.json === 'function') {
|
|
2089
|
+
rawDoc = asyncapi.json();
|
|
2090
|
+
} else if (asyncapi._json) {
|
|
2091
|
+
rawDoc = asyncapi._json;
|
|
2092
|
+
}
|
|
2093
|
+
} catch (e) {
|
|
2094
|
+
// Ignore
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Extract schemas from raw document if available
|
|
2098
|
+
if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
|
|
2099
|
+
Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
|
|
2100
|
+
if (name && typeof name === 'string' && schema && typeof schema === 'object') {
|
|
2101
|
+
schemaRegistry.set(name, schema);
|
|
2102
|
+
componentSchemas.push({
|
|
2103
|
+
name,
|
|
2104
|
+
rustName: toRustTypeName(name),
|
|
2105
|
+
schema: schema,
|
|
2106
|
+
description: schema.description
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// Fallback: try the components.schemas() method
|
|
2113
|
+
if (componentSchemas.length === 0 && components && components.schemas) {
|
|
2114
|
+
try {
|
|
2115
|
+
const schemas = components.schemas();
|
|
2116
|
+
if (schemas) {
|
|
2117
|
+
// Try different ways to access schemas
|
|
2118
|
+
let schemaEntries = [];
|
|
2119
|
+
if (schemas instanceof Map) {
|
|
2120
|
+
schemaEntries = Array.from(schemas.entries());
|
|
2121
|
+
} else if (typeof schemas === 'object') {
|
|
2122
|
+
schemaEntries = Object.entries(schemas);
|
|
2123
|
+
} else if (schemas.all && typeof schemas.all === 'function') {
|
|
2124
|
+
// AsyncAPI parser might have an all() method
|
|
2125
|
+
const allSchemas = schemas.all();
|
|
2126
|
+
if (Array.isArray(allSchemas)) {
|
|
2127
|
+
schemaEntries = allSchemas.map(schema => {
|
|
2128
|
+
const name = schema.uid ? schema.uid() : schema.id ? schema.id() : null;
|
|
2129
|
+
return [name, schema];
|
|
2130
|
+
}).filter(([name]) => name);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
schemaEntries.forEach(([name, schema]) => {
|
|
2134
|
+
// Skip internal AsyncAPI parser objects and numeric keys
|
|
2135
|
+
if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
let schemaData = null;
|
|
2139
|
+
let description = null;
|
|
2140
|
+
try {
|
|
2141
|
+
// Handle different schema object types
|
|
2142
|
+
if (schema && typeof schema.json === 'function') {
|
|
2143
|
+
schemaData = schema.json();
|
|
2144
|
+
} else if (schema && typeof schema === 'object') {
|
|
2145
|
+
schemaData = schema;
|
|
2146
|
+
}
|
|
2147
|
+
if (schema && typeof schema.description === 'function') {
|
|
2148
|
+
description = schema.description();
|
|
2149
|
+
} else if (schema && schema.description) {
|
|
2150
|
+
description = schema.description;
|
|
2151
|
+
}
|
|
2152
|
+
} catch (e) {
|
|
2153
|
+
// Ignore schema extraction errors
|
|
2154
|
+
console.warn(`Failed to extract schema for ${name}:`, e.message);
|
|
2155
|
+
}
|
|
2156
|
+
if (schemaData && typeof name === 'string' && name.length > 0) {
|
|
2157
|
+
schemaRegistry.set(name, schemaData);
|
|
2158
|
+
componentSchemas.push({
|
|
2159
|
+
name,
|
|
2160
|
+
rustName: toRustTypeName(name),
|
|
2161
|
+
schema: schemaData,
|
|
2162
|
+
description
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
} catch (e) {
|
|
2168
|
+
console.warn('Failed to extract component schemas:', e.message);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// Extract messages from components
|
|
2173
|
+
if (components && components.messages) {
|
|
2174
|
+
const messages = components.messages();
|
|
2175
|
+
if (messages) {
|
|
2176
|
+
Object.entries(messages).forEach(([name, message]) => {
|
|
2177
|
+
let payload = null;
|
|
2178
|
+
let description = null;
|
|
2179
|
+
let title = null;
|
|
2180
|
+
let messageName = name;
|
|
2181
|
+
try {
|
|
2182
|
+
let rawPayload = null;
|
|
2183
|
+
if (message.payload && typeof message.payload === 'function') {
|
|
2184
|
+
const payloadSchema = message.payload();
|
|
2185
|
+
payload = payloadSchema && payloadSchema.json ? payloadSchema.json() : payloadSchema;
|
|
2186
|
+
// Try to get the raw payload reference from the message
|
|
2187
|
+
if (message._json && message._json.payload) {
|
|
2188
|
+
rawPayload = message._json.payload;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
description = message.description && typeof message.description === 'function' ? message.description() : null;
|
|
2192
|
+
title = message.title && typeof message.title === 'function' ? message.title() : null;
|
|
2193
|
+
|
|
2194
|
+
// Try to get the actual message name
|
|
2195
|
+
if (message.name && typeof message.name === 'function') {
|
|
2196
|
+
messageName = message.name();
|
|
2197
|
+
} else if (message.name) {
|
|
2198
|
+
messageName = message.name;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// If we have raw document access, try to get the payload reference from there
|
|
2202
|
+
if (!rawPayload && rawDoc && rawDoc.components && rawDoc.components.messages && rawDoc.components.messages[name]) {
|
|
2203
|
+
rawPayload = rawDoc.components.messages[name].payload;
|
|
2204
|
+
}
|
|
2205
|
+
const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
|
|
2206
|
+
messageSchemas.push({
|
|
2207
|
+
name: messageName,
|
|
2208
|
+
rustName: toRustTypeName(messageName),
|
|
2209
|
+
payload,
|
|
2210
|
+
rawPayload,
|
|
2211
|
+
description: description || title,
|
|
2212
|
+
channels
|
|
2213
|
+
});
|
|
2214
|
+
} catch (e) {
|
|
2215
|
+
// Ignore payload extraction errors
|
|
2216
|
+
const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
|
|
2217
|
+
messageSchemas.push({
|
|
2218
|
+
name: messageName,
|
|
2219
|
+
rustName: toRustTypeName(messageName),
|
|
2220
|
+
payload,
|
|
2221
|
+
rawPayload: null,
|
|
2222
|
+
description: description || title,
|
|
2223
|
+
channels
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Helper function to convert JSON schema to Rust type
|
|
2231
|
+
function jsonSchemaToRustType(schema, typeName = null) {
|
|
2232
|
+
if (!schema) return 'serde_json::Value';
|
|
2233
|
+
|
|
2234
|
+
// Handle $ref
|
|
2235
|
+
if (schema.$ref) {
|
|
2236
|
+
const refName = schema.$ref.split('/').pop();
|
|
2237
|
+
const rustTypeName = toRustTypeName(refName);
|
|
2238
|
+
|
|
2239
|
+
// Generate the referenced schema if we have access to components
|
|
2240
|
+
if (components && components.schemas) {
|
|
2241
|
+
const schemas = components.schemas();
|
|
2242
|
+
if (schemas && schemas[refName] && !generatedTypes.has(rustTypeName)) {
|
|
2243
|
+
generatedTypes.add(rustTypeName);
|
|
2244
|
+
const referencedSchema = schemas[refName];
|
|
2245
|
+
const schemaJson = referencedSchema.json ? referencedSchema.json() : referencedSchema;
|
|
2246
|
+
nestedSchemas.set(rustTypeName, {
|
|
2247
|
+
type: 'struct',
|
|
2248
|
+
schema: schemaJson,
|
|
2249
|
+
description: referencedSchema.description && typeof referencedSchema.description === 'function' ? referencedSchema.description() : null
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return rustTypeName;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
|
|
2257
|
+
if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
|
|
2258
|
+
const schemaId = schema['x-parser-schema-id'];
|
|
2259
|
+
// Check if this matches a known component schema
|
|
2260
|
+
if (schemaRegistry.has(schemaId)) {
|
|
2261
|
+
const rustTypeName = toRustTypeName(schemaId);
|
|
2262
|
+
return rustTypeName;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (!schema.type) {
|
|
2266
|
+
// If no type specified, check for properties (object) or items (array)
|
|
2267
|
+
if (schema.properties) {
|
|
2268
|
+
schema.type = 'object';
|
|
2269
|
+
} else if (schema.items) {
|
|
2270
|
+
schema.type = 'array';
|
|
2271
|
+
} else {
|
|
2272
|
+
return 'serde_json::Value';
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
switch (schema.type) {
|
|
2276
|
+
case 'string':
|
|
2277
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
2278
|
+
// Generate enum type
|
|
2279
|
+
if (typeName) {
|
|
2280
|
+
const enumName = `${typeName}Enum`;
|
|
2281
|
+
if (!generatedTypes.has(enumName)) {
|
|
2282
|
+
generatedTypes.add(enumName);
|
|
2283
|
+
nestedSchemas.set(enumName, {
|
|
2284
|
+
type: 'enum',
|
|
2285
|
+
variants: schema.enum,
|
|
2286
|
+
description: schema.description
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
return enumName;
|
|
2290
|
+
}
|
|
2291
|
+
return 'String'; // Fallback if no type name provided
|
|
2292
|
+
}
|
|
2293
|
+
if (schema.format === 'date-time') return 'chrono::DateTime<chrono::Utc>';
|
|
2294
|
+
if (schema.format === 'uuid') return 'uuid::Uuid';
|
|
2295
|
+
if (schema.format === 'email') return 'String';
|
|
2296
|
+
if (schema.format === 'uri') return 'String';
|
|
2297
|
+
return 'String';
|
|
2298
|
+
case 'integer':
|
|
2299
|
+
switch (schema.format) {
|
|
2300
|
+
case 'int32':
|
|
2301
|
+
return 'i32';
|
|
2302
|
+
case 'int64':
|
|
2303
|
+
return 'i64';
|
|
2304
|
+
case 'uint32':
|
|
2305
|
+
return 'u32';
|
|
2306
|
+
case 'uint64':
|
|
2307
|
+
return 'u64';
|
|
2308
|
+
default:
|
|
2309
|
+
// Default to i32 for unspecified format (maintains backward compatibility)
|
|
2310
|
+
return 'i32';
|
|
2311
|
+
}
|
|
2312
|
+
case 'number':
|
|
2313
|
+
return 'f64';
|
|
2314
|
+
case 'boolean':
|
|
2315
|
+
return 'bool';
|
|
2316
|
+
case 'array':
|
|
2317
|
+
{
|
|
2318
|
+
const itemType = jsonSchemaToRustType(schema.items);
|
|
2319
|
+
return `Vec<${itemType}>`;
|
|
2320
|
+
}
|
|
2321
|
+
case 'object':
|
|
2322
|
+
if (schema.properties && Object.keys(schema.properties).length > 0) {
|
|
2323
|
+
// Generate nested struct
|
|
2324
|
+
if (typeName) {
|
|
2325
|
+
const structName = toRustTypeName(typeName);
|
|
2326
|
+
if (!generatedTypes.has(structName)) {
|
|
2327
|
+
generatedTypes.add(structName);
|
|
2328
|
+
nestedSchemas.set(structName, {
|
|
2329
|
+
type: 'struct',
|
|
2330
|
+
schema: schema,
|
|
2331
|
+
description: schema.description
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
return structName;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
return 'serde_json::Value';
|
|
2338
|
+
default:
|
|
2339
|
+
return 'serde_json::Value';
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// Generate message structs
|
|
2344
|
+
function generateMessageStruct(schema, messageName) {
|
|
2345
|
+
if (!schema || !schema.properties) {
|
|
2346
|
+
return ' pub data: serde_json::Value,';
|
|
2347
|
+
}
|
|
2348
|
+
const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
|
|
2349
|
+
const rustFieldName = toRustFieldName(fieldName);
|
|
2350
|
+
const fieldTypeName = `${messageName}${toRustTypeName(fieldName)}`;
|
|
2351
|
+
const rustType = jsonSchemaToRustType(fieldSchema, fieldTypeName);
|
|
2352
|
+
const requiredFields = schema.required;
|
|
2353
|
+
const optional = !requiredFields || !Array.isArray(requiredFields) || requiredFields.indexOf(fieldName) === -1;
|
|
2354
|
+
const finalType = optional ? `Option<${rustType}>` : rustType;
|
|
2355
|
+
let fieldDoc = '';
|
|
2356
|
+
if (fieldSchema.description) {
|
|
2357
|
+
fieldDoc = ` /// ${fieldSchema.description}\n`;
|
|
2358
|
+
}
|
|
2359
|
+
let serdeRename = '';
|
|
2360
|
+
if (rustFieldName !== fieldName) {
|
|
2361
|
+
serdeRename = ` #[serde(rename = "${fieldName}")]\n`;
|
|
2362
|
+
}
|
|
2363
|
+
let skipSerializing = '';
|
|
2364
|
+
if (optional) {
|
|
2365
|
+
skipSerializing = ' #[serde(skip_serializing_if = "Option::is_none")]\n';
|
|
2366
|
+
}
|
|
2367
|
+
return `${fieldDoc}${serdeRename}${skipSerializing} pub ${rustFieldName}: ${finalType},`;
|
|
2368
|
+
}).join('\n');
|
|
2369
|
+
return fields;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// Process all component schemas first to ensure they are available for references
|
|
2373
|
+
componentSchemas.forEach(schema => {
|
|
2374
|
+
jsonSchemaToRustType(schema.schema, schema.rustName);
|
|
2375
|
+
generatedTypes.add(schema.rustName);
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
// Process all message schemas to ensure all referenced types are generated
|
|
2379
|
+
messageSchemas.forEach(schema => {
|
|
2380
|
+
if (schema.payload) {
|
|
2381
|
+
jsonSchemaToRustType(schema.payload, schema.rustName);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
// Return the processed data and generation functions
|
|
2386
|
+
return {
|
|
2387
|
+
messageSchemas,
|
|
2388
|
+
componentSchemas,
|
|
2389
|
+
messageToChannels,
|
|
2390
|
+
generatedTypes,
|
|
2391
|
+
nestedSchemas,
|
|
2392
|
+
schemaRegistry,
|
|
2393
|
+
// Generation functions
|
|
2394
|
+
generateMessageStruct,
|
|
2395
|
+
jsonSchemaToRustType,
|
|
2396
|
+
// Generate component schema definitions
|
|
2397
|
+
generateComponentSchemas() {
|
|
2398
|
+
let result = '';
|
|
2399
|
+
componentSchemas.forEach(schema => {
|
|
2400
|
+
const doc = schema.description ? `/// ${schema.description}\n` : `/// ${schema.name}\n`;
|
|
2401
|
+
|
|
2402
|
+
// Check if this is a standalone enum schema
|
|
2403
|
+
if (schema.schema.type === 'string' && schema.schema.enum && Array.isArray(schema.schema.enum)) {
|
|
2404
|
+
// Generate enum definition with serde rename attributes for lowercase serialization
|
|
2405
|
+
const variants = schema.schema.enum.map(variant => {
|
|
2406
|
+
const {
|
|
2407
|
+
rustName,
|
|
2408
|
+
serializedName
|
|
2409
|
+
} = toRustEnumVariantWithSerde(variant);
|
|
2410
|
+
return ` #[serde(rename = "${serializedName}")]\n ${rustName}`;
|
|
2411
|
+
}).join(',\n');
|
|
2412
|
+
result += `
|
|
2413
|
+
${doc}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
2414
|
+
pub enum ${schema.rustName} {
|
|
2415
|
+
${variants},
|
|
2416
|
+
}
|
|
2417
|
+
`;
|
|
2418
|
+
} else {
|
|
2419
|
+
// Generate struct definition
|
|
2420
|
+
const fields = generateMessageStruct(schema.schema, schema.rustName);
|
|
2421
|
+
result += `
|
|
2422
|
+
${doc}#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
2423
|
+
pub struct ${schema.rustName} {
|
|
2424
|
+
${fields}
|
|
2425
|
+
}
|
|
2426
|
+
`;
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
return result;
|
|
2430
|
+
},
|
|
2431
|
+
// Generate nested type definitions
|
|
2432
|
+
generateNestedTypes() {
|
|
2433
|
+
let result = '';
|
|
2434
|
+
for (const [typeName, typeInfo] of nestedSchemas.entries()) {
|
|
2435
|
+
// Don't skip enums - they need to be generated even if the parent type exists
|
|
2436
|
+
const isEnum = typeInfo.type === 'enum';
|
|
2437
|
+
const isComponentSchema = componentSchemas.some(cs => cs.rustName === typeName);
|
|
2438
|
+
|
|
2439
|
+
// Skip if this type was already generated as a component schema (but not enums)
|
|
2440
|
+
if (!isEnum && isComponentSchema) {
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
if (typeInfo.type === 'enum') {
|
|
2444
|
+
const variants = typeInfo.variants.map(variant => {
|
|
2445
|
+
const {
|
|
2446
|
+
rustName,
|
|
2447
|
+
serializedName
|
|
2448
|
+
} = toRustEnumVariantWithSerde(variant);
|
|
2449
|
+
return ` #[serde(rename = "${serializedName}")]\n ${rustName}`;
|
|
2450
|
+
}).join(',\n');
|
|
2451
|
+
const doc = typeInfo.description ? `/// ${typeInfo.description}\n` : '';
|
|
2452
|
+
result += `
|
|
2453
|
+
${doc}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
2454
|
+
pub enum ${typeName} {
|
|
2455
|
+
${variants},
|
|
2456
|
+
}
|
|
2457
|
+
`;
|
|
2458
|
+
} else if (typeInfo.type === 'struct') {
|
|
2459
|
+
const fields = generateMessageStruct(typeInfo.schema, typeName);
|
|
2460
|
+
const doc = typeInfo.description ? `/// ${typeInfo.description}\n` : '';
|
|
2461
|
+
result += `
|
|
2462
|
+
${doc}#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
2463
|
+
pub struct ${typeName} {
|
|
2464
|
+
${fields}
|
|
2465
|
+
}
|
|
2466
|
+
`;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return result;
|
|
2470
|
+
},
|
|
2471
|
+
// Generate AsyncApiMessage trait implementations (optional)
|
|
2472
|
+
generateAsyncApiTrait() {
|
|
2473
|
+
if (!includeAsyncApiTrait) {
|
|
2474
|
+
return '';
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Track which types have already had AsyncApiMessage implementations generated
|
|
2478
|
+
const implementedTypes = new Set();
|
|
2479
|
+
const implementations = [];
|
|
2480
|
+
|
|
2481
|
+
// First add the trait definition
|
|
2482
|
+
implementations.push(`
|
|
2483
|
+
/// Base trait for all AsyncAPI messages providing runtime type information
|
|
2484
|
+
///
|
|
2485
|
+
/// This trait enables:
|
|
2486
|
+
/// - **Dynamic message routing**: Route messages based on their type at runtime
|
|
2487
|
+
/// - **Channel identification**: Determine which channel a message belongs to
|
|
2488
|
+
/// - **Logging and monitoring**: Track message types for observability
|
|
2489
|
+
/// - **Protocol abstraction**: Handle different message types uniformly
|
|
2490
|
+
pub trait AsyncApiMessage {
|
|
2491
|
+
/// Returns the message type identifier as defined in the AsyncAPI specification
|
|
2492
|
+
///
|
|
2493
|
+
/// This is used for:
|
|
2494
|
+
/// - Message routing and dispatch
|
|
2495
|
+
/// - Logging and monitoring
|
|
2496
|
+
/// - Protocol-level message identification
|
|
2497
|
+
fn message_type(&self) -> &'static str;
|
|
2498
|
+
|
|
2499
|
+
/// Returns the primary channel this message is associated with
|
|
2500
|
+
///
|
|
2501
|
+
/// Used for:
|
|
2502
|
+
/// - Default routing when channel is not explicitly specified
|
|
2503
|
+
/// - Message categorization and organization
|
|
2504
|
+
/// - Channel-based access control and filtering
|
|
2505
|
+
fn channel(&self) -> &'static str;
|
|
2506
|
+
}`);
|
|
2507
|
+
messageSchemas.forEach(schema => {
|
|
2508
|
+
const doc = schema.description ? `/// ${schema.description}` : `/// ${schema.name} message`;
|
|
2509
|
+
const primaryChannel = schema.channels.length > 0 ? schema.channels[0] : 'default';
|
|
2510
|
+
|
|
2511
|
+
// Check if the message payload references a component schema
|
|
2512
|
+
let payloadRustName = null;
|
|
2513
|
+
let isComponentMessage = false;
|
|
2514
|
+
if (schema.rawPayload && schema.rawPayload.$ref) {
|
|
2515
|
+
const refName = schema.rawPayload.$ref.split('/').pop();
|
|
2516
|
+
payloadRustName = toRustTypeName(refName);
|
|
2517
|
+
isComponentMessage = true;
|
|
2518
|
+
} else if (schema.payload && schema.payload.$ref) {
|
|
2519
|
+
const refName = schema.payload.$ref.split('/').pop();
|
|
2520
|
+
payloadRustName = toRustTypeName(refName);
|
|
2521
|
+
isComponentMessage = true;
|
|
2522
|
+
} else if (schema.payload && schema.payload['x-parser-schema-id']) {
|
|
2523
|
+
// Handle resolved $ref references
|
|
2524
|
+
const schemaId = schema.payload['x-parser-schema-id'];
|
|
2525
|
+
if (schemaRegistry.has(schemaId)) {
|
|
2526
|
+
payloadRustName = toRustTypeName(schemaId);
|
|
2527
|
+
isComponentMessage = true;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// For component messages, always generate the message wrapper type
|
|
2532
|
+
// even if the payload schema already exists
|
|
2533
|
+
if (isComponentMessage && payloadRustName && !implementedTypes.has(schema.rustName)) {
|
|
2534
|
+
implementedTypes.add(schema.rustName);
|
|
2535
|
+
implementations.push(`
|
|
2536
|
+
${doc}
|
|
2537
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
2538
|
+
pub struct ${schema.rustName} {
|
|
2539
|
+
#[serde(flatten)]
|
|
2540
|
+
pub payload: ${payloadRustName},
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
impl AsyncApiMessage for ${schema.rustName} {
|
|
2544
|
+
fn message_type(&self) -> &'static str {
|
|
2545
|
+
"${schema.name}"
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
fn channel(&self) -> &'static str {
|
|
2549
|
+
"${primaryChannel}"
|
|
2550
|
+
}
|
|
2551
|
+
}`);
|
|
2552
|
+
} else if (!generatedTypes.has(schema.rustName) && !implementedTypes.has(schema.rustName)) {
|
|
2553
|
+
// Generate both struct and implementation for inline message schemas
|
|
2554
|
+
implementedTypes.add(schema.rustName);
|
|
2555
|
+
implementations.push(`
|
|
2556
|
+
${doc}
|
|
2557
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
2558
|
+
pub struct ${schema.rustName} {
|
|
2559
|
+
${generateMessageStruct(schema.payload, schema.rustName)}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
impl AsyncApiMessage for ${schema.rustName} {
|
|
2563
|
+
fn message_type(&self) -> &'static str {
|
|
2564
|
+
"${schema.name}"
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
fn channel(&self) -> &'static str {
|
|
2568
|
+
"${primaryChannel}"
|
|
2569
|
+
}
|
|
2570
|
+
}`);
|
|
2571
|
+
} else if (payloadRustName && generatedTypes.has(payloadRustName) && !implementedTypes.has(payloadRustName)) {
|
|
2572
|
+
// Generate AsyncApiMessage implementation for existing component schema
|
|
2573
|
+
implementedTypes.add(payloadRustName);
|
|
2574
|
+
implementations.push(`
|
|
2575
|
+
impl AsyncApiMessage for ${payloadRustName} {
|
|
2576
|
+
fn message_type(&self) -> &'static str {
|
|
2577
|
+
"${schema.name}"
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
fn channel(&self) -> &'static str {
|
|
2581
|
+
"${primaryChannel}"
|
|
2582
|
+
}
|
|
2583
|
+
}`);
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
return implementations.join('');
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
;// ../common/src/envelope-rust.js
|
|
2591
|
+
/* eslint-disable no-unused-vars */
|
|
2592
|
+
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* Generate a unified MessageEnvelope for both rust-server and rust-client templates
|
|
2596
|
+
* This envelope includes all features needed by both templates:
|
|
2597
|
+
* - Basic message structure (id, operation, payload, timestamp)
|
|
2598
|
+
* - Request/response patterns (correlation_id, create_response)
|
|
2599
|
+
* - Channel routing (channel field)
|
|
2600
|
+
* - Error handling (error field, error methods)
|
|
2601
|
+
* - Authentication (auth header methods)
|
|
2602
|
+
* - Serialization (to_bytes, from_bytes)
|
|
2603
|
+
*/
|
|
2604
|
+
function generateMessageEnvelope() {
|
|
2605
|
+
return `use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
2606
|
+
use std::collections::HashMap;
|
|
2607
|
+
use uuid::Uuid;
|
|
2608
|
+
|
|
2609
|
+
/// Unified message envelope for consistent AsyncAPI message format
|
|
2610
|
+
///
|
|
2611
|
+
/// This envelope provides a standardized structure for all messages sent through the system,
|
|
2612
|
+
/// enabling better correlation, error handling, authentication, and observability.
|
|
2613
|
+
///
|
|
2614
|
+
/// ## Features
|
|
2615
|
+
///
|
|
2616
|
+
/// - **Request/Response Patterns**: Correlation IDs for matching requests with responses
|
|
2617
|
+
/// - **Error Handling**: Built-in error information for failed operations
|
|
2618
|
+
/// - **Authentication**: Integrated auth header support
|
|
2619
|
+
/// - **Channel Routing**: Optional channel context for message routing
|
|
2620
|
+
/// - **Serialization**: Efficient byte conversion for transport layers
|
|
2621
|
+
/// - **Type Safety**: Strongly-typed payload extraction
|
|
2622
|
+
///
|
|
2623
|
+
/// ## Usage
|
|
2624
|
+
///
|
|
2625
|
+
/// \`\`\`no-run
|
|
2626
|
+
/// use crate::models::*;
|
|
2627
|
+
/// use uuid::Uuid;
|
|
2628
|
+
/// use std::collections::HashMap;
|
|
2629
|
+
///
|
|
2630
|
+
/// // Create a basic message envelope
|
|
2631
|
+
/// let envelope = MessageEnvelope::new("sendChatMessage", chat_message)?;
|
|
2632
|
+
///
|
|
2633
|
+
/// // Create with correlation ID for request/response
|
|
2634
|
+
/// let request = MessageEnvelope::new_with_correlation_id(
|
|
2635
|
+
/// "getUserProfile",
|
|
2636
|
+
/// user_request,
|
|
2637
|
+
/// Uuid::new_v4().to_string()
|
|
2638
|
+
/// )?;
|
|
2639
|
+
///
|
|
2640
|
+
/// // Create response with same correlation ID
|
|
2641
|
+
/// let response = request.create_response("getUserProfile_response", user_profile)?;
|
|
2642
|
+
///
|
|
2643
|
+
/// // Create error response
|
|
2644
|
+
/// let error = MessageEnvelope::error_response(
|
|
2645
|
+
/// "getUserProfile_response",
|
|
2646
|
+
/// "USER_NOT_FOUND",
|
|
2647
|
+
/// "User does not exist",
|
|
2648
|
+
/// request.correlation_id().map(|s| s.to_string())
|
|
2649
|
+
/// );
|
|
2650
|
+
///
|
|
2651
|
+
/// // Add authentication headers
|
|
2652
|
+
/// let mut headers = HashMap::new();
|
|
2653
|
+
/// headers.insert("Authorization".to_string(), "Bearer token123".to_string());
|
|
2654
|
+
/// let auth_envelope = envelope.with_headers(headers);
|
|
2655
|
+
///
|
|
2656
|
+
/// // Serialize for transport
|
|
2657
|
+
/// let bytes = envelope.to_bytes()?;
|
|
2658
|
+
/// let deserialized = MessageEnvelope::from_bytes(&bytes)?;
|
|
2659
|
+
/// \`\`\`
|
|
2660
|
+
|
|
2661
|
+
/// Standard message envelope for all AsyncAPI messages
|
|
2662
|
+
///
|
|
2663
|
+
/// This envelope provides a consistent structure for all messages sent through the system,
|
|
2664
|
+
/// enabling better correlation, error handling, and observability.
|
|
2665
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
2666
|
+
pub struct MessageEnvelope {
|
|
2667
|
+
/// Unique message identifier
|
|
2668
|
+
pub id: String,
|
|
2669
|
+
/// AsyncAPI operation ID
|
|
2670
|
+
pub operation: String,
|
|
2671
|
+
/// Message payload (any serializable type)
|
|
2672
|
+
pub payload: serde_json::Value,
|
|
2673
|
+
/// ISO 8601 timestamp when message was created
|
|
2674
|
+
pub timestamp: String,
|
|
2675
|
+
/// Correlation ID for request/response patterns
|
|
2676
|
+
pub correlation_id: Option<String>,
|
|
2677
|
+
/// Optional channel context for routing
|
|
2678
|
+
pub channel: Option<String>,
|
|
2679
|
+
/// Transport-level headers (auth, routing, etc.)
|
|
2680
|
+
pub headers: Option<HashMap<String, String>>,
|
|
2681
|
+
/// Error information if applicable
|
|
2682
|
+
pub error: Option<MessageError>,
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/// Error information for failed operations
|
|
2686
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
2687
|
+
pub struct MessageError {
|
|
2688
|
+
/// Error code (e.g., "VALIDATION_ERROR", "TIMEOUT", "UNAUTHORIZED")
|
|
2689
|
+
pub code: String,
|
|
2690
|
+
/// Human-readable error message
|
|
2691
|
+
pub message: String,
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
impl MessageEnvelope {
|
|
2695
|
+
/// Create a new message envelope with the given operation and payload
|
|
2696
|
+
pub fn new<T: Serialize>(operation: &str, payload: T) -> Result<Self, serde_json::Error> {
|
|
2697
|
+
Ok(Self {
|
|
2698
|
+
id: Uuid::new_v4().to_string(),
|
|
2699
|
+
operation: operation.to_string(),
|
|
2700
|
+
payload: serde_json::to_value(payload)?,
|
|
2701
|
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
2702
|
+
correlation_id: None,
|
|
2703
|
+
channel: None,
|
|
2704
|
+
headers: None,
|
|
2705
|
+
error: None,
|
|
2706
|
+
})
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/// Create a new envelope with automatic correlation ID generation
|
|
2710
|
+
pub fn new_with_id<T: Serialize>(
|
|
2711
|
+
operation: &str,
|
|
2712
|
+
payload: T,
|
|
2713
|
+
) -> Result<Self, serde_json::Error> {
|
|
2714
|
+
Self::new(operation, payload)
|
|
2715
|
+
.map(|envelope| envelope.with_correlation_id(Uuid::new_v4().to_string()))
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
/// Create a new message envelope with a specific correlation ID
|
|
2719
|
+
pub fn new_with_correlation_id<T: Serialize>(
|
|
2720
|
+
operation: &str,
|
|
2721
|
+
payload: T,
|
|
2722
|
+
correlation_id: String,
|
|
2723
|
+
) -> Result<Self, serde_json::Error> {
|
|
2724
|
+
let mut envelope = Self::new(operation, payload)?;
|
|
2725
|
+
envelope.correlation_id = Some(correlation_id);
|
|
2726
|
+
Ok(envelope)
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/// Create an error response envelope
|
|
2730
|
+
pub fn error_response(
|
|
2731
|
+
operation: &str,
|
|
2732
|
+
error_code: &str,
|
|
2733
|
+
error_message: &str,
|
|
2734
|
+
correlation_id: Option<String>,
|
|
2735
|
+
) -> Self {
|
|
2736
|
+
Self {
|
|
2737
|
+
id: Uuid::new_v4().to_string(),
|
|
2738
|
+
operation: operation.to_string(),
|
|
2739
|
+
payload: serde_json::Value::Null,
|
|
2740
|
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
2741
|
+
correlation_id,
|
|
2742
|
+
channel: None,
|
|
2743
|
+
headers: None,
|
|
2744
|
+
error: Some(MessageError {
|
|
2745
|
+
code: error_code.to_string(),
|
|
2746
|
+
message: error_message.to_string(),
|
|
2747
|
+
}),
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
/// Set the correlation ID for this envelope
|
|
2752
|
+
pub fn with_correlation_id(mut self, id: String) -> Self {
|
|
2753
|
+
self.correlation_id = Some(id);
|
|
2754
|
+
self
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/// Set the channel for this envelope
|
|
2758
|
+
pub fn with_channel(mut self, channel: String) -> Self {
|
|
2759
|
+
self.channel = Some(channel);
|
|
2760
|
+
self
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/// Set headers for this envelope
|
|
2764
|
+
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
|
|
2765
|
+
self.headers = Some(headers);
|
|
2766
|
+
self
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
/// Add a single header to this envelope
|
|
2770
|
+
pub fn with_header(mut self, key: String, value: String) -> Self {
|
|
2771
|
+
if self.headers.is_none() {
|
|
2772
|
+
self.headers = Some(HashMap::new());
|
|
2773
|
+
}
|
|
2774
|
+
if let Some(ref mut headers) = self.headers {
|
|
2775
|
+
headers.insert(key, value);
|
|
2776
|
+
}
|
|
2777
|
+
self
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/// Add authentication headers to the envelope
|
|
2781
|
+
/// This method accepts any headers map, allowing templates to integrate their own auth systems
|
|
2782
|
+
pub fn with_auth_headers(mut self, auth_headers: HashMap<String, String>) -> Self {
|
|
2783
|
+
if !auth_headers.is_empty() {
|
|
2784
|
+
if let Some(ref mut headers) = self.headers {
|
|
2785
|
+
headers.extend(auth_headers);
|
|
2786
|
+
} else {
|
|
2787
|
+
self.headers = Some(auth_headers);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
self
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
/// Set an error on this envelope
|
|
2794
|
+
pub fn with_error(mut self, code: &str, message: &str) -> Self {
|
|
2795
|
+
self.error = Some(MessageError {
|
|
2796
|
+
code: code.to_string(),
|
|
2797
|
+
message: message.to_string(),
|
|
2798
|
+
});
|
|
2799
|
+
self
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/// Extract the payload as a strongly-typed message
|
|
2803
|
+
pub fn extract_payload<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
|
|
2804
|
+
serde_json::from_value(self.payload.clone())
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
/// Check if this envelope contains an error
|
|
2808
|
+
pub fn is_error(&self) -> bool {
|
|
2809
|
+
self.error.is_some()
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
/// Get the correlation ID if present
|
|
2813
|
+
pub fn correlation_id(&self) -> Option<&str> {
|
|
2814
|
+
self.correlation_id.as_deref()
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
/// Create a response envelope with the same correlation ID
|
|
2818
|
+
pub fn create_response<T: Serialize>(
|
|
2819
|
+
&self,
|
|
2820
|
+
response_operation: &str,
|
|
2821
|
+
payload: T,
|
|
2822
|
+
) -> Result<Self, serde_json::Error> {
|
|
2823
|
+
let mut response = Self::new(response_operation, payload)?;
|
|
2824
|
+
response.correlation_id = self.correlation_id.clone();
|
|
2825
|
+
response.channel = self.channel.clone();
|
|
2826
|
+
Ok(response)
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
/// Convert the envelope to bytes for transport
|
|
2830
|
+
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
|
|
2831
|
+
serde_json::to_vec(self)
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
/// Parse envelope from bytes received from transport
|
|
2835
|
+
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
|
2836
|
+
serde_json::from_slice(bytes)
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
#[cfg(test)]
|
|
2841
|
+
mod tests {
|
|
2842
|
+
use super::*;
|
|
2843
|
+
use serde::{Deserialize, Serialize};
|
|
2844
|
+
|
|
2845
|
+
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
2846
|
+
struct TestPayload {
|
|
2847
|
+
message: String,
|
|
2848
|
+
count: u32,
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
#[test]
|
|
2852
|
+
fn test_envelope_creation() {
|
|
2853
|
+
let payload = TestPayload {
|
|
2854
|
+
message: "test".to_string(),
|
|
2855
|
+
count: 42,
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
2859
|
+
|
|
2860
|
+
assert_eq!(envelope.operation, "test_operation");
|
|
2861
|
+
assert!(!envelope.id.is_empty());
|
|
2862
|
+
assert!(!envelope.timestamp.is_empty());
|
|
2863
|
+
assert_eq!(envelope.correlation_id, None);
|
|
2864
|
+
assert_eq!(envelope.error, None);
|
|
2865
|
+
|
|
2866
|
+
let extracted: TestPayload = envelope.extract_payload().unwrap();
|
|
2867
|
+
assert_eq!(extracted, payload);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
#[test]
|
|
2871
|
+
fn test_envelope_with_correlation_id() {
|
|
2872
|
+
let payload = TestPayload {
|
|
2873
|
+
message: "test".to_string(),
|
|
2874
|
+
count: 42,
|
|
2875
|
+
};
|
|
2876
|
+
|
|
2877
|
+
let correlation_id = "test-correlation-id".to_string();
|
|
2878
|
+
let envelope = MessageEnvelope::new_with_correlation_id(
|
|
2879
|
+
"test_operation",
|
|
2880
|
+
&payload,
|
|
2881
|
+
correlation_id.clone(),
|
|
2882
|
+
).unwrap();
|
|
2883
|
+
|
|
2884
|
+
assert_eq!(envelope.correlation_id, Some(correlation_id));
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
#[test]
|
|
2888
|
+
fn test_error_response() {
|
|
2889
|
+
let error_envelope = MessageEnvelope::error_response(
|
|
2890
|
+
"test_operation_response",
|
|
2891
|
+
"TEST_ERROR",
|
|
2892
|
+
"Test error message",
|
|
2893
|
+
Some("correlation-123".to_string()),
|
|
2894
|
+
);
|
|
2895
|
+
|
|
2896
|
+
assert!(error_envelope.is_error());
|
|
2897
|
+
assert_eq!(error_envelope.correlation_id, Some("correlation-123".to_string()));
|
|
2898
|
+
if let Some(error) = &error_envelope.error {
|
|
2899
|
+
assert_eq!(error.code, "TEST_ERROR");
|
|
2900
|
+
assert_eq!(error.message, "Test error message");
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
#[test]
|
|
2905
|
+
fn test_envelope_serialization() {
|
|
2906
|
+
let payload = TestPayload {
|
|
2907
|
+
message: "test".to_string(),
|
|
2908
|
+
count: 42,
|
|
2909
|
+
};
|
|
2910
|
+
|
|
2911
|
+
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
2912
|
+
let bytes = envelope.to_bytes().unwrap();
|
|
2913
|
+
let deserialized = MessageEnvelope::from_bytes(&bytes).unwrap();
|
|
2914
|
+
|
|
2915
|
+
assert_eq!(envelope.id, deserialized.id);
|
|
2916
|
+
assert_eq!(envelope.operation, deserialized.operation);
|
|
2917
|
+
assert_eq!(envelope.timestamp, deserialized.timestamp);
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
#[test]
|
|
2921
|
+
fn test_response_creation() {
|
|
2922
|
+
let request_payload = TestPayload {
|
|
2923
|
+
message: "request".to_string(),
|
|
2924
|
+
count: 1,
|
|
2925
|
+
};
|
|
2926
|
+
|
|
2927
|
+
let response_payload = TestPayload {
|
|
2928
|
+
message: "response".to_string(),
|
|
2929
|
+
count: 2,
|
|
2930
|
+
};
|
|
2931
|
+
|
|
2932
|
+
let request = MessageEnvelope::new_with_correlation_id(
|
|
2933
|
+
"test_request",
|
|
2934
|
+
&request_payload,
|
|
2935
|
+
"test-correlation".to_string(),
|
|
2936
|
+
).unwrap();
|
|
2937
|
+
|
|
2938
|
+
let response = request.create_response("test_response", &response_payload).unwrap();
|
|
2939
|
+
|
|
2940
|
+
assert_eq!(response.operation, "test_response");
|
|
2941
|
+
assert_eq!(response.correlation_id, request.correlation_id);
|
|
2942
|
+
|
|
2943
|
+
let extracted: TestPayload = response.extract_payload().unwrap();
|
|
2944
|
+
assert_eq!(extracted, response_payload);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
#[test]
|
|
2948
|
+
fn test_headers_and_auth() {
|
|
2949
|
+
let payload = TestPayload {
|
|
2950
|
+
message: "test".to_string(),
|
|
2951
|
+
count: 42,
|
|
2952
|
+
};
|
|
2953
|
+
|
|
2954
|
+
let mut auth_headers = HashMap::new();
|
|
2955
|
+
auth_headers.insert("Authorization".to_string(), "Bearer token123".to_string());
|
|
2956
|
+
|
|
2957
|
+
let envelope = MessageEnvelope::new("test_operation", &payload)
|
|
2958
|
+
.unwrap()
|
|
2959
|
+
.with_auth_headers(auth_headers)
|
|
2960
|
+
.with_header("Custom-Header".to_string(), "custom-value".to_string());
|
|
2961
|
+
|
|
2962
|
+
assert!(envelope.headers.is_some());
|
|
2963
|
+
let headers = envelope.headers.unwrap();
|
|
2964
|
+
assert_eq!(headers.get("Authorization"), Some(&"Bearer token123".to_string()));
|
|
2965
|
+
assert_eq!(headers.get("Custom-Header"), Some(&"custom-value".to_string()));
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
`;
|
|
2969
|
+
}
|
|
2970
|
+
;// ../common/src/models-ts.js
|
|
2971
|
+
/* eslint-disable no-unused-vars */
|
|
2972
|
+
|
|
2973
|
+
|
|
2974
|
+
/**
|
|
2975
|
+
* Generate TypeScript models from AsyncAPI specification
|
|
2976
|
+
* This helper extracts the common schema processing logic used by TypeScript templates
|
|
2977
|
+
*
|
|
2978
|
+
* @param {Object} asyncapi - AsyncAPI document
|
|
2979
|
+
* @param {Object} options - Generation options
|
|
2980
|
+
* @param {Function} options.toTypeScriptTypeName - Function to convert names to TypeScript type names
|
|
2981
|
+
* @param {Function} options.toTypeScriptIdentifier - Function to convert names to TypeScript identifiers
|
|
2982
|
+
* @param {boolean} options.includeMessageTypes - Whether to include message type constants
|
|
2983
|
+
* @returns {Object} Generated models data and functions
|
|
2984
|
+
*/
|
|
2985
|
+
function generateTypeScriptModels(asyncapi, options = {}) {
|
|
2986
|
+
const {
|
|
2987
|
+
toTypeScriptTypeName,
|
|
2988
|
+
toTypeScriptIdentifier,
|
|
2989
|
+
includeMessageTypes = true
|
|
2990
|
+
} = options;
|
|
2991
|
+
|
|
2992
|
+
// Helper functions for TypeScript identifier generation
|
|
2993
|
+
function defaultToTypeScriptIdentifier(str) {
|
|
2994
|
+
if (!str) return 'unknown';
|
|
2995
|
+
let identifier = str.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[0-9]/, '_$&').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
|
2996
|
+
if (/^[0-9]/.test(identifier)) {
|
|
2997
|
+
identifier = 'item_' + identifier;
|
|
2998
|
+
}
|
|
2999
|
+
if (!identifier) {
|
|
3000
|
+
identifier = 'unknown';
|
|
3001
|
+
}
|
|
3002
|
+
return identifier;
|
|
3003
|
+
}
|
|
3004
|
+
function defaultToTypeScriptTypeName(str) {
|
|
3005
|
+
if (!str) return 'Unknown';
|
|
3006
|
+
// Handle camelCase and PascalCase properly
|
|
3007
|
+
const identifier = str.replace(/[^a-zA-Z0-9]/g, '_').replace(/^[0-9]/, '_$&').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
|
3008
|
+
|
|
3009
|
+
// Split on underscores and camelCase boundaries
|
|
3010
|
+
const parts = identifier.split(/[_\s]+|(?=[A-Z])/);
|
|
3011
|
+
return parts.filter(part => part.length > 0).map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join('');
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// Use provided functions or defaults
|
|
3015
|
+
const toTSTypeName = toTypeScriptTypeName || defaultToTypeScriptTypeName;
|
|
3016
|
+
const toTSIdentifier = toTypeScriptIdentifier || defaultToTypeScriptIdentifier;
|
|
3017
|
+
|
|
3018
|
+
// Extract message schemas and build channel mapping
|
|
3019
|
+
const components = asyncapi.components();
|
|
3020
|
+
const messageSchemas = [];
|
|
3021
|
+
const componentSchemas = [];
|
|
3022
|
+
const messageToChannels = new Map();
|
|
3023
|
+
const generatedTypes = new Set();
|
|
3024
|
+
const schemaRegistry = new Map();
|
|
3025
|
+
|
|
3026
|
+
// Build schema registry from components.schemas
|
|
3027
|
+
// Try to access the raw AsyncAPI document
|
|
3028
|
+
let rawDoc = null;
|
|
3029
|
+
try {
|
|
3030
|
+
if (asyncapi.json && typeof asyncapi.json === 'function') {
|
|
3031
|
+
rawDoc = asyncapi.json();
|
|
3032
|
+
} else if (asyncapi._json) {
|
|
3033
|
+
rawDoc = asyncapi._json;
|
|
3034
|
+
}
|
|
3035
|
+
} catch (e) {
|
|
3036
|
+
// Ignore
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// Extract schemas from raw document if available
|
|
3040
|
+
if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
|
|
3041
|
+
Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
|
|
3042
|
+
if (name && typeof name === 'string' && schema && typeof schema === 'object') {
|
|
3043
|
+
schemaRegistry.set(name, schema);
|
|
3044
|
+
componentSchemas.push({
|
|
3045
|
+
name,
|
|
3046
|
+
typeName: toTSTypeName(name),
|
|
3047
|
+
schema: schema,
|
|
3048
|
+
description: schema.description
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Fallback: try the components.schemas() method
|
|
3055
|
+
if (componentSchemas.length === 0 && components && components.schemas) {
|
|
3056
|
+
try {
|
|
3057
|
+
const schemas = components.schemas();
|
|
3058
|
+
if (schemas) {
|
|
3059
|
+
// Try different ways to access schemas
|
|
3060
|
+
let schemaEntries = [];
|
|
3061
|
+
if (schemas instanceof Map) {
|
|
3062
|
+
schemaEntries = Array.from(schemas.entries());
|
|
3063
|
+
} else if (typeof schemas === 'object') {
|
|
3064
|
+
schemaEntries = Object.entries(schemas);
|
|
3065
|
+
} else if (schemas.all && typeof schemas.all === 'function') {
|
|
3066
|
+
// AsyncAPI parser might have an all() method
|
|
3067
|
+
const allSchemas = schemas.all();
|
|
3068
|
+
if (Array.isArray(allSchemas)) {
|
|
3069
|
+
schemaEntries = allSchemas.map(schema => {
|
|
3070
|
+
const name = schema.uid ? schema.uid() : schema.id ? schema.id() : null;
|
|
3071
|
+
return [name, schema];
|
|
3072
|
+
}).filter(([name]) => name);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
schemaEntries.forEach(([name, schema]) => {
|
|
3076
|
+
// Skip internal AsyncAPI parser objects and numeric keys
|
|
3077
|
+
if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
let schemaData = null;
|
|
3081
|
+
let description = null;
|
|
3082
|
+
try {
|
|
3083
|
+
// Handle different schema object types
|
|
3084
|
+
if (schema && typeof schema.json === 'function') {
|
|
3085
|
+
schemaData = schema.json();
|
|
3086
|
+
} else if (schema && typeof schema === 'object') {
|
|
3087
|
+
schemaData = schema;
|
|
3088
|
+
}
|
|
3089
|
+
if (schema && typeof schema.description === 'function') {
|
|
3090
|
+
description = schema.description();
|
|
3091
|
+
} else if (schema && schema.description) {
|
|
3092
|
+
description = schema.description;
|
|
3093
|
+
}
|
|
3094
|
+
} catch (e) {
|
|
3095
|
+
// Ignore schema extraction errors
|
|
3096
|
+
console.warn(`Failed to extract schema for ${name}:`, e.message);
|
|
3097
|
+
}
|
|
3098
|
+
if (schemaData && typeof name === 'string' && name.length > 0) {
|
|
3099
|
+
schemaRegistry.set(name, schemaData);
|
|
3100
|
+
componentSchemas.push({
|
|
3101
|
+
name,
|
|
3102
|
+
typeName: toTSTypeName(name),
|
|
3103
|
+
schema: schemaData,
|
|
3104
|
+
description
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
} catch (e) {
|
|
3110
|
+
console.warn('Failed to extract component schemas:', e.message);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// First, build channel to message mapping
|
|
3115
|
+
if (asyncapi.channels) {
|
|
3116
|
+
const channels = asyncapi.channels();
|
|
3117
|
+
if (channels) {
|
|
3118
|
+
Object.entries(channels).forEach(([channelName, channel]) => {
|
|
3119
|
+
try {
|
|
3120
|
+
// Handle AsyncAPI 3.x format
|
|
3121
|
+
if (channel.messages) {
|
|
3122
|
+
const messages = channel.messages();
|
|
3123
|
+
if (messages) {
|
|
3124
|
+
Object.entries(messages).forEach(([msgKey, message]) => {
|
|
3125
|
+
if (message) {
|
|
3126
|
+
let messageName = null;
|
|
3127
|
+
if (message.$ref) {
|
|
3128
|
+
messageName = message.$ref.split('/').pop();
|
|
3129
|
+
} else if (message.name) {
|
|
3130
|
+
messageName = typeof message.name === 'function' ? message.name() : message.name;
|
|
3131
|
+
}
|
|
3132
|
+
if (messageName) {
|
|
3133
|
+
if (!messageToChannels.has(messageName)) {
|
|
3134
|
+
messageToChannels.set(messageName, []);
|
|
3135
|
+
}
|
|
3136
|
+
messageToChannels.get(messageName).push(channelName);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
// Ignore channel processing errors
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// Extract messages from components
|
|
3150
|
+
if (components && components.messages) {
|
|
3151
|
+
const messages = components.messages();
|
|
3152
|
+
if (messages) {
|
|
3153
|
+
Object.entries(messages).forEach(([name, message]) => {
|
|
3154
|
+
// Skip internal AsyncAPI parser objects
|
|
3155
|
+
if (name === 'collections' || name === '_meta' || name.startsWith('_')) {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
let payload = null;
|
|
3159
|
+
let description = null;
|
|
3160
|
+
let title = null;
|
|
3161
|
+
let messageName = name;
|
|
3162
|
+
try {
|
|
3163
|
+
if (message.payload && typeof message.payload === 'function') {
|
|
3164
|
+
const payloadSchema = message.payload();
|
|
3165
|
+
payload = payloadSchema && payloadSchema.json ? payloadSchema.json() : payloadSchema;
|
|
3166
|
+
}
|
|
3167
|
+
description = message.description && typeof message.description === 'function' ? message.description() : message.description;
|
|
3168
|
+
title = message.title && typeof message.title === 'function' ? message.title() : message.title;
|
|
3169
|
+
|
|
3170
|
+
// Try to get the actual message name
|
|
3171
|
+
if (message.name && typeof message.name === 'function') {
|
|
3172
|
+
messageName = message.name();
|
|
3173
|
+
} else if (message.name) {
|
|
3174
|
+
messageName = message.name;
|
|
3175
|
+
}
|
|
3176
|
+
} catch (e) {
|
|
3177
|
+
// Ignore payload extraction errors
|
|
3178
|
+
}
|
|
3179
|
+
const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
|
|
3180
|
+
messageSchemas.push({
|
|
3181
|
+
name: messageName,
|
|
3182
|
+
typeName: toTSTypeName(messageName),
|
|
3183
|
+
payload,
|
|
3184
|
+
description: description || title,
|
|
3185
|
+
channels
|
|
3186
|
+
});
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// Helper function to convert JSON schema to TypeScript type
|
|
3192
|
+
function jsonSchemaToTypeScriptType(schema, fieldName = '') {
|
|
3193
|
+
if (!schema) return 'any';
|
|
3194
|
+
|
|
3195
|
+
// Handle $ref - resolve from schema registry
|
|
3196
|
+
if (schema.$ref) {
|
|
3197
|
+
const refName = schema.$ref.split('/').pop();
|
|
3198
|
+
// Always return the type name for $ref, since we generate all component schemas
|
|
3199
|
+
const typeName = toTSTypeName(refName);
|
|
3200
|
+
return typeName;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
|
|
3204
|
+
if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
|
|
3205
|
+
const schemaId = schema['x-parser-schema-id'];
|
|
3206
|
+
// Check if this matches a known component schema
|
|
3207
|
+
if (schemaRegistry.has(schemaId)) {
|
|
3208
|
+
const typeName = toTSTypeName(schemaId);
|
|
3209
|
+
return typeName;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (!schema.type) {
|
|
3213
|
+
// If no type specified, check for properties (object) or items (array)
|
|
3214
|
+
if (schema.properties) {
|
|
3215
|
+
schema.type = 'object';
|
|
3216
|
+
} else if (schema.items) {
|
|
3217
|
+
schema.type = 'array';
|
|
3218
|
+
} else {
|
|
3219
|
+
return 'any';
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
switch (schema.type) {
|
|
3223
|
+
case 'string':
|
|
3224
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
3225
|
+
return schema.enum.map(val => `'${val}'`).join(' | ');
|
|
3226
|
+
}
|
|
3227
|
+
return 'string';
|
|
3228
|
+
case 'integer':
|
|
3229
|
+
case 'number':
|
|
3230
|
+
return 'number';
|
|
3231
|
+
case 'boolean':
|
|
3232
|
+
return 'boolean';
|
|
3233
|
+
case 'array':
|
|
3234
|
+
{
|
|
3235
|
+
if (schema.items) {
|
|
3236
|
+
const itemType = jsonSchemaToTypeScriptType(schema.items, fieldName);
|
|
3237
|
+
return `${itemType}[]`;
|
|
3238
|
+
}
|
|
3239
|
+
return 'any[]';
|
|
3240
|
+
}
|
|
3241
|
+
case 'object':
|
|
3242
|
+
// For objects with properties, we should generate inline types or check if it's a known schema
|
|
3243
|
+
if (schema.properties) {
|
|
3244
|
+
// This is a complex object - for now return Record<string, any>
|
|
3245
|
+
// In a more sophisticated implementation, we could generate inline types
|
|
3246
|
+
return 'Record<string, any>';
|
|
3247
|
+
}
|
|
3248
|
+
return 'Record<string, any>';
|
|
3249
|
+
default:
|
|
3250
|
+
return 'any';
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// Generate message interfaces
|
|
3255
|
+
function generateMessageInterface(schema, messageName) {
|
|
3256
|
+
if (!schema || !schema.properties) {
|
|
3257
|
+
return ' [key: string]: any;';
|
|
3258
|
+
}
|
|
3259
|
+
const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
|
|
3260
|
+
const tsType = jsonSchemaToTypeScriptType(fieldSchema, fieldName);
|
|
3261
|
+
const optional = !schema.required || !schema.required.includes(fieldName);
|
|
3262
|
+
const optionalMarker = optional ? '?' : '';
|
|
3263
|
+
let fieldDoc = '';
|
|
3264
|
+
if (fieldSchema.description) {
|
|
3265
|
+
fieldDoc = ` /** ${fieldSchema.description} */\n`;
|
|
3266
|
+
}
|
|
3267
|
+
return `${fieldDoc} ${fieldName}${optionalMarker}: ${tsType};`;
|
|
3268
|
+
}).join('\n');
|
|
3269
|
+
return fields;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// Return the processed data and generation functions
|
|
3273
|
+
return {
|
|
3274
|
+
messageSchemas,
|
|
3275
|
+
componentSchemas,
|
|
3276
|
+
messageToChannels,
|
|
3277
|
+
generatedTypes,
|
|
3278
|
+
schemaRegistry,
|
|
3279
|
+
// Generation functions
|
|
3280
|
+
generateMessageInterface,
|
|
3281
|
+
jsonSchemaToTypeScriptType,
|
|
3282
|
+
// Generate interfaces for component schemas
|
|
3283
|
+
generateComponentSchemas() {
|
|
3284
|
+
let content = '';
|
|
3285
|
+
componentSchemas.forEach(schema => {
|
|
3286
|
+
const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} */\n`;
|
|
3287
|
+
|
|
3288
|
+
// Check if this is a standalone enum schema
|
|
3289
|
+
if (schema.schema.type === 'string' && schema.schema.enum && Array.isArray(schema.schema.enum)) {
|
|
3290
|
+
// Generate union type for enum
|
|
3291
|
+
const enumValues = schema.schema.enum.map(val => `'${val}'`).join(' | ');
|
|
3292
|
+
content += `${doc}export type ${schema.typeName} = ${enumValues};\n\n`;
|
|
3293
|
+
} else {
|
|
3294
|
+
// Generate interface for object schema
|
|
3295
|
+
content += `${doc}export interface ${schema.typeName} {\n`;
|
|
3296
|
+
content += generateMessageInterface(schema.schema, schema.typeName);
|
|
3297
|
+
content += '\n}\n\n';
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// Track generated types to avoid duplicates
|
|
3301
|
+
generatedTypes.add(schema.typeName);
|
|
3302
|
+
});
|
|
3303
|
+
return content;
|
|
3304
|
+
},
|
|
3305
|
+
// Generate interfaces for each message
|
|
3306
|
+
generateMessageSchemas() {
|
|
3307
|
+
let content = '';
|
|
3308
|
+
messageSchemas.forEach(schema => {
|
|
3309
|
+
const interfaceName = `${schema.typeName}Payload`;
|
|
3310
|
+
|
|
3311
|
+
// Check if this is a duplicate of a component schema
|
|
3312
|
+
// For message payloads that match component schema names, skip the payload version
|
|
3313
|
+
if (generatedTypes.has(schema.typeName) || generatedTypes.has(interfaceName)) {
|
|
3314
|
+
// Skip generating the payload version if we already have the component schema
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} message payload */\n`;
|
|
3318
|
+
content += `${doc}export interface ${interfaceName} {\n`;
|
|
3319
|
+
content += generateMessageInterface(schema.payload, schema.typeName);
|
|
3320
|
+
content += '\n}\n\n';
|
|
3321
|
+
generatedTypes.add(interfaceName);
|
|
3322
|
+
});
|
|
3323
|
+
return content;
|
|
3324
|
+
},
|
|
3325
|
+
// Generate message type constants and unions
|
|
3326
|
+
generateMessageTypes() {
|
|
3327
|
+
if (!includeMessageTypes || messageSchemas.length === 0) {
|
|
3328
|
+
return '';
|
|
3329
|
+
}
|
|
3330
|
+
let content = '';
|
|
3331
|
+
|
|
3332
|
+
// Generate a union type for all message payloads
|
|
3333
|
+
const payloadTypes = messageSchemas.map(schema => `${schema.typeName}Payload`).join(' | ');
|
|
3334
|
+
content += '/** Union type for all message payloads */\n';
|
|
3335
|
+
content += `export type MessagePayload = ${payloadTypes};\n\n`;
|
|
3336
|
+
|
|
3337
|
+
// Generate message type constants
|
|
3338
|
+
content += '/** Message type constants */\n';
|
|
3339
|
+
content += 'export const MessageTypes = {\n';
|
|
3340
|
+
messageSchemas.forEach(schema => {
|
|
3341
|
+
content += ` ${schema.typeName.toUpperCase()}: '${schema.name}',\n`;
|
|
3342
|
+
});
|
|
3343
|
+
content += '} as const;\n\n';
|
|
3344
|
+
content += '/** Message type union */\n';
|
|
3345
|
+
content += 'export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n';
|
|
3346
|
+
return content;
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
1886
3350
|
;// ../common/src/index.js
|
|
1887
3351
|
/**
|
|
1888
3352
|
* AsyncAPI Common Template Utilities
|
|
@@ -1909,6 +3373,11 @@ function generateOperationHandlerName(operation, suffix = 'Handler') {
|
|
|
1909
3373
|
// Operation utilities
|
|
1910
3374
|
|
|
1911
3375
|
|
|
3376
|
+
// Model generation utilities
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
|
|
1912
3381
|
// Re-export all utilities
|
|
1913
3382
|
|
|
1914
3383
|
|
|
@@ -1998,7 +3467,12 @@ function getAllUtilities() {
|
|
|
1998
3467
|
operationHasTraits: operationHasTraits,
|
|
1999
3468
|
getOperationTags: getOperationTags,
|
|
2000
3469
|
generateOperationHandlerName: generateOperationHandlerName
|
|
3470
|
+
},
|
|
3471
|
+
models: {
|
|
3472
|
+
generateRustModels: generateRustModels,
|
|
3473
|
+
generateMessageEnvelope: generateMessageEnvelope,
|
|
3474
|
+
generateTypeScriptModels: generateTypeScriptModels
|
|
2001
3475
|
}
|
|
2002
3476
|
};
|
|
2003
3477
|
}
|
|
2004
|
-
export { VERSION, analyzeChannelServerMappings, analyzeOperationSecurity, channelHasParameters, extractAllOperations, extractAsyncApiInfo, extractChannelParameters, extractChannelVariables, extractOperationSecurityMap, extractServerNameFromRef, formatGenerationDate, generateChannelFormatting, generateChannelParameterArgs, generateEslintConfig, generateFileHeader, generateOperationHandlerName, generatePackageJson, generateReadmeContent, generateTypeScriptChannelFormatting, generateTypeScriptChannelParameterArgs, generateTypeScriptConfig, getAllSecuritySchemes, getAllUtilities, getChannelAddress, getChannelParameters, getDefaultPort, getMessageContentType, getMessageRustTypeName, getMessageTypeName, getMessageTypeScriptTypeName, getNatsSubject, getOperationAction, getOperationChannel, getOperationDescription, getOperationMessages, getOperationName, getOperationRustFunctionName, getOperationSummary, getOperationTags, getOperationTraits, getOperationTypeScriptMethodName, getPayloadRustTypeName, getPayloadTypeScriptTypeName, getSecuritySchemeLocation, getSecuritySchemeName, getSecuritySchemeType, groupOperationsByAction, hasSecuritySchemes, isChannelAllowedOnServer, isDynamicChannel, isOperationReceive, isOperationSend, isSecuritySchemeType, isTemplateVariable, messageHasPayload, operationHasSecurity, operationHasTraits, operationRequiresAuth, resolveChannelAddress, resolveTemplateParameters, toCamelCase, toKebabCase, toPascalCase, toRustEnumVariant, toRustEnumVariantWithSerde, toRustFieldName, toRustIdentifier, toRustTypeName, toSnakeCase, validateChannelServerReferences, validateTemplateParameters };
|
|
3478
|
+
export { VERSION, analyzeChannelServerMappings, analyzeOperationSecurity, channelHasParameters, extractAllOperations, extractAsyncApiInfo, extractChannelParameters, extractChannelVariables, extractOperationSecurityMap, extractServerNameFromRef, formatGenerationDate, generateChannelFormatting, generateChannelParameterArgs, generateEslintConfig, generateFileHeader, generateMessageEnvelope, generateOperationHandlerName, generatePackageJson, generateReadmeContent, generateRustModels, generateTypeScriptChannelFormatting, generateTypeScriptChannelParameterArgs, generateTypeScriptConfig, generateTypeScriptModels, getAllSecuritySchemes, getAllUtilities, getChannelAddress, getChannelParameters, getDefaultPort, getMessageContentType, getMessageRustTypeName, getMessageTypeName, getMessageTypeScriptTypeName, getNatsSubject, getOperationAction, getOperationChannel, getOperationDescription, getOperationMessages, getOperationName, getOperationRustFunctionName, getOperationSummary, getOperationTags, getOperationTraits, getOperationTypeScriptMethodName, getPayloadRustTypeName, getPayloadTypeScriptTypeName, getSecuritySchemeLocation, getSecuritySchemeName, getSecuritySchemeType, groupOperationsByAction, hasSecuritySchemes, isChannelAllowedOnServer, isDynamicChannel, isOperationReceive, isOperationSend, isSecuritySchemeType, isTemplateVariable, messageHasPayload, operationHasSecurity, operationHasTraits, operationRequiresAuth, resolveChannelAddress, resolveTemplateParameters, toCamelCase, toKebabCase, toPascalCase, toRustEnumVariant, toRustEnumVariantWithSerde, toRustFieldName, toRustIdentifier, toRustTypeName, toSnakeCase, validateChannelServerReferences, validateTemplateParameters };
|