Skip to content

Commit

Permalink
feat: render any z.union() as strings
Browse files Browse the repository at this point in the history
Zod unions can now be rendered as an array of string.
The Zod union can contain multiple nested levels of Zod unions.
The generated array of string is flattened and sorted.

Added string representation for JS Symbol and BigInt in the `stringify`
function.
  • Loading branch information
jackdbd committed Apr 25, 2024
1 parent a9913ab commit e95b108
Show file tree
Hide file tree
Showing 19 changed files with 367 additions and 14 deletions.
91 changes: 91 additions & 0 deletions .ae/doc/zod-to-doc.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,65 @@
"endIndex": 6
}
},
{
"kind": "Variable",
"canonicalReference": "@jackdbd/zod-to-doc!arrayFromZodUnion:var",
"docComment": "/**\n * Converts a Zod union into an array of strings.\n *\n * @public @experimental\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "arrayFromZodUnion: "
},
{
"kind": "Content",
"text": "<S extends "
},
{
"kind": "Reference",
"text": "z.ZodUnion",
"canonicalReference": "zod!ZodUnion:class"
},
{
"kind": "Content",
"text": "<readonly ["
},
{
"kind": "Reference",
"text": "z.ZodTypeAny",
"canonicalReference": "zod!ZodTypeAny:type"
},
{
"kind": "Content",
"text": ", ..."
},
{
"kind": "Reference",
"text": "z.ZodTypeAny",
"canonicalReference": "zod!ZodTypeAny:type"
},
{
"kind": "Content",
"text": "[]]>>(schema: S) => {\n error: "
},
{
"kind": "Reference",
"text": "Error",
"canonicalReference": "!Error:interface"
},
{
"kind": "Content",
"text": ";\n value?: undefined;\n} | {\n value: string[];\n error?: undefined;\n}"
}
],
"fileUrlPath": "src/lib.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "arrayFromZodUnion",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 10
}
},
{
"kind": "Variable",
"canonicalReference": "@jackdbd/zod-to-doc!markdownTableFromZodSchema:var",
Expand Down Expand Up @@ -253,6 +312,38 @@
"startIndex": 1,
"endIndex": 6
}
},
{
"kind": "Variable",
"canonicalReference": "@jackdbd/zod-to-doc!stringsFromZodAnyType:var",
"docComment": "/**\n * Converts any Zod type into an array of strings.\n *\n * @public @experimental\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "stringsFromZodAnyType: "
},
{
"kind": "Content",
"text": "(x: "
},
{
"kind": "Reference",
"text": "ZodTypeAny",
"canonicalReference": "zod!ZodTypeAny:type"
},
{
"kind": "Content",
"text": ") => string[]"
}
],
"fileUrlPath": "src/lib.ts",
"isReadonly": true,
"releaseTag": "Public",
"name": "stringsFromZodAnyType",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 4
}
}
]
}
Expand Down
13 changes: 13 additions & 0 deletions .ae/zod-to-doc.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```ts

import { z } from 'zod';
import type { ZodTypeAny } from 'zod';

// @public
export const arrayFromZodSchema: <S extends z.AnyZodObject>(schema: S) => {
Expand All @@ -19,6 +20,15 @@ export const arrayFromZodSchema: <S extends z.AnyZodObject>(schema: S) => {
error?: undefined;
};

// @public
export const arrayFromZodUnion: <S extends z.ZodUnion<readonly [z.ZodTypeAny, ...z.ZodTypeAny[]]>>(schema: S) => {
error: Error;
value?: undefined;
} | {
value: string[];
error?: undefined;
};

// Warning: (ae-internal-missing-underscore) The name "defaultZodValue" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
Expand All @@ -38,6 +48,9 @@ export const markdownTableFromZodSchema: <S extends z.AnyZodObject>(schema: S) =
// @internal (undocumented)
export const stringify: (x: any) => string;

// @public
export const stringsFromZodAnyType: (x: ZodTypeAny) => string[];

// (No @packageDocumentation comment for this package)

```
19 changes: 19 additions & 0 deletions api-docs/zod-to-doc.arrayfromzodunion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@jackdbd/zod-to-doc](./zod-to-doc.md) &gt; [arrayFromZodUnion](./zod-to-doc.arrayfromzodunion.md)

