@rocketmq/protobuf 0.1.0 → 0.1.2
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/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/README.md +26 -0
- package/dist/index.cjs +99 -4
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24 -6
- package/dist/index.d.ts +24 -6
- package/dist/index.js +100 -4
- package/dist/index.js.map +1 -0
- package/package.json +2 -2
- package/src/fields-to-proto.test.ts +73 -0
- package/src/fields-to-proto.ts +61 -0
- package/src/generator.test.ts +132 -30
- package/src/generator.ts +68 -25
- package/src/index.ts +1 -0
- package/tsup.config.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @rocketmq/protobuf@0.1.
|
|
3
|
+
> @rocketmq/protobuf@0.1.2 build /home/edilson/learnspace/rocketmq-broker/rocketmq.js/packages/protobuf
|
|
4
4
|
> tsup
|
|
5
5
|
|
|
6
6
|
CLI Building entry: src/index.ts
|
|
7
7
|
CLI Using tsconfig: tsconfig.json
|
|
8
8
|
CLI tsup v8.5.1
|
|
9
|
-
CLI Using tsup config: /home/edilson/learnspace/rocketmq-broker/
|
|
9
|
+
CLI Using tsup config: /home/edilson/learnspace/rocketmq-broker/rocketmq.js/packages/protobuf/tsup.config.ts
|
|
10
10
|
CLI Target: es2022
|
|
11
11
|
CLI Cleaning output folder
|
|
12
12
|
ESM Build start
|
|
13
13
|
CJS Build start
|
|
14
|
-
CJS dist/index.cjs
|
|
15
|
-
CJS
|
|
16
|
-
|
|
17
|
-
ESM
|
|
14
|
+
CJS dist/index.cjs 4.97 KB
|
|
15
|
+
CJS dist/index.cjs.map 8.75 KB
|
|
16
|
+
CJS ⚡️ Build success in 17ms
|
|
17
|
+
ESM dist/index.js 3.88 KB
|
|
18
|
+
ESM dist/index.js.map 8.63 KB
|
|
19
|
+
ESM ⚡️ Build success in 17ms
|
|
18
20
|
DTS Build start
|
|
19
|
-
DTS ⚡️ Build success in
|
|
20
|
-
DTS dist/index.d.ts
|
|
21
|
-
DTS dist/index.d.cts
|
|
21
|
+
DTS ⚡️ Build success in 483ms
|
|
22
|
+
DTS dist/index.d.ts 1.24 KB
|
|
23
|
+
DTS dist/index.d.cts 1.24 KB
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @rocketmq/protobuf
|
|
2
|
+
|
|
3
|
+
Protobuf generation utilities for RocketMQ.
|
|
4
|
+
|
|
5
|
+
This package translates TypeScript schema metadata (gathered via `@rocketmq/schema` decorators) into dynamic Protobuf v3 definitions. These definitions are passed as AMQP headers to the broker, enabling native, broker-side schema validation without requiring a separate schema registry service.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @rocketmq/protobuf
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
This is an internal package used by `@rocketmq/core`.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { toProto } from '@rocketmq/protobuf';
|
|
19
|
+
|
|
20
|
+
// Translates a decorated class into a proto3 definition string
|
|
21
|
+
const protoDefinition = toProto(MyDecoratedClass);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
Apache 2.0
|
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
7
|
var __export = (target, all) => {
|
|
7
8
|
for (var name in all)
|
|
8
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -20,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
21
|
// src/index.ts
|
|
21
22
|
var index_exports = {};
|
|
22
23
|
__export(index_exports, {
|
|
24
|
+
fieldsToProto: () => fieldsToProto,
|
|
23
25
|
toProto: () => toProto
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -33,16 +35,109 @@ function ensureFieldsRegistered(schema) {
|
|
|
33
35
|
} catch {
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
__name(ensureFieldsRegistered, "ensureFieldsRegistered");
|
|
39
|
+
function collectDependentSchemas(schema, seen = /* @__PURE__ */ new Set(), orderedList = []) {
|
|
40
|
+
if (seen.has(schema)) return orderedList;
|
|
41
|
+
seen.add(schema);
|
|
42
|
+
const fields = import_schema.defaultRegistry.getFields(schema);
|
|
43
|
+
for (const field of fields) {
|
|
44
|
+
if (field.reflectedType && import_schema.defaultRegistry.isSchema(field.reflectedType)) {
|
|
45
|
+
collectDependentSchemas(field.reflectedType, seen, orderedList);
|
|
46
|
+
}
|
|
47
|
+
if (field.type) {
|
|
48
|
+
const depSchema = import_schema.defaultRegistry.getSchemaByName(field.type);
|
|
49
|
+
if (depSchema) {
|
|
50
|
+
collectDependentSchemas(depSchema, seen, orderedList);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
orderedList.push(schema);
|
|
55
|
+
return orderedList;
|
|
56
|
+
}
|
|
57
|
+
__name(collectDependentSchemas, "collectDependentSchemas");
|
|
58
|
+
function needsTimestamp(schemas) {
|
|
59
|
+
return schemas.some((s) => import_schema.defaultRegistry.getFields(s).some((f) => f.protoType === "google.protobuf.Timestamp"));
|
|
60
|
+
}
|
|
61
|
+
__name(needsTimestamp, "needsTimestamp");
|
|
62
|
+
function buildFieldLine(field) {
|
|
63
|
+
const modifier = field.repeated ? "repeated " : field.optional ? "optional " : "";
|
|
64
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
65
|
+
}
|
|
66
|
+
__name(buildFieldLine, "buildFieldLine");
|
|
67
|
+
function buildMessageBlock(schema, lines) {
|
|
38
68
|
const fields = import_schema.defaultRegistry.getFields(schema);
|
|
39
69
|
if (fields.length === 0) {
|
|
40
70
|
throw new Error(`Schema '${schema.name}' has no @Field() decorators \u2014 cannot generate proto`);
|
|
41
71
|
}
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
lines.push(`message ${schema.name} {`);
|
|
73
|
+
for (const field of fields) {
|
|
74
|
+
if (field.comment) {
|
|
75
|
+
lines.push(` // ${field.comment}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(` ${buildFieldLine(field)}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("}", "");
|
|
80
|
+
}
|
|
81
|
+
__name(buildMessageBlock, "buildMessageBlock");
|
|
82
|
+
function toProto(schema) {
|
|
83
|
+
ensureFieldsRegistered(schema);
|
|
84
|
+
const orderedList = collectDependentSchemas(schema);
|
|
85
|
+
if (orderedList.length === 0 || import_schema.defaultRegistry.getFields(schema).length === 0) {
|
|
86
|
+
throw new Error(`Schema '${schema.name}' has no @Field() decorators \u2014 cannot generate proto`);
|
|
87
|
+
}
|
|
88
|
+
const lines = [
|
|
89
|
+
'syntax = "proto3";',
|
|
90
|
+
""
|
|
91
|
+
];
|
|
92
|
+
if (needsTimestamp(orderedList)) {
|
|
93
|
+
lines.push('import "google/protobuf/timestamp.proto";', "");
|
|
94
|
+
}
|
|
95
|
+
for (const s of orderedList) {
|
|
96
|
+
buildMessageBlock(s, lines);
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n").trimEnd();
|
|
99
|
+
}
|
|
100
|
+
__name(toProto, "toProto");
|
|
101
|
+
|
|
102
|
+
// src/fields-to-proto.ts
|
|
103
|
+
function buildFieldLine2(field) {
|
|
104
|
+
const modifier = field.repeated ? "repeated " : field.optional ? "optional " : "";
|
|
105
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
106
|
+
}
|
|
107
|
+
__name(buildFieldLine2, "buildFieldLine");
|
|
108
|
+
function needsTimestampImport(fields) {
|
|
109
|
+
return fields.some((f) => f.protoType === "google.protobuf.Timestamp");
|
|
110
|
+
}
|
|
111
|
+
__name(needsTimestampImport, "needsTimestampImport");
|
|
112
|
+
function appendMessageBlock(messageName, fields, lines) {
|
|
113
|
+
if (fields.length === 0) {
|
|
114
|
+
throw new Error(`Schema '${messageName}' has no fields \u2014 cannot generate proto. Got 0 fields.`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(`message ${messageName} {`);
|
|
117
|
+
for (const field of fields) {
|
|
118
|
+
if (field.comment) {
|
|
119
|
+
lines.push(` // ${field.comment}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(` ${buildFieldLine2(field)}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("}", "");
|
|
124
|
+
}
|
|
125
|
+
__name(appendMessageBlock, "appendMessageBlock");
|
|
126
|
+
function fieldsToProto(messageName, fields) {
|
|
127
|
+
const lines = [
|
|
128
|
+
'syntax = "proto3";',
|
|
129
|
+
""
|
|
130
|
+
];
|
|
131
|
+
if (needsTimestampImport(fields)) {
|
|
132
|
+
lines.push('import "google/protobuf/timestamp.proto";', "");
|
|
133
|
+
}
|
|
134
|
+
appendMessageBlock(messageName, fields, lines);
|
|
135
|
+
return lines.join("\n").trimEnd();
|
|
44
136
|
}
|
|
137
|
+
__name(fieldsToProto, "fieldsToProto");
|
|
45
138
|
// Annotate the CommonJS export names for ESM import in node:
|
|
46
139
|
0 && (module.exports = {
|
|
140
|
+
fieldsToProto,
|
|
47
141
|
toProto
|
|
48
142
|
});
|
|
143
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/generator.ts","../src/fields-to-proto.ts"],"sourcesContent":["export { toProto } from './generator.js';\nexport { fieldsToProto } from './fields-to-proto.js';\n","/**\n * Generates a proto3 schema string from a decorated class.\n *\n * Reads field metadata from the default SchemaRegistry and produces\n * the exact format the Rust broker expects in the `x-schema` queue argument.\n */\n\nimport { defaultRegistry, type FieldMeta, type Constructor } from '@rocketmq/schema';\n\n/** Forces field registration by instantiating the class once. */\nfunction ensureFieldsRegistered(schema: Constructor): void {\n if (defaultRegistry.getFields(schema).length > 0) return;\n try {\n new schema();\n } catch {\n // Constructor may throw — fields are registered via addInitializer\n void 0;\n }\n}\n\n/** Recursively collects all schema constructors referenced by the class. */\nfunction collectDependentSchemas(\n schema: Constructor,\n seen = new Set<Constructor>(),\n orderedList: Constructor[] = [],\n): Constructor[] {\n if (seen.has(schema)) return orderedList;\n seen.add(schema);\n\n const fields = defaultRegistry.getFields(schema);\n for (const field of fields) {\n if (field.reflectedType && defaultRegistry.isSchema(field.reflectedType)) {\n collectDependentSchemas(field.reflectedType, seen, orderedList);\n }\n if (field.type) {\n const depSchema = defaultRegistry.getSchemaByName(field.type);\n if (depSchema) {\n collectDependentSchemas(depSchema, seen, orderedList);\n }\n }\n }\n orderedList.push(schema);\n return orderedList;\n}\n\n/** Determines if any schema in the list uses a Timestamp type. */\nfunction needsTimestamp(schemas: Constructor[]): boolean {\n return schemas.some((s) =>\n defaultRegistry.getFields(s).some((f) => f.protoType === 'google.protobuf.Timestamp'),\n );\n}\n\n/** Builds the protobuf field line using the pre-resolved protoType. */\nfunction buildFieldLine(field: FieldMeta): string {\n const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';\n return `${modifier}${field.protoType} ${field.name} = ${field.number};`;\n}\n\n/** Formats a schema class as a protobuf message block. */\nfunction buildMessageBlock(schema: Constructor, lines: string[]): void {\n const fields = defaultRegistry.getFields(schema);\n if (fields.length === 0) {\n throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);\n }\n lines.push(`message ${schema.name} {`);\n for (const field of fields) {\n if (field.comment) {\n lines.push(` // ${field.comment}`);\n }\n lines.push(` ${buildFieldLine(field)}`);\n }\n lines.push('}', '');\n}\n\n/** Converts a decorated schema class to a proto3 definition string. */\nexport function toProto(schema: Constructor): string {\n ensureFieldsRegistered(schema);\n const orderedList = collectDependentSchemas(schema);\n if (orderedList.length === 0 || defaultRegistry.getFields(schema).length === 0) {\n throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);\n }\n const lines: string[] = ['syntax = \"proto3\";', ''];\n if (needsTimestamp(orderedList)) {\n lines.push('import \"google/protobuf/timestamp.proto\";', '');\n }\n for (const s of orderedList) {\n buildMessageBlock(s, lines);\n }\n return lines.join('\\n').trimEnd();\n}\n","/**\n * Generates proto3 from raw FieldMeta arrays — no class/decorator dependency.\n *\n * This is the shared codegen path used by both the decorator-based `toProto()`\n * and the Zod-based `zodToProto()` pipelines. Factoring it here avoids\n * duplicating proto string assembly logic.\n *\n * Usage:\n * fieldsToProto('Order', [{ name: 'id', protoType: 'string', number: 1 }]);\n */\n\nimport type { FieldMeta } from '@rocketmq/schema';\n\n/** Builds a single proto3 field line from resolved metadata. */\nfunction buildFieldLine(field: FieldMeta): string {\n const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';\n return `${modifier}${field.protoType} ${field.name} = ${field.number};`;\n}\n\n/**\n * Checks whether any field in the list needs the Timestamp import.\n *\n * Extracted so callers don't need to know the magic string.\n */\nfunction needsTimestampImport(fields: FieldMeta[]): boolean {\n return fields.some((f) => f.protoType === 'google.protobuf.Timestamp');\n}\n\n/** Appends one protobuf message block to the output lines array. */\nfunction appendMessageBlock(messageName: string, fields: FieldMeta[], lines: string[]): void {\n if (fields.length === 0) {\n throw new Error(`Schema '${messageName}' has no fields — cannot generate proto. Got 0 fields.`);\n }\n lines.push(`message ${messageName} {`);\n for (const field of fields) {\n if (field.comment) {\n lines.push(` // ${field.comment}`);\n }\n lines.push(` ${buildFieldLine(field)}`);\n }\n lines.push('}', '');\n}\n\n/**\n * Converts a message name + field metadata list into a proto3 definition.\n *\n * This is the low-level entrypoint — no class introspection, no registry\n * lookups. Works with any source that can produce `FieldMeta[]`.\n *\n * Usage:\n * const proto = fieldsToProto('Notification', fields);\n * // => 'syntax = \"proto3\";\\n\\nmessage Notification { ... }'\n */\nexport function fieldsToProto(messageName: string, fields: FieldMeta[]): string {\n const lines: string[] = ['syntax = \"proto3\";', ''];\n if (needsTimestampImport(fields)) {\n lines.push('import \"google/protobuf/timestamp.proto\";', '');\n }\n appendMessageBlock(messageName, fields, lines);\n return lines.join('\\n').trimEnd();\n}\n"],"mappings":"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","names":["ensureFieldsRegistered","schema","defaultRegistry","getFields","length","collectDependentSchemas","seen","Set","orderedList","has","add","fields","field","reflectedType","isSchema","type","depSchema","getSchemaByName","push","needsTimestamp","schemas","some","s","f","protoType","buildFieldLine","modifier","repeated","optional","name","number","buildMessageBlock","lines","Error","comment","toProto","join","trimEnd","buildFieldLine","field","modifier","repeated","optional","protoType","name","number","needsTimestampImport","fields","some","f","appendMessageBlock","messageName","lines","length","Error","push","comment","fieldsToProto","join","trimEnd"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
import { Constructor, FieldMeta } from '@rocketmq/schema';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generates a proto3 schema string from a decorated class.
|
|
3
5
|
*
|
|
4
6
|
* Reads field metadata from the default SchemaRegistry and produces
|
|
5
7
|
* the exact format the Rust broker expects in the `x-schema` queue argument.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Converts a decorated schema class to a proto3 definition string. */
|
|
11
|
+
declare function toProto(schema: Constructor): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates proto3 from raw FieldMeta arrays — no class/decorator dependency.
|
|
15
|
+
*
|
|
16
|
+
* This is the shared codegen path used by both the decorator-based `toProto()`
|
|
17
|
+
* and the Zod-based `zodToProto()` pipelines. Factoring it here avoids
|
|
18
|
+
* duplicating proto string assembly logic.
|
|
6
19
|
*
|
|
7
20
|
* Usage:
|
|
8
|
-
*
|
|
9
|
-
* // => 'syntax = "proto3"; message Notification { string id = 1; int64 timestamp = 2; }'
|
|
21
|
+
* fieldsToProto('Order', [{ name: 'id', protoType: 'string', number: 1 }]);
|
|
10
22
|
*/
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
|
-
* Converts a
|
|
25
|
+
* Converts a message name + field metadata list into a proto3 definition.
|
|
13
26
|
*
|
|
14
|
-
*
|
|
27
|
+
* This is the low-level entrypoint — no class introspection, no registry
|
|
28
|
+
* lookups. Works with any source that can produce `FieldMeta[]`.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* const proto = fieldsToProto('Notification', fields);
|
|
32
|
+
* // => 'syntax = "proto3";\n\nmessage Notification { ... }'
|
|
15
33
|
*/
|
|
16
|
-
declare function
|
|
34
|
+
declare function fieldsToProto(messageName: string, fields: FieldMeta[]): string;
|
|
17
35
|
|
|
18
|
-
export { toProto };
|
|
36
|
+
export { fieldsToProto, toProto };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
import { Constructor, FieldMeta } from '@rocketmq/schema';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generates a proto3 schema string from a decorated class.
|
|
3
5
|
*
|
|
4
6
|
* Reads field metadata from the default SchemaRegistry and produces
|
|
5
7
|
* the exact format the Rust broker expects in the `x-schema` queue argument.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Converts a decorated schema class to a proto3 definition string. */
|
|
11
|
+
declare function toProto(schema: Constructor): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates proto3 from raw FieldMeta arrays — no class/decorator dependency.
|
|
15
|
+
*
|
|
16
|
+
* This is the shared codegen path used by both the decorator-based `toProto()`
|
|
17
|
+
* and the Zod-based `zodToProto()` pipelines. Factoring it here avoids
|
|
18
|
+
* duplicating proto string assembly logic.
|
|
6
19
|
*
|
|
7
20
|
* Usage:
|
|
8
|
-
*
|
|
9
|
-
* // => 'syntax = "proto3"; message Notification { string id = 1; int64 timestamp = 2; }'
|
|
21
|
+
* fieldsToProto('Order', [{ name: 'id', protoType: 'string', number: 1 }]);
|
|
10
22
|
*/
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
|
-
* Converts a
|
|
25
|
+
* Converts a message name + field metadata list into a proto3 definition.
|
|
13
26
|
*
|
|
14
|
-
*
|
|
27
|
+
* This is the low-level entrypoint — no class introspection, no registry
|
|
28
|
+
* lookups. Works with any source that can produce `FieldMeta[]`.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* const proto = fieldsToProto('Notification', fields);
|
|
32
|
+
* // => 'syntax = "proto3";\n\nmessage Notification { ... }'
|
|
15
33
|
*/
|
|
16
|
-
declare function
|
|
34
|
+
declare function fieldsToProto(messageName: string, fields: FieldMeta[]): string;
|
|
17
35
|
|
|
18
|
-
export { toProto };
|
|
36
|
+
export { fieldsToProto, toProto };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
1
4
|
// src/generator.ts
|
|
2
5
|
import { defaultRegistry } from "@rocketmq/schema";
|
|
3
6
|
function ensureFieldsRegistered(schema) {
|
|
@@ -7,15 +10,108 @@ function ensureFieldsRegistered(schema) {
|
|
|
7
10
|
} catch {
|
|
8
11
|
}
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
__name(ensureFieldsRegistered, "ensureFieldsRegistered");
|
|
14
|
+
function collectDependentSchemas(schema, seen = /* @__PURE__ */ new Set(), orderedList = []) {
|
|
15
|
+
if (seen.has(schema)) return orderedList;
|
|
16
|
+
seen.add(schema);
|
|
17
|
+
const fields = defaultRegistry.getFields(schema);
|
|
18
|
+
for (const field of fields) {
|
|
19
|
+
if (field.reflectedType && defaultRegistry.isSchema(field.reflectedType)) {
|
|
20
|
+
collectDependentSchemas(field.reflectedType, seen, orderedList);
|
|
21
|
+
}
|
|
22
|
+
if (field.type) {
|
|
23
|
+
const depSchema = defaultRegistry.getSchemaByName(field.type);
|
|
24
|
+
if (depSchema) {
|
|
25
|
+
collectDependentSchemas(depSchema, seen, orderedList);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
orderedList.push(schema);
|
|
30
|
+
return orderedList;
|
|
31
|
+
}
|
|
32
|
+
__name(collectDependentSchemas, "collectDependentSchemas");
|
|
33
|
+
function needsTimestamp(schemas) {
|
|
34
|
+
return schemas.some((s) => defaultRegistry.getFields(s).some((f) => f.protoType === "google.protobuf.Timestamp"));
|
|
35
|
+
}
|
|
36
|
+
__name(needsTimestamp, "needsTimestamp");
|
|
37
|
+
function buildFieldLine(field) {
|
|
38
|
+
const modifier = field.repeated ? "repeated " : field.optional ? "optional " : "";
|
|
39
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
40
|
+
}
|
|
41
|
+
__name(buildFieldLine, "buildFieldLine");
|
|
42
|
+
function buildMessageBlock(schema, lines) {
|
|
12
43
|
const fields = defaultRegistry.getFields(schema);
|
|
13
44
|
if (fields.length === 0) {
|
|
14
45
|
throw new Error(`Schema '${schema.name}' has no @Field() decorators \u2014 cannot generate proto`);
|
|
15
46
|
}
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
lines.push(`message ${schema.name} {`);
|
|
48
|
+
for (const field of fields) {
|
|
49
|
+
if (field.comment) {
|
|
50
|
+
lines.push(` // ${field.comment}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push(` ${buildFieldLine(field)}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("}", "");
|
|
55
|
+
}
|
|
56
|
+
__name(buildMessageBlock, "buildMessageBlock");
|
|
57
|
+
function toProto(schema) {
|
|
58
|
+
ensureFieldsRegistered(schema);
|
|
59
|
+
const orderedList = collectDependentSchemas(schema);
|
|
60
|
+
if (orderedList.length === 0 || defaultRegistry.getFields(schema).length === 0) {
|
|
61
|
+
throw new Error(`Schema '${schema.name}' has no @Field() decorators \u2014 cannot generate proto`);
|
|
62
|
+
}
|
|
63
|
+
const lines = [
|
|
64
|
+
'syntax = "proto3";',
|
|
65
|
+
""
|
|
66
|
+
];
|
|
67
|
+
if (needsTimestamp(orderedList)) {
|
|
68
|
+
lines.push('import "google/protobuf/timestamp.proto";', "");
|
|
69
|
+
}
|
|
70
|
+
for (const s of orderedList) {
|
|
71
|
+
buildMessageBlock(s, lines);
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n").trimEnd();
|
|
74
|
+
}
|
|
75
|
+
__name(toProto, "toProto");
|
|
76
|
+
|
|
77
|
+
// src/fields-to-proto.ts
|
|
78
|
+
function buildFieldLine2(field) {
|
|
79
|
+
const modifier = field.repeated ? "repeated " : field.optional ? "optional " : "";
|
|
80
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
81
|
+
}
|
|
82
|
+
__name(buildFieldLine2, "buildFieldLine");
|
|
83
|
+
function needsTimestampImport(fields) {
|
|
84
|
+
return fields.some((f) => f.protoType === "google.protobuf.Timestamp");
|
|
85
|
+
}
|
|
86
|
+
__name(needsTimestampImport, "needsTimestampImport");
|
|
87
|
+
function appendMessageBlock(messageName, fields, lines) {
|
|
88
|
+
if (fields.length === 0) {
|
|
89
|
+
throw new Error(`Schema '${messageName}' has no fields \u2014 cannot generate proto. Got 0 fields.`);
|
|
90
|
+
}
|
|
91
|
+
lines.push(`message ${messageName} {`);
|
|
92
|
+
for (const field of fields) {
|
|
93
|
+
if (field.comment) {
|
|
94
|
+
lines.push(` // ${field.comment}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(` ${buildFieldLine2(field)}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push("}", "");
|
|
99
|
+
}
|
|
100
|
+
__name(appendMessageBlock, "appendMessageBlock");
|
|
101
|
+
function fieldsToProto(messageName, fields) {
|
|
102
|
+
const lines = [
|
|
103
|
+
'syntax = "proto3";',
|
|
104
|
+
""
|
|
105
|
+
];
|
|
106
|
+
if (needsTimestampImport(fields)) {
|
|
107
|
+
lines.push('import "google/protobuf/timestamp.proto";', "");
|
|
108
|
+
}
|
|
109
|
+
appendMessageBlock(messageName, fields, lines);
|
|
110
|
+
return lines.join("\n").trimEnd();
|
|
18
111
|
}
|
|
112
|
+
__name(fieldsToProto, "fieldsToProto");
|
|
19
113
|
export {
|
|
114
|
+
fieldsToProto,
|
|
20
115
|
toProto
|
|
21
116
|
};
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generator.ts","../src/fields-to-proto.ts"],"sourcesContent":["/**\n * Generates a proto3 schema string from a decorated class.\n *\n * Reads field metadata from the default SchemaRegistry and produces\n * the exact format the Rust broker expects in the `x-schema` queue argument.\n */\n\nimport { defaultRegistry, type FieldMeta, type Constructor } from '@rocketmq/schema';\n\n/** Forces field registration by instantiating the class once. */\nfunction ensureFieldsRegistered(schema: Constructor): void {\n if (defaultRegistry.getFields(schema).length > 0) return;\n try {\n new schema();\n } catch {\n // Constructor may throw — fields are registered via addInitializer\n void 0;\n }\n}\n\n/** Recursively collects all schema constructors referenced by the class. */\nfunction collectDependentSchemas(\n schema: Constructor,\n seen = new Set<Constructor>(),\n orderedList: Constructor[] = [],\n): Constructor[] {\n if (seen.has(schema)) return orderedList;\n seen.add(schema);\n\n const fields = defaultRegistry.getFields(schema);\n for (const field of fields) {\n if (field.reflectedType && defaultRegistry.isSchema(field.reflectedType)) {\n collectDependentSchemas(field.reflectedType, seen, orderedList);\n }\n if (field.type) {\n const depSchema = defaultRegistry.getSchemaByName(field.type);\n if (depSchema) {\n collectDependentSchemas(depSchema, seen, orderedList);\n }\n }\n }\n orderedList.push(schema);\n return orderedList;\n}\n\n/** Determines if any schema in the list uses a Timestamp type. */\nfunction needsTimestamp(schemas: Constructor[]): boolean {\n return schemas.some((s) =>\n defaultRegistry.getFields(s).some((f) => f.protoType === 'google.protobuf.Timestamp'),\n );\n}\n\n/** Builds the protobuf field line using the pre-resolved protoType. */\nfunction buildFieldLine(field: FieldMeta): string {\n const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';\n return `${modifier}${field.protoType} ${field.name} = ${field.number};`;\n}\n\n/** Formats a schema class as a protobuf message block. */\nfunction buildMessageBlock(schema: Constructor, lines: string[]): void {\n const fields = defaultRegistry.getFields(schema);\n if (fields.length === 0) {\n throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);\n }\n lines.push(`message ${schema.name} {`);\n for (const field of fields) {\n if (field.comment) {\n lines.push(` // ${field.comment}`);\n }\n lines.push(` ${buildFieldLine(field)}`);\n }\n lines.push('}', '');\n}\n\n/** Converts a decorated schema class to a proto3 definition string. */\nexport function toProto(schema: Constructor): string {\n ensureFieldsRegistered(schema);\n const orderedList = collectDependentSchemas(schema);\n if (orderedList.length === 0 || defaultRegistry.getFields(schema).length === 0) {\n throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);\n }\n const lines: string[] = ['syntax = \"proto3\";', ''];\n if (needsTimestamp(orderedList)) {\n lines.push('import \"google/protobuf/timestamp.proto\";', '');\n }\n for (const s of orderedList) {\n buildMessageBlock(s, lines);\n }\n return lines.join('\\n').trimEnd();\n}\n","/**\n * Generates proto3 from raw FieldMeta arrays — no class/decorator dependency.\n *\n * This is the shared codegen path used by both the decorator-based `toProto()`\n * and the Zod-based `zodToProto()` pipelines. Factoring it here avoids\n * duplicating proto string assembly logic.\n *\n * Usage:\n * fieldsToProto('Order', [{ name: 'id', protoType: 'string', number: 1 }]);\n */\n\nimport type { FieldMeta } from '@rocketmq/schema';\n\n/** Builds a single proto3 field line from resolved metadata. */\nfunction buildFieldLine(field: FieldMeta): string {\n const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';\n return `${modifier}${field.protoType} ${field.name} = ${field.number};`;\n}\n\n/**\n * Checks whether any field in the list needs the Timestamp import.\n *\n * Extracted so callers don't need to know the magic string.\n */\nfunction needsTimestampImport(fields: FieldMeta[]): boolean {\n return fields.some((f) => f.protoType === 'google.protobuf.Timestamp');\n}\n\n/** Appends one protobuf message block to the output lines array. */\nfunction appendMessageBlock(messageName: string, fields: FieldMeta[], lines: string[]): void {\n if (fields.length === 0) {\n throw new Error(`Schema '${messageName}' has no fields — cannot generate proto. Got 0 fields.`);\n }\n lines.push(`message ${messageName} {`);\n for (const field of fields) {\n if (field.comment) {\n lines.push(` // ${field.comment}`);\n }\n lines.push(` ${buildFieldLine(field)}`);\n }\n lines.push('}', '');\n}\n\n/**\n * Converts a message name + field metadata list into a proto3 definition.\n *\n * This is the low-level entrypoint — no class introspection, no registry\n * lookups. Works with any source that can produce `FieldMeta[]`.\n *\n * Usage:\n * const proto = fieldsToProto('Notification', fields);\n * // => 'syntax = \"proto3\";\\n\\nmessage Notification { ... }'\n */\nexport function fieldsToProto(messageName: string, fields: FieldMeta[]): string {\n const lines: string[] = ['syntax = \"proto3\";', ''];\n if (needsTimestampImport(fields)) {\n lines.push('import \"google/protobuf/timestamp.proto\";', '');\n }\n appendMessageBlock(messageName, fields, lines);\n return lines.join('\\n').trimEnd();\n}\n"],"mappings":"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","names":["defaultRegistry","ensureFieldsRegistered","schema","defaultRegistry","getFields","length","collectDependentSchemas","seen","Set","orderedList","has","add","fields","field","reflectedType","isSchema","type","depSchema","getSchemaByName","push","needsTimestamp","schemas","some","s","f","protoType","buildFieldLine","modifier","repeated","optional","name","number","buildMessageBlock","lines","Error","comment","toProto","join","trimEnd","buildFieldLine","field","modifier","repeated","optional","protoType","name","number","needsTimestampImport","fields","some","f","appendMessageBlock","messageName","lines","length","Error","push","comment","fieldsToProto","join","trimEnd"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rocketmq/protobuf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@rocketmq/schema": "0.1.
|
|
12
|
+
"@rocketmq/schema": "0.1.2"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsup",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for fieldsToProto() — FieldMeta[] → proto3 string generation.
|
|
3
|
+
*
|
|
4
|
+
* This is the shared codegen path used by both decorator and Zod pipelines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import type { FieldMeta } from '@rocketmq/schema';
|
|
9
|
+
import { fieldsToProto } from './fields-to-proto.js';
|
|
10
|
+
|
|
11
|
+
describe('fieldsToProto', () => {
|
|
12
|
+
it('generates proto3 from a single field', () => {
|
|
13
|
+
const fields: FieldMeta[] = [{ name: 'id', protoType: 'string', number: 1 }];
|
|
14
|
+
|
|
15
|
+
const expected = ['syntax = "proto3";', '', 'message Order {', ' string id = 1;', '}'].join(
|
|
16
|
+
'\n',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(fieldsToProto('Order', fields)).toBe(expected);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('generates proto3 with multiple fields in order', () => {
|
|
23
|
+
const fields: FieldMeta[] = [
|
|
24
|
+
{ name: 'name', protoType: 'string', number: 1 },
|
|
25
|
+
{ name: 'age', protoType: 'int32', number: 2 },
|
|
26
|
+
{ name: 'active', protoType: 'bool', number: 3 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const proto = fieldsToProto('Person', fields);
|
|
30
|
+
expect(proto).toContain('string name = 1;');
|
|
31
|
+
expect(proto).toContain('int32 age = 2;');
|
|
32
|
+
expect(proto).toContain('bool active = 3;');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('emits optional modifier', () => {
|
|
36
|
+
const fields: FieldMeta[] = [
|
|
37
|
+
{ name: 'nickname', protoType: 'string', number: 1, optional: true },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const proto = fieldsToProto('Profile', fields);
|
|
41
|
+
expect(proto).toContain('optional string nickname = 1;');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('emits repeated modifier', () => {
|
|
45
|
+
const fields: FieldMeta[] = [{ name: 'tags', protoType: 'string', number: 1, repeated: true }];
|
|
46
|
+
|
|
47
|
+
const proto = fieldsToProto('Item', fields);
|
|
48
|
+
expect(proto).toContain('repeated string tags = 1;');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('emits comment above field', () => {
|
|
52
|
+
const fields: FieldMeta[] = [
|
|
53
|
+
{ name: 'score', protoType: 'double', number: 1, comment: 'Player score' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const proto = fieldsToProto('Game', fields);
|
|
57
|
+
expect(proto).toContain('// Player score');
|
|
58
|
+
expect(proto).toContain('double score = 1;');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('includes Timestamp import when needed', () => {
|
|
62
|
+
const fields: FieldMeta[] = [
|
|
63
|
+
{ name: 'createdAt', protoType: 'google.protobuf.Timestamp', number: 1 },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const proto = fieldsToProto('Event', fields);
|
|
67
|
+
expect(proto).toContain('import "google/protobuf/timestamp.proto";');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws for empty fields array', () => {
|
|
71
|
+
expect(() => fieldsToProto('Empty', [])).toThrow("Schema 'Empty' has no fields");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates proto3 from raw FieldMeta arrays — no class/decorator dependency.
|
|
3
|
+
*
|
|
4
|
+
* This is the shared codegen path used by both the decorator-based `toProto()`
|
|
5
|
+
* and the Zod-based `zodToProto()` pipelines. Factoring it here avoids
|
|
6
|
+
* duplicating proto string assembly logic.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* fieldsToProto('Order', [{ name: 'id', protoType: 'string', number: 1 }]);
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FieldMeta } from '@rocketmq/schema';
|
|
13
|
+
|
|
14
|
+
/** Builds a single proto3 field line from resolved metadata. */
|
|
15
|
+
function buildFieldLine(field: FieldMeta): string {
|
|
16
|
+
const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';
|
|
17
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks whether any field in the list needs the Timestamp import.
|
|
22
|
+
*
|
|
23
|
+
* Extracted so callers don't need to know the magic string.
|
|
24
|
+
*/
|
|
25
|
+
function needsTimestampImport(fields: FieldMeta[]): boolean {
|
|
26
|
+
return fields.some((f) => f.protoType === 'google.protobuf.Timestamp');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Appends one protobuf message block to the output lines array. */
|
|
30
|
+
function appendMessageBlock(messageName: string, fields: FieldMeta[], lines: string[]): void {
|
|
31
|
+
if (fields.length === 0) {
|
|
32
|
+
throw new Error(`Schema '${messageName}' has no fields — cannot generate proto. Got 0 fields.`);
|
|
33
|
+
}
|
|
34
|
+
lines.push(`message ${messageName} {`);
|
|
35
|
+
for (const field of fields) {
|
|
36
|
+
if (field.comment) {
|
|
37
|
+
lines.push(` // ${field.comment}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push(` ${buildFieldLine(field)}`);
|
|
40
|
+
}
|
|
41
|
+
lines.push('}', '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a message name + field metadata list into a proto3 definition.
|
|
46
|
+
*
|
|
47
|
+
* This is the low-level entrypoint — no class introspection, no registry
|
|
48
|
+
* lookups. Works with any source that can produce `FieldMeta[]`.
|
|
49
|
+
*
|
|
50
|
+
* Usage:
|
|
51
|
+
* const proto = fieldsToProto('Notification', fields);
|
|
52
|
+
* // => 'syntax = "proto3";\n\nmessage Notification { ... }'
|
|
53
|
+
*/
|
|
54
|
+
export function fieldsToProto(messageName: string, fields: FieldMeta[]): string {
|
|
55
|
+
const lines: string[] = ['syntax = "proto3";', ''];
|
|
56
|
+
if (needsTimestampImport(fields)) {
|
|
57
|
+
lines.push('import "google/protobuf/timestamp.proto";', '');
|
|
58
|
+
}
|
|
59
|
+
appendMessageBlock(messageName, fields, lines);
|
|
60
|
+
return lines.join('\n').trimEnd();
|
|
61
|
+
}
|
package/src/generator.test.ts
CHANGED
|
@@ -1,45 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for toProto() — proto3 string generation.
|
|
3
|
-
*
|
|
4
|
-
* Covers: valid schemas, empty schemas (error), multi-field ordering,
|
|
5
|
-
* and the force-instantiation path for uninitialized classes.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import { describe,
|
|
9
|
-
import { toProto } from './generator.js';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
10
6
|
import { Schema, Field, defaultRegistry } from '@rocketmq/schema';
|
|
7
|
+
import { toProto } from './generator.js';
|
|
11
8
|
|
|
12
9
|
describe('toProto', () => {
|
|
13
10
|
it('generates proto3 string for a single-field class', () => {
|
|
14
11
|
@Schema()
|
|
15
12
|
class SingleField {
|
|
16
13
|
@Field()
|
|
17
|
-
id
|
|
14
|
+
id = '';
|
|
18
15
|
}
|
|
19
16
|
new SingleField();
|
|
20
17
|
|
|
21
|
-
const
|
|
22
|
-
|
|
18
|
+
const expected = [
|
|
19
|
+
'syntax = "proto3";',
|
|
20
|
+
'',
|
|
21
|
+
'message SingleField {',
|
|
22
|
+
' string id = 1;',
|
|
23
|
+
'}',
|
|
24
|
+
].join('\n');
|
|
25
|
+
expect(toProto(SingleField)).toBe(expected);
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
it('generates proto3 with multiple fields in order', () => {
|
|
26
29
|
@Schema()
|
|
27
30
|
class MultiField {
|
|
28
31
|
@Field()
|
|
29
|
-
name
|
|
32
|
+
name = '';
|
|
30
33
|
|
|
31
34
|
@Field({ type: 'int32' })
|
|
32
|
-
age
|
|
35
|
+
age = 0;
|
|
33
36
|
|
|
34
37
|
@Field({ type: 'bool' })
|
|
35
|
-
active
|
|
38
|
+
active = false;
|
|
36
39
|
}
|
|
37
40
|
new MultiField();
|
|
38
41
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
|
|
42
|
+
const expected = [
|
|
43
|
+
'syntax = "proto3";',
|
|
44
|
+
'',
|
|
45
|
+
'message MultiField {',
|
|
46
|
+
' string name = 1;',
|
|
47
|
+
' int32 age = 2;',
|
|
48
|
+
' bool active = 3;',
|
|
49
|
+
'}',
|
|
50
|
+
].join('\n');
|
|
51
|
+
expect(toProto(MultiField)).toBe(expected);
|
|
43
52
|
});
|
|
44
53
|
|
|
45
54
|
it('throws for class with no @Field() decorators', () => {
|
|
@@ -52,40 +61,133 @@ describe('toProto', () => {
|
|
|
52
61
|
});
|
|
53
62
|
|
|
54
63
|
it('forces instantiation when fields are not yet registered', () => {
|
|
55
|
-
// Simulate a class that hasn't been instantiated yet
|
|
56
64
|
@Schema()
|
|
57
65
|
class LazyInit {
|
|
58
66
|
@Field()
|
|
59
|
-
value
|
|
67
|
+
value = '';
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
const proto = toProto(LazyInit);
|
|
63
|
-
expect(proto).toContain('string value = 1');
|
|
69
|
+
expect(toProto(LazyInit)).toContain('string value = 1;');
|
|
64
70
|
});
|
|
65
71
|
|
|
66
72
|
it('handles class whose constructor throws', () => {
|
|
67
|
-
// WHY: toProto wraps instantiation in try/catch for classes
|
|
68
|
-
// that require constructor arguments
|
|
69
73
|
class ThrowingCtor {
|
|
70
74
|
@Field()
|
|
71
|
-
x
|
|
75
|
+
x = '';
|
|
72
76
|
|
|
73
77
|
constructor() {
|
|
74
|
-
// Fields registered via addInitializer before the throw
|
|
75
78
|
throw new Error('required args');
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
// Force-register the field store entry
|
|
79
81
|
defaultRegistry.getOrCreateFields(ThrowingCtor);
|
|
80
82
|
|
|
81
|
-
// toProto will try `new ThrowingCtor()`, catch, and check fields
|
|
82
|
-
// Since addInitializer runs before constructor body, field might be registered
|
|
83
|
-
// If not, it should throw "no @Field() decorators"
|
|
84
83
|
try {
|
|
85
|
-
|
|
86
|
-
expect(proto).toContain('string x = 1');
|
|
84
|
+
expect(toProto(ThrowingCtor)).toContain('string x = 1;');
|
|
87
85
|
} catch (err) {
|
|
88
86
|
expect((err as Error).message).toContain('no @Field() decorators');
|
|
89
87
|
}
|
|
90
88
|
});
|
|
89
|
+
|
|
90
|
+
it('generates optional, repeated, and commented fields', () => {
|
|
91
|
+
@Schema()
|
|
92
|
+
class CustomField {
|
|
93
|
+
@Field({ type: 'string', repeated: true })
|
|
94
|
+
tags: string[] = [];
|
|
95
|
+
|
|
96
|
+
@Field({ type: 'string', optional: true })
|
|
97
|
+
nickname?: string;
|
|
98
|
+
|
|
99
|
+
@Field({ type: 'double', comment: 'User age in years' })
|
|
100
|
+
age!: number;
|
|
101
|
+
}
|
|
102
|
+
new CustomField();
|
|
103
|
+
|
|
104
|
+
const proto = toProto(CustomField);
|
|
105
|
+
expect(proto).toContain('repeated string tags = 1;');
|
|
106
|
+
expect(proto).toContain('optional string nickname = 2;');
|
|
107
|
+
expect(proto).toContain('// User age in years');
|
|
108
|
+
expect(proto).toContain('double age = 3;');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles Date as Timestamp with imports', () => {
|
|
112
|
+
@Schema()
|
|
113
|
+
class TimeField {
|
|
114
|
+
@Field({ type: 'google.protobuf.Timestamp' })
|
|
115
|
+
createdAt = new Date();
|
|
116
|
+
}
|
|
117
|
+
new TimeField();
|
|
118
|
+
|
|
119
|
+
const proto = toProto(TimeField);
|
|
120
|
+
expect(proto).toContain('import "google/protobuf/timestamp.proto";');
|
|
121
|
+
expect(proto).toContain('google.protobuf.Timestamp createdAt = 1;');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles nested schemas and orders them correctly', () => {
|
|
125
|
+
@Schema()
|
|
126
|
+
class Child {
|
|
127
|
+
@Field()
|
|
128
|
+
name = '';
|
|
129
|
+
}
|
|
130
|
+
@Schema()
|
|
131
|
+
class Parent {
|
|
132
|
+
@Field({ type: 'Child' })
|
|
133
|
+
child = new Child();
|
|
134
|
+
}
|
|
135
|
+
new Child();
|
|
136
|
+
new Parent();
|
|
137
|
+
|
|
138
|
+
const proto = toProto(Parent);
|
|
139
|
+
expect(proto).toContain('message Child {');
|
|
140
|
+
expect(proto).toContain('message Parent {');
|
|
141
|
+
expect(proto.indexOf('message Child {')).toBeLessThan(proto.indexOf('message Parent {'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('edge cases', () => {
|
|
145
|
+
it('handles constructors that throw', () => {
|
|
146
|
+
@Schema()
|
|
147
|
+
class ThrowingClass {
|
|
148
|
+
@Field()
|
|
149
|
+
id!: string;
|
|
150
|
+
constructor() {
|
|
151
|
+
throw new Error('fail');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const proto = toProto(ThrowingClass);
|
|
155
|
+
expect(proto).toContain('message ThrowingClass');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('throws if dependent schema has no fields', () => {
|
|
159
|
+
@Schema()
|
|
160
|
+
class NoFieldsDep {}
|
|
161
|
+
|
|
162
|
+
@Schema()
|
|
163
|
+
class DependentParent {
|
|
164
|
+
@Field({ type: 'NoFieldsDep' })
|
|
165
|
+
child!: NoFieldsDep;
|
|
166
|
+
}
|
|
167
|
+
// manually register to ensure lookup works
|
|
168
|
+
defaultRegistry.registerSchema(NoFieldsDep);
|
|
169
|
+
|
|
170
|
+
expect(() => toProto(DependentParent)).toThrow(/no @Field/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('collects nested schemas via reflect metadata type', () => {
|
|
174
|
+
@Schema()
|
|
175
|
+
class NestedReflectChild {
|
|
176
|
+
@Field()
|
|
177
|
+
id!: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Schema()
|
|
181
|
+
class ReflectParent {
|
|
182
|
+
@Field({ type: 'NestedReflectChild' })
|
|
183
|
+
child!: NestedReflectChild;
|
|
184
|
+
}
|
|
185
|
+
// Force reflectedType manually since we don't emit it fully in vitest by default
|
|
186
|
+
defaultRegistry.getOrCreateFields(ReflectParent)[0].reflectedType = NestedReflectChild;
|
|
187
|
+
|
|
188
|
+
const proto = toProto(ReflectParent);
|
|
189
|
+
expect(proto).toContain('message NestedReflectChild');
|
|
190
|
+
expect(proto).toContain('message ReflectParent');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
91
193
|
});
|
package/src/generator.ts
CHANGED
|
@@ -3,45 +3,88 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reads field metadata from the default SchemaRegistry and produces
|
|
5
5
|
* the exact format the Rust broker expects in the `x-schema` queue argument.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const proto = toProto(NotificationClass);
|
|
9
|
-
* // => 'syntax = "proto3"; message Notification { string id = 1; int64 timestamp = 2; }'
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
import { defaultRegistry } from '@rocketmq/schema';
|
|
8
|
+
import { defaultRegistry, type FieldMeta, type Constructor } from '@rocketmq/schema';
|
|
13
9
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
* TC39 field decorators defer registration via `addInitializer`,
|
|
18
|
-
* which only runs on construction. If no instance exists yet,
|
|
19
|
-
* the registry will have zero fields for the class.
|
|
20
|
-
*/
|
|
21
|
-
function ensureFieldsRegistered(schema: Function): void {
|
|
10
|
+
/** Forces field registration by instantiating the class once. */
|
|
11
|
+
function ensureFieldsRegistered(schema: Constructor): void {
|
|
22
12
|
if (defaultRegistry.getFields(schema).length > 0) return;
|
|
23
|
-
|
|
24
13
|
try {
|
|
25
|
-
new
|
|
14
|
+
new schema();
|
|
26
15
|
} catch {
|
|
27
16
|
// Constructor may throw — fields are registered via addInitializer
|
|
17
|
+
void 0;
|
|
28
18
|
}
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
/** Recursively collects all schema constructors referenced by the class. */
|
|
22
|
+
function collectDependentSchemas(
|
|
23
|
+
schema: Constructor,
|
|
24
|
+
seen = new Set<Constructor>(),
|
|
25
|
+
orderedList: Constructor[] = [],
|
|
26
|
+
): Constructor[] {
|
|
27
|
+
if (seen.has(schema)) return orderedList;
|
|
28
|
+
seen.add(schema);
|
|
38
29
|
|
|
30
|
+
const fields = defaultRegistry.getFields(schema);
|
|
31
|
+
for (const field of fields) {
|
|
32
|
+
if (field.reflectedType && defaultRegistry.isSchema(field.reflectedType)) {
|
|
33
|
+
collectDependentSchemas(field.reflectedType, seen, orderedList);
|
|
34
|
+
}
|
|
35
|
+
if (field.type) {
|
|
36
|
+
const depSchema = defaultRegistry.getSchemaByName(field.type);
|
|
37
|
+
if (depSchema) {
|
|
38
|
+
collectDependentSchemas(depSchema, seen, orderedList);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
orderedList.push(schema);
|
|
43
|
+
return orderedList;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Determines if any schema in the list uses a Timestamp type. */
|
|
47
|
+
function needsTimestamp(schemas: Constructor[]): boolean {
|
|
48
|
+
return schemas.some((s) =>
|
|
49
|
+
defaultRegistry.getFields(s).some((f) => f.protoType === 'google.protobuf.Timestamp'),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Builds the protobuf field line using the pre-resolved protoType. */
|
|
54
|
+
function buildFieldLine(field: FieldMeta): string {
|
|
55
|
+
const modifier = field.repeated ? 'repeated ' : field.optional ? 'optional ' : '';
|
|
56
|
+
return `${modifier}${field.protoType} ${field.name} = ${field.number};`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Formats a schema class as a protobuf message block. */
|
|
60
|
+
function buildMessageBlock(schema: Constructor, lines: string[]): void {
|
|
39
61
|
const fields = defaultRegistry.getFields(schema);
|
|
40
62
|
if (fields.length === 0) {
|
|
41
63
|
throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);
|
|
42
64
|
}
|
|
65
|
+
lines.push(`message ${schema.name} {`);
|
|
66
|
+
for (const field of fields) {
|
|
67
|
+
if (field.comment) {
|
|
68
|
+
lines.push(` // ${field.comment}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push(` ${buildFieldLine(field)}`);
|
|
71
|
+
}
|
|
72
|
+
lines.push('}', '');
|
|
73
|
+
}
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
/** Converts a decorated schema class to a proto3 definition string. */
|
|
76
|
+
export function toProto(schema: Constructor): string {
|
|
77
|
+
ensureFieldsRegistered(schema);
|
|
78
|
+
const orderedList = collectDependentSchemas(schema);
|
|
79
|
+
if (orderedList.length === 0 || defaultRegistry.getFields(schema).length === 0) {
|
|
80
|
+
throw new Error(`Schema '${schema.name}' has no @Field() decorators — cannot generate proto`);
|
|
81
|
+
}
|
|
82
|
+
const lines: string[] = ['syntax = "proto3";', ''];
|
|
83
|
+
if (needsTimestamp(orderedList)) {
|
|
84
|
+
lines.push('import "google/protobuf/timestamp.proto";', '');
|
|
85
|
+
}
|
|
86
|
+
for (const s of orderedList) {
|
|
87
|
+
buildMessageBlock(s, lines);
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n').trimEnd();
|
|
47
90
|
}
|
package/src/index.ts
CHANGED