@globalart/zod-to-proto 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +264 -0
- package/package.json +50 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ZodTypeAny } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface ZodToProtobufOptions {
|
|
5
|
+
packageName?: string;
|
|
6
|
+
rootMessageName?: string;
|
|
7
|
+
typePrefix?: string;
|
|
8
|
+
services?: ServiceDefinition[];
|
|
9
|
+
skipRootMessage?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ServiceMethod {
|
|
12
|
+
name: string;
|
|
13
|
+
request: ZodTypeAny;
|
|
14
|
+
response: ZodTypeAny;
|
|
15
|
+
streaming?: 'client' | 'server' | 'bidirectional';
|
|
16
|
+
}
|
|
17
|
+
interface ServiceDefinition {
|
|
18
|
+
name: string;
|
|
19
|
+
methods: ServiceMethod[];
|
|
20
|
+
}
|
|
21
|
+
declare class UnsupportedTypeException extends Error {
|
|
22
|
+
constructor(type: string);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/zod-to-protobuf.d.ts
|
|
26
|
+
declare const zodToProtobuf: (schema?: ZodTypeAny, options?: ZodToProtobufOptions) => string;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { type ServiceDefinition, type ServiceMethod, UnsupportedTypeException, type ZodToProtobufOptions, zodToProtobuf };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import * as inflection from "inflection";
|
|
2
|
+
import { ZodArray, ZodBigInt, ZodBoolean, ZodDate, ZodEnum, ZodMap, ZodNullable, ZodNumber, ZodObject, ZodOptional, ZodSet, ZodString, ZodTuple, ZodType } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/types.ts
|
|
5
|
+
var UnsupportedTypeException = class extends Error {
|
|
6
|
+
constructor(type) {
|
|
7
|
+
super(`Unsupported type: ${type}`);
|
|
8
|
+
this.name = "UnsupportedTypeException";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/utils.ts
|
|
14
|
+
const getNumberTypeName = ({ value }) => {
|
|
15
|
+
return value.isInt ? "int32" : "double";
|
|
16
|
+
};
|
|
17
|
+
const toPascalCase = ({ value }) => {
|
|
18
|
+
return value.split(".").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
19
|
+
};
|
|
20
|
+
const protobufFieldToType = ({ field }) => {
|
|
21
|
+
return field.types.filter(Boolean).join(" ");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/traversers.ts
|
|
26
|
+
const traverseArray = ({ key, value, messages, enums, typePrefix }) => {
|
|
27
|
+
const nestedValue = value instanceof ZodArray ? value.element : value instanceof ZodSet ? value._def.valueType : value._def.element;
|
|
28
|
+
const singularKey = inflection.singularize(key);
|
|
29
|
+
return traverseKey({
|
|
30
|
+
key: singularKey,
|
|
31
|
+
value: nestedValue,
|
|
32
|
+
messages,
|
|
33
|
+
enums,
|
|
34
|
+
isOptional: false,
|
|
35
|
+
isInArray: true,
|
|
36
|
+
typePrefix
|
|
37
|
+
}).map((field) => ({
|
|
38
|
+
...field,
|
|
39
|
+
types: ["repeated", ...field.types],
|
|
40
|
+
name: field.name.replace(singularKey, key)
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
const traverseMap = ({ key, value, messages, enums, typePrefix }) => {
|
|
44
|
+
const keyType = traverseKey({
|
|
45
|
+
key: `${key}Key`,
|
|
46
|
+
value: value._def.keyType,
|
|
47
|
+
messages,
|
|
48
|
+
enums,
|
|
49
|
+
isOptional: false,
|
|
50
|
+
isInArray: true,
|
|
51
|
+
typePrefix
|
|
52
|
+
});
|
|
53
|
+
const valueType = traverseKey({
|
|
54
|
+
key: `${key}Value`,
|
|
55
|
+
value: value._def.valueType,
|
|
56
|
+
messages,
|
|
57
|
+
enums,
|
|
58
|
+
isOptional: false,
|
|
59
|
+
isInArray: true,
|
|
60
|
+
typePrefix
|
|
61
|
+
});
|
|
62
|
+
if (!keyType[0] || keyType.length !== 1) throw new UnsupportedTypeException(`${key} map key`);
|
|
63
|
+
if (!valueType[0] || valueType.length !== 1) throw new UnsupportedTypeException(`${key} map value`);
|
|
64
|
+
return [{
|
|
65
|
+
types: [`map<${protobufFieldToType({ field: keyType[0] })}, ${protobufFieldToType({ field: valueType[0] })}>`],
|
|
66
|
+
name: key
|
|
67
|
+
}];
|
|
68
|
+
};
|
|
69
|
+
const traverseKey = ({ key, value, messages, enums, isOptional, isInArray, typePrefix }) => {
|
|
70
|
+
if (value instanceof ZodOptional || value instanceof ZodNullable) return traverseKey({
|
|
71
|
+
key,
|
|
72
|
+
value: value.unwrap(),
|
|
73
|
+
messages,
|
|
74
|
+
enums,
|
|
75
|
+
isOptional: true,
|
|
76
|
+
isInArray,
|
|
77
|
+
typePrefix
|
|
78
|
+
});
|
|
79
|
+
if (value instanceof ZodArray || value instanceof ZodSet) return traverseArray({
|
|
80
|
+
key,
|
|
81
|
+
value,
|
|
82
|
+
messages,
|
|
83
|
+
enums,
|
|
84
|
+
typePrefix
|
|
85
|
+
});
|
|
86
|
+
if (value instanceof ZodMap) return traverseMap({
|
|
87
|
+
key,
|
|
88
|
+
value,
|
|
89
|
+
messages,
|
|
90
|
+
enums,
|
|
91
|
+
typePrefix
|
|
92
|
+
});
|
|
93
|
+
const optional = isOptional && !isInArray ? "optional" : null;
|
|
94
|
+
if (value instanceof ZodObject) {
|
|
95
|
+
let messageName = toPascalCase({ value: key });
|
|
96
|
+
if (typePrefix) messageName = `${typePrefix}${messageName}`;
|
|
97
|
+
const nestedMessageFields = traverseSchema({
|
|
98
|
+
schema: value,
|
|
99
|
+
messages,
|
|
100
|
+
enums,
|
|
101
|
+
typePrefix
|
|
102
|
+
});
|
|
103
|
+
messages.set(messageName, nestedMessageFields);
|
|
104
|
+
return [{
|
|
105
|
+
types: [optional, messageName],
|
|
106
|
+
name: key
|
|
107
|
+
}];
|
|
108
|
+
}
|
|
109
|
+
if (value instanceof ZodString) return [{
|
|
110
|
+
types: [optional, "string"],
|
|
111
|
+
name: key
|
|
112
|
+
}];
|
|
113
|
+
if (value instanceof ZodNumber) return [{
|
|
114
|
+
types: [optional, getNumberTypeName({ value })],
|
|
115
|
+
name: key
|
|
116
|
+
}];
|
|
117
|
+
if (value instanceof ZodBoolean) return [{
|
|
118
|
+
types: [optional, "bool"],
|
|
119
|
+
name: key
|
|
120
|
+
}];
|
|
121
|
+
if (value instanceof ZodEnum) {
|
|
122
|
+
const enumFields = value.options.map((option, index) => ` ${String(option)} = ${index};`).join("\n");
|
|
123
|
+
let enumName = toPascalCase({ value: key });
|
|
124
|
+
if (typePrefix) enumName = `${typePrefix}${enumName}`;
|
|
125
|
+
enums.set(enumName, [`enum ${enumName} {\n${enumFields}\n}`]);
|
|
126
|
+
return [{
|
|
127
|
+
types: [optional, enumName],
|
|
128
|
+
name: key
|
|
129
|
+
}];
|
|
130
|
+
}
|
|
131
|
+
if (value instanceof ZodDate) return [{
|
|
132
|
+
types: [optional, "string"],
|
|
133
|
+
name: key
|
|
134
|
+
}];
|
|
135
|
+
if (value instanceof ZodBigInt) return [{
|
|
136
|
+
types: [optional, "int64"],
|
|
137
|
+
name: key
|
|
138
|
+
}];
|
|
139
|
+
if (value instanceof ZodTuple) {
|
|
140
|
+
const tupleFields = value._def.items.flatMap((item, index) => {
|
|
141
|
+
return traverseKey({
|
|
142
|
+
key: `${key}_${index}`,
|
|
143
|
+
value: item,
|
|
144
|
+
messages,
|
|
145
|
+
enums,
|
|
146
|
+
isOptional: false,
|
|
147
|
+
isInArray,
|
|
148
|
+
typePrefix
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
const tupleMessageName = toPascalCase({ value: key });
|
|
152
|
+
messages.set(tupleMessageName, tupleFields.map((field, index) => ` ${field.types.join(" ")} ${field.name} = ${index + 1};`));
|
|
153
|
+
return [{
|
|
154
|
+
types: [optional, tupleMessageName],
|
|
155
|
+
name: key
|
|
156
|
+
}];
|
|
157
|
+
}
|
|
158
|
+
if (value instanceof ZodType) throw new UnsupportedTypeException(value.constructor.name);
|
|
159
|
+
throw new UnsupportedTypeException(typeof value);
|
|
160
|
+
};
|
|
161
|
+
const traverseSchema = ({ schema, messages, enums, typePrefix }) => {
|
|
162
|
+
if (!(schema instanceof ZodObject)) throw new UnsupportedTypeException(schema.constructor.name);
|
|
163
|
+
return Object.entries(schema.shape).flatMap(([key, value]) => {
|
|
164
|
+
return traverseKey({
|
|
165
|
+
key,
|
|
166
|
+
value,
|
|
167
|
+
messages,
|
|
168
|
+
enums,
|
|
169
|
+
isOptional: false,
|
|
170
|
+
isInArray: false,
|
|
171
|
+
typePrefix
|
|
172
|
+
});
|
|
173
|
+
}).map((field, index) => `${protobufFieldToType({ field })} ${field.name} = ${index + 1};`);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/service-generator.ts
|
|
178
|
+
const generateRequestMessageName = (methodName, typePrefix) => {
|
|
179
|
+
const messageName = toPascalCase({ value: `${methodName}Request` });
|
|
180
|
+
return typePrefix ? `${typePrefix}${messageName}` : messageName;
|
|
181
|
+
};
|
|
182
|
+
const generateResponseMessageName = (methodName, typePrefix) => {
|
|
183
|
+
const messageName = toPascalCase({ value: `${methodName}Response` });
|
|
184
|
+
return typePrefix ? `${typePrefix}${messageName}` : messageName;
|
|
185
|
+
};
|
|
186
|
+
const processServiceMethod = (method, context) => {
|
|
187
|
+
const { messages, enums, typePrefix } = context;
|
|
188
|
+
const requestName = generateRequestMessageName(method.name, typePrefix);
|
|
189
|
+
const responseName = generateResponseMessageName(method.name, typePrefix);
|
|
190
|
+
if (!messages.has(requestName)) {
|
|
191
|
+
const requestFields = traverseSchema({
|
|
192
|
+
schema: method.request,
|
|
193
|
+
messages,
|
|
194
|
+
enums,
|
|
195
|
+
typePrefix
|
|
196
|
+
});
|
|
197
|
+
messages.set(requestName, requestFields);
|
|
198
|
+
}
|
|
199
|
+
if (!messages.has(responseName)) {
|
|
200
|
+
const responseFields = traverseSchema({
|
|
201
|
+
schema: method.response,
|
|
202
|
+
messages,
|
|
203
|
+
enums,
|
|
204
|
+
typePrefix
|
|
205
|
+
});
|
|
206
|
+
messages.set(responseName, responseFields);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
requestName,
|
|
210
|
+
responseName
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const generateServices = (services, context) => {
|
|
214
|
+
return services.map((service) => {
|
|
215
|
+
const methods = service.methods.map((method) => {
|
|
216
|
+
const { requestName, responseName } = processServiceMethod(method, context);
|
|
217
|
+
const requestStreaming = method.streaming === "client" || method.streaming === "bidirectional";
|
|
218
|
+
const responseStreaming = method.streaming === "server" || method.streaming === "bidirectional";
|
|
219
|
+
const requestType = requestStreaming ? `stream ${requestName}` : requestName;
|
|
220
|
+
const responseType = responseStreaming ? `stream ${responseName}` : responseName;
|
|
221
|
+
return ` rpc ${method.name}(${requestType}) returns (${responseType});`;
|
|
222
|
+
});
|
|
223
|
+
return `service ${service.name} {\n${methods.join("\n")}\n}`;
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/zod-to-protobuf.ts
|
|
229
|
+
const zodToProtobuf = (schema, options = {}) => {
|
|
230
|
+
const { packageName = "default", rootMessageName = "Message", typePrefix = "", services = [], skipRootMessage = false } = options;
|
|
231
|
+
const messages = /* @__PURE__ */ new Map();
|
|
232
|
+
const enums = /* @__PURE__ */ new Map();
|
|
233
|
+
if (schema && !skipRootMessage) {
|
|
234
|
+
const fields = traverseSchema({
|
|
235
|
+
schema,
|
|
236
|
+
messages,
|
|
237
|
+
enums,
|
|
238
|
+
typePrefix
|
|
239
|
+
});
|
|
240
|
+
if (fields.length > 0) {
|
|
241
|
+
const rootMessageKey = `${typePrefix}${rootMessageName}`;
|
|
242
|
+
messages.set(rootMessageKey, fields);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const context = {
|
|
246
|
+
messages,
|
|
247
|
+
enums,
|
|
248
|
+
typePrefix: typePrefix || null
|
|
249
|
+
};
|
|
250
|
+
if (services.length > 0) generateServices(services, context);
|
|
251
|
+
return `
|
|
252
|
+
syntax = "proto3";
|
|
253
|
+
package ${packageName};
|
|
254
|
+
|
|
255
|
+
${[
|
|
256
|
+
Array.from(enums.values()).map((enumDef) => enumDef.join("\n")),
|
|
257
|
+
Array.from(messages.entries()).map(([name, fields]) => `message ${name} {\n${fields.map((field) => ` ${field}`).join("\n")}\n}`),
|
|
258
|
+
services.length > 0 ? generateServices(services, context) : []
|
|
259
|
+
].filter((strings) => !!strings.length).map((strings) => strings.join("\n\n")).join("\n\n")}
|
|
260
|
+
`.trim();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
//#endregion
|
|
264
|
+
export { UnsupportedTypeException, zodToProtobuf };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@globalart/zod-to-proto",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "GlobalArt, Inc"
|
|
6
|
+
},
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"format": "prettier --write \"**/*.ts\"",
|
|
23
|
+
"test": "jest --runInBand --passWithNoTests",
|
|
24
|
+
"test:cov": "jest --coverage --passWithNoTests",
|
|
25
|
+
"coveralls": "yarn run test:cov --coverageReporters=text-lcov | coveralls",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"build:watch": "tsdown --watch",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"publish:dev": "npm publish --access public --tag dev",
|
|
30
|
+
"publish:npm": "release-it --config ../../.release-it.json"
|
|
31
|
+
},
|
|
32
|
+
"description": "OpenID Connect strategy for GlobalArt SSO integration",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/express": "^5.0.0",
|
|
35
|
+
"@types/node": "^24.9.1",
|
|
36
|
+
"@types/passport": "^1.0.17",
|
|
37
|
+
"prettier": "^3.6.2",
|
|
38
|
+
"release-it": "19.0.6",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
40
|
+
"tsdown": "0.16.4",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"inflection": "3.0.2",
|
|
45
|
+
"zod": "4.1.12"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|