## arrayFromZodUnion variable

Converts a Zod union into an array of strings.

**Signature:**

```typescript
arrayFromZodUnion: <S extends z.ZodUnion<readonly [z.ZodTypeAny, ...z.ZodTypeAny[]]>>(schema: S) => {
error: Error;
value?: undefined;
} | {
value: string[];
error?: undefined;
}
```
2 changes: 2 additions & 0 deletions api-docs/zod-to-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
| Variable | Description |
| --- | --- |
| [arrayFromZodSchema](./zod-to-doc.arrayfromzodschema.md) | Converts a Zod schema into an array of objects. |
| [arrayFromZodUnion](./zod-to-doc.arrayfromzodunion.md) | Converts a Zod union into an array of strings. |
| [markdownTableFromZodSchema](./zod-to-doc.markdowntablefromzodschema.md) | Creates a markdown table from a Zod schema. |
| [stringsFromZodAnyType](./zod-to-doc.stringsfromzodanytype.md) | Converts any Zod type into an array of strings. |

13 changes: 13 additions & 0 deletions api-docs/zod-to-doc.stringsfromzodanytype.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@jackdbd/zod-to-doc](./zod-to-doc.md) &gt; [stringsFromZodAnyType](./zod-to-doc.stringsfromzodanytype.md)

## stringsFromZodAnyType variable

Converts any Zod type into an array of strings.

**Signature:**

```typescript
stringsFromZodAnyType: (x: ZodTypeAny) => string[]
```
2 changes: 1 addition & 1 deletion docs/assets/navigation.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/assets/search.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/functions/arrayFromZodSchema.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/functions/arrayFromZodUnion.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/functions/markdownTableFromZodSchema.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/functions/stringsFromZodAnyType.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion docs/modules.html

Large diffs are not rendered by default.

37 changes: 35 additions & 2 deletions fixtures/schemas.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import { z } from 'zod'

export const color = z.union([
z.literal('red'),
z.literal('green').describe('The green color'),
z.literal('blue')
])

// https://zod.dev/?id=literals
export const assorted_literals = z.union([
z.literal(),
z.literal().describe('An empty literal'),
z.literal(123).describe('The literal number `123`'),
z.literal(456),
z.literal(BigInt(9007199254740991)).describe('A very big number'),
z.literal('tuna').describe('The string `tuna`'),
z.literal(true).describe('The boolean `true`'),
z.literal(Symbol('terrific')).describe('The symbol `terrific`')
])

// https://zod.dev/?id=unions
export const assorted_union = z.union([
z.literal('tuna').describe('The literal string `tuna`'),
z.boolean(),
z.literal(123).describe('The literal number `123`'),
z.literal(456),
z.number().min(5).max(9),
z.number().min(1).max(10).describe('A number between 1 and 10'),
z.bigint(),
z.string().min(3).max(5).describe('A string with 3 to 5 characters'),
z
.object({ foo: z.string(), color: color })
.describe('An object with a given description'),
z.object({ bar: z.string(), baz: z.number() })
])

export const car_manufacturer = z
.literal('Ferrari')
.or(z.literal('Ford'))
.or(z.literal('Ford'))
.or(z.literal('Honda'))
.or(z.literal('Honda').describe('The Honda car manufacturer'))
.or(z.literal('Peugeot'))
.or(z.literal('Toyota'))
.or(z.literal('Volkswagen'))
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"build:ts:watch": "tsc -p tsconfig.json --watch",
"clean": "rimraf coverage/lcov.info dist/ tsconfig.tsbuildinfo",
"commitlint": "commitlint --config ./config/commitlint.cjs --to HEAD --verbose",
"dev": "run-p 'build:ts:watch' 'test:watch'",
"dev": "DEBUG='' run-p 'build:ts:watch' 'test:watch'",
"preexample": "chmod u+x ./dist/cli.js",
"example": "run-s 'example:car'",
"example:car": "./dist/cli.js --module ./fixtures/schemas.mjs --schema car --placeholder car-table --title '#### Car table' --filepath tpl.readme.md",
Expand Down
85 changes: 83 additions & 2 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import defDebug from 'debug'
import { z } from 'zod'
import type { ZodTypeAny, ZodUnionOptions } from 'zod'
import { DEBUG_PREFIX } from './constants.js'

