timdeschryver / zod-fixture

Creating fixtures based on zod schemas
https://zod-fixture.timdeschryver.dev/
MIT License
120 stars 10 forks source link

Matching generators on simple custom types is tricky #79

Closed jjmalina closed 1 year ago

jjmalina commented 1 year ago

If I have a schema like:

import { z } from 'zod';

const MySchema = z.object({
  name: z.string(),
  nicknames: z.optional(z.array(z.string())),
});

Then this generator won't work:

import { Fixture, Generator } from 'zod-fixture';

const data = new Fixture().extend([
  Generator({
    schema: z.optional(z.array(z.string())),
    filter: ({ context }) => context.path.at(-1) === 'nicknames',
    output: () => ['The Raven'],
  }),
]).fromSchema(MySchema);

In order to get it to work I need to do this:

const NicknamesSchema = z.optional(z.array(z.string()));
const MySchema = z.object({
  name: z.string(),
  nicknames:  NicknamesSchema,
});

const data = new Fixture().extend([
  Generator({
    schema: NicknamesSchema,
    filter: ({ context }) => context.path.at(-1) === 'nicknames',
    output: () => ['The Raven'],
  }),
]).fromSchema(MySchema);

Coming from zod-fixture v1, this feels like a bit of a setback because I can't match on just the property name anymore when I'm trying to generate the data for a nested schema. For larger nested schemas I'm ok with separately defining them to export and then importing it into my tests to match on, but for something as small and common as an array of primitives, is there a way around this?

THEtheChad commented 1 year ago

For your particular example, you could do:

import { ZodOptional } from 'zod'

const NicknameGenerator = Generator({
    schema: ZodOptional,
    filter: ({ context }) => context.path.at(-1) === 'nicknames',
    output: () => ['The Raven'],
})

const NicknamesSchema = z.optional(z.array(z.string()));

const data = new Fixture().extend(NicknameGenerator).fromSchema(MySchema);

But I think the bigger issue you're highlighting here is that you don't care what the type is, you just want to replace anything that matches the key, which isn't possible with the current implementation of Generator.

Does my example at least provide you with a path forward?

THEtheChad commented 1 year ago

I thought about this some more. I think you're targeting the wrong thing here. A nickname is a string.

import { Fixture, Generator } from 'zod-fixture';

const NicknameGenerator = Generator({
   schema: ZodString,
   filter: ({ context }) => context.path.at(-2) === 'nicknames',
   output: () => 'The Raven'
})

const data = new Fixture().extend(NicknameGenerator).fromSchema(MySchema);

OR

import { Fixture, Generator } from 'zod-fixture';
import { z } from 'zod';

const nicknameSchema = z.custom<string>((v) => typeof v === 'string')

const NicknameGenerator = Generator({
   schema: nicknameSchema,
   output: () => 'The Raven'
})

const MySchema = z.object({
  name: z.string(),
  nicknames: z.optional(z.array(nicknameSchema)),
});

const data = new Fixture().extend(NicknameGenerator).fromSchema(MySchema);
timdeschryver commented 1 year ago

While I agree with the examples @THEtheChad what do you think about making schema optional for custom defined schemas? I think it would makes custom generators easier to write, especially when using z.instanceof and z.custom?

jjmalina commented 1 year ago

I thought about this some more. I think you're targeting the wrong thing here. A nickname is a string.

import { Fixture, Generator } from 'zod-fixture';

const NicknameGenerator = Generator({
   schema: ZodString,
   filter: ({ context }) => context.path.at(-2) === 'nicknames',
   output: () => 'The Raven'
})

const data = new Fixture().extend(NicknameGenerator).fromSchema(MySchema);

OR

import { Fixture, Generator } from 'zod-fixture';
import { z } from 'zod';

const nicknameSchema = z.custom<string>((v) => typeof v === 'string')

const NicknameGenerator = Generator({
   schema: nicknameSchema,
   output: () => 'The Raven'
})

const MySchema = z.object({
  name: z.string(),
  nicknames: z.optional(z.array(nicknameSchema)),
});

const data = new Fixture().extend(NicknameGenerator).fromSchema(MySchema);

sorry I should have given my real example and not this contrived one. what I actually have is an optional array of UUID strings. in my test I have to override the array of UUIDs to be either absent or have values which I'm providing directly.

I'll give ZodOptional a try. At least for now I was able to work around by sharing the array of string schema between my schema and my tests.

jjmalina commented 1 year ago

While I agree with the examples @THEtheChad what do you think about making schema optional for custom defined schemas? I think it would makes custom generators easier to write, especially when using z.instanceof and z.custom?

I think if schema was optional and filter was called first then it would be easier to override the types like I have in my case

github-actions[bot] commented 1 year ago

:tada: This issue has been resolved in version 2.2.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

timdeschryver commented 1 year ago

@jjmalina v2.2.0 makes the schema optional. Feel free to provide feedback ;)