Skip to content

Commit

Permalink
feat: add JSDoc tags for string and number array elements (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
schiller-manuel committed Nov 28, 2023
1 parent 124054b commit df5be8b
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 4 deletions.
58 changes: 58 additions & 0 deletions README.md
Expand Up @@ -152,6 +152,64 @@ Other JSDoc tags are available:
| `@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}` |

Example:

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

/**
* The names of the enemy.
*
* @elementMinLength 5
* @elementMaxLength 10
* @minLength 2
* @maxLength 50
*/
names: string[];

/**
* The phone numbers of the enemy.
*
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumbers: string[];
}

// output.ts
export const enemyContactSchema = z.object({
/**
* The names of the enemy.
*
* @elementMinLength 5
* @elementMaxLength 10
* @minLength 2
* @maxLength 50
*/
name: z.array(z.string().min(5).max(10)).min(2).max(50),

/**
* The phone numbers of the enemy.
*
* @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}$/)),
});
```

## Advanced configuration

If you want to customize the schema name or restrict the exported schemas, you can do this by adding a `ts-to-zod.config.js` at the root of your project.
Expand Down
53 changes: 52 additions & 1 deletion src/core/generateZodSchema.test.ts
Expand Up @@ -728,6 +728,33 @@ describe("generateZodSchema", () => {
* @format ip
*/
ip: string;
/**
* The hero's known IPs
*
* @elementFormat ip
* @maxLength 5
*/
knownIps: Array<string>;
/**
* The hero's last ping times
*
* @elementMinimum 0
* @elementMaximum 100
* @minLength 1
* @maxLength 10
*/
pingTimes: number[];
/**
* The hero's blocked phone numbers.
*
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
* @minLength 56
* @maxLength 123
*/
blockedPhoneNumbers: string[];
}`;
expect(generate(source)).toMatchInlineSnapshot(`
"export const heroContactSchema = z.object({
Expand Down Expand Up @@ -786,7 +813,31 @@ describe("generateZodSchema", () => {
*
* @format ip
*/
ip: z.string().ip()
ip: z.string().ip(),
/**
* The hero's known IPs
*
* @elementFormat ip
* @maxLength 5
*/
knownIps: z.array(z.string().ip()).max(5),
/**
* The hero's last ping times
*
* @elementMinimum 0
* @elementMaximum 100
* @minLength 1
* @maxLength 10
*/
pingTimes: z.array(z.number().min(0).max(100)).min(1).max(10),
/**
* The hero's blocked phone numbers.
*
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
* @minLength 56
* @maxLength 123
*/
blockedPhoneNumbers: z.array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/)).min(56).max(123)
});"
`);
});
Expand Down
11 changes: 9 additions & 2 deletions src/core/generateZodSchema.ts
Expand Up @@ -289,7 +289,7 @@ function buildZodPrimitive({
typeNode: f.createArrayTypeNode(typeNode.typeArguments[0]),
isOptional,
isNullable,
jsDocTags: {},
jsDocTags,
sourceFile,
dependencies,
getDependencyName,
Expand Down Expand Up @@ -696,7 +696,14 @@ function buildZodPrimitive({
z,
typeNode: typeNode.elementType,
isOptional: false,
jsDocTags: {},
jsDocTags: {
minimum: jsDocTags.elementMinimum,
maximum: jsDocTags.elementMaximum,
minLength: jsDocTags.elementMinLength,
maxLength: jsDocTags.elementMaxLength,
format: jsDocTags.elementFormat,
pattern: jsDocTags.elementPattern,
},
sourceFile,
dependencies,
getDependencyName,
Expand Down
23 changes: 22 additions & 1 deletion src/core/jsDocTags.ts
Expand Up @@ -52,7 +52,7 @@ type TagWithError<T> = {
/**
* JSDoc special tags that can be converted in zod flags.
*/
export interface JSDocTags {
export interface JSDocTagsBase {
minimum?: TagWithError<number>;
maximum?: TagWithError<number>;
default?: number | string | boolean;
Expand All @@ -67,6 +67,15 @@ export interface JSDocTags {
strict?: boolean;
}

export type ElementJSDocTags = Pick<
JSDocTagsBase,
"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> = [
"minimum",
"maximum",
Expand All @@ -75,6 +84,12 @@ const jsDocTagKeys: Array<keyof JSDocTags> = [
"maxLength",
"format",
"pattern",
"elementMinimum",
"elementMaximum",
"elementMinLength",
"elementMaxLength",
"elementPattern",
"elementFormat",
];

/**
Expand Down Expand Up @@ -141,16 +156,22 @@ export function getJSDocTags(nodeType: ts.Node, sourceFile: ts.SourceFile) {
case "maximum":
case "minLength":
case "maxLength":
case "elementMinLength":
case "elementMaxLength":
case "elementMinimum":
case "elementMaximum":
if (value && !Number.isNaN(parseInt(value))) {
jsDocTags[tagName] = { value: parseInt(value), errorMessage };
}
break;
case "pattern":
case "elementPattern":
if (tag.comment) {
jsDocTags[tagName] = tag.comment;
}
break;
case "format":
case "elementFormat":
jsDocTags[tagName] = { value, errorMessage };
break;
case "default":
Expand Down

0 comments on commit df5be8b

Please sign in to comment.