From 78ae17f600a9b76d948ac5c4137d3f3bca8cb132 Mon Sep 17 00:00:00 2001 From: Loris Sigrist <43482866+LorisSigrist@users.noreply.github.com> Date: Tue, 9 May 2023 12:12:52 +0200 Subject: [PATCH] Fluentify (#13) * Got started on fluent API * Refactored away from GeneratorDefinitionFactory * Update Docs * Added changeset --- .changeset/twelve-hornets-worry.md | 11 + .gitignore | 3 +- README.md | 141 ++++++++----- src/index.ts | 9 +- src/lib/default_generators.ts | 56 ++--- src/lib/generate.ts | 34 ++- src/lib/generators/any.ts | 35 ++-- src/lib/generators/array.ts | 18 +- src/lib/generators/bigint.ts | 32 +-- src/lib/generators/boolean.ts | 18 +- src/lib/generators/branded.ts | 20 +- src/lib/generators/dates.ts | 36 ++-- src/lib/generators/default.ts | 41 ++-- src/lib/generators/discriminated-union.ts | 20 +- src/lib/generators/effects.ts | 20 +- src/lib/generators/enum.ts | 23 +- src/lib/generators/intersection/index.ts | 20 +- src/lib/generators/lazy.ts | 18 +- src/lib/generators/map.ts | 87 ++++---- src/lib/generators/native-enum.ts | 20 +- src/lib/generators/nullable.ts | 53 ++--- src/lib/generators/numbers.ts | 240 ++++++++++----------- src/lib/generators/object.ts | 19 +- src/lib/generators/optional.ts | 57 +++-- src/lib/generators/promise.ts | 20 +- src/lib/generators/record.ts | 88 ++++---- src/lib/generators/set.ts | 69 +++--- src/lib/generators/string/generators.ts | 2 +- src/lib/generators/string/index.ts | 115 +++++----- src/lib/generators/symbol.ts | 18 +- src/lib/generators/tuple.ts | 18 +- src/lib/generators/union.ts | 18 +- src/lib/zocker.ts | 245 +++++++++++++++++----- tests/bigint.test.ts | 8 +- tests/invalid-strings.test.ts | 2 +- tests/optional.test.ts | 22 +- tests/partial-objects.test.ts | 21 +- tests/promises.test.ts | 2 +- tests/refinements.test.ts | 12 +- tests/repeatability.test.ts | 14 +- tests/strings.test.ts | 6 +- tests/transform.test.ts | 4 +- tests/utils.ts | 2 +- 43 files changed, 885 insertions(+), 832 deletions(-) create mode 100644 .changeset/twelve-hornets-worry.md diff --git a/.changeset/twelve-hornets-worry.md b/.changeset/twelve-hornets-worry.md new file mode 100644 index 0000000..0ed1bc3 --- /dev/null +++ b/.changeset/twelve-hornets-worry.md @@ -0,0 +1,11 @@ +--- +"zocker": major +--- + +Replaced old Generator-based configuration API with fluent-API + +This breaks all existing zocker-setups, including the ones with no custom generators. + +This was done to make zocker more user-friendly going forward, as the new API is signifficantly more intuitive to use, and requires less understanding of zod's internals. + +Consult the README for the new API documentation. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0564eb1..405e97a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist node_modules build -*.experiment.js \ No newline at end of file +*.experiment.js +*.experiment.ts \ No newline at end of file diff --git a/README.md b/README.md index b081fa5..90dd0f5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ npm install --save-dev zocker ## Usage -Wrapping your zod-schema in `zocker()` will return mock data that matches your schema. +Like `zod`, we use a fluent API to make customization easy. Get started by wrapping your schema in `zocker`, and then call `generate()` on it to generate some data. ```typescript import { z } from "zod"; @@ -26,7 +26,7 @@ const person_schema = z.object({ children: z.array(z.lazy(() => person_schema)) }); -const mockData = zocker(person_schema); +const mockData = zocker(person_schema).generate(); /* { name: "John Doe", @@ -47,31 +47,22 @@ const mockData = zocker(person_schema); ### Features & Limitations -`zocker` is still in early development, but it already is the most feature-complete library of its kind. It's easier to list the limitations than the features. All these limitations can be worked around by providing your own generator (see below). +`zocker` is still in early development, but it already is the most feature-complete library of its kind. It's easier to list the limitations than the features. All these limitations can be worked around by customizing the generation process (see below). 1. `z.preprocess` and `z.refine` are not supported out of the box (and probably never will be) 2. `toUpperCase`, `toLowerCase` and `trim` only work if they are the last operation in the chain 3. `z.function` is not supported 4. `z.Intersection` is not supported 5. `z.transform` is only supported if it's the last operation in the chain -6. `z.regex` can be used at most once per string -7. The generation-customization options are very limited (ideas are welcome) +6. `z.string` supports at most one format (e.g regex, cuid, ip) at a time +7. The customization for the built-in generators is still limited (suggestions welcome) -### Providing a custom generator - -You can override any part of the Generation Process by providing your own generator. This allows you to bypass all limitation listed above. - -To register a custom-generator, you must provide two things: - -1. A function that generates data -2. A way to instruct zocker when to use this generator +### Supply your own value +If you have a value that you would like to control explicitly, you can supply your own. Let's learn by example: ```typescript -import { z } from "zod"; -import { zocker } from "zocker"; - const name_schema = z.string().refine((name) => name.length > 5); const schema = z.object({ @@ -79,47 +70,103 @@ const schema = z.object({ age: z.number() }); -const generators = [ - { - //The function that returns data - generator: () => "John Doe", +const data = zocker(schema).supply(name_schema, "Jonathan").generate(); +``` - //The matching-configuration - match: "reference", - schema: name_schema - } -]; +The `supply` method allows you to provide a value, or a function that returns a value, for a specific sub-schema. +It will be used whenever a sub-schema is encoutnered, that matches the one you passed into `supply` by reference. -const data = zocker(schema, { generators }); -``` +> The supplied value is not enforced to be valid -Here we've told zocker to always generate the name "John Doe" for the `name_schema`. We check equality for the name schema by using the `match: "reference"` configuration. This means that we check if the schema is the same object as the one we provided. +This is the main way to work around unsupported types. -Alternatively, you can also use `match: "instanceof"` to match based on the type of the schema. This is useful for overriding the default generator for a specific type. Eg. `z.number()`. +### Customizing the generation process -Generator functions always recive two arguments: +#### Providing Options to Built-Ins -1. The schema that they are generating data for -2. A context object that contains information about the current generation process. This one is rarely used. +You can customize the behaviour of many built-in generators by passing options to their corresponding method on `zocker`. The methods have the same name as the datatype. -### Customizing the generation process +You can mostly autocomplete your way through these. + +```typescript +const data = zocker(my_schema) + .set({min: 2, max: 20}) //How many items should be in a set + .number({ extreme_value_chance: 0.3 }) //The probability that the most extreme value allowed will be generated + ... + .generate() +``` + +#### Overriding Built-ins -The main way to customize the generation process is to override the built-in generators. But this doesn't mean that you have to write your own generators from scratch. All built-in generators have factory-functions that generate a configuration for you, with the behavior you want. For example, you could have a number generator that always generates the most extreme values possible. +If you want to outright override one of the built-in generators (E.g `z.ZodNumber`), then you can use the `override` method. Pass it a schema and a value / function that generates a value, and it will be used whenever a schema is encountered that is an instance of the schema you provided. + +Let's override the number generation to only return `Infinity`, regardless of anything. ```typescript -import { z } from "zod"; -import { zocker, NumberGenerator } from "zocker"; +const data = zocker(my_schema).override(z.ZodNumber, Infinity).generate(); +``` + +> There is currently an issue, where the types don't play well when passing the classes themselves as arguments. If you get a type-error on `z.ZodNumber`, type-cast it to itself it with `z.ZodNumber as any as z.ZodNumber`. It's silly, I know. If you know how to fix it, contributions are welcome. + +In practice you would probably want to return different values based on the exact number-schema we are working on. +To do that, you can provide a function to the override. It will recieve two arguments, first the schema that we are working on, and second, a generation-context. You usually only utilize the first one. -const generators = [ - NumberGenerator({ - extreme_value_chance: 1 //Set the chance of generating an extreme value to 100% +```typescript +const data = zocker(my_schema) + .override(z.ZodNumber, (schema, _ctx) => { + //Example: Return 0 if there is a minimum specified, and 1 if there isn't + if (schema._def.checks.some((check) => check.kind == "min")) return 0; + return 1; }) -]; + .generate(); +``` -const data = zocker(my_schema, { generators }); +If you are overriding a schema with children, you might want to re-enter `zocker`'s generation. You could do this by definging a second mock generation inside your override function, but that would loose all the outside-customization you've done. Instead, use the `generate` function that is exported from the `"zocker"` module. Pass it the schema you would like to generate, as well as the generation-context. + +```typescript +import { zocker, generate } from "zocker"; + +const data = zocker(my_schema) + .override(z.ZodRecord, (schema, ctx) => { + const keys = ["one", "two", "three"]; + const obj = {}; + for (const key of keys) { + obj[key] = generate(schema._def.valueType, ctx); + } + return obj; + }) + .generate(); ``` -Notice that you can pass the return-value directly into the `generators` field, as it comes included with the matching-configuration. This is the case for all built-in generators. If you only want the function, you can just access the `generator` field of the return-value. +`generate` is what zocker's built-in generators use aswell. This is the only point where you need to interact with it. + +> The generation-context is passed by reference between different generations, it is not immutable. If you mutate it (which you probably don't need to), make sure to undo the mutation before returning from your function, even if it throws. + +### Code Reuse + +When writing unit-tests, you often end up with many slightly different `zocker` setups. The might only differ in one `supply` call to force a specific edge case. + +To make this easier to deal with, each step in `zocker`'s fluent API is immutable, so you can reuse most of your configuration for many slight variations. + +E.g + +```typescript +const zock = zocker(my_schema).supply(...)...setSeed(0); //Do a bunch of customization + +test("test 1" , ()=>{ + const data = zock + .supply(my_sub_schema, 0); //Extra-customization - Does not affect `zock` + .generate() + ... +}) + +test("test 2", ()=> { + const data = zock + .supply(my_sub_schema, 1); //Extra-customization - Does not affect `zock` + .generate() + ... +}) +``` ### Repeatability @@ -127,11 +174,11 @@ You can specify a seed to make the generation process repeatable. This ensures t ```typescript test("my repeatable test", () => { - const data = zocker(schema, { seed: 23 }); // always the same + const data = zocker(schema).setSeed(123).generate(); // always the same }); ``` -We guarantee that the same seed will always produce the same data, with the same schema and the same generator configuration. Different generator configurations might produce different data, even if the differences are never actually called. +We guarantee that the same seed will always produce the same data, with the same schema and same options. ## Examples @@ -145,14 +192,12 @@ const jsonSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) ); -const data = zocker(jsonSchema, { - recursion_limit: 5 // default value -}); +const data = zocker(jsonSchema).setDepthLimit(5).generate(); //defaults to 5 ``` ### Regular Expressions -Zocker supports `z.string().regex()` out of the box, thanks to the amazing [randexp](https://npmjs.com/package/randexp) library. It doesn't play very well with other string validators though (e.g `min`, `length` and other formats), so try to encode as much as possible in the regex itself. If you need to, you can always override the generator for a specific schema. +Zocker supports `z.string().regex()` out of the box, thanks to the amazing [randexp](https://npmjs.com/package/randexp) library. It doesn't play very well with other string validators though (e.g `min`, `length` and other formats), so try to encode as much as possible in the regex itself. If you need to, you can always supply your own generator. ```typescript const regex_schema = z.string().regex(/^[a-z0-9]{5,10}$/); diff --git a/src/index.ts b/src/index.ts index d52bbce..5f70a49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,5 @@ //It is the only file that is exported to the outside world. //Every publicly exposed export is re-exported from here. -export { - zocker, - type ZockerOptions, - type GeneratorDefinition -} from "./lib/zocker.js"; - -export * from "./lib/generators/index.js"; +export { zocker } from "./lib/zocker.js"; +export { generate } from "./lib/generate.js"; diff --git a/src/lib/default_generators.ts b/src/lib/default_generators.ts index 52c5f2c..2913e45 100644 --- a/src/lib/default_generators.ts +++ b/src/lib/default_generators.ts @@ -1,4 +1,4 @@ -import { GeneratorDefinition } from "./zocker.js"; +import { InstanceofGeneratorDefinition } from "./zocker.js"; import { z } from "zod"; import { @@ -30,32 +30,32 @@ import { IntersectionGenerator } from "./generators/index.js"; -export const default_generators: GeneratorDefinition[] = [ - StringGenerator(), - NumberGenerator(), - BigintGenerator(), - BooleanGenerator(), - DateGenerator(), - SymbolGenerator(), - OptionalGenerator(), - NullableGenerator(), - AnyGenerator(), - UnknownGenerator(), - EffectsGenerator(), - ArrayGenerator(), - TupleGenerator(), - RecordGenerator(), - MapGenerator(), - SetGenerator(), - ObjectGenerator(), - UnionGenerator(), - NativeEnumGenerator(), - EnumGenerator(), - DefaultGenerator(), - DiscriminatedUnionGenerator(), - PromiseGenerator(), - LazyGenerator(), - BrandedGenerator(), +export const default_generators: InstanceofGeneratorDefinition[] = [ + StringGenerator, + NumberGenerator, + BigintGenerator, + BooleanGenerator, + DateGenerator, + SymbolGenerator, + OptionalGenerator, + NullableGenerator, + AnyGenerator, + UnknownGenerator, + EffectsGenerator, + ArrayGenerator, + TupleGenerator, + RecordGenerator, + MapGenerator, + SetGenerator, + ObjectGenerator, + UnionGenerator, + NativeEnumGenerator, + EnumGenerator, + DefaultGenerator, + DiscriminatedUnionGenerator, + PromiseGenerator, + LazyGenerator, + BrandedGenerator, { schema: z.ZodVoid, generator: () => {}, @@ -81,5 +81,5 @@ export const default_generators: GeneratorDefinition[] = [ generator: (schema) => schema._def.value, match: "instanceof" }, - IntersectionGenerator() + IntersectionGenerator ]; diff --git a/src/lib/generate.ts b/src/lib/generate.ts index 9273c5e..e13df51 100644 --- a/src/lib/generate.ts +++ b/src/lib/generate.ts @@ -3,16 +3,27 @@ import { NoGeneratorException, RecursionLimitReachedException } from "./exceptions.js"; -import { GeneratorDefinition } from "./zocker.js"; +import { + InstanceofGeneratorDefinition, + ReferenceGeneratorDefinition +} from "./zocker.js"; import { SemanticFlag } from "./semantics.js"; +import { NumberGeneratorOptions } from "./generators/numbers.js"; +import { OptionalOptions } from "./generators/optional.js"; +import { NullableOptions } from "./generators/nullable.js"; +import { DefaultOptions } from "./generators/default.js"; +import { MapOptions } from "./generators/map.js"; +import { RecordOptions } from "./generators/record.js"; +import { SetOptions } from "./generators/set.js"; /** * Contains all the necessary configuration to generate a value for a given schema. */ export type GenerationContext = { - instanceof_generators: GeneratorDefinition[]; - reference_generators: GeneratorDefinition[]; + instanceof_generators: InstanceofGeneratorDefinition[]; + reference_generators: ReferenceGeneratorDefinition[]; + /** A Map that keeps count of how often we've seen a parent schema - Used for cycle detection */ parent_schemas: Map; recursion_limit: number; @@ -20,6 +31,21 @@ export type GenerationContext = { semantic_context: SemanticFlag; seed: number; + + /** Options for the z.ZodNumber generator */ + number_options: NumberGeneratorOptions; + /** Options for the z.ZodOptional generator */ + optional_options: OptionalOptions; + /** Options for the z.ZodNullable generator */ + nullable_options: NullableOptions; + /** Options for the z.ZodDefault generator */ + default_options: DefaultOptions; + /** Options for the z.ZodMap generator */ + map_options: MapOptions; + /** Options for the z.ZodRecord generator */ + record_options: RecordOptions; + /** Options for the z.ZodSet generator */ + set_options: SetOptions; }; export type Generator = ( @@ -33,7 +59,7 @@ export type Generator = ( * * @param schema - The schema to generate a value for. * @param ctx - The context and configuration for the generation process. - * @returns - A random value that matches the given schema. + * @returns - A pseudo-random value that matches the given schema. */ export function generate( schema: Z, diff --git a/src/lib/generators/any.ts b/src/lib/generators/any.ts index 27a647e..b16c697 100644 --- a/src/lib/generators/any.ts +++ b/src/lib/generators/any.ts @@ -1,26 +1,18 @@ import { z } from "zod"; import { generate, Generator } from "../generate.js"; import { pick } from "../utils/random.js"; -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; -export const AnyGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodAny as any), - generator: Any() as Generator, - match: options.match ?? "instanceof" - }; +export const AnyGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodAny as any, + generator: Any("true-any"), + match: "instanceof" }; -export const UnknownGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodUnknown as any), - generator: Any() as Generator, - match: options.match ?? "instanceof" - }; +export const UnknownGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodUnknown as any, + generator: Any(), + match: "instanceof" }; /** @@ -28,9 +20,9 @@ export const UnknownGenerator: GeneratorDefinitionFactory = ( * @param strategy - How to generate the value. "true-any" will generate any possible value, "json-compatible" will generate any JSON-compatible value, and "fast" will just return undefined, but is vastly faster. * @returns */ -function Any( +function Any( strategy: "true-any" | "json-compatible" | "fast" = "true-any" -): Generator { +): Generator { if (strategy === "fast") { return () => undefined; } @@ -74,10 +66,7 @@ function Any( z.promise(any) ].map((schema) => schema.optional()); - const generate_any: Generator = ( - _schema, - generation_context - ) => { + const generate_any: Generator = (_schema, generation_context) => { const schema_to_use = pick(potential_schemas); const generated = generate(schema_to_use, generation_context); return generated; diff --git a/src/lib/generators/array.ts b/src/lib/generators/array.ts index 7678a08..a381088 100644 --- a/src/lib/generators/array.ts +++ b/src/lib/generators/array.ts @@ -2,17 +2,7 @@ import { faker } from "@faker-js/faker"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; - -export const ArrayGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodArray as any), - generator: generate_array, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; const generate_array: Generator> = (array_schema, ctx) => { const exact_length = array_schema._def.exactLength?.value ?? null; @@ -49,3 +39,9 @@ const generate_array: Generator> = (array_schema, ctx) => { return []; } }; + +export const ArrayGenerator: InstanceofGeneratorDefinition> = { + schema: z.ZodArray as any, + generator: generate_array, + match: "instanceof" +}; diff --git a/src/lib/generators/bigint.ts b/src/lib/generators/bigint.ts index 32daa7c..9e1e6df 100644 --- a/src/lib/generators/bigint.ts +++ b/src/lib/generators/bigint.ts @@ -1,19 +1,9 @@ import { faker } from "@faker-js/faker"; import { Generator } from "../generate.js"; import { z } from "zod"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; import { InvalidSchemaException } from "../exceptions.js"; -export const BigintGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodBigInt as any), - generator: generate_bigint, - match: options.match ?? "instanceof" - }; -}; - const generate_bigint: Generator = (bigint_schema, ctx) => { const multiple_of_checks = get_bigint_checks(bigint_schema, "multipleOf"); @@ -34,7 +24,6 @@ const generate_bigint: Generator = (bigint_schema, ctx) => { return acc; }, BigInt(Number.MAX_SAFE_INTEGER)); - const multipleof = multiple_of_checks.reduce((acc, check) => { return lcm(acc, check.value); }, 1n); @@ -43,10 +32,12 @@ const generate_bigint: Generator = (bigint_schema, ctx) => { const next_larger_multiple = value + (multipleof - (value % multipleof)); const next_smaller_multiple = value - (value % multipleof); - if (next_larger_multiple <= max) value = next_larger_multiple; else if (next_smaller_multiple >= min) value = next_smaller_multiple; - else throw new InvalidSchemaException("Cannot generate a valid BigInt that satisfies the constraints"); + else + throw new InvalidSchemaException( + "Cannot generate a valid BigInt that satisfies the constraints" + ); return value; }; @@ -55,7 +46,10 @@ function get_bigint_checks( schema: z.ZodBigInt, kind: Kind ): Extract[] { - return schema._def.checks.filter((check) => check.kind === kind) as Extract[] + return schema._def.checks.filter((check) => check.kind === kind) as Extract< + z.ZodBigIntCheck, + { kind: Kind } + >[]; } function lcm(a: bigint, b: bigint) { @@ -65,4 +59,10 @@ function lcm(a: bigint, b: bigint) { function gcd(a: bigint, b: bigint): bigint { if (b === 0n) return a; return gcd(b, a % b); -} \ No newline at end of file +} + +export const BigintGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodBigInt as any, + generator: generate_bigint, + match: "instanceof" +}; diff --git a/src/lib/generators/boolean.ts b/src/lib/generators/boolean.ts index fb6bcc7..8beb807 100644 --- a/src/lib/generators/boolean.ts +++ b/src/lib/generators/boolean.ts @@ -1,18 +1,14 @@ import { faker } from "@faker-js/faker"; import { Generator } from "../generate.js"; import { z } from "zod"; -import { GeneratorDefinitionFactory } from "../zocker.js"; - -export const BooleanGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodBoolean as any), - generator: generate_boolean, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "../zocker.js"; const generate_boolean: Generator = () => { return faker.datatype.boolean(); }; + +export const BooleanGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodBoolean as any, + generator: generate_boolean, + match: "instanceof" +}; diff --git a/src/lib/generators/branded.ts b/src/lib/generators/branded.ts index bf8b9b9..673ef1b 100644 --- a/src/lib/generators/branded.ts +++ b/src/lib/generators/branded.ts @@ -1,20 +1,18 @@ -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; -export const BrandedGenerator: GeneratorDefinitionFactory< - z.ZodBranded -> = (options = {}) => { - return { - schema: options.schema ?? (z.ZodBranded as any), - generator: generate_branded, - match: options.match ?? "instanceof" - }; -}; - const generate_branded: Generator> = ( schema, generation_context ) => { return generate(schema._def.type, generation_context); }; + +export const BrandedGenerator: InstanceofGeneratorDefinition< + z.ZodBranded +> = { + schema: z.ZodBranded as any, + generator: generate_branded, + match: "instanceof" +}; diff --git a/src/lib/generators/dates.ts b/src/lib/generators/dates.ts index 1e42e54..4512f06 100644 --- a/src/lib/generators/dates.ts +++ b/src/lib/generators/dates.ts @@ -1,30 +1,20 @@ import { faker } from "@faker-js/faker"; import { InvalidSchemaException } from "../exceptions.js"; import { Generator } from "lib/generate.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; import { z } from "zod"; -export const DateGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - const generate_date: Generator = (date_schema, options) => { - const min = get_date_check(date_schema, "min")?.value ?? null; - const max = get_date_check(date_schema, "max")?.value ?? null; +const generate_date: Generator = (date_schema, ctx) => { + const min = get_date_check(date_schema, "min")?.value ?? null; + const max = get_date_check(date_schema, "max")?.value ?? null; - if (min && max && max < min) - throw new InvalidSchemaException("max date is less than min date"); + if (min && max && max < min) + throw new InvalidSchemaException("max date is less than min date"); - return faker.datatype.datetime({ - min: min ?? undefined, - max: max ?? undefined - }); - }; - - return { - schema: options.schema ?? (z.ZodDate as any), - generator: generate_date, - match: options.match ?? "instanceof" - }; + return faker.datatype.datetime({ + min: min ?? undefined, + max: max ?? undefined + }); }; function get_date_check( @@ -36,3 +26,9 @@ function get_date_check( | undefined; return check; } + +export const DateGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodDate as any, + generator: generate_date, + match: "instanceof" +}; diff --git a/src/lib/generators/default.ts b/src/lib/generators/default.ts index 6a9e844..bce39a0 100644 --- a/src/lib/generators/default.ts +++ b/src/lib/generators/default.ts @@ -1,35 +1,26 @@ import { Generator, generate } from "../generate.js"; import { z } from "zod"; import { weighted_random_boolean } from "../utils/random.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; -type DefaultOptions = { +export type DefaultOptions = { default_chance: number; }; -const default_options: DefaultOptions = { - default_chance: 0.3 +const generator: Generator> = (schema, ctx) => { + const should_use_default = weighted_random_boolean( + ctx.default_options.default_chance + ); + const default_value = schema._def.defaultValue; + return should_use_default + ? default_value() + : generate(schema._def.innerType, ctx); }; -export const DefaultGenerator: GeneratorDefinitionFactory< - z.ZodDefault, - Partial -> = (partial_options = {}) => { - const options = { ...default_options, ...partial_options }; - - return { - schema: options.schema ?? (z.ZodDefault as any), - generator: Default(options.default_chance), - match: options.match ?? "instanceof" - }; +export const DefaultGenerator: InstanceofGeneratorDefinition< + z.ZodDefault +> = { + schema: z.ZodDefault as any, + generator: generator, + match: "instanceof" }; - -function Default(default_chance: number = 0.3): Generator> { - return (schema, ctx) => { - const should_use_default = weighted_random_boolean(default_chance); - const default_value = schema._def.defaultValue; - return should_use_default - ? default_value() - : generate(schema._def.innerType, ctx); - }; -} diff --git a/src/lib/generators/discriminated-union.ts b/src/lib/generators/discriminated-union.ts index 495b867..d8868e2 100644 --- a/src/lib/generators/discriminated-union.ts +++ b/src/lib/generators/discriminated-union.ts @@ -2,17 +2,7 @@ import { z } from "zod"; import { Generator, generate } from "../generate.js"; import { faker } from "@faker-js/faker"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "../zocker.js"; - -export const DiscriminatedUnionGenerator: GeneratorDefinitionFactory< - z.ZodDiscriminatedUnion -> = (options = {}) => { - return { - schema: options.schema ?? (z.ZodDiscriminatedUnion as any), - generator: generate_discriminated_union, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "../zocker.js"; const generate_discriminated_union: Generator< z.ZodDiscriminatedUnion @@ -43,3 +33,11 @@ const generate_discriminated_union: Generator< //and we should throw a RecursionLimitReachedException throw new RecursionLimitReachedException(); }; + +export const DiscriminatedUnionGenerator: InstanceofGeneratorDefinition< + z.ZodDiscriminatedUnion +> = { + schema: z.ZodDiscriminatedUnion as any, + generator: generate_discriminated_union, + match: "instanceof" +}; diff --git a/src/lib/generators/effects.ts b/src/lib/generators/effects.ts index 072a94f..90843b8 100644 --- a/src/lib/generators/effects.ts +++ b/src/lib/generators/effects.ts @@ -1,18 +1,8 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; import { NoGeneratorException } from "../exceptions.js"; -export const EffectsGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodEffects as any), - generator: generate_effects, - match: options.match ?? "instanceof" - }; -}; - const generate_effects: Generator> = ( effects_schema, generation_options @@ -36,3 +26,11 @@ const generate_effects: Generator> = ( path: [] }); }; + +export const EffectsGenerator: InstanceofGeneratorDefinition< + z.ZodEffects +> = { + schema: z.ZodEffects as any, + generator: generate_effects, + match: "instanceof" +}; diff --git a/src/lib/generators/enum.ts b/src/lib/generators/enum.ts index 803fe8b..67e2c89 100644 --- a/src/lib/generators/enum.ts +++ b/src/lib/generators/enum.ts @@ -1,23 +1,16 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { Generator } from "../generate.js"; import { pick } from "../utils/random.js"; import { z } from "zod"; -export const EnumGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodEnum as any), - generator: generate_enum, - match: options.match ?? "instanceof" - }; -}; - -const generate_enum: Generator> = ( - schema, - generation_context -) => { +const generate_enum: Generator> = (schema, ctx) => { const values = schema._def.values; const value = pick(values); return value; }; + +export const EnumGenerator: InstanceofGeneratorDefinition> = { + schema: z.ZodEnum as any, + generator: generate_enum, + match: "instanceof" +}; diff --git a/src/lib/generators/intersection/index.ts b/src/lib/generators/intersection/index.ts index ae66f01..e8cf618 100644 --- a/src/lib/generators/intersection/index.ts +++ b/src/lib/generators/intersection/index.ts @@ -1,4 +1,4 @@ -import { GeneratorDefinitionFactory } from "../../zocker.js"; +import { InstanceofGeneratorDefinition } from "../../zocker.js"; import { Generator, generate } from "../../generate.js"; import { z } from "zod"; import { @@ -6,16 +6,6 @@ import { NoGeneratorException } from "../../exceptions.js"; -export const IntersectionGenerator: GeneratorDefinitionFactory< - z.ZodIntersection -> = () => { - return { - schema: z.ZodIntersection as any, - generator: generate_intersection, - match: "instanceof" - }; -}; - const generate_intersection: Generator> = ( schema, ctx @@ -65,3 +55,11 @@ const merge_schema = ( "ZodIntersections only have very limited support at the moment." ); }; + +export const IntersectionGenerator: InstanceofGeneratorDefinition< + z.ZodIntersection +> = { + schema: z.ZodIntersection as any, + generator: generate_intersection, + match: "instanceof" +}; diff --git a/src/lib/generators/lazy.ts b/src/lib/generators/lazy.ts index a5324a3..4873a19 100644 --- a/src/lib/generators/lazy.ts +++ b/src/lib/generators/lazy.ts @@ -1,17 +1,7 @@ -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; -export const LazyGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodLazy as any), - generator: generate_lazy, - match: options.match ?? "instanceof" - }; -}; - const generate_lazy: Generator> = ( schema, generation_context @@ -19,3 +9,9 @@ const generate_lazy: Generator> = ( const getter = schema._def.getter(); return generate(getter, generation_context); }; + +export const LazyGenerator: InstanceofGeneratorDefinition> = { + schema: z.ZodLazy as any, + generator: generate_lazy, + match: "instanceof" +}; diff --git a/src/lib/generators/map.ts b/src/lib/generators/map.ts index ae610ab..2a09927 100644 --- a/src/lib/generators/map.ts +++ b/src/lib/generators/map.ts @@ -2,65 +2,56 @@ import { faker } from "@faker-js/faker"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; -type MapOptions = { +export type MapOptions = { max: number; min: number; }; -const default_map_options: MapOptions = { - max: 10, - min: 0 -}; - -export const MapGenerator: GeneratorDefinitionFactory< - z.ZodMap, - Partial -> = (partial_options = {}) => { - const options = { ...default_map_options, ...partial_options }; - - const generate_map: Generator = (schema, generation_context) => { - const size = faker.datatype.number({ min: options.min, max: options.max }); +const generate_map: Generator = (schema, ctx) => { + const size = faker.datatype.number({ + min: ctx.map_options.min, + max: ctx.map_options.max + }); - type Key = z.infer<(typeof schema)["_def"]["keyType"]>; - type Value = z.infer<(typeof schema)["_def"]["valueType"]>; + type Key = z.infer<(typeof schema)["_def"]["keyType"]>; + type Value = z.infer<(typeof schema)["_def"]["valueType"]>; - const map = new Map(); - - try { - const keys: Key[] = []; - for (let i = 0; i < size; i++) { - const key = generate(schema._def.keyType, generation_context); - keys.push(key); - } + const map = new Map(); - for (const key of keys) { - let prev_semantic_context = generation_context.semantic_context; - try { - generation_context.path.push(key); - generation_context.semantic_context = "key"; + try { + const keys: Key[] = []; + for (let i = 0; i < size; i++) { + const key = generate(schema._def.keyType, ctx); + keys.push(key); + } - const value = generate(schema._def.valueType, generation_context); - map.set(key, value); - } finally { - generation_context.path.pop(); - generation_context.semantic_context = prev_semantic_context; - } - } - } catch (error) { - if (error instanceof RecursionLimitReachedException) { - return map; + for (const key of keys) { + let prev_semantic_context = ctx.semantic_context; + try { + ctx.path.push(key); + ctx.semantic_context = "key"; + + const value = generate(schema._def.valueType, ctx); + map.set(key, value); + } finally { + ctx.path.pop(); + ctx.semantic_context = prev_semantic_context; } - throw error; } + } catch (error) { + if (error instanceof RecursionLimitReachedException) { + return map; + } + throw error; + } - return map; - }; + return map; +}; - return { - schema: options.schema ?? (z.ZodMap as any), - generator: generate_map, - match: options.match ?? "instanceof" - }; +export const MapGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodMap as any, + generator: generate_map, + match: "instanceof" }; diff --git a/src/lib/generators/native-enum.ts b/src/lib/generators/native-enum.ts index 3806238..3043e13 100644 --- a/src/lib/generators/native-enum.ts +++ b/src/lib/generators/native-enum.ts @@ -1,20 +1,18 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { Generator } from "../generate.js"; import { pick } from "../utils/random.js"; import { z } from "zod"; -export const NativeEnumGenerator: GeneratorDefinitionFactory< - z.ZodNativeEnum -> = (options = {}) => { - return { - schema: options.schema ?? (z.ZodNativeEnum as any), - generator: generate_native_enum, - match: options.match ?? "instanceof" - }; -}; - const generate_native_enum: Generator> = (schema, ctx) => { const values = Object.values(schema._def.values); const value = pick(values); return value; }; + +export const NativeEnumGenerator: InstanceofGeneratorDefinition< + z.ZodNativeEnum +> = { + schema: z.ZodNativeEnum as any, + generator: generate_native_enum, + match: "instanceof" +}; diff --git a/src/lib/generators/nullable.ts b/src/lib/generators/nullable.ts index 0b9e487..42f2295 100644 --- a/src/lib/generators/nullable.ts +++ b/src/lib/generators/nullable.ts @@ -1,40 +1,33 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { generate } from "../generate.js"; +import { Generator, generate } from "../generate.js"; import { weighted_random_boolean } from "../utils/random.js"; import { z } from "zod"; -type NullableOptions = { +export type NullableOptions = { null_chance: number; }; -const default_options: NullableOptions = { - null_chance: 0.3 -}; - -export const NullableGenerator: GeneratorDefinitionFactory< - z.ZodNullable, - Partial -> = (partial_options = {}) => { - const options = { ...default_options, ...partial_options }; +const generator: Generator> = (schema, ctx) => { + const should_be_null = weighted_random_boolean( + ctx.nullable_options.null_chance + ); - return { - schema: options.schema ?? (z.ZodNullable as any), - generator: (schema, generation_context) => { - const should_be_null = weighted_random_boolean(options.null_chance); + try { + return should_be_null ? null : generate(schema._def.innerType, ctx); + } catch (e) { + if (e instanceof RecursionLimitReachedException) { + return null; + } else { + throw e; + } + } +}; - try { - return should_be_null - ? null - : generate(schema._def.innerType, generation_context); - } catch (e) { - if (e instanceof RecursionLimitReachedException) { - return null; - } else { - throw e; - } - } - }, - match: options.match ?? "instanceof" - }; +export const NullableGenerator: InstanceofGeneratorDefinition< + z.ZodNullable +> = { + schema: z.ZodNullable as any, + generator, + match: "instanceof" }; diff --git a/src/lib/generators/numbers.ts b/src/lib/generators/numbers.ts index a403e1f..8000700 100644 --- a/src/lib/generators/numbers.ts +++ b/src/lib/generators/numbers.ts @@ -1,142 +1,129 @@ import { z } from "zod"; import { faker } from "@faker-js/faker"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; import { Generator } from "../generate.js"; import { weighted_random_boolean } from "../utils/random.js"; import { InvalidSchemaException } from "../exceptions.js"; import { SemanticFlag } from "../semantics.js"; -type NumberGeneratorOptions = { +export type NumberGeneratorOptions = { extreme_value_chance: number; }; -const default_options: NumberGeneratorOptions = { - extreme_value_chance: 0.3 -}; +const generate_number: Generator = (number_schema, ctx) => { + try { + //Generate semantically meaningful number + let proposed_number = NaN; + + const semantic_generators: { + [flag in SemanticFlag]?: () => number; + } = { + age: () => faker.datatype.number({ min: 0, max: 120 }), + year: () => faker.datatype.number({ min: 1200, max: 3000 }), + month: () => faker.datatype.number({ min: 1, max: 12 }), + "day-of-the-month": () => faker.datatype.number({ min: 1, max: 31 }), + hour: () => faker.datatype.number({ min: 0, max: 23 }), + minute: () => faker.datatype.number({ min: 0, max: 59 }), + second: () => faker.datatype.number({ min: 0, max: 59 }), + millisecond: () => faker.datatype.number({ min: 0, max: 999 }), + weekday: () => faker.datatype.number({ min: 0, max: 6 }) + }; + + const generator = semantic_generators[ctx.semantic_context]; + if (!generator) + throw new Error( + "No generator found for semantic context - Falling back to random number" + ); -export const NumberGenerator: GeneratorDefinitionFactory< - z.ZodNumber, - Partial -> = (partial_options = {}) => { - const options = { ...default_options, ...partial_options }; - - const generate_number: Generator = (number_schema, ctx) => { - try { - let proposed_number = NaN; - - const semantic_generators: { - [flag in SemanticFlag]?: () => number; - } = { - age: () => faker.datatype.number({ min: 0, max: 120 }), - year: () => faker.datatype.number({ min: 1200, max: 3000 }), - month: () => faker.datatype.number({ min: 1, max: 12 }), - "day-of-the-month": () => faker.datatype.number({ min: 1, max: 31 }), - hour: () => faker.datatype.number({ min: 0, max: 23 }), - minute: () => faker.datatype.number({ min: 0, max: 59 }), - second: () => faker.datatype.number({ min: 0, max: 59 }), - millisecond: () => faker.datatype.number({ min: 0, max: 999 }), - weekday: () => faker.datatype.number({ min: 0, max: 6 }) - }; - - const generator = semantic_generators[ctx.semantic_context]; - if (!generator) - throw new Error( - "No generator found for semantic context - Falling back to random number" - ); - - proposed_number = generator(); - - return number_schema.parse(proposed_number); - } catch (e) {} - - let is_extreme_value = weighted_random_boolean( - options.extreme_value_chance - ); - let is_int = get_number_checks(number_schema, "int").length !== 0; - let is_finite = get_number_checks(number_schema, "finite").length !== 0; - - let min_checks = get_number_checks(number_schema, "min"); - let max_checks = get_number_checks(number_schema, "max"); - - let min_check = - min_checks.length === 0 - ? null - : min_checks.reduce((prev, curr) => - prev.value > curr.value ? prev : curr - ); - let max_check = - max_checks.length === 0 - ? null - : max_checks.reduce((prev, curr) => - prev.value < curr.value ? prev : curr - ); - - let inclusive_min = min_check?.inclusive ?? true; - let inclusive_max = max_check?.inclusive ?? true; - - let min = min_check?.value ?? Number.MIN_SAFE_INTEGER / 2; - let max = max_check?.value ?? Number.MAX_SAFE_INTEGER / 2; - - if (!inclusive_min) { - const float_step = float_step_size(min); - min += is_int ? 1 : float_step; - } + proposed_number = generator(); - if (!inclusive_max) { - const float_step = float_step_size(max); - max -= is_int ? 1 : float_step; - } + return number_schema.parse(proposed_number); + } catch (e) {} - if (max < min) { - throw new InvalidSchemaException( - "max must be greater than min if specified" - ); - } - - let value: number; + let is_extreme_value = weighted_random_boolean( + ctx.number_options.extreme_value_chance + ); + let is_int = get_number_checks(number_schema, "int").length !== 0; + let is_finite = get_number_checks(number_schema, "finite").length !== 0; + + let min_checks = get_number_checks(number_schema, "min"); + let max_checks = get_number_checks(number_schema, "max"); + + let min_check = + min_checks.length === 0 + ? null + : min_checks.reduce((prev, curr) => + prev.value > curr.value ? prev : curr + ); + let max_check = + max_checks.length === 0 + ? null + : max_checks.reduce((prev, curr) => + prev.value < curr.value ? prev : curr + ); + + let inclusive_min = min_check?.inclusive ?? true; + let inclusive_max = max_check?.inclusive ?? true; + + let min = min_check?.value ?? Number.MIN_SAFE_INTEGER / 2; + let max = max_check?.value ?? Number.MAX_SAFE_INTEGER / 2; + + if (!inclusive_min) { + const float_step = float_step_size(min); + min += is_int ? 1 : float_step; + } + + if (!inclusive_max) { + const float_step = float_step_size(max); + max -= is_int ? 1 : float_step; + } + + if (max < min) { + throw new InvalidSchemaException( + "max must be greater than min if specified" + ); + } - if (is_int) { - value = faker.datatype.number({ min, max }); - } else { - if (is_extreme_value) { - const use_lower_extreme = weighted_random_boolean(0.5); - if (use_lower_extreme) value = is_finite ? -Infinity : min; - else value = is_finite ? Infinity : max; - } + let value: number; - value = faker.datatype.float({ min, max }); + if (is_int) { + value = faker.datatype.number({ min, max }); + } else { + if (is_extreme_value) { + const use_lower_extreme = weighted_random_boolean(0.5); + if (use_lower_extreme) value = is_finite ? -Infinity : min; + else value = is_finite ? Infinity : max; } - if (value === undefined) - throw new Error( - "Failed to generate Number. This is a bug in the built-in generator" - ); + value = faker.datatype.float({ min, max }); + } - let multipleof_checks = get_number_checks(number_schema, "multipleOf"); - let multipleof = - multipleof_checks.length === 0 - ? null - : multipleof_checks.reduce((acc, check) => { - return lcm(acc, check.value); - }, multipleof_checks[0]?.value!); - - if (multipleof !== null) { - let next_higher = value + (multipleof - (value % multipleof)); - let next_lower = value - (value % multipleof); - - if (next_higher <= max) value = next_higher; - else if (next_lower >= min) value = next_lower; - else throw new InvalidSchemaException("No valid multipleOf value found"); - } + if (value === undefined) + throw new Error( + "Failed to generate Number. This is a bug in the built-in generator" + ); - return value; - }; + let multipleof_checks = get_number_checks(number_schema, "multipleOf"); + let multipleof = + multipleof_checks.length === 0 + ? null + : multipleof_checks.reduce((acc, check) => { + return lcm(acc, check.value); + }, multipleof_checks[0]?.value!); + + if (multipleof !== null) { + let next_higher = value + (multipleof - (value % multipleof)); + let next_lower = value - (value % multipleof); + + if (next_higher <= max) value = next_higher; + else if (next_lower >= min) value = next_lower; + else + throw new InvalidSchemaException( + `There exists no valid multiple of ${multipleof} between ${min} and ${max}.` + ); + } - return { - schema: options.schema ?? (z.ZodNumber as any), - generator: generate_number, - match: options.match ?? "instanceof" - }; + return value; }; //Get a check from a ZodNumber schema in a type-safe way @@ -158,12 +145,17 @@ function float_step_size(n: number) { ); } - -function lcm(a: N, b: N):N { - return (a * b) / gcd(a, b) as N; +function lcm(a: N, b: N): N { + return ((a * b) / gcd(a, b)) as N; } function gcd(a: N, b: N): N { if (b === 0n || b === 0) return a; - return gcd(b, a % b as N); -} \ No newline at end of file + return gcd(b, (a % b) as N); +} + +export const NumberGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodNumber as any, + generator: generate_number, + match: "instanceof" +}; diff --git a/src/lib/generators/object.ts b/src/lib/generators/object.ts index 3af97d8..2c4a2b2 100644 --- a/src/lib/generators/object.ts +++ b/src/lib/generators/object.ts @@ -1,19 +1,9 @@ import { get_semantic_flag } from "../semantics.js"; import { GenerationContext, generate } from "../generate.js"; -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { z } from "zod"; import { faker } from "@faker-js/faker"; -export const ObjectGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodObject as any), - generator: generate_object, - match: options.match ?? "instanceof" - }; -}; - const generate_object = ( object_schema: z.ZodObject, generation_context: GenerationContext> @@ -74,3 +64,10 @@ const generate_object = ( return Object.fromEntries(mock_entries) as Shape; }; + +export const ObjectGenerator: InstanceofGeneratorDefinition> = + { + schema: z.ZodObject as any, + generator: generate_object, + match: "instanceof" + }; diff --git a/src/lib/generators/optional.ts b/src/lib/generators/optional.ts index 7c1acd1..eb5f0c4 100644 --- a/src/lib/generators/optional.ts +++ b/src/lib/generators/optional.ts @@ -1,42 +1,35 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { generate } from "../generate.js"; +import { Generator, generate } from "../generate.js"; import { weighted_random_boolean } from "../utils/random.js"; import { z } from "zod"; -type OptionalOptions = { +export type OptionalOptions = { undefined_chance: number; }; -const default_options: OptionalOptions = { - undefined_chance: 0.3 -}; - -export const OptionalGenerator: GeneratorDefinitionFactory< - z.ZodOptional, - Partial -> = (partial_options = {}) => { - const options = { ...default_options, ...partial_options }; +const generator: Generator> = (schema, ctx) => { + const should_be_undefined = weighted_random_boolean( + ctx.optional_options.undefined_chance + ); - return { - schema: options.schema ?? (z.ZodOptional as any), - generator: (schema, generation_context) => { - const should_be_undefined = weighted_random_boolean( - options.undefined_chance - ); + try { + return should_be_undefined + ? undefined + : generate(schema._def.innerType, ctx); + } catch (e) { + if (e instanceof RecursionLimitReachedException) { + return undefined; + } else { + throw e; + } + } +}; - try { - return should_be_undefined - ? undefined - : generate(schema._def.innerType, generation_context); - } catch (e) { - if (e instanceof RecursionLimitReachedException) { - return undefined; - } else { - throw e; - } - } - }, - match: options.match ?? "instanceof" - }; +export const OptionalGenerator: InstanceofGeneratorDefinition< + z.ZodOptional +> = { + schema: z.ZodOptional as any, + generator: generator, + match: "instanceof" }; diff --git a/src/lib/generators/promise.ts b/src/lib/generators/promise.ts index 5d281a8..ae15b3a 100644 --- a/src/lib/generators/promise.ts +++ b/src/lib/generators/promise.ts @@ -1,16 +1,6 @@ import { Generator, generate } from "../generate.js"; import { z } from "zod"; -import { GeneratorDefinitionFactory } from "../zocker.js"; - -export const PromiseGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodPromise as any), - generator: generate_promise, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "../zocker.js"; const generate_promise: Generator> = ( schema, @@ -18,3 +8,11 @@ const generate_promise: Generator> = ( ) => { return generate(schema._def.type, generation_context); }; + +export const PromiseGenerator: InstanceofGeneratorDefinition< + z.ZodPromise +> = { + schema: z.ZodPromise as any, + generator: generate_promise, + match: "instanceof" +}; diff --git a/src/lib/generators/record.ts b/src/lib/generators/record.ts index e7875d7..177ad10 100644 --- a/src/lib/generators/record.ts +++ b/src/lib/generators/record.ts @@ -2,69 +2,57 @@ import { faker } from "@faker-js/faker"; import { Generator, generate } from "../generate.js"; import { z } from "zod"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; -type RecordOptions = { +export type RecordOptions = { max: number; min: number; }; -const default_record_options: RecordOptions = { - max: 10, - min: 0 -}; - -export const RecordGenerator: GeneratorDefinitionFactory< - z.ZodRecord, - Partial -> = (partial_options = {}) => { - const options = { ...default_record_options, ...partial_options }; - - const generate_record: Generator = ( - schema, - generation_context - ) => { - const size = faker.datatype.number({ min: options.min, max: options.max }); - - type Key = z.infer<(typeof schema)["_def"]["keyType"]>; - type Value = z.infer<(typeof schema)["_def"]["valueType"]>; +const generate_record: Generator = (schema, ctx) => { + const size = faker.datatype.number({ + min: ctx.record_options.min, + max: ctx.record_options.max + }); - const record = {} as any as Record; + type Key = z.infer<(typeof schema)["_def"]["keyType"]>; + type Value = z.infer<(typeof schema)["_def"]["valueType"]>; - try { - const keys: Key[] = []; - for (let i = 0; i < size; i++) { - const key = generate(schema._def.keyType, generation_context) as Key; - keys.push(key); - } + const record = {} as any as Record; - for (const key of keys) { - let value: Value; - let prev_semantic_context = generation_context.semantic_context; + try { + const keys: Key[] = []; + for (let i = 0; i < size; i++) { + const key = generate(schema._def.keyType, ctx) as Key; + keys.push(key); + } - try { - generation_context.path.push(key); - generation_context.semantic_context = "key"; + for (const key of keys) { + let value: Value; + let prev_semantic_context = ctx.semantic_context; - value = generate(schema._def.valueType, generation_context) as Value; - } finally { - generation_context.path.pop(); - generation_context.semantic_context = prev_semantic_context; - } + try { + ctx.path.push(key); + ctx.semantic_context = "key"; - record[key] = value; + value = generate(schema._def.valueType, ctx) as Value; + } finally { + ctx.path.pop(); + ctx.semantic_context = prev_semantic_context; } - } catch (error) { - if (error instanceof RecursionLimitReachedException) return record; - throw error; + + record[key] = value; } + } catch (error) { + if (error instanceof RecursionLimitReachedException) return record; + throw error; + } - return record; - }; + return record; +}; - return { - schema: options.schema ?? (z.ZodRecord as any), - generator: generate_record, - match: options.match ?? "instanceof" - }; +export const RecordGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodRecord as any, + generator: generate_record, + match: "instanceof" }; diff --git a/src/lib/generators/set.ts b/src/lib/generators/set.ts index 2e3b3b7..57fd794 100644 --- a/src/lib/generators/set.ts +++ b/src/lib/generators/set.ts @@ -2,52 +2,43 @@ import { z } from "zod"; import { faker } from "@faker-js/faker"; import { generate, Generator } from "../generate.js"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; -type SetOptions = { +export type SetOptions = { max: number; min: number; }; -const default_set_options: SetOptions = { - max: 10, - min: 0 -}; - -export const SetGenerator: GeneratorDefinitionFactory< - z.ZodSet, - Partial -> = (partial_options = {}) => { - const options = { ...default_set_options, ...partial_options }; - - const generate_set: Generator> = (schema, ctx) => { - const size = faker.datatype.number({ min: options.min, max: options.max }); - - const set: z.infer = new Set(); - - try { - for (let i = 0; i < size; i++) { - try { - ctx.path.push(i); - const value = generate(schema._def.valueType, ctx); - set.add(value); - } finally { - ctx.path.pop(); - } - } - } catch (error) { - if (error instanceof RecursionLimitReachedException) { - return set; +const generate_set: Generator> = (schema, ctx) => { + const size = faker.datatype.number({ + min: ctx.set_options.min, + max: ctx.set_options.max + }); + + const set: z.infer = new Set(); + + try { + for (let i = 0; i < size; i++) { + try { + ctx.path.push(i); + const value = generate(schema._def.valueType, ctx); + set.add(value); + } finally { + ctx.path.pop(); } - throw error; } + } catch (error) { + if (error instanceof RecursionLimitReachedException) { + return set; + } + throw error; + } - return set; - }; + return set; +}; - return { - schema: options.schema ?? (z.ZodSet as any), - generator: generate_set, - match: options.match ?? "instanceof" - }; +export const SetGenerator: InstanceofGeneratorDefinition> = { + schema: z.ZodSet as any, + generator: generate_set, + match: "instanceof" }; diff --git a/src/lib/generators/string/generators.ts b/src/lib/generators/string/generators.ts index 1c4fb00..ecc7f61 100644 --- a/src/lib/generators/string/generators.ts +++ b/src/lib/generators/string/generators.ts @@ -7,7 +7,7 @@ import { faker } from "@faker-js/faker"; import { InvalidSchemaException } from "../../exceptions.js"; import Randexp from "randexp"; import { pick, weighted_random_boolean } from "../../utils/random.js"; -import { SemanticFlag } from "lib/semantics.js"; +import { SemanticFlag } from "../../semantics.js"; export const uuid: StringKindGenerator = (ctx, lc, cc, td) => { if (lc.exact && lc.exact !== 36) diff --git a/src/lib/generators/string/index.ts b/src/lib/generators/string/index.ts index ba66257..ff86f88 100644 --- a/src/lib/generators/string/index.ts +++ b/src/lib/generators/string/index.ts @@ -1,4 +1,4 @@ -import { GeneratorDefinitionFactory } from "../../zocker.js"; +import { InstanceofGeneratorDefinition } from "../../zocker.js"; import { GenerationContext, Generator } from "../../generate.js"; import { z } from "zod"; import { @@ -63,75 +63,52 @@ export type StringKindGenerator = ( td: TransformDefinition ) => string; -export const StringGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - const cache = new WeakMap(); - - const generate_string: Generator = (string_schema, ctx) => { - const cache_hit = cache.get(string_schema); - - const cc = - cache_hit?.content_constraints ?? content_constraints(string_schema); - const lc = - cache_hit?.length_constraints ?? length_constraints(string_schema); - const tf = cache_hit?.transform_constaints ?? transforms(string_schema); - - if (!cache_hit) - cache.set(string_schema, { - length_constraints: lc, - content_constraints: cc, - transform_constaints: tf - }); - - const generate_raw_string = () => { - switch (cc.format.kind) { - case "ip": - return StrGens.ip(ctx, lc, cc, tf); - - case "datetime": - return StrGens.datetime(ctx, lc, cc, tf); - - case "email": - return StrGens.email(ctx, lc, cc, tf); - case "url": - return StrGens.url(ctx, lc, cc, tf); - case "uuid": - return StrGens.uuid(ctx, lc, cc, tf); - - case "cuid": - return StrGens.cuid(ctx, lc, cc, tf); - case "cuid2": - return StrGens.cuid2(ctx, lc, cc, tf); - case "ulid": - return StrGens.ulid(ctx, lc, cc, tf); - - case "emoji": - return StrGens.emoji(ctx, lc, cc, tf); - - case "regex": - return StrGens.regex(ctx, lc, cc, tf); - case "any": - default: - return StrGens.any(ctx, lc, cc, tf); - } - }; +const generate_string: Generator = (string_schema, ctx) => { + const cc = content_constraints(string_schema); + const lc = length_constraints(string_schema); + const tf = transforms(string_schema); + + const generate_raw_string = () => { + switch (cc.format.kind) { + case "ip": + return StrGens.ip(ctx, lc, cc, tf); + + case "datetime": + return StrGens.datetime(ctx, lc, cc, tf); + + case "email": + return StrGens.email(ctx, lc, cc, tf); + case "url": + return StrGens.url(ctx, lc, cc, tf); + case "uuid": + return StrGens.uuid(ctx, lc, cc, tf); + + case "cuid": + return StrGens.cuid(ctx, lc, cc, tf); + case "cuid2": + return StrGens.cuid2(ctx, lc, cc, tf); + case "ulid": + return StrGens.ulid(ctx, lc, cc, tf); + + case "emoji": + return StrGens.emoji(ctx, lc, cc, tf); + + case "regex": + return StrGens.regex(ctx, lc, cc, tf); + case "any": + default: + return StrGens.any(ctx, lc, cc, tf); + } + }; - let string = generate_raw_string(); + let string = generate_raw_string(); - if (tf.trim) string = string.trim(); + if (tf.trim) string = string.trim(); - if (tf.case === "upper") string = string.toUpperCase(); - else if (tf.case === "lower") string = string.toLowerCase(); + if (tf.case === "upper") string = string.toUpperCase(); + else if (tf.case === "lower") string = string.toLowerCase(); - return string; - }; - - return { - schema: options.schema ?? (z.ZodString as any), - generator: generate_string, - match: options.match ?? "instanceof" - }; + return string; }; function get_string_checks( @@ -378,3 +355,9 @@ function transforms(schema: z.ZodString): TransformDefinition { } return transform_definition; } + +export const StringGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodString as any, + generator: generate_string, + match: "instanceof" +}; diff --git a/src/lib/generators/symbol.ts b/src/lib/generators/symbol.ts index f174b81..6f7d1cd 100644 --- a/src/lib/generators/symbol.ts +++ b/src/lib/generators/symbol.ts @@ -1,19 +1,15 @@ import { Generator } from "../generate.js"; import { z } from "zod"; import { faker } from "@faker-js/faker"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; - -export const SymbolGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodSymbol as any), - generator: generate_symbol, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; const generate_symbol: Generator = () => { const symbol_key = faker.datatype.string(); return Symbol.for(symbol_key); }; + +export const SymbolGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodSymbol as any, + generator: generate_symbol, + match: "instanceof" +}; diff --git a/src/lib/generators/tuple.ts b/src/lib/generators/tuple.ts index fc5506c..53ba3e2 100644 --- a/src/lib/generators/tuple.ts +++ b/src/lib/generators/tuple.ts @@ -1,20 +1,16 @@ -import { GeneratorDefinitionFactory } from "../zocker.js"; +import { InstanceofGeneratorDefinition } from "../zocker.js"; import { generate, Generator } from "../generate.js"; import { z } from "zod"; -export const TupleGenerator: GeneratorDefinitionFactory = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodTuple as any), - generator: generate_tuple, - match: options.match ?? "instanceof" - }; -}; - const generate_tuple: Generator = (schema, generation_context) => { const tuple = schema._def.items.map((item: Z) => generate(item, generation_context) ); return tuple as z.infer; }; + +export const TupleGenerator: InstanceofGeneratorDefinition = { + schema: z.ZodTuple as any, + generator: generate_tuple, + match: "instanceof" +}; diff --git a/src/lib/generators/union.ts b/src/lib/generators/union.ts index 3c9eb8f..f2c3722 100644 --- a/src/lib/generators/union.ts +++ b/src/lib/generators/union.ts @@ -2,17 +2,7 @@ import { z } from "zod"; import { Generator, generate } from "../generate.js"; import { faker } from "@faker-js/faker"; import { RecursionLimitReachedException } from "../exceptions.js"; -import { GeneratorDefinitionFactory } from "lib/zocker.js"; - -export const UnionGenerator: GeneratorDefinitionFactory> = ( - options = {} -) => { - return { - schema: options.schema ?? (z.ZodUnion as any), - generator: generate_union, - match: options.match ?? "instanceof" - }; -}; +import { InstanceofGeneratorDefinition } from "lib/zocker.js"; const generate_union: Generator> = (schema, ctx) => { const schemas = schema._def.options as z.ZodTypeAny[]; @@ -41,3 +31,9 @@ const generate_union: Generator> = (schema, ctx) => { //and we should throw a RecursionLimitReachedException throw new RecursionLimitReachedException(); }; + +export const UnionGenerator: InstanceofGeneratorDefinition> = { + schema: z.ZodUnion as any, + generator: generate_union, + match: "instanceof" +}; diff --git a/src/lib/zocker.ts b/src/lib/zocker.ts index 96470ce..b588ce9 100644 --- a/src/lib/zocker.ts +++ b/src/lib/zocker.ts @@ -2,77 +2,208 @@ import { z } from "zod"; import { GenerationContext, generate, Generator } from "./generate.js"; import { faker } from "@faker-js/faker"; import { default_generators } from "./default_generators.js"; +import { NumberGeneratorOptions } from "./generators/numbers.js"; +import { OptionalOptions } from "./generators/optional.js"; +import { NullableOptions } from "./generators/nullable.js"; +import { DefaultOptions } from "./generators/default.js"; +import { MapOptions } from "./generators/map.js"; +import { RecordOptions } from "./generators/record.js"; +import { SetOptions } from "./generators/set.js"; -/** - * A factory function that creates a GeneratorDefinition with the given options. - */ -export type GeneratorDefinitionFactory< - Z extends z.ZodSchema, - O extends {} = {} -> = ( - options?: O & { - match?: "instanceof" | "reference"; - schema?: Z; - } -) => GeneratorDefinition; - -/** - * A definition of a generator and when it should be used. - */ -export type GeneratorDefinition = { +export type InstanceofGeneratorDefinition = { schema: Z; generator: Generator; - match: "instanceof" | "reference"; + match: "instanceof"; }; -export type ZockerOptions = { - /** A list of generators to use for generation. This will be appended by the built-in generators */ - generators?: GeneratorDefinition[]; - /** The seed to use for the random number generator */ - seed?: number; - - /** The maximum number of times a schema can be cyclically generated */ - recursion_limit?: number; +export type ReferenceGeneratorDefinition = { + schema: Z; + generator: Generator; + match: "reference"; }; -/** - * Generate random* valid mock-data from a Zod-Schem - * @param schema A Zod-Schema - * @returns A Zocker-Function that can be used to generate random data that matches the schema. - */ -export function zocker( - schema: Z, - options: ZockerOptions = {} -): z.infer { - //add the default generators to the list of generators - const generators: GeneratorDefinition[] = [ - ...(options.generators ?? []), +export function zocker(schema: Z) { + return new Zocker(schema); +} + +class Zocker { + private instanceof_generators: InstanceofGeneratorDefinition[] = [ ...default_generators ]; + private reference_generators: ReferenceGeneratorDefinition[] = []; + private seed: number | undefined = undefined; + private recursion_limit = 5; - //Split the generators into instanceof and reference generators - const reference_generators = generators.filter( - (g) => g.match === "reference" - ); + private number_options: NumberGeneratorOptions = { + extreme_value_chance: 0.3 + }; + + private optional_options: OptionalOptions = { + undefined_chance: 0.3 + }; - const instanceof_generators = generators.filter( - (g) => g.match === "instanceof" - ); + private nullable_options: NullableOptions = { + null_chance: 0.3 + }; - //Set the seed for the random number generator - const seed = options.seed ?? Math.random() * 10_000_000; - faker.seed(seed); + private default_options: DefaultOptions = { + default_chance: 0.3 + }; - const root_generation_context: GenerationContext = { - reference_generators, - instanceof_generators, - recursion_limit: options.recursion_limit ?? 5, + private map_options: MapOptions = { + max: 10, + min: 0 + }; - path: [], - semantic_context: "unspecified", - parent_schemas: new Map(), - seed + private record_options: RecordOptions = { + max: 10, + min: 0 }; - return generate(schema, root_generation_context); + private set_options: SetOptions = { + max: 10, + min: 0 + }; + + constructor(public schema: Z) {} + + /** + * Supply your own value / function for generating values for a given schema + * It will be used whenever the given schema matches an encountered schema by referebce + * + * @param schema - The schema for which this value will be used + * @param generator - A value, or a function that generates a value that matches the schema + */ + supply( + schema: Z, + generator: Generator | z.infer + ) { + const next = this.clone(); + + const generator_function = + typeof generator === "function" ? generator : () => generator; + + next.reference_generators = [ + { + schema, + generator: generator_function, + match: "reference" + }, + ...next.reference_generators + ]; + + return next; + } + + + /** + * Override one of the built-in generators using your own. + * It will be used whenever an encoutntered Schema matches the one specified by **instance** + * + * @param schema - Which schema to override. E.g: `z.ZodNumber`. + * @param generator - A value, or a function that generates a value that matches the schema + */ + override( + schema: Z, + generator: Generator | z.infer + ) { + const next = this.clone(); + const generator_function = + typeof generator === "function" ? generator : () => generator; + + next.instanceof_generators = [ + { + schema, + generator: generator_function, + match: "instanceof" + }, + ...next.instanceof_generators + ]; + + return next; + } + + setSeed(seed: number) { + const next = this.clone(); + next.seed = seed; + return next; + } + + setDepthLimit(limit: number) { + const next = this.clone(); + next.recursion_limit = limit; + return next; + } + + number(options: Partial) { + const next = this.clone(); + next.number_options = { ...next.number_options, ...options }; + return next; + } + + optional(options: Partial) { + const next = this.clone(); + next.optional_options = { ...next.optional_options, ...options }; + return next; + } + + nullable(options: Partial) { + const next = this.clone(); + next.nullable_options = { ...next.nullable_options, ...options }; + return next; + } + + default(options: Partial) { + const next = this.clone(); + next.default_options = { ...next.default_options, ...options }; + return next; + } + + map(options: Partial) { + const next = this.clone(); + next.map_options = { ...next.map_options, ...options }; + return next; + } + + record(options: Partial) { + const next = this.clone(); + next.record_options = { ...next.record_options, ...options }; + return next; + } + + set(options: Partial) { + const next = this.clone(); + next.set_options = { ...next.set_options, ...options }; + return next; + } + + generate(): z.infer { + const ctx: GenerationContext = { + reference_generators: this.reference_generators, + instanceof_generators: this.instanceof_generators, + recursion_limit: this.recursion_limit, + path: [], + semantic_context: "unspecified", + parent_schemas: new Map(), + seed: this.seed ?? Math.random() * 10_000_000, + + number_options: this.number_options, + optional_options: this.optional_options, + nullable_options: this.nullable_options, + default_options: this.default_options, + map_options: this.map_options, + record_options: this.record_options, + set_options: this.set_options + }; + + faker.seed(ctx.seed); + return generate(this.schema, ctx); + } + + private clone(): Zocker { + return Object.create( + Object.getPrototypeOf(this), + Object.getOwnPropertyDescriptors(this) + ); + } } diff --git a/tests/bigint.test.ts b/tests/bigint.test.ts index 4b3b8ef..af73a99 100644 --- a/tests/bigint.test.ts +++ b/tests/bigint.test.ts @@ -8,8 +8,12 @@ const bigint_schemas = { "bigint with min and max": z.bigint().min(10_000n).max(20_000n), "bigint with min and max negative": z.bigint().min(-20n).max(-10n), "bigint multiple of 10": z.bigint().multipleOf(10n), - "bigint multiple of 10 and 5": z.bigint().multipleOf(10n).multipleOf(5n).multipleOf(3n), - "bigint multiple min & max" : z.bigint().min(10n).max(20n).min(15n).max(25n), + "bigint multiple of 10 and 5": z + .bigint() + .multipleOf(10n) + .multipleOf(5n) + .multipleOf(3n), + "bigint multiple min & max": z.bigint().min(10n).max(20n).min(15n).max(25n) } as const; describe("BigInt generation", () => { diff --git a/tests/invalid-strings.test.ts b/tests/invalid-strings.test.ts index 030f5b5..d2c7057 100644 --- a/tests/invalid-strings.test.ts +++ b/tests/invalid-strings.test.ts @@ -26,7 +26,7 @@ const invalid_string_schemas = { describe("Invalid string generation", () => { for (const [name, schema] of Object.entries(invalid_string_schemas)) { it("fails on " + name, () => { - expect(() => zocker(schema)).toThrow(); + expect(() => zocker(schema).generate()).toThrow(); }); } }); diff --git a/tests/optional.test.ts b/tests/optional.test.ts index f2857ec..14cb096 100644 --- a/tests/optional.test.ts +++ b/tests/optional.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { z } from "zod"; -import { zocker, OptionalGenerator } from "../src/"; +import { zocker } from "../src/"; import { test_schema_generation } from "./utils"; const optional_schemas = { @@ -20,27 +20,19 @@ describe("Optional generation", () => { test_schema_generation(optional_schemas); it("only generates undefined if the undefined chance is 1", () => { - const generators = [ - OptionalGenerator({ - undefined_chance: 1 - }) - ]; - for (let i = 0; i < 100; i++) { - const data = zocker(optional_schema, { generators }); + const data = zocker(optional_schema) + .optional({ undefined_chance: 1 }) + .generate(); expect(() => undefined_schema.parse(data)).not.toThrow(); } }); it("never generates undefined if the undefined chance is 0", () => { - const generators = [ - OptionalGenerator({ - undefined_chance: 0 - }) - ]; - for (let i = 0; i < 100; i++) { - const data = zocker(optional_schema, { generators }); + const data = zocker(optional_schema) + .optional({ undefined_chance: 0 }) + .generate(); expect(() => requred_schema.parse(data)).not.toThrow(); } diff --git a/tests/partial-objects.test.ts b/tests/partial-objects.test.ts index 0d00a72..dcedd97 100644 --- a/tests/partial-objects.test.ts +++ b/tests/partial-objects.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { z } from "zod"; -import { OptionalGenerator, zocker } from "../src"; +import { zocker } from "../src"; const object_schema = z.object({ a: z.string(), @@ -12,12 +12,9 @@ const partial_object = object_schema.partial(); describe("Partial object generation", () => { it("should make some properties optional, if the schema is partial", () => { - const generators = [ - OptionalGenerator({ - undefined_chance: 1 - }) - ]; - const data = zocker(partial_object, { generators }); + const data = zocker(partial_object) + .optional({ undefined_chance: 1 }) + .generate(); expect(data).toEqual({ a: undefined, b: undefined, @@ -26,13 +23,9 @@ describe("Partial object generation", () => { }); it("should not make properties undefined, if the undefined chance is 0", () => { - const generators = [ - OptionalGenerator({ - undefined_chance: 0 - }) - ]; - - const data = zocker(partial_object, { generators }); + const data = zocker(partial_object) + .optional({ undefined_chance: 0 }) + .generate(); expect(() => object_schema.parse(data)).not.toThrow(); }); }); diff --git a/tests/promises.test.ts b/tests/promises.test.ts index 44f2eab..a1fe141 100644 --- a/tests/promises.test.ts +++ b/tests/promises.test.ts @@ -25,7 +25,7 @@ describe("Promise generation", () => { test.each(schema_keys)("generates valid data for %s", async (key) => { const schema = promise_schemas[key]; for (let i = 0; i < repeats; i++) { - const data = zocker(schema); + const data = zocker(schema).generate(); await expect(schema.parseAsync(data)).resolves.toEqual(await data); } }); diff --git a/tests/refinements.test.ts b/tests/refinements.test.ts index d294151..f488295 100644 --- a/tests/refinements.test.ts +++ b/tests/refinements.test.ts @@ -9,16 +9,10 @@ const object_with_refined_string = z.object({ }); describe("Provide a generation function for a schema (by reference)", () => { - const generators = [ - { - schema: refined_string, - generator: () => "123456", - match: "reference" - } - ]; - it("should generate a valid value", () => { - const value = zocker(object_with_refined_string, { generators }); + const value = zocker(object_with_refined_string) + .supply(refined_string, "123456") + .generate(); object_with_refined_string.parse(value); }); }); diff --git a/tests/repeatability.test.ts b/tests/repeatability.test.ts index fe37fe2..3446bdb 100644 --- a/tests/repeatability.test.ts +++ b/tests/repeatability.test.ts @@ -23,24 +23,24 @@ const schema = z.object({ describe("repeatability", () => { it("should generate identcal values for the same seed", () => { const seed = 0; - const first = zocker(schema, { seed }); + const first = zocker(schema).setSeed(seed).generate(); for (let i = 0; i < 10; i++) { - const next = zocker(schema, { seed }); - expect(next).toEqual(first); + const second = zocker(schema).setSeed(seed).generate(); + expect(second).toEqual(first); } }); it("should generate different values for different seeds", () => { - const first = zocker(schema, { seed: 0 }); - const second = zocker(schema, { seed: 1 }); + const first = zocker(schema).setSeed(0).generate(); + const second = zocker(schema).setSeed(1).generate(); expect(first).not.toEqual(second); }); it("should generate different values if the seed is not specified", () => { - const first = zocker(schema); - const second = zocker(schema); + const first = zocker(schema).generate(); + const second = zocker(schema).generate(); expect(first).not.toEqual(second); }); diff --git a/tests/strings.test.ts b/tests/strings.test.ts index fb7a654..3a5b3de 100644 --- a/tests/strings.test.ts +++ b/tests/strings.test.ts @@ -72,7 +72,7 @@ describe("String generation", () => { .toUpperCase(); const uppercase_schema = z.string().regex(/[A-Z]*/); - const generated = zocker(transformed_schema); + const generated = zocker(transformed_schema).generate(); expect(() => uppercase_schema.parse(generated)).not.toThrow(); }); @@ -83,7 +83,7 @@ describe("String generation", () => { .toLowerCase(); const lowercase_schema = z.string().regex(/[a-z]*/); - const generated = zocker(transformed_schema); + const generated = zocker(transformed_schema).generate(); expect(() => lowercase_schema.parse(generated)).not.toThrow(); }); @@ -94,7 +94,7 @@ describe("String generation", () => { .trim(); const trimmed_schema = z.string().regex(/foo/); - const generated = zocker(transformed_schema); + const generated = zocker(transformed_schema).generate(); expect(() => trimmed_schema.parse(generated)).not.toThrow(); }); }); diff --git a/tests/transform.test.ts b/tests/transform.test.ts index 8c838b6..37aa164 100644 --- a/tests/transform.test.ts +++ b/tests/transform.test.ts @@ -9,7 +9,7 @@ describe("Transform generation", () => { .length(2) .transform((s) => s + s); const result_schema = z.string().length(4); - const result = zocker(chained_schema); + const result = zocker(chained_schema).generate(); expect(() => result_schema.parse(result)).not.toThrow(); }); @@ -23,7 +23,7 @@ describe("Transform generation", () => { const even_schema = z.number().int().positive().multipleOf(2); for (let i = 0; i < 100; i++) { - const result = zocker(doubled); + const result = zocker(doubled).generate(); expect(() => even_schema.parse(result)).not.toThrow(); } }); diff --git a/tests/utils.ts b/tests/utils.ts index 84a4b19..5b6c47f 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -19,7 +19,7 @@ export function test_schema_generation( test.concurrent.each(schema_keys)("generates valid data for %s", (key) => { const schema = schemas[key]; for (let i = 0; i < repeats; i++) { - const data = zocker(schema); + const data = zocker(schema).generate(); expect(() => { try { schema.parse(data);