Skip to content

Commit

Permalink
feat: Add JSDoc @description support (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
grrowl committed Jan 7, 2024
1 parent 54a1f23 commit b3b3d11
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 16 deletions.
34 changes: 19 additions & 15 deletions README.md
Expand Up @@ -147,33 +147,34 @@ export const heroContactSchema = z.object({

Other JSDoc tags are available:

| JSDoc keyword | JSDoc Example | Description | Generated Zod |
| ------------------ | ------------- | ----------------------------------------- | ------------------------ |
| `@default {value}` | `@default 42` | Sets a default value for the property | `z.number().default(42)` |
| `@strict` | `@strict` | Adds the `strict()` modifier to an object | `z.object().strict()` |
| JSDoc keyword | JSDoc Example | Description | Generated Zod |
| ---------------------- | ------------------------ | ----------------------------------------- | ---------------------------------- |
| `@description {value}` | `@description Full name` | Sets the description of the property | `z.string().describe("Full name")` |
| `@default {value}` | `@default 42` | Sets a default value for the property | `z.number().default(42)` |
| `@strict` | `@strict` | Adds the `strict()` modifier to an object | `z.object().strict()` |

## JSDoc tags for elements of `string` and `number` arrays

Elements of `string` and `number` arrays can be validated using the following JSDoc tags (for details see above).

| JSDoc keyword |
| ---------------------------------------|
| `@elementMinimum {number} [err_msg]` |
| `@elementMaximum {number} [err_msg]` |
| `@elementMinLength {number} [err_msg]` |
| `@elementMaxLength {number} [err_msg]` |
| `@elementFormat {FormatType} [err_msg]`|
| `@elementPattern {regex}` |
| JSDoc keyword |
| --------------------------------------- |
| `@elementDescription {value}` |
| `@elementMinimum {number} [err_msg]` |
| `@elementMaximum {number} [err_msg]` |
| `@elementMinLength {number} [err_msg]` |
| `@elementMaxLength {number} [err_msg]` |
| `@elementFormat {FormatType} [err_msg]` |
| `@elementPattern {regex}` |

Example:

```ts
// source.ts
export interface EnemyContact {

/**
* The names of the enemy.
*
*
* @elementMinLength 5
* @elementMaxLength 10
* @minLength 2
Expand All @@ -184,6 +185,7 @@ export interface EnemyContact {
/**
* The phone numbers of the enemy.
*
* @description Include home and work numbers
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumbers: string[];
Expand All @@ -206,7 +208,9 @@ export const enemyContactSchema = z.object({
*
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumbers: z.array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/)),
phoneNumbers: z
.array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/))
.describe("Include home and work numbers"),
});
```

Expand Down
71 changes: 71 additions & 0 deletions src/core/generateZodSchema.test.ts
Expand Up @@ -1114,6 +1114,77 @@ describe("generateZodSchema", () => {
`);
});

it("should add describe() when @description is used (top-level)", () => {
const source = `/**
* @description Originally Superman could leap, but not fly.
*/
export type Superman = {
name: "superman";
weakness: Kryptonite;
age: number;
enemies: Array<string>;
};`;
expect(generate(source)).toMatchInlineSnapshot(`
"/**
* @description Originally Superman could leap, but not fly.
*/
export const supermanSchema = z.object({
name: z.literal("superman"),
weakness: kryptoniteSchema,
age: z.number(),
enemies: z.array(z.string())
}).describe("Originally Superman could leap, but not fly.");"
`);
});

it("should add describe() when @description is used (property-level)", () => {
const source = `
export type Superman = {
name: "superman";
weakness: Kryptonite;
age: number;
/**
* @description Lex Luthor, Branaic, etc.
*/
enemies: Array<string>;
};`;
expect(generate(source)).toMatchInlineSnapshot(`
"export const supermanSchema = z.object({
name: z.literal("superman"),
weakness: kryptoniteSchema,
age: z.number(),
/**
* @description Lex Luthor, Branaic, etc.
*/
enemies: z.array(z.string()).describe("Lex Luthor, Branaic, etc.")
});"
`);
});

it("should add describe() when @description is used (array elements)", () => {
const source = `
export type Superman = {
name: "superman";
weakness: Kryptonite;
age: number;
/**
* @elementDescription Name of an enemy
*/
enemies: Array<string>;
};`;
expect(generate(source)).toMatchInlineSnapshot(`
"export const supermanSchema = z.object({
name: z.literal("superman"),
weakness: kryptoniteSchema,
age: z.number(),
/**
* @elementDescription Name of an enemy
*/
enemies: z.array(z.string().describe("Name of an enemy"))
});"
`);
});

it("should deal with nullable", () => {
const source = `export interface A {
/** @minimum 0 */
Expand Down
1 change: 1 addition & 0 deletions src/core/generateZodSchema.ts
Expand Up @@ -697,6 +697,7 @@ function buildZodPrimitive({
typeNode: typeNode.elementType,
isOptional: false,
jsDocTags: {
description: jsDocTags.elementDescription,
minimum: jsDocTags.elementMinimum,
maximum: jsDocTags.elementMaximum,
minLength: jsDocTags.elementMinLength,
Expand Down
24 changes: 23 additions & 1 deletion src/core/jsDocTags.ts
Expand Up @@ -53,6 +53,7 @@ type TagWithError<T> = {
* JSDoc special tags that can be converted in zod flags.
*/
export interface JSDocTagsBase {
description?: string;
minimum?: TagWithError<number>;
maximum?: TagWithError<number>;
default?: number | string | boolean | null;
Expand All @@ -69,21 +70,29 @@ export interface JSDocTagsBase {

export type ElementJSDocTags = Pick<
JSDocTagsBase,
"minimum" | "maximum" | "minLength" | "maxLength" | "pattern" | "format"
| "description"
| "minimum"
| "maximum"
| "minLength"
| "maxLength"
| "pattern"
| "format"
>;

export type JSDocTags = JSDocTagsBase & {
[K in keyof ElementJSDocTags as `element${Capitalize<K>}`]: ElementJSDocTags[K];
};

const jsDocTagKeys: Array<keyof JSDocTags> = [
"description",
"minimum",
"maximum",
"default",
"minLength",
"maxLength",
"format",
"pattern",
"elementDescription",
"elementMinimum",
"elementMaximum",
"elementMinLength",
Expand Down Expand Up @@ -164,6 +173,8 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
jsDocTags[tagName] = { value: parseInt(value), errorMessage };
}
break;
case "description":
case "elementDescription":
case "pattern":
case "elementPattern":
if (tag.comment) {
Expand Down Expand Up @@ -199,6 +210,10 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
jsDocTags[tagName] = tag.comment;
}
break;
case "strict":
break;
default:
tagName satisfies never;
}
});
});
Expand Down Expand Up @@ -306,6 +321,13 @@ export function jsDocTagToZodProperties(
identifier: "required",
});
}
if (jsDocTags.description !== undefined) {
zodProperties.push({
identifier: "describe",
expressions: [f.createStringLiteral(jsDocTags.description)],
});
}

if (jsDocTags.default !== undefined) {
zodProperties.push({
identifier: "default",
Expand Down

0 comments on commit b3b3d11

Please sign in to comment.