timdeschryver / zod-fixture

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

issue with generators on the examples provided in the docs #95

Open alexgherman opened 6 months ago

alexgherman commented 6 months ago

There seems to be an issue with the generators, even the examples provided in the docs are not returning the expected values.

TL;DR: run the stackblitz here to see the issue: https://stackblitz.com/edit/vitejs-vite-yg6ndm?file=src%2Fpreview.ts

Example 1:

  const totalVisitsGenerator = Generator({
    schema: ZodNumber,
    filter: ({ context }) => context.path.at(-1) === 'totalVisits',
    /**
     * The `context` provides a path to the current field
     *
     * {
     *   totalVisits: ...,
     *   nested: {
     *     totalVisits: ...,
     *   }
     * }
     *
     * Would match twice with the following paths:
     *   ['totalVisits']
     *   ['nested', 'totalVisits']
     */

    // returns a more realistic number of visits.
    output: ({ transform }) => transform.utils.random.int({ min: 0, max: 25 }),
  });

  const addressGenerator = Generator({
    schema: ZodObject,
    filter: ({ context }) => context.path.at(-1) === 'address',
    // returns a custom address object
    output: () => ({
      street: 'My Street',
      city: 'My City',
      state: 'My State',
    }),
  });

  const personSchema = z.object({
    name: z.string(),
    birthday: z.date(),
    address: z.object({
      street: z.string(),
      city: z.string(),
      state: z.string(),
    }),
    pets: z.array(z.object({ name: z.string(), breed: z.string() })),
    totalVisits: z.number().int().positive(),
  });

  const fixture = new Fixture({ seed: 38 }).extend([
    addressGenerator,
    totalVisitsGenerator,
  ]);
  const person = fixture.fromSchema(personSchema);
  console.log(person);

  // Expected:
// {
//  address: {
//      city: 'My City',
//      state: 'My State',
//      street: 'My Street',
//  },
//  birthday: new Date('1952-01-21T17:32:42.094Z'),
//  name: 'yxyzyskryqofekd',
//  pets: [
//      {
//          breed: 'dnlwozmxaigobrz',
//          name: 'vhvlrnsxroqpuma',
//      },
//      {
//          breed: 'ifbgglityarecl-',
//          name: 'c-lmtvotjcevmyi',
//      },
//      {
//          breed: 'fmylchvprjdgelk',
//          name: 'ydevqfcctdx-lin',
//      },
//  ],
//  totalVisits: 15,
// },

  // Actual:
  // {
  //   "name": "sdnlwozmxaigobr",
  //   "birthday": "2091-09-17T10:54:58.574Z",
  //   "address": { // incorrect address remains untouched
  //     "street": "c-lmtvotjcevmyi",
  //     "city": "ifbgglityarecl-",
  //     "state": "ydevqfcctdx-lin"
  //   },
  //   "pets": [
  //     {
  //       "name": "mylchvprjdgelkq",
  //       "breed": "ivrplyhts-yypas"
  //     },
  //     {
  //       "name": "rcrrkytqrdmzajo",
  //       "breed": "m-rfot-rvbqmlcu"
  //     },
  //     {
  //       "name": "adahtinsiooiwrj",
  //       "breed": "xdxkgurszvshnvg"
  //     }
  //   ],
  //   "totalVisits": 7
  // }

Example 2:

  const pxSchema = z.custom<`${number}px`>((val) => {
    return /^\d+px$/.test(val as string);
  });

  const StringGenerator = Generator({
    schema: ZodString,
    output: () => 'John Doe',
  });

  const PixelGenerator = Generator({
    schema: pxSchema,
    output: () => '100px',
  });

  const developerSchema = z.object({
    name: z.string().max(10),
    resolution: z.object({
      height: pxSchema,
      width: pxSchema,
    }),
  });

  const fixture = new Fixture({ seed: 7 }).extend([
    PixelGenerator,
    StringGenerator,
  ]);
  const developer = fixture.fromSchema(developerSchema);
  console.log(developer);

// Expected:
// {
//  name: 'John Doe',
//  resolution: {
//      height: '100px',
//      width: '100px',
//  },
// }

// Actual:
// {
//   "name": "mzzhauqgzw", // incorrect
//   "resolution": {
//     "height": "100px", // correct
//     "width": "100px" // correct
//   }
// }

What am I missing? And thank you for a great project! =)

timdeschryver commented 6 months ago

Hey @alexgherman thanks for the kind words. It seems like you're right and that there is something off within the StackBlitz, at first I thought it was an outdated version but even after updating to the latest version the issue still occurs.

