Skip to content

Commit

Permalink
feat: add JSDocTag filter (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabien0102 committed Feb 22, 2022
1 parent e349c33 commit 5f6bb7f
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 6 deletions.
47 changes: 47 additions & 0 deletions README.md
Expand Up @@ -134,6 +134,53 @@ If you want to customized the schema name or restrict the exported schemas, you

Just run `yarn ts-to-zod --init` and you will have a ready to use configuration file (with a bit of typesafety).

You have two ways to restrict the scope of ts-to-zod:

- `nameFilter` will filter by interface/type name
- `jsDocTagFilter` will filter on jsDocTag

Example:

```ts
// ts-to-zod.config.js
/**
* ts-to-zod configuration.
*
* @type {import("./src/config").TsToZodConfig}
*/
module.exports = [
{
name: "example",
input: "example/heros.ts",
output: "example/heros.zod.ts",
jsDocTagFilter: (tags) => tags.map(tag => tag.name).includes("toExtract")) // <= rule here
},
];

// example/heros.ts
/**
* Will not be part of `example/heros.zod.ts`
*/
export interface Enemy {
name: string;
powers: string[];
inPrison: boolean;
}

/**
* Will be part of `example/heros.zod.ts`
* @toExtract
*/
export interface Superman {
name: "superman" | "clark kent" | "kal-l";
enemies: Record<string, Enemy>;
age: number;
underKryptonite?: boolean;
}
```

/!\ Please note: if your exported interface/type have a reference to a non-exported interface/type, ts-to-zod will not be able to generate anything (a circular dependency will be report due to the missing reference).

## Limitation

Since we are generating Zod schemas, we are limited by what Zod actually supports:
Expand Down
24 changes: 23 additions & 1 deletion src/config.ts
@@ -1,5 +1,22 @@
export interface SimplifiedJSDocTag {
/**
* Name of the tag
*
* @ref tag.tagName.escapedText.toString()
*/
name: string;

/**
* Value of the tag
*
* @ref tag.comment
*/
value?: string;
}

export type GetSchemaName = (identifier: string) => string;
export type NameFilter = (name: string) => boolean;
export type JSDocTagFilter = (tags: SimplifiedJSDocTag[]) => boolean;

export type Config = {
/**
Expand All @@ -23,10 +40,15 @@ export type Config = {
maxRun?: number;

/**
* Filter function on type/interface name.
* Filter on type/interface name.
*/
nameFilter?: NameFilter;

/**
* Filter on JSDocTag.
*/
jsDocTagFilter?: JSDocTagFilter;

/**
* Schema name generator.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/config.zod.ts
@@ -1,6 +1,11 @@
// Generated by ts-to-zod
import { z } from "zod";

export const simplifiedJSDocTagSchema = z.object({
name: z.string(),
value: z.string().optional(),
});

export const getSchemaNameSchema = z
.function()
.args(z.string())
Expand All @@ -11,12 +16,18 @@ export const nameFilterSchema = z
.args(z.string())
.returns(z.boolean());

export const jSDocTagFilterSchema = z
.function()
.args(z.array(simplifiedJSDocTagSchema))
.returns(z.boolean());

export const configSchema = z.object({
input: z.string(),
output: z.string(),
skipValidation: z.boolean().optional(),
maxRun: z.number().optional(),
nameFilter: nameFilterSchema.optional(),
jsDocTagFilter: jSDocTagFilterSchema.optional(),
getSchemaName: getSchemaNameSchema.optional(),
keepComments: z.boolean().optional().default(false),
skipParseJSDoc: z.boolean().optional().default(false),
Expand Down
51 changes: 51 additions & 0 deletions src/core/generate.test.ts
Expand Up @@ -247,6 +247,57 @@ describe("generate", () => {
});
});

describe("with jsdocTags filter", () => {
it("should generate only types with @zod", () => {
const sourceText = `
/**
* @zod
**/
export type Name = "superman" | "clark kent" | "kal-l";
/**
* @nop
*/
export type BadassSuperman = Omit<Superman, "underKryptonite">;
/**
* Only this interface should be generated
*
* @zod
*/
export interface Superman {
name: Name;
age: number;
underKryptonite?: boolean;
/**
* @format email
**/
email: string;
}
`;

const { getZodSchemasFile } = generate({
sourceText,
jsDocTagFilter: (tags) => tags.map((tag) => tag.name).includes("zod"),
});

expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
"// Generated by ts-to-zod
import { z } from \\"zod\\";
export const nameSchema = z.union([z.literal(\\"superman\\"), z.literal(\\"clark kent\\"), z.literal(\\"kal-l\\")]);
export const supermanSchema = z.object({
name: nameSchema,
age: z.number(),
underKryptonite: z.boolean().optional(),
email: z.string().email()
});
"
`);
});
});

describe("with non-exported types", () => {
it("should generate tests for exported schemas", () => {
const sourceText = `
Expand Down
21 changes: 16 additions & 5 deletions src/core/generate.ts
@@ -1,6 +1,9 @@
import { camel } from "case";
import { getJsDoc } from "tsutils";
import ts from "typescript";
import { resolveModules } from "../utils/resolveModules";
import { JSDocTagFilter, NameFilter } from "../config";
import { getSimplifiedJsDocTags } from "../utils/getSimplifiedJsDocTags";
import { generateIntegrationTests } from "./generateIntegrationTests";
import { generateZodInferredType } from "./generateZodInferredType";
import { generateZodSchemaVariableStatement } from "./generateZodSchema";
Expand All @@ -18,9 +21,14 @@ export interface GenerateProps {
maxRun?: number;

/**
* Filter function on type/interface name.
* Filter on type/interface name.
*/
nameFilter?: (name: string) => boolean;
nameFilter?: NameFilter;

/**
* Filter on JSDocTag.
*/
jsDocTagFilter?: JSDocTagFilter;

/**
* Schema name generator.
Expand Down Expand Up @@ -50,6 +58,7 @@ export function generate({
sourceText,
maxRun = 10,
nameFilter = () => true,
jsDocTagFilter = () => true,
getSchemaName = (id) => camel(id) + "Schema",
keepComments = false,
skipParseJSDoc = false,
Expand All @@ -68,9 +77,11 @@ export function generate({
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)
) {
if (nameFilter(node.name.text)) {
nodes.push(node);
}
const jsDoc = getJsDoc(node, sourceFile);
const tags = getSimplifiedJsDocTags(jsDoc);
if (!jsDocTagFilter(tags)) return;
if (!nameFilter(node.name.text)) return;
nodes.push(node);
}
};
ts.forEachChild(sourceFile, visitor);
Expand Down
22 changes: 22 additions & 0 deletions src/utils/getSimplifiedJsDocTags.ts
@@ -0,0 +1,22 @@
import ts from "typescript";
import { SimplifiedJSDocTag } from "../config";

/**
* Get a simplified version of a node JSDocTags.
*
* @param jsDocs
*/
export function getSimplifiedJsDocTags(
jsDocs: ts.JSDoc[]
): SimplifiedJSDocTag[] {
const tags: SimplifiedJSDocTag[] = [];
jsDocs.forEach((jsDoc) => {
(jsDoc.tags || []).forEach((tag) => {
const name = tag.tagName.escapedText.toString();
const value = typeof tag.comment === "string" ? tag.comment : undefined;
tags.push({ name, value });
});
});

return tags;
}

0 comments on commit 5f6bb7f

Please sign in to comment.