const debug = defDebug(`${DEBUG_PREFIX}:lib`)
Expand All @@ -22,6 +23,73 @@ export const defaultZodValue = (value: any) => {
}
}

/**
* Converts any Zod type into an array of strings.
*
* @public
* @experimental
*/
export const stringsFromZodAnyType = (x: ZodTypeAny) => {
if (x instanceof z.ZodBigInt) {
return x.description ? [x.description] : ['A BigInt']
} else if (x instanceof z.ZodBoolean) {
return x.description ? [x.description] : ['A Boolean']
} else if (x instanceof z.ZodLiteral) {
if (x.value) {
if (x.description) {
return [`${stringify(x.value)} (${x.description})`]
} else {
return [stringify(x.value)]
}
} else {
if (x.description) {
return [`A literal (${x.description})`]
} else {
return [`A literal`]
}
}
} else if (x instanceof z.ZodNumber) {
// TODO: get min,max from these checks?
// console.log('=== x._def.checks ===', x._def.checks)
return x.description ? [x.description] : ['A Number']
} else if (x instanceof z.ZodObject) {
// const res = arrayFromZodSchema(x as any)
return x.description ? [x.description] : ['An objects']
} else if (x instanceof z.ZodString) {
return x.description ? [x.description] : ['A String']
} else if (x instanceof z.ZodUnion) {
const arr: string[] = x.options.map((opt: z.ZodAny) => {
return stringsFromZodAnyType(opt)
})
const strings = arr.flat()
strings.sort()
return strings
} else {
// console.log('=== stringFromZodAnyType x._def ===', x._def)
return x.description ? [x.description] : ['TODO']
}
}

/**
* Converts a Zod union into an array of strings.
*
* @public
* @experimental
*/
export const arrayFromZodUnion = <S extends z.ZodUnion<ZodUnionOptions>>(
schema: S
) => {
if (!schema.options) {
return { error: new Error(`schema.options is not defined.`) }
}

debug(`Zod schema.options => JS array`)
const arr = schema.options.map(stringsFromZodAnyType).flat()
arr.sort()

return { value: arr }
}

/**
* Converts a Zod schema into an array of objects.
*
Expand All @@ -32,8 +100,7 @@ export const arrayFromZodSchema = <S extends z.AnyZodObject>(schema: S) => {
if (!schema.shape) {
return { error: new Error(`schema.shape is not defined.`) }
}

debug(`Zod schema => JS array`)
debug(`Zod schema.shape => JS array`)
const arr = Object.entries(schema.shape).map(([key, value]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val = value as any
Expand Down Expand Up @@ -92,6 +159,20 @@ export const stringify = (x: any) => {
return `\`${x}\``
}

if (typeof x === 'bigint') {
// The trailing "n" is not part of the string.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString
// https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-521449285
return x.toString()
}

if (typeof x === 'symbol') {
// Because Symbol has a [@@toPrimitive]() method, that method always takes
// priority over toString() when a Symbol object is coerced to a string.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString
return x.toString()
}

if (x.length === 0) {
return `\`[]\``
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('cli', () => {
const { stderr, stdout } = exception

assert.equal(stdout, '')
assert.match(stderr, /Module containing Zod schemas not found/)
assert.match(stderr, /module not found/)
}
}
)
Expand Down
Loading

0 comments on commit e95b108

Please sign in to comment.