Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: support custom brands handling in Documentation and Integration #1750

Merged
merged 33 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e90bdfb
Feature #1470 draft.
RobinTail May 9, 2024
26b7ba9
REF: Moving the subject schema in front of the arguments of the Schem…
RobinTail May 10, 2024
79166c6
REF: extracting ctx to a dedicated property of Schema Walker.
RobinTail May 10, 2024
abd4034
REF: Easier types for Schema Walker.
RobinTail May 10, 2024
e0240c9
Ref: making for HandlingRules the paramteric keys.
RobinTail May 10, 2024
3c9294a
Ref: naming: brandHandling.
RobinTail May 10, 2024
ee964bb
Jsdoc for brandHandling in Documentation.
RobinTail May 10, 2024
d683a8f
FEAT: Integration with brandHandling.
RobinTail May 10, 2024
7ba4de4
Test for custom brands handling in Integration.
RobinTail May 10, 2024
7a444bd
Testing of calling next().
RobinTail May 10, 2024
2f71bbe
Also testing symbols.
RobinTail May 10, 2024
307c605
Changelog: featuring 19.1.0 example.
RobinTail May 10, 2024
e0c7930
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 10, 2024
4634d97
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 11, 2024
be9ca50
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 11, 2024
e57f903
Readme: listing the feature.
RobinTail May 11, 2024
c75460a
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 11, 2024
c1d7aa5
Readme: alignment and index.
RobinTail May 11, 2024
d71bd8c
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 12, 2024
47e12e6
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 13, 2024
be805e0
Merge branch 'make-v19' into feat-support-custom-schema
RobinTail May 13, 2024
9ce7f0f
Merge branch 'master' into feat-support-custom-schema
RobinTail May 13, 2024
8cb0466
Ref: extracting NestedSchemaLookupProps.
RobinTail May 14, 2024
7670935
Exposing Producer type.
RobinTail May 14, 2024
1511fb8
Removing argument from Depicter type.
RobinTail May 14, 2024
f0836b6
Exposing type Depicter.
RobinTail May 14, 2024
b3f93fe
Changelog: update on reusing Depicter and Producer.
RobinTail May 14, 2024
802a40a
Apply suggestions from code review
RobinTail May 14, 2024
cafa4aa
Ref: shortening all documentation helper tests.
RobinTail May 14, 2024
11c2840
Ref: less imports in test.
RobinTail May 14, 2024
18bf02f
Changelog: mentioning that brands in runtime is the plugin feature.
RobinTail May 14, 2024
4d42a68
Merge branch 'master' into feat-support-custom-schema
RobinTail May 14, 2024
3c93150
Readme: copy from changelog.
RobinTail May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,53 @@

## Version 19

### v19.1.0

- Feature: customizable handling rules for your branded schemas in Documentation and Integration:
- You can make your schemas special by branding them using `.brand()` method;
- The library (being a Zod Plugin as well) distinguishes the branded schemas in runtime;
- The constructors of `Documentation` and `Integration` now accept new property `brandHandling` (object);
- Its keys should be the brands you want to handle in a special way;
- Its values are functions having your schema as the first argument and a context in the second place;
- In case you need to reuse a handling rule for multiple brands, use the exposed types `Depicter` and `Producer`.

```ts
import ts from "typescript";
import { z } from "zod";
import {
Documentation,
Integration,
Depicter,
Producer,
} from "express-zod-api";

const myBrand = Symbol("MamaToldMeImSpecial"); // I recommend to use symbols for this purpose
const myBrandedSchema = z.string().brand(myBrand);

const ruleForDocs: Depicter = (
schema: typeof myBrandedSchema, // you should assign type yourself
{ next, path, method, isResponse }, // handle a nested schema using next()
) => {
const defaultDepiction = next(schema.unwrap()); // { type: string }
return { summary: "Special type of data" };
};

const ruleForClient: Producer = (
schema: typeof myBrandedSchema, // you should assign type yourself
{ next, isResponse, serializer }, // handle a nested schema using next()
) => ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);

new Documentation({
/* config, routing, title, version */
brandHandling: { [myBrand]: ruleForDocs },
});

new Integration({
/* routing */
brandHandling: { [myBrand]: ruleForClient },
});
```

### v19.0.0

