Skip to content

Commit

Permalink
Better customisation (#15)
Browse files Browse the repository at this point in the history
* Added options for any and unknown

* Added options for extra properties on objects

* Housekeeping

* Added API section to docs

* Added note on `.shape` property
  • Loading branch information
LorisSigrist authored May 9, 2023
1 parent 8d94605 commit bb882d3
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 101 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-donuts-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zocker": minor
---

Added configuration for extra properties on objects
5 changes: 5 additions & 0 deletions .changeset/sweet-crews-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"zocker": minor
---

Added customization options for `any` and `unknown` data-types
174 changes: 162 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ Zocker is a library that automatically generates reasonable mock data from your
npm install --save-dev zocker
```

## Features & Limitations

`zocker` is still in 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 on a string
3. `z.function` is not supported
4. `z.Intersection` is not supported (yet)
5. `z.transform` is only supported if it's the last operation on a schema
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, but expanding rapidly (suggestions welcome)

## Usage

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.
Expand Down Expand Up @@ -43,18 +55,6 @@ const mockData = zocker(person_schema).generate();
*/
```

### 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 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.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)

### Supply your own value

If you have a value that you would like to control explicitly, you can supply your own.
Expand All @@ -78,6 +78,19 @@ It will be used whenever a sub-schema is encoutnered, that matches the one you p
This is the main way to work around unsupported types.

One convenient way to get a sub_schema by reference is through the `shape` property on the schema.

```typescript
const schema = z.object({
name: z.string(),
age: z.number()
});

const data = zocker(schema).supply(schema.shape.name, "Jonathan").generate();
```

This way you don't need to break out the sub-schema into a separate variable.

### Customizing the generation process

#### Providing Options to Built-Ins
Expand Down Expand Up @@ -202,6 +215,143 @@ const regex_schema = z.string().regex(/^[a-z0-9]{5,10}$/);
const data = zocker(regex_schema);
```

## API
### `.supply`
Allows you to supply a specific value for a specific schema. This is useful for testing edge-cases.

```typescript
const data = zocker(my_schema).supply(my_sub_schema, 0).generate();
```

The supplied value will be used if during the generation process a schema is encoutered, that matches the supplied `sub_schema` by *reference*.

You can also supply a function that returns a value. The function must follow the `Generator` type.

### `.override`
Allows you to override the generator for an entire category of schemas. This is useful if you want to override the generation of `z.ZodNumber` for example.

```typescript
const data = zocker(my_schema).override(z.ZodNumber, 0).generate();
```

The supplied value will be used if during the generation process a schema is encoutered, that is an *instance* of the supplied `schema`.

You can also supply a function that returns a value. The function must follow the `Generator` type.


### `.setDepthLimit`
Allows you to set the maximum depth of cyclic data. Defaults to 5.

### `.setSeed`
Allows you to set the seed for the random number generator. This ensures that the generation process is repeatable. If you don't set a seed, a random one will be chosen.

### `.generate`
Executes the generation process. Returns the generated data that matches the schema provided to `zocker`.


### `.set`
Options for the built-in `z.ZodSet` generator.

```typescript
{
max: 10,
min: 0
}
```

### `.array`
Options for the built-in `z.ZodArray` generator.

```typescript
{
max: 10,
min: 0
}
```

### `.map`
Options for the built-in `z.ZodMap` generator.

```typescript
{
max: 10,
min: 0
}
```


### `.record`
Options for the built-in `z.ZodRecord` generator.

```typescript
{
max: 10,
min: 0
}
```

### `.object`
Options for the built-in `z.ZodObject` generator.

```typescript
{
generate_extra_keys: true //extra keys will be generated if allowed by the schema
}
```

### `.any` / `.unknown`
Options for the built-in `z.ZodAny` and `z.ZodUnknown` generators.

```typescript
{
strategy: "true-any" | "json-compatible" | "fast"
}
```

### `.optional`
Options for the built-in `z.ZodOptional` generator.

```typescript
{
undefined_chance: 0.3
}
```

### `.nullable`
Options for the built-in `z.ZodNullable` generator.

```typescript
{
null_chance: 0.3
}
```

### `.default`
Options for the built-in `z.ZodDefault` generator.

```typescript
{
default_chance: 0.3
}
```

### `.number`
Options for the built-in `z.ZodNumber` generator.

```typescript
{
extreme_value_chance: 0.3
}
```


## `type Generator`
A generator is a function that takes a schema and a generation-context, and returns a value that matches the schema.

```typescript
type Generator<Z extends z.ZodTypeAny> = (schema: Z, ctx: GenerationContext) => z.infer<Z>;
```

## The Future

I intend to continue expanding the number of built-in generators, and make the generation process more customizable. If you have any ideas, please open an issue or a pull request - I'd love to hear your thoughts.
Expand Down
14 changes: 7 additions & 7 deletions src/lib/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { DefaultOptions } from "./generators/default.js";
import { MapOptions } from "./generators/map.js";
import { RecordOptions } from "./generators/record.js";
import { SetOptions } from "./generators/set.js";
import { AnyOptions } from "./generators/any.js";
import { ArrayOptions } from "./generators/array.js";
import { ObjectOptions } from "./generators/object.js";

/**
* Contains all the necessary configuration to generate a value for a given schema.
Expand All @@ -32,20 +35,17 @@ export type GenerationContext<Z extends z.ZodSchema> = {

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;
any_options: AnyOptions;
unknown_options: AnyOptions;
array_options: ArrayOptions;
object_options: ObjectOptions;
};

export type Generator<Z extends z.ZodSchema> = (
Expand Down
120 changes: 60 additions & 60 deletions src/lib/generators/any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,74 @@ import { generate, Generator } from "../generate.js";
import { pick } from "../utils/random.js";
import { InstanceofGeneratorDefinition } from "../zocker.js";

export const AnyGenerator: InstanceofGeneratorDefinition<z.ZodAny> = {
schema: z.ZodAny as any,
generator: Any("true-any"),
match: "instanceof"
export type AnyOptions = {
strategy: "true-any" | "json-compatible" | "fast";
};

export const UnknownGenerator: InstanceofGeneratorDefinition<z.ZodUnknown> = {
schema: z.ZodUnknown as any,
generator: Any(),
match: "instanceof"
};
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
const jsonSchema: z.ZodSchema = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);

/**
* Create a Generator for the `z.any()` and `z.unknown()` schemas.
* @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<Z extends z.ZodAny | z.ZodUnknown>(
strategy: "true-any" | "json-compatible" | "fast" = "true-any"
): Generator<Z> {
if (strategy === "fast") {
return () => undefined;
}
//It's important to have the schemas outside the generator, so that they have reference equality accross invocations.
//This allows us to not worry about infinite recursion, as the cyclic generation logic will protect us.
const any = z.any();
const potential_schemas = [
z.undefined(),
z.null(),
z.boolean(),
z.number(),
z.string(),
z.bigint(),
z.date(),
z.symbol(),
z.unknown(),
z.nan(),
z.record(any), //`z.object` is just a subset of this - no need for a separate case.
z.array(any), //Tuples are just a subset of this - no need for a separate case.
z.map(any, any),
z.set(any),
z.promise(any)
].map((schema) => schema.optional());

if (strategy === "json-compatible") {
const literalSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null()
]);
const jsonSchema: z.ZodSchema = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);
const generate_any: Generator<z.ZodAny> = (schema, ctx) => {
if (ctx.any_options.strategy === "fast") {
return undefined;
}

return (_schema, generation_context) => {
const generated = generate(jsonSchema, generation_context);
return generated;
};
if (ctx.any_options.strategy === "json-compatible") {
const generated = generate(jsonSchema, ctx);
return generated;
}

const any = z.any();
const schema_to_use = pick(potential_schemas);
const generated = generate(schema_to_use, ctx);
return generated;
};

//It's important to have the schemas outside the generator, so that they have reference equality accross invocations.
//This allows us to not worry about infinite recursion, as the cyclic generation logic will protect us.
const potential_schemas = [
z.undefined(),
z.null(),
z.boolean(),
z.number(),
z.string(),
z.bigint(),
z.date(),
z.symbol(),
z.unknown(),
z.nan(),
z.record(any), //`z.object` is just a subset of this - no need for a separate case.
z.array(any), //Tuples are just a subset of this - no need for a separate case.
z.map(any, any),
z.set(any),
z.promise(any)
].map((schema) => schema.optional());
const generate_unknown: Generator<z.ZodUnknown> = (schema, ctx) => {
if (ctx.unknown_options.strategy === "fast") {
return undefined;
}

const generate_any: Generator<Z> = (_schema, generation_context) => {
const schema_to_use = pick(potential_schemas);
const generated = generate(schema_to_use, generation_context);
if (ctx.unknown_options.strategy === "json-compatible") {
const generated = generate(jsonSchema, ctx);
return generated;
};
}

const schema_to_use = pick(potential_schemas);
const generated = generate(schema_to_use, ctx);
return generated;
};

export const AnyGenerator: InstanceofGeneratorDefinition<z.ZodAny> = {
schema: z.ZodAny as any,
generator: generate_any,
match: "instanceof"
};

return generate_any;
}
export const UnknownGenerator: InstanceofGeneratorDefinition<z.ZodUnknown> = {
schema: z.ZodUnknown as any,
generator: generate_unknown,
match: "instanceof"
};
Loading

0 comments on commit bb882d3

Please sign in to comment.