zod-to-x
Version:
Multi language types generation from Zod schemas.
400 lines (399 loc) • 17.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Zod2Go = void 0;
const case_1 = __importDefault(require("case"));
const core_1 = require("../../core");
const number_limits_1 = require("../../utils/number_limits");
const libs_1 = require("./libs");
const options_1 = require("./options");
/**
* Transpiler for Zod schemas to Go structs and types.
*/
class Zod2Go extends core_1.Zod2X {
constructor(opt = {}) {
super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
this.commentKey = "//";
// ── Primitive type methods ──────────────────────────────────────────────
this.getStringType = () => "string";
this.getBooleanType = () => "bool";
this.getAnyType = () => "any";
this.getDateType = () => {
this.imports.add(this.lib.timePackage);
return "time.Time";
};
this.getNumberType = (isInt, range) => {
if (!isInt)
return "float64";
const min = range.min;
const max = range.max;
if (min !== undefined &&
max !== undefined &&
min >= number_limits_1.INT32_RANGES[0] &&
max <= number_limits_1.INT32_RANGES[1]) {
return "int32";
}
return "int64";
};
/** Ex: map[TypeA]struct{} */
this.getSetType = (itemType) => `map[${itemType}]struct{}`;
/** Ex: map[KeyType]ValueType */
this.getMapType = (keyType, valueType) => `map[${keyType}]${valueType}`;
/** Ex: map[KeyType]ValueType */
this.getRecordType = (keyType, valueType) => `map[${keyType}]${valueType}`;
/** Go has no native tuple; use []any */
this.getTupleType = (_itemsType) => "[]any";
/** Go has no native union; use any */
this.getUnionType = (_itemsType) => "any";
/** Handled entirely in transpileIntersection via struct embedding */
this.getIntersectionType = () => "";
this.lib = (0, libs_1.getLibs)();
}
runBefore() {
var _a;
this.preImports.add(`package ${(_a = this.opt.packageName) !== null && _a !== void 0 ? _a : "models"}`);
}
runAfter() {
this._consolidateImports();
}
/**
* Consolidates all collected imports into a proper Go import block.
* Single import → `import "pkg"`, multiple → `import (\n\t"pkg"\n)`.
*/
_consolidateImports() {
if (this.imports.size === 0)
return;
const sorted = Array.from(this.imports).sort();
let block;
if (sorted.length === 1) {
block = `import ${sorted[0]}`;
}
else {
block = `import (\n${sorted.map((s) => `\t${s}`).join("\n")}\n)`;
}
// Replace the individual entries with the consolidated block
this.imports.clear();
this.imports.add(block);
}
addImportFromFile(filename, namespace) {
const base = filename.endsWith(".go") ? filename.slice(0, -3) : filename;
return `${namespace} "./${base}"`;
}
getTypeFromExternalNamespace(namespace, typeName) {
return `${namespace}.${typeName}`;
}
addExtendedType(name, parentNamespace, aliasOf, opt) {
var _a, _b;
const extendedType = (opt === null || opt === void 0 ? void 0 : opt.isInternal)
? aliasOf
: this.getTypeFromExternalNamespace(parentNamespace, aliasOf);
const templates = (_a = opt === null || opt === void 0 ? void 0 : opt.templates) !== null && _a !== void 0 ? _a : "";
const declaredTemplates = (_b = opt === null || opt === void 0 ? void 0 : opt.declaredTemplates) !== null && _b !== void 0 ? _b : "";
if ((opt === null || opt === void 0 ? void 0 : opt.type) === "alias" || (opt === null || opt === void 0 ? void 0 : opt.type) === "union") {
this.push0(`type ${name}${declaredTemplates} = ${extendedType}${templates}\n`);
}
else {
// Struct embedding: type ChildName struct { ParentType }
this.push0(`type ${name}${declaredTemplates} struct {`);
this.push1(`${extendedType}${templates}`);
this.push0(`}\n`);
}
}
getGenericTemplatesTranslation(data) {
if ((data instanceof core_1.ASTObject || data instanceof core_1.ASTDefinition) &&
data.templatesTranslation.length > 0) {
return ("[" +
data.templatesTranslation
.map((t) => {
if (this.isExternalTypeImport(t)) {
this.addExternalTypeImport(t);
return this.getTypeFromExternalNamespace(t.parentNamespace, t.aliasOf);
}
else {
return t.aliasOf;
}
})
.join(", ") +
"]");
}
}
/**
* Emits an alias/extension declaration early when a node references another layered type.
*/
checkExtendedTypeInclusion(data, type) {
const isStruct = data instanceof core_1.ASTObject ||
(data instanceof core_1.ASTIntersection && data.newObject !== undefined);
const translatedTemplates = this.getGenericTemplatesTranslation(data);
const templates = translatedTemplates || undefined;
// For declared-template fallback on the declared (definition) side
const declaredTemplates = !translatedTemplates && data instanceof core_1.ASTObject && data.templates.size > 0
? `[${[...data.templates].map((t) => `${t} any`).join(", ")}]`
: undefined;
if (this.isExternalTypeImport(data)) {
if (data.aliasOf) {
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
type: isStruct ? undefined : type,
templates,
declaredTemplates,
});
this.addExternalTypeImport(data);
}
return true;
}
else if (data.aliasOf) {
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
type: isStruct ? undefined : type,
isInternal: true,
templates,
declaredTemplates,
});
return true;
}
return false;
}
getLiteralStringType(value, parentEnumNameKey) {
if (parentEnumNameKey) {
// Go constants cannot be used as types; use the parent enum type name
return parentEnumNameKey[0];
}
// Go has no literal types; return the underlying primitive type
if (typeof value === "boolean")
return "bool";
if (typeof value === "number")
return isNaN(value) ? "float64" : Number.isInteger(value) ? "int64" : "float64";
return "string";
}
// ── Composite type methods ──────────────────────────────────────────────
/** Ex: []TypeA, [][]TypeA */
getArrayType(arrayType, arrayDeep) {
let output = `[]${arrayType}`;
for (let i = 0; i < arrayDeep - 1; i++) {
output = `[]${output}`;
}
return output;
}
// ── Transpile methods ───────────────────────────────────────────────────
transpileAliasedType(data) {
if (this.checkExtendedTypeInclusion(data, "alias")) {
return;
}
this.addComment(data.description);
let extendedType;
if (data instanceof core_1.ASTArray) {
extendedType = this.getAttributeType(data.item);
}
else {
extendedType = this.getAttributeType(data);
}
this.push0(`type ${data.name} = ${extendedType}\n`);
}
/**
* Emit a Go enum using a typed string or int alias + const block.
*
* All-string values:
* type EnumItem string
* const (
* EnumItemEnum1 EnumItem = "Enum1"
* )
*
* All-int values:
* type EnumItem int
* const (
* EnumItemNativeEnum1 EnumItem = 1
* )
*
* Mixed (int + string): untyped constants with warning comment.
*/
transpileEnum(data) {
if (this.checkExtendedTypeInclusion(data, "alias")) {
return;
}
this.addComment(data.description);
const allStrings = data.values.every(([, v]) => typeof v === "string");
const allInts = data.values.every(([, v]) => typeof v === "number");
if (allStrings) {
this.push0(`type ${data.name} string\n`);
this.push0(`const (`);
data.values.forEach(([key, value]) => {
const constName = `${data.name}${case_1.default.pascal(key)}`;
this.push1(`${constName} ${data.name} = "${value}"`);
});
this.push0(`)\n`);
}
else if (allInts) {
this.push0(`type ${data.name} int\n`);
this.push0(`const (`);
data.values.forEach(([key, value]) => {
const constName = `${data.name}${case_1.default.pascal(key)}`;
this.push1(`${constName} ${data.name} = ${value}`);
});
this.push0(`)\n`);
}
else {
// Mixed types — Go cannot express this as a single typed enum.
// Declare as `any` so struct fields can reference the type name.
this.push0(`type ${data.name} = any\n`);
this.output.push(`// ${data.name}: mixed-type enum — no single Go base type available`);
this.push0(`const (`);
data.values.forEach(([key, value]) => {
const constName = `${data.name}${case_1.default.pascal(key)}`;
const v = typeof value === "string" ? `"${value}"` : `${value}`;
this.push1(`${constName} = ${v}`);
});
this.push0(`)\n`);
}
}
/**
* Go union: emit `type Name any` with a comment listing possible types.
*
* For discriminated unions: emit a marker interface + marker stubs on each
* member type + an `UnmarshalXxx` helper that dispatches on the discriminant
* key using a `json.RawMessage` probe (uniform for string, bool, and number
* discriminant values).
*/
transpileUnion(data) {
if (this.checkExtendedTypeInclusion(data, "union")) {
return;
}
this.addComment(data.description);
const optionNames = data.options.map(this.getAttributeType.bind(this));
if (data.discriminantKey) {
const methodName = `is${data.name}`;
this.push0(`// ${data.name} is a discriminated union on "${data.discriminantKey}".`);
this.push0(`// Possible types: ${optionNames.join(", ")}`);
this.push0(`type ${data.name} interface {`);
this.push1(`${methodName}()`);
this.push0(`}\n`);
// Marker stubs: one no-op method per member type so each satisfies the interface.
// For generic instantiations (e.g. "HttpSuccessfulResponse[SomeDtoResult]") we emit
// a generic receiver "func (t Base[T]) isXxx() {}" using the base type name only.
for (const name of optionNames) {
const bracketIdx = name.indexOf("[");
const receiver = bracketIdx !== -1 ? `${name.slice(0, bracketIdx)}[T]` : name;
this.push0(`func (t ${receiver}) ${methodName}() {}\n`);
}
// UnmarshalXxx helper — only when every member has a discriminant value in the AST.
const optionsData = data.options.map((opt, i) => {
var _a;
return ({
typeName: optionNames[i],
discriminantValue: (_a = opt.constraints) === null || _a === void 0 ? void 0 : _a.discriminantValue,
});
});
const allHaveDiscriminantValue = optionsData.every((o) => o.discriminantValue !== undefined);
if (allHaveDiscriminantValue) {
this.imports.add(this.lib.jsonPackage);
this.imports.add(this.lib.fmtPackage);
const probeField = data.discriminantKey.charAt(0).toUpperCase() + data.discriminantKey.slice(1);
this.push0(`// Unmarshal${data.name} deserializes JSON into the correct ${data.name} concrete type`);
this.push0(`// by probing the "${data.discriminantKey}" discriminant field.`);
this.push0(`func Unmarshal${data.name}(data []byte) (${data.name}, error) {`);
this.push1(`var probe struct {`);
this.push2(`${probeField} json.RawMessage \`json:"${data.discriminantKey}"\``);
this.push1(`}`);
this.push1(`if err := json.Unmarshal(data, &probe); err != nil {`);
this.push2(`return nil, err`);
this.push1(`}`);
this.push1(`switch string(probe.${probeField}) {`);
for (const opt of optionsData) {
const dv = opt.discriminantValue;
// Determine raw JSON representation of the case value.
// String literals appear in JSON with surrounding quotes ("Enum1" → "Enum1").
// Bool and number literals appear without quotes (true → true, 42 → 42).
const isStringLiteral = isNaN(Number(dv)) && dv !== "true" && dv !== "false";
const caseVal = isStringLiteral ? `\`"${dv}"\`` : `"${dv}"`;
this.push1(`case ${caseVal}:`);
this.push2(`var v ${opt.typeName}`);
this.push2(`if err := json.Unmarshal(data, &v); err != nil {`);
this.push3(`return nil, err`);
this.push2(`}`);
this.push2(`return v, nil`);
}
this.push1(`}`);
this.push1(`return nil, fmt.Errorf("failed to deserialize ${data.name}: unknown discriminator %s", string(probe.${probeField}))`);
this.push0(`}\n`);
}
}
else {
this.push0(`// ${data.name} is a union of: ${optionNames.join(", ")}`);
this.push0(`type ${data.name} = any\n`);
}
}
/**
* Go intersection: struct embedding.
*
* type IntersectionItem struct {
* ObjectItem
* OtherObjectItem
* }
*/
transpileIntersection(data) {
if (this.checkExtendedTypeInclusion(data)) {
return;
}
this.addComment(data.description);
if (data.newObject) {
// Flatten the merged object into a plain struct
this._transpileStructBody(data.newObject);
}
else {
// Embed both sides
const leftType = this.getAttributeType(data.left);
const rightType = this.getAttributeType(data.right);
this.push0(`type ${data.name} struct {`);
this.push1(leftType);
this.push1(rightType);
this.push0(`}\n`);
}
}
transpileStruct(data) {
if (this.checkExtendedTypeInclusion(data)) {
return;
}
this.addComment(data.description);
this._transpileStructBody(data);
}
/** Render a Go struct body for an ASTObject. */
_transpileStructBody(data) {
const templateParams = data.templates.size > 0
? `[${[...data.templates].map((t) => `${t} any`).join(", ")}]`
: "";
this.push0(`type ${data.name}${templateParams} struct {`);
const hasProperties = Object.keys(data.properties).length > 0;
if (!hasProperties) {
this.push0(`}\n`);
return;
}
for (const [key, value] of Object.entries(data.properties)) {
const fieldName = this.opt.keepKeys === true ? key : case_1.default.pascal(key);
this._transpileMember(fieldName, key, value);
}
this.push0(`}\n`);
}
/** Render a single struct field: `FieldName Type \`json:"key"\`` */
_transpileMember(fieldName, originalKey, memberNode) {
const isOptionalOrNullable = memberNode.isOptional || memberNode.isNullable;
let varType = this.getAttributeType(memberNode);
// Optional/nullable fields use pointer types
if (isOptionalOrNullable) {
// Avoid double-pointer for types already expressed as pointers/interfaces
if (!varType.startsWith("*") && varType !== "any") {
varType = `*${varType}`;
}
}
if (memberNode.description && !memberNode.name && !this.isTranspilerable(memberNode)) {
this.addComment(memberNode.description, `\n${this.indent[1]}`);
}
const tag = this._buildJsonTag(originalKey, isOptionalOrNullable !== null && isOptionalOrNullable !== void 0 ? isOptionalOrNullable : false);
this.push1(`${fieldName} ${varType}${tag}`);
}
_buildJsonTag(originalKey, omitempty) {
if (this.opt.useJsonTags === false)
return "";
const flags = omitempty ? `,omitempty` : "";
return ` \`json:"${originalKey}${flags}"\``;
}
}
exports.Zod2Go = Zod2Go;