The main issue is that the generator isn't registered/invoked because the ZodNumber schema can't be matched with zod's type. When you remove the schema property it should work.

    const totalVisitsGenerator = Generator({
//      👇 remove this 
//      schema: ZodNumber,
        filter: ({ context }) => {
            return context.path.at(-1) === 'totalVisits';
        },

Do you also experience this issue outside of StackBlitz, and if so in what environment? The reason I ask this is because when I copy paste the example to a local project, it just works.

alexgherman commented 6 months ago

ah I see! glad I'm not crazy, thank you @timdeschryver. I went down a rabbit hole trying different version combinations of zod and zod-fixture with the hope to find a combination that works :)

Either removing the schema altogether or defining the schema separately and sharing it with the generator did it in stackblitz: https://stackblitz.com/edit/vitejs-vite-mjxbz7?file=src%2Fpreview.ts

However, I see the same issue in my vite project. here's a reproducible fork, hopefully it will shed some more light on the issue: https://github.com/alexgherman/zod-fixture-issue-95

Seeing the same output as in stackblitz

[
  {
    "name": "yxyzyskryqofekd",
    "birthday": "1952-01-21T17:32:42.094Z",
    "address": {
      "street": "cvhvlrnsxroqpum",
      "city": "sdnlwozmxaigobr",
      "state": "zc-lmtvotjcevmy"
    },
    "pets": [
      {
        "name": "ifbgglityarecl-",
        "breed": "ydevqfcctdx-lin"
      },
      {
        "name": "fmylchvprjdgelk",
        "breed": "livrplyhts-yypa"
      },
      {
        "name": "jrcrrkytqrdmzaj",
        "breed": "em-rfot-rvbqmlc"
      }
    ],
    "totalVisits": 75
  },
  {
    "name": "gvil-tm-io",
    "resolution": {
      "height": "100px",
      "width": "100px"
    }
  }
]
timdeschryver commented 6 months ago

Thanks for the reproduction. It seems as it's a runtime problem.

Installing vitest in the referenced repository, and copying the example within to a test seems to work as expected.

We probably have to change how we reference Zod's type within generators (https://github.com/timdeschryver/zod-fixture/blob/main/src/internal/zod.ts) to make this compatible with multiple runtimes.

timdeschryver commented 5 months ago

I was hoping to find some time to take a look at this but last weeks have been quite busy. Feel free to take a look at this issue if you want @alexgherman

dxlbnl commented 3 months ago

I've looked a bit into the issue. And the problem lies in the function isType,

the condition:

schema._def.typeName === target.name

is not valid in compiled situations. as target.name === '_zodString' and schema._def.typeName === 'zodString'

We could compare constructors instead possibly: schema.constructor === target (But this fails a bunch of tests)

Looking into that I see a bit of naughtyness around faking constructors.

@timdeschryver Do you have some pointers in the right direction?

I've pushed this, but it does not seem to work properly (tests pass, but I'm getting errors in my private codebase) https://github.com/dxlbnl/zod-fixture/commit/587e67e64c1b2d0f76bea93ffa40aae778746d98

timdeschryver commented 3 months ago

@dxlbnl thanks for the investigation. It seems like this is the root cause of this issue. It's probably not the cleanest solution, but what if we remove/add the _ when we check the types?

dxlbnl commented 3 months ago

If that's sufficient, why not. Yet it seems fragile. I'm not sure how to fix it properly though how to compares the schemas properly.

I did find this repo: https://github.com/lawvs/zod-compare might be a solution.

timdeschryver commented 3 months ago

@dxlbnl that seems to be a useful library as it will do most of the heavy lifting for us (and looking at the source code there are many "edge cases"). If you want you can try to incorporate it within this library.

dxlbnl commented 2 months ago

I'm having difficulty making it work properly. Largely due to the quirkyness regarding the zodTypes stringnames being cast as zodTypes. And also due to the schema types (ZodString, ... ) being tricky to match to schema instances.

I'm not sure how to proceed, but maybe you'd be willing to drop in a call, and discuss it a bit. @timdeschryver

timdeschryver commented 2 months ago

@dxlbnl sorry, I missed this message - I also tried to implement a fix. I tried zod-compare, but I quickly gave up 😅

If you have the time could you take a look at #97 and provide feedback? I like that the implementation is simpler, but it might be not so clear to implement a custom generator (I assume not many use this feature). The fix gives the desired output within the repo with the reproduction.

dxlbnl commented 2 months ago

Somehow its now working in my repo, it seems to pick up the right generators. Maybe something has changed with how vite deals with it.

timdeschryver commented 2 months ago

Somehow its now working in my repo, it seems to pick up the right generators. Maybe something has changed with how vite deals with it.

Is it working with the current implementation or the implementation of the PR?