zod-to-x
Version:
Multi language types generation from Zod schemas.
450 lines (449 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Zod2Ast = void 0;
const zod_1 = require("zod");
const logger_1 = require("../utils/logger");
const errors_1 = require("./errors");
/**
* This class creates AST nodes used to transpile Zod Schemas to other languages.
* Simply create an instance and call build with a ZodObject to obtain a list with transpilerable
* nodes.
*/
class Zod2Ast {
constructor(opt = {}) {
var _a;
this.nodes = new Map();
this.lazyPointers = [];
this.warnings = [];
this.opt = Object.assign(Object.assign({}, opt), { strict: (_a = opt.strict) !== null && _a !== void 0 ? _a : true });
}
/**
* Check if the layer of the item is compatible with the layer of the schema. If does and the
* transpilerable item is in a different file, it returns the file name.
*
* @param itemName
* @param layerMetadata
* @returns
*/
_getTranspilerableFile(itemName, metadata) {
var _a;
const layer = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.parentLayer) !== null && _a !== void 0 ? _a : metadata === null || metadata === void 0 ? void 0 : metadata.layer;
if (this.opt.layer && layer) {
if (this.opt.layer.index < layer.index) {
throw new errors_1.BadLayerDefinitionError(`${itemName}: Layer with number ${this.opt.layer.index} can only use models` +
`from the same or lower layer. Found layer with number ${layer.index}`);
}
if (this.opt.layer.file !== layer.file) {
return {
parentFile: layer.file,
parentNamespace: layer.namespace,
parentTypeName: metadata === null || metadata === void 0 ? void 0 : metadata.parentTypeName,
};
}
}
return {};
}
/**
* Transpilerable items are treated as references in the AST
* @param ref
* @param refType
* @param discriminantValue
* @returns
*/
_createDefinition(ref, refType, discriminantValue, parentNamespace) {
return {
type: "definition",
reference: ref,
referenceType: refType,
discriminantValue,
parentNamespace,
};
}
/**
* Extracts and formats the enumeration values from a given ZodEnum or ZodNativeEnum schema.
* @param schema - A ZodEnum or ZodNativeEnum schema containing the enumeration values.
* @returns A list of key-value pairs where the key is a formatted string and the value
* is either a string or a number.
*/
_getEnumValues(schema) {
if (schema instanceof zod_1.ZodEnum) {
return Object.entries(schema.Enum).map(([key, value]) => {
// Creates a string key if it starts with number.
key = isNaN(Number(key.at(0))) ? key : `"${key}"`;
return [key, value];
});
}
else {
return Object.entries(schema.enum)
.filter(([key, _value]) => isNaN(Number(key)))
.map(([key, value]) => {
// Creates a string key if it starts with number.
key = isNaN(Number(key.at(0))) ? key : `"${key}"`;
return [key, value];
});
}
}
/**
* Intersects the properties of two AST nodes and returns the combined properties.
*
* @param left - The left AST definition to intersect.
* @param right - The right AST definition to intersect.
* @returns An object containing the combined properties of the left and right AST nodes.
*/
_intersectAstNodes(left, right) {
const leftData = this.nodes.get(left.reference);
const rightData = this.nodes.get(right.reference);
return {
properties: Object.assign(Object.assign({}, leftData.properties), rightData.properties),
};
}
/**
* Merges multiple AST definitions into a single AST object containing combined properties.
* - Equal properties mush have the same type and array dimension.
* - If a property is optional in one definition and required in another, it will be considered
* optional in the merged object.
* - If a property is nullable in one definition and non-nullable in another, it will be
* considered nullable in the merged object.
*
* @param options - An array of AST definitions to be merged.
* @returns An object containing the merged properties.
* @throws AstNodeError - If properties with different types or array dimensions are encountered.
*/
_unionAstNodes(options) {
const data = options.map((i) => this.nodes.get(i.reference));
return {
properties: data.reduce((acc, i, j) => {
var _a, _b;
for (const key in i.properties) {
if (acc[key]) {
acc[key] = structuredClone(acc[key]);
if (acc[key].type !== i.properties[key].type) {
this.warnings.push(`Merging properties with different types: ${acc[key].type} ` +
`(from ${(_a = data[j - 1]) === null || _a === void 0 ? void 0 : _a.name}) and ${i.properties[key].type} ` +
`(from ${i.name})`);
acc[key].type = i.properties[key].type;
}
if (acc[key].arrayDimension !== i.properties[key].arrayDimension) {
this.warnings.push(`Merging properties with different array dimensions: ` +
`${acc[key].arrayDimension} (from ${(_b = data[j - 1]) === null || _b === void 0 ? void 0 : _b.name}) and ` +
`${i.properties[key].arrayDimension} (from ${i.name})`);
acc[key].arrayDimension = Math.max(acc[key].arrayDimension || 0, i.properties[key].arrayDimension || 0);
}
if (acc[key].isNullable !== i.properties[key].isNullable) {
acc[key].isNullable = true;
}
if (acc[key].isOptional !== i.properties[key].isOptional) {
acc[key].isOptional = true;
}
if (i.properties[key].description) {
acc[key].description = i.properties[key].description;
}
}
else {
acc[key] = i.properties[key];
}
}
return acc;
}, {}),
};
}
_getNames(schema, errorString) {
var _a;
const name = (_a = schema._zod2x) === null || _a === void 0 ? void 0 : _a.typeName;
if (!name) {
throw new errors_1.AstTypeNameDefinitionError(errorString);
}
return Object.assign({ name, zodTypeName: schema._def.typeName }, this._getTranspilerableFile(name, schema._zod2x));
}
_getEnumAst(schema, opt) {
const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodEnum/ZodNativeEnum type must have a typeName. Use zod2x method to provide one.");
const item = {
type: zodTypeName,
name,
values: this._getEnumValues(schema),
description: schema._def.description,
parentFile,
parentNamespace,
parentTypeName,
isFromDiscriminatedUnion: opt === null || opt === void 0 ? void 0 : opt.isInjectedEnum,
};
if (!this.nodes.has(name)) {
this.nodes.set(name, item);
}
return this._createDefinition(name, zodTypeName, undefined, parentNamespace);
}
_getObjectAst(schema, opt) {
const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodObject type must have a typeName. Use zod2x method to provide one.");
let discriminantValue = undefined;
const shape = schema._def.shape();
if (!this.nodes.has(name)) {
const properties = {};
for (const key in shape) {
properties[key] = this._zodToAST(shape[key]);
}
this.nodes.set(name, {
type: zod_1.ZodFirstPartyTypeKind.ZodObject,
name,
properties,
description: schema.description,
parentFile,
parentNamespace,
parentTypeName,
});
}
if (opt === null || opt === void 0 ? void 0 : opt.discriminantKey) {
const item = this.nodes.get(name);
if (Object.keys(item.properties).includes(opt.discriminantKey)) {
const key = opt.discriminantKey;
if (item.properties[key].type === zod_1.ZodFirstPartyTypeKind.ZodLiteral) {
/* Used for serialization purposes, it is parsed as string for
* convenience */
discriminantValue = String(item.properties[key].value);
}
else {
console.warn(`Consider to set '${key}' key of '${name}' as ZodLiteral`);
}
}
}
return this._createDefinition(name, zodTypeName, discriminantValue, parentTypeName ? undefined : parentNamespace);
}
_getUnionAst(schema) {
const def = schema._def;
const discriminator = schema instanceof zod_1.ZodDiscriminatedUnion ? schema._def.discriminator : undefined;
const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodUnion/ZodDiscriminatedUnion type must have a typeName. " +
"Use zod2x method to provide one.");
const item = {
type: zodTypeName,
name,
options: def.options.map((i) => this._zodToAST(i, { discriminantKey: discriminator })),
description: schema.description,
discriminantKey: discriminator,
parentFile,
parentNamespace,
parentTypeName,
};
if (!def.options.every((i) => i instanceof zod_1.ZodObject)) {
this.warnings.push("Union of non-object types is a bad data modeling practice, " +
"and could lead to unexpected results.");
}
else if (schema instanceof zod_1.ZodUnion) {
this.warnings.push("Using ZodUnion is a bad data modeling practice. " +
"Use ZodDiscriminatedUnion instead.");
item.newObject = {
name,
type: zod_1.ZodFirstPartyTypeKind.ZodObject,
properties: this._unionAstNodes(item.options).properties,
description: (schema.description ? `${schema.description} - ` : "") +
`Built from union of ` +
`${item.options.map((i) => i.reference).join(", ")}`,
};
}
if (name && !this.nodes.has(name)) {
this.nodes.set(name, item);
}
return this._createDefinition(name, zodTypeName, undefined, parentTypeName ? undefined : parentNamespace);
}
_getIntersectionAst(schema) {
const def = schema._def;
const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodIntersection type must have a typeName. Use zod2x method to provide one.");
const item = {
type: zod_1.ZodFirstPartyTypeKind.ZodIntersection,
name,
left: this._zodToAST(def.left),
right: this._zodToAST(def.right),
description: schema.description,
parentFile,
parentNamespace,
parentTypeName,
};
if (def.left._def.typeName !== "ZodObject" || def.right._def.typeName !== "ZodObject") {
this.warnings.push("Intersection of non-object is a bad data modeling practice, " +
"and could lead to unexpected results.");
}
else {
item.newObject = {
type: zod_1.ZodFirstPartyTypeKind.ZodObject,
name,
properties: this._intersectAstNodes(item.left, item.right).properties,
description: (schema.description ? `${schema.description} - ` : "") +
`Built from intersection of ` +
`${item.left.reference} and ` +
`${item.right.reference}`,
};
}
if (name && !this.nodes.has(name)) {
this.nodes.set(name, item);
}
return this._createDefinition(name, zodTypeName, undefined, parentTypeName ? undefined : parentNamespace);
}
/**
* Build the AST node of provided Zod Schema
* @param schema
* @returns
*/
_zodToAST(schema, opt) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const def = schema._def;
if (schema instanceof zod_1.ZodString) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodString,
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodNumber || schema instanceof zod_1.ZodBigInt) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodNumber,
constraints: {
min: (_a = def.checks.find((i) => i.kind === "min")) === null || _a === void 0 ? void 0 : _a.value,
max: (_b = def.checks.find((i) => i.kind === "max")) === null || _b === void 0 ? void 0 : _b.value,
isInt: schema instanceof zod_1.ZodBigInt ||
def.checks.find((i) => i.kind === "int") != undefined,
},
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodBoolean) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodBoolean,
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodDate) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodDate,
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodAny) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodAny,
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodNullable) {
const subSchema = this._zodToAST(def.innerType);
return Object.assign(Object.assign({ isNullable: true }, subSchema), { description: schema.description || subSchema.description });
}
else if (schema instanceof zod_1.ZodOptional) {
const subSchema = this._zodToAST(def.innerType);
return Object.assign(Object.assign({ isOptional: true }, subSchema), { description: schema.description || subSchema.description });
}
else if (schema instanceof zod_1.ZodDefault) {
const subSchema = this._zodToAST(def.innerType);
return Object.assign(Object.assign({}, subSchema), { description: schema.description || subSchema.description });
}
else if (schema instanceof zod_1.ZodArray) {
const subSchema = this._zodToAST(def.type);
return Object.assign(Object.assign({}, subSchema), { description: schema.description || subSchema.description, arrayDimension: Number.isInteger(subSchema.arrayDimension)
? ++subSchema.arrayDimension
: 1 });
}
else if (schema instanceof zod_1.ZodSet) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodSet,
value: this._zodToAST(def.valueType),
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodLiteral) {
let parentEnumName = undefined;
let parentEnumKey = undefined;
if ((_c = schema._zod2x) === null || _c === void 0 ? void 0 : _c.parentEnum) {
parentEnumName = (_e = (_d = schema._zod2x) === null || _d === void 0 ? void 0 : _d.parentEnum._zod2x) === null || _e === void 0 ? void 0 : _e.typeName;
parentEnumKey = (_g = this._getEnumValues((_f = schema._zod2x) === null || _f === void 0 ? void 0 : _f.parentEnum).find((i) => i[1] === def.value)) === null || _g === void 0 ? void 0 : _g[0];
this._zodToAST((_h = schema._zod2x) === null || _h === void 0 ? void 0 : _h.parentEnum, { isInjectedEnum: true });
}
return {
type: zod_1.ZodFirstPartyTypeKind.ZodLiteral,
value: def.value,
description: schema.description,
parentEnumName,
parentEnumKey,
};
}
else if (schema instanceof zod_1.ZodRecord) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodRecord,
key: this._zodToAST(def.keyType),
value: this._zodToAST(def.valueType),
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodLazy) {
/** Lazy items use to be recursive schemas of its own, so the are trated as another
* definition */
const lazySchema = def.getter();
const lazyPointer = this._createDefinition("pending", zod_1.ZodFirstPartyTypeKind.ZodAny);
this.lazyPointers.push([lazyPointer, lazySchema]);
return lazyPointer;
}
else if (schema instanceof zod_1.ZodTuple) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodTuple,
items: def.items.map(this._zodToAST.bind(this)),
description: schema.description,
};
}
else if (schema instanceof zod_1.ZodMap) {
return {
type: zod_1.ZodFirstPartyTypeKind.ZodMap,
key: this._zodToAST(def.keyType),
value: this._zodToAST(def.valueType),
description: schema.description,
};
/**
*
*
* Transpilerable items
*
*
* */
}
else if (schema instanceof zod_1.ZodNativeEnum || schema instanceof zod_1.ZodEnum) {
return this._getEnumAst(schema, opt);
}
else if (schema instanceof zod_1.ZodObject) {
return this._getObjectAst(schema, opt);
}
else if (schema instanceof zod_1.ZodUnion || schema instanceof zod_1.ZodDiscriminatedUnion) {
return this._getUnionAst(schema);
}
else if (schema instanceof zod_1.ZodIntersection) {
return this._getIntersectionAst(schema);
}
else {
logger_1.log.warn(`Unsupported Zod type: ${JSON.stringify(schema)}`);
return {
type: zod_1.ZodFirstPartyTypeKind.ZodAny,
description: `Unsupported Zod type: ${schema._def.typeName}`,
};
}
}
/**
* Create the AST identifying the nodes that can be transpiled.
* @param schema
* @returns Transpilerable nodes.
*/
build(schema) {
this._zodToAST(schema);
while (this.lazyPointers.length > 0) {
const [pointer, schema] = this.lazyPointers.shift();
const lazyResolve = this._zodToAST(schema);
/** Pointer to the pending AST node is updated with the lazy resolve */
Object.keys(pointer).forEach((key) => {
delete pointer[key];
});
Object.entries(lazyResolve).forEach(([key, value]) => {
pointer[key] = value;
});
}
if (this.opt.strict !== false && this.warnings.length > 0) {
throw new errors_1.AstNodeError(this.warnings.join("\n"));
}
return {
nodes: this.nodes,
warnings: this.warnings,
};
}
}
exports.Zod2Ast = Zod2Ast;