@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.
@@ -1,21 +1,23 @@
1
1
 
2
2
 
3
- > @rocketmq/protobuf@0.1.0 build /home/edilson/learnspace/rocketmq-broker/client-ts/packages/protobuf
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/client-ts/packages/protobuf/tsup.config.ts
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 1.65 KB
15
- CJS ⚡️ Build success in 10ms
16
- ESM dist/index.js 645.00 B
17
- ESM ⚡️ Build success in 10ms
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 896ms
20
- DTS dist/index.d.ts 581.00 B
21
- DTS dist/index.d.cts 581.00 B
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
@@ -0,0 +1,10 @@
1
+ # @rocketmq/protobuf
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - docs: comprehensive README and API docs like Python SDK
8
+
9
+ - Updated dependencies []:
10
+ - @rocketmq/schema@0.1.2
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
- function toProto(schema) {
37
- ensureFieldsRegistered(schema);
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
- const body = fields.map((f) => `${f.protoType} ${f.name} = ${f.number};`).join(" ");
43
- return `syntax = "proto3"; message ${schema.name} { ${body} }`;
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":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;ACOA,oBAAkE;AAGlE,SAASA,uBAAuBC,QAAmB;AACjD,MAAIC,8BAAgBC,UAAUF,MAAAA,EAAQG,SAAS,EAAG;AAClD,MAAI;AACF,QAAIH,OAAAA;EACN,QAAQ;EAGR;AACF;AARSD;AAWT,SAASK,wBACPJ,QACAK,OAAO,oBAAIC,IAAAA,GACXC,cAA6B,CAAA,GAAE;AAE/B,MAAIF,KAAKG,IAAIR,MAAAA,EAAS,QAAOO;AAC7BF,OAAKI,IAAIT,MAAAA;AAET,QAAMU,SAAST,8BAAgBC,UAAUF,MAAAA;AACzC,aAAWW,SAASD,QAAQ;AAC1B,QAAIC,MAAMC,iBAAiBX,8BAAgBY,SAASF,MAAMC,aAAa,GAAG;AACxER,8BAAwBO,MAAMC,eAAeP,MAAME,WAAAA;IACrD;AACA,QAAII,MAAMG,MAAM;AACd,YAAMC,YAAYd,8BAAgBe,gBAAgBL,MAAMG,IAAI;AAC5D,UAAIC,WAAW;AACbX,gCAAwBW,WAAWV,MAAME,WAAAA;MAC3C;IACF;EACF;AACAA,cAAYU,KAAKjB,MAAAA;AACjB,SAAOO;AACT;AAtBSH;AAyBT,SAASc,eAAeC,SAAsB;AAC5C,SAAOA,QAAQC,KAAK,CAACC,MACnBpB,8BAAgBC,UAAUmB,CAAAA,EAAGD,KAAK,CAACE,MAAMA,EAAEC,cAAc,2BAAA,CAAA;AAE7D;AAJSL;AAOT,SAASM,eAAeb,OAAgB;AACtC,QAAMc,WAAWd,MAAMe,WAAW,cAAcf,MAAMgB,WAAW,cAAc;AAC/E,SAAO,GAAGF,QAAAA,GAAWd,MAAMY,SAAS,IAAIZ,MAAMiB,IAAI,MAAMjB,MAAMkB,MAAM;AACtE;AAHSL;AAMT,SAASM,kBAAkB9B,QAAqB+B,OAAe;AAC7D,QAAMrB,SAAST,8BAAgBC,UAAUF,MAAAA;AACzC,MAAIU,OAAOP,WAAW,GAAG;AACvB,UAAM,IAAI6B,MAAM,WAAWhC,OAAO4B,IAAI,2DAAsD;EAC9F;AACAG,QAAMd,KAAK,WAAWjB,OAAO4B,IAAI,IAAI;AACrC,aAAWjB,SAASD,QAAQ;AAC1B,QAAIC,MAAMsB,SAAS;AACjBF,YAAMd,KAAK,QAAQN,MAAMsB,OAAO,EAAE;IACpC;AACAF,UAAMd,KAAK,KAAKO,eAAeb,KAAAA,CAAAA,EAAQ;EACzC;AACAoB,QAAMd,KAAK,KAAK,EAAA;AAClB;AAbSa;AAgBF,SAASI,QAAQlC,QAAmB;AACzCD,yBAAuBC,MAAAA;AACvB,QAAMO,cAAcH,wBAAwBJ,MAAAA;AAC5C,MAAIO,YAAYJ,WAAW,KAAKF,8BAAgBC,UAAUF,MAAAA,EAAQG,WAAW,GAAG;AAC9E,UAAM,IAAI6B,MAAM,WAAWhC,OAAO4B,IAAI,2DAAsD;EAC9F;AACA,QAAMG,QAAkB;IAAC;IAAsB;;AAC/C,MAAIb,eAAeX,WAAAA,GAAc;AAC/BwB,UAAMd,KAAK,6CAA6C,EAAA;EAC1D;AACA,aAAWI,KAAKd,aAAa;AAC3BuB,sBAAkBT,GAAGU,KAAAA;EACvB;AACA,SAAOA,MAAMI,KAAK,IAAA,EAAMC,QAAO;AACjC;AAdgBF;;;AC7DhB,SAASG,gBAAeC,OAAgB;AACtC,QAAMC,WAAWD,MAAME,WAAW,cAAcF,MAAMG,WAAW,cAAc;AAC/E,SAAO,GAAGF,QAAAA,GAAWD,MAAMI,SAAS,IAAIJ,MAAMK,IAAI,MAAML,MAAMM,MAAM;AACtE;AAHSP,OAAAA,iBAAAA;AAUT,SAASQ,qBAAqBC,QAAmB;AAC/C,SAAOA,OAAOC,KAAK,CAACC,MAAMA,EAAEN,cAAc,2BAAA;AAC5C;AAFSG;AAKT,SAASI,mBAAmBC,aAAqBJ,QAAqBK,OAAe;AACnF,MAAIL,OAAOM,WAAW,GAAG;AACvB,UAAM,IAAIC,MAAM,WAAWH,WAAAA,6DAAmE;EAChG;AACAC,QAAMG,KAAK,WAAWJ,WAAAA,IAAe;AACrC,aAAWZ,SAASQ,QAAQ;AAC1B,QAAIR,MAAMiB,SAAS;AACjBJ,YAAMG,KAAK,QAAQhB,MAAMiB,OAAO,EAAE;IACpC;AACAJ,UAAMG,KAAK,KAAKjB,gBAAeC,KAAAA,CAAAA,EAAQ;EACzC;AACAa,QAAMG,KAAK,KAAK,EAAA;AAClB;AAZSL;AAwBF,SAASO,cAAcN,aAAqBJ,QAAmB;AACpE,QAAMK,QAAkB;IAAC;IAAsB;;AAC/C,MAAIN,qBAAqBC,MAAAA,GAAS;AAChCK,UAAMG,KAAK,6CAA6C,EAAA;EAC1D;AACAL,qBAAmBC,aAAaJ,QAAQK,KAAAA;AACxC,SAAOA,MAAMM,KAAK,IAAA,EAAMC,QAAO;AACjC;AAPgBF;","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
- * const proto = toProto(NotificationClass);
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 decorated schema class to a proto3 definition string.
25
+ * Converts a message name + field metadata list into a proto3 definition.
13
26
  *
14
- * @throws Error if the class has no @Field() decorators.
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 toProto(schema: Function): string;
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
- * const proto = toProto(NotificationClass);
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 decorated schema class to a proto3 definition string.
25
+ * Converts a message name + field metadata list into a proto3 definition.
13
26
  *
14
- * @throws Error if the class has no @Field() decorators.
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 toProto(schema: Function): string;
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
- function toProto(schema) {
11
- ensureFieldsRegistered(schema);
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
- const body = fields.map((f) => `${f.protoType} ${f.name} = ${f.number};`).join(" ");
17
- return `syntax = "proto3"; message ${schema.name} { ${body} }`;
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.0",
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.0"
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
+ }
@@ -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, it, expect } from 'vitest';
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!: string;
14
+ id = '';
18
15
  }
