Skip to content

Commit

Permalink
fix: replace es2019 class fields with local members to address #3611
Browse files Browse the repository at this point in the history
  • Loading branch information
alon-gb committed Dec 28, 2024
1 parent f7ad261 commit 3acdf74
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 24 deletions.
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

0 comments on commit 3acdf74

Please sign in to comment.