Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: replace es2019 class fields with local members to address #3611 #3926

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions deno/lib/__tests__/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,62 @@ test("readonly in ZodEnumDef", () => {
let _t!: z.ZodEnumDef<readonly ["a", "b"]>;
_t;
});

test("enum parsing works after cloning", () => {
function deepClone(value: any) {
// Handle null and undefined
if (value == null) {
return value;
}

// Get the constructor and prototype
const constructor = Object.getPrototypeOf(value).constructor;

// Handle primitive wrappers
if ([Boolean, Number, String].includes(constructor)) {
return new constructor(value);
}

// Handle Date objects
if (constructor === Date) {
return new Date(value.getTime());
}

// Handle Arrays
if (constructor === Array) {
return value.map((item: any) => deepClone(item));
}

// Handle basic RegExp
if (constructor === RegExp) {
return new RegExp(value.source, value.flags);
}

// Handle Objects (including custom classes)
if (typeof value === 'object') {
// Create new instance while preserving the prototype chain
const cloned = Object.create(Object.getPrototypeOf(value));

// Clone own properties
const descriptors = Object.getOwnPropertyDescriptors(value);
for (const [key, descriptor] of Object.entries(descriptors)) {
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value);
}
Object.defineProperty(cloned, key, descriptor);
}

return cloned;
}

// Return primitives and functions as is
return value;
}

const schema = {
mood: z.enum(["happy", "sad", "neutral", "feisty"]),
};
z.object(schema).safeParse({ mood: "feisty" }); //Sanity check before cloning
const clonedDeep2 = deepClone(schema);
z.object(clonedDeep2).safeParse({ mood: "feisty" });
});
24 changes: 12 additions & 12 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4342,14 +4342,19 @@ function createZodEnum(
});
}

const lookupSymbol = Symbol("lookup");
export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
T[number],
ZodEnumDef<T>,
T[number]
> {
#cache: Set<T[number]> | undefined;

private [lookupSymbol]: Set<T[number]> | undefined;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
let lookup = this[lookupSymbol];
if (!lookup) {
lookup = new Set(this._def.values);
this[lookupSymbol] = lookup;
}
if (typeof input.data !== "string") {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;
Expand All @@ -4361,14 +4366,9 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
return INVALID;
}

if (!this.#cache) {
this.#cache = new Set(this._def.values);
}

if (!this.#cache.has(input.data)) {
if (!lookup.has(input.data)) {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;

addIssueToContext(ctx, {
received: ctx.data,
code: ZodIssueCode.invalid_enum_value,
Expand Down Expand Up @@ -4458,7 +4458,7 @@ export class ZodNativeEnum<T extends EnumLike> extends ZodType<
ZodNativeEnumDef<T>,
T[keyof T]
> {
#cache: Set<T[keyof T]> | undefined;
private [lookupSymbol]: Set<T[keyof T]> | undefined;
_parse(input: ParseInput): ParseReturnType<T[keyof T]> {
const nativeEnumValues = util.getValidEnumValues(this._def.values);

Expand All @@ -4476,11 +4476,11 @@ export class ZodNativeEnum<T extends EnumLike> extends ZodType<
return INVALID;
}

if (!this.#cache) {
this.#cache = new Set(util.getValidEnumValues(this._def.values));
if (!this[lookupSymbol]) {
this[lookupSymbol] = new Set(util.getValidEnumValues(this._def.values));
}

if (!this.#cache.has(input.data)) {
if (!this[lookupSymbol].has(input.data)) {
const expectedValues = util.objectValues(nativeEnumValues);

addIssueToContext(ctx, {
Expand Down
59 changes: 59 additions & 0 deletions src/__tests__/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,62 @@ test("readonly in ZodEnumDef", () => {
let _t!: z.ZodEnumDef<readonly ["a", "b"]>;
_t;
});

test("enum parsing works after cloning", () => {
function deepClone(value: any) {
// Handle null and undefined
if (value == null) {
return value;
}

// Get the constructor and prototype
const constructor = Object.getPrototypeOf(value).constructor;

// Handle primitive wrappers
if ([Boolean, Number, String].includes(constructor)) {
return new constructor(value);
}

// Handle Date objects
if (constructor === Date) {
return new Date(value.getTime());
}

// Handle Arrays
if (constructor === Array) {
return value.map((item: any) => deepClone(item));
}

// Handle basic RegExp
if (constructor === RegExp) {
return new RegExp(value.source, value.flags);
}

// Handle Objects (including custom classes)
if (typeof value === 'object') {
// Create new instance while preserving the prototype chain
const cloned = Object.create(Object.getPrototypeOf(value));

// Clone own properties
const descriptors = Object.getOwnPropertyDescriptors(value);
for (const [key, descriptor] of Object.entries(descriptors)) {
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value);
}
Object.defineProperty(cloned, key, descriptor);
}

return cloned;
}

// Return primitives and functions as is
return value;
}

const schema = {
mood: z.enum(["happy", "sad", "neutral", "feisty"]),
};
z.object(schema).safeParse({ mood: "feisty" }); //Sanity check before cloning
const clonedDeep2 = deepClone(schema);
z.object(clonedDeep2).safeParse({ mood: "feisty" });
});
24 changes: 12 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4342,14 +4342,19 @@ function createZodEnum(
});
}

const lookupSymbol = Symbol("lookup");
export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
T[number],
ZodEnumDef<T>,
T[number]
> {
#cache: Set<T[number]> | undefined;

private [lookupSymbol]: Set<T[number]> | undefined;
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
let lookup = this[lookupSymbol];
if (!lookup) {
lookup = new Set(this._def.values);
this[lookupSymbol] = lookup;
}
if (typeof input.data !== "string") {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;
Expand All @@ -4361,14 +4366,9 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<
return INVALID;
}

if (!this.#cache) {
this.#cache = new Set(this._def.values);
}

if (!this.#cache.has(input.data)) {
if (!lookup.has(input.data)) {
const ctx = this._getOrReturnCtx(input);
const expectedValues = this._def.values;

addIssueToContext(ctx, {
received: ctx.data,
code: ZodIssueCode.invalid_enum_value,
Expand Down Expand Up @@ -4458,7 +4458,7 @@ export class ZodNativeEnum<T extends EnumLike> extends ZodType<
ZodNativeEnumDef<T>,
T[keyof T]
> {
#cache: Set<T[keyof T]> | undefined;
private [lookupSymbol]: Set<T[keyof T]> | undefined;
_parse(input: ParseInput): ParseReturnType<T[keyof T]> {
const nativeEnumValues = util.getValidEnumValues(this._def.values);

Expand All @@ -4476,11 +4476,11 @@ export class ZodNativeEnum<T extends EnumLike> extends ZodType<
return INVALID;
}

if (!this.#cache) {
this.#cache = new Set(util.getValidEnumValues(this._def.values));
if (!this[lookupSymbol]) {
this[lookupSymbol] = new Set(util.getValidEnumValues(this._def.values));
}

if (!this.#cache.has(input.data)) {
if (!this[lookupSymbol].has(input.data)) {
const expectedValues = util.objectValues(nativeEnumValues);

addIssueToContext(ctx, {
Expand Down