19
16
  new SingleField();
20
17
 
21
- const proto = toProto(SingleField);
22
- expect(proto).toBe('syntax = "proto3"; message SingleField { string id = 1; }');
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!: string;
32
+ name = '';
30
33
 
31
34
  @Field({ type: 'int32' })
32
- age!: number;
35
+ age = 0;
33
36
 
34
37
  @Field({ type: 'bool' })
35
- active!: boolean;
38
+ active = false;
36
39
  }
37
40
  new MultiField();
38
41
 
39
- const proto = toProto(MultiField);
40
- expect(proto).toBe(
41
- 'syntax = "proto3"; message MultiField { string name = 1; int32 age = 2; bool active = 3; }',
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!: string;
67
+ value = '';
60
68
  }
61
- // Do NOT call `new LazyInit()` toProto should do it internally
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!: string;
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
- const proto = toProto(ThrowingCtor);
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
- * Forces field registration by instantiating the class once.
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 (schema as new () => unknown)();
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
- * Converts a decorated schema class to a proto3 definition string.
33
- *
34
- * @throws Error if the class has no @Field() decorators.
35
- */
36
- export function toProto(schema: Function): string {
37
- ensureFieldsRegistered(schema);
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
- const body = fields.map((f) => `${f.protoType} ${f.name} = ${f.number};`).join(' ');
45
-
46
- return `syntax = "proto3"; message ${schema.name} { ${body} }`;
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
@@ -1 +1,2 @@
1
1
  export { toProto } from './generator.js';
2
+ export { fieldsToProto } from './fields-to-proto.js';
package/tsup.config.ts CHANGED
@@ -5,4 +5,5 @@ export default defineConfig({
5
5
  format: ["esm", "cjs"],
6
6
  dts: true,
7
7
  clean: true,
8
+ sourcemap: true,
8
9
  });