- **Breaking changes**:
Expand Down
39 changes: 39 additions & 0 deletions README.md
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Start your API server with I/O schema validation and custom middlewares in minut
2. [Generating a Frontend Client](#generating-a-frontend-client)
3. [Creating a documentation](#creating-a-documentation)
4. [Tagging the endpoints](#tagging-the-endpoints)
5. [Customizable brands handling](#customizable-brands-handling)
8. [Caveats](#caveats)
1. [Coercive schema of Zod](#coercive-schema-of-zod)
2. [Excessive properties in endpoint output](#excessive-properties-in-endpoint-output)
Expand Down Expand Up @@ -1170,6 +1171,44 @@ const exampleEndpoint = taggedEndpointsFactory.build({
});
```

## Customizable brands handling

You can customize handling rules for your schemas in Documentation and Integration. Use the `.brand()` method on your
schema to make it special and distinguishable for the library in runtime. Using symbols is recommended for branding.
After that utilize the `brandHandling` feature of both constructors to declare your custom implementation.

```ts
import { z } from "zod";
import { Documentation, Integration } from "express-zod-api";

const myBrand = Symbol("MamaToldMeImSpecial");
const myBrandedSchema = z.string().brand(myBrand);

new Documentation({
brandHandling: {
[myBrand]: (
schema: typeof myBrandedSchema, // you should assign type yourself
{ next, path, method, isResponse }, // handle a nested schema using next()
) => {
const defaultResult = next(schema.unwrap()); // { type: string }
return { summary: "Special type of data" };
},
},
});

import ts from "typescript";
const { factory: f } = ts;

new Integration({
brandHandling: {
[myBrand]: (
schema: typeof myBrandedSchema, // you should assign type yourself
{ next, isResponse, serializer }, // handle a nested schema using next()
) => f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
},
});
```

# Caveats

There are some well-known issues and limitations, or third party bugs that cannot be fixed in the usual way, but you
Expand Down
90 changes: 42 additions & 48 deletions src/deep-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,58 @@ import { HandlingRules, SchemaHandler } from "./schema-walker";
import { ezUploadBrand } from "./upload-schema";

/** @desc Check is a schema handling rule returning boolean */
type Check<T extends z.ZodTypeAny> = SchemaHandler<T, boolean>;
type Check = SchemaHandler<boolean>;

const onSomeUnion: Check<
| z.ZodUnion<z.ZodUnionOptions>
| z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>
> = ({ schema: { options }, next }) => options.some(next);
const onSomeUnion: Check = (
schema:
| z.ZodUnion<z.ZodUnionOptions>
| z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>,
{ next },
) => schema.options.some(next);

const onIntersection: Check<z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>> = ({
schema: { _def },
next,
}) => [_def.left, _def.right].some(next);
const onIntersection: Check = (
{ _def }: z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>,
{ next },
) => [_def.left, _def.right].some(next);

const onObject: Check<z.ZodObject<z.ZodRawShape>> = ({ schema, next }) =>
Object.values(schema.shape).some(next);
const onElective: Check<
z.ZodOptional<z.ZodTypeAny> | z.ZodNullable<z.ZodTypeAny>
> = ({ schema, next }) => next(schema.unwrap());
const onEffects: Check<z.ZodEffects<z.ZodTypeAny>> = ({ schema, next }) =>
next(schema.innerType());
const onRecord: Check<z.ZodRecord> = ({ schema, next }) =>
next(schema.valueSchema);
const onArray: Check<z.ZodArray<z.ZodTypeAny>> = ({ schema, next }) =>
next(schema.element);
const onDefault: Check<z.ZodDefault<z.ZodTypeAny>> = ({ schema, next }) =>
next(schema._def.innerType);
const onElective: Check = (
schema: z.ZodOptional<z.ZodTypeAny> | z.ZodNullable<z.ZodTypeAny>,
{ next },
) => next(schema.unwrap());

const checks: HandlingRules<boolean> = {
ZodObject: onObject,
const checks: HandlingRules<boolean, {}, z.ZodFirstPartyTypeKind> = {
ZodObject: ({ shape }: z.ZodObject<z.ZodRawShape>, { next }) =>
Object.values(shape).some(next),
ZodUnion: onSomeUnion,
ZodDiscriminatedUnion: onSomeUnion,
ZodIntersection: onIntersection,
ZodEffects: onEffects,
ZodEffects: (schema: z.ZodEffects<z.ZodTypeAny>, { next }) =>
next(schema.innerType()),
ZodOptional: onElective,
ZodNullable: onElective,
ZodRecord: onRecord,
ZodArray: onArray,
ZodDefault: onDefault,
ZodRecord: ({ valueSchema }: z.ZodRecord, { next }) => next(valueSchema),
ZodArray: ({ element }: z.ZodArray<z.ZodTypeAny>, { next }) => next(element),
ZodDefault: ({ _def }: z.ZodDefault<z.ZodTypeAny>, { next }) =>
next(_def.innerType),
};

/** @desc The optimized version of the schema walker for boolean checks */
export const hasNestedSchema = ({
subject,
condition,
rules = checks,
depth = 1,
maxDepth = Number.POSITIVE_INFINITY,
}: {
subject: z.ZodTypeAny;
interface NestedSchemaLookupProps {
condition: (schema: z.ZodTypeAny) => boolean;
rules?: HandlingRules<boolean>;
maxDepth?: number;
depth?: number;
}): boolean => {
}

/** @desc The optimized version of the schema walker for boolean checks */
export const hasNestedSchema = (
subject: z.ZodTypeAny,
{
condition,
rules = checks,
depth = 1,
maxDepth = Number.POSITIVE_INFINITY,
}: NestedSchemaLookupProps,
): boolean => {
if (condition(subject)) {
return true;
}
Expand All @@ -67,11 +66,9 @@ export const hasNestedSchema = ({
? rules[subject._def.typeName as keyof typeof rules]
: undefined;
if (handler) {
return handler({
schema: subject,
return handler(subject, {
next: (schema) =>
hasNestedSchema({
subject: schema,
hasNestedSchema(schema, {
condition,
rules,
maxDepth,
Expand All @@ -83,8 +80,7 @@ export const hasNestedSchema = ({
};

export const hasTransformationOnTop = (subject: IOSchema): boolean =>
hasNestedSchema({
subject,
hasNestedSchema(subject, {
maxDepth: 3,
rules: { ZodUnion: onSomeUnion, ZodIntersection: onIntersection },
condition: (schema) =>
Expand All @@ -93,14 +89,12 @@ export const hasTransformationOnTop = (subject: IOSchema): boolean =>
});

export const hasUpload = (subject: IOSchema) =>
hasNestedSchema({
subject,
hasNestedSchema(subject, {
condition: (schema) => schema._def[metaSymbol]?.brand === ezUploadBrand,
});

export const hasRaw = (subject: IOSchema) =>
hasNestedSchema({
subject,
hasNestedSchema(subject, {
condition: (schema) => schema._def[metaSymbol]?.brand === ezRawBrand,
maxDepth: 3,
});