Skip to content

Commit

Permalink
feat!: improve support for function types (#1910)
Browse files Browse the repository at this point in the history
BREAKING CHANGE
  • Loading branch information
domoritz committed Apr 13, 2024
1 parent 9353127 commit 91a0c9f
Show file tree
Hide file tree
Showing 95 changed files with 2,309 additions and 2,087 deletions.
97 changes: 28 additions & 69 deletions README.md
Expand Up @@ -35,6 +35,31 @@ Note that different platforms (e.g. Windows) may use different path separators s

Also note that you need to quote paths with `*` as otherwise the shell will expand the paths and therefore only pass the first path to the generator.

### Options

```
-p, --path <path> Source file path
-t, --type <name> Type name
-i, --id <name> $id for generated schema
-f, --tsconfig <path> Custom tsconfig.json path
-e, --expose <expose> Type exposing (choices: "all", "none", "export", default: "export")
-j, --jsDoc <extended> Read JsDoc annotations (choices: "none", "basic", "extended", default: "extended")
--markdown-description Generate `markdownDescription` in addition to `description`.
--functions <functions> How to handle functions. `fail` will throw an error. `comment` will add a comment. `hide` will treat the function like a NeverType or HiddenType.
(choices: "fail", "comment", "hide", default: "comment")
--minify Minify generated schema (default: false)
--unstable Do not sort properties
--strict-tuples Do not allow additional items on tuples
--no-top-ref Do not create a top-level $ref definition
--no-type-check Skip type checks to improve performance
--no-ref-encode Do not encode references
-o, --out <file> Set the output file (default: stdout)
--validation-keywords [value] Provide additional validation keywords to include (default: [])
--additional-properties Allow additional properties for objects with no index signature (default: false)
-V, --version output the version number
-h, --help display help for command
```

## Programmatic Usage

```js
Expand Down Expand Up @@ -65,7 +90,7 @@ Run the schema generator via `node main.js`.

Extending the built-in formatting is possible by creating a custom formatter and adding it to the main formatter:

1. First we create a formatter, in this case for formatting function types:
1. First we create a formatter, in this case for formatting function types (note that there is a built in one):

```ts
// my-function-formatter.ts
Expand All @@ -76,7 +101,7 @@ export class MyFunctionTypeFormatter implements SubTypeFormatter {
// You can skip this line if you don't need childTypeFormatter
public constructor(private childTypeFormatter: TypeFormatter) {}

public supportsType(type: FunctionType): boolean {
public supportsType(type: BaseType): boolean {
return type instanceof FunctionType;
}

Expand Down Expand Up @@ -192,73 +217,6 @@ fs.writeFile(outputPath, schemaString, (err) => {
});
```

## Options

```
-p, --path 'index.ts'
The path to the TypeScript source file. If this is not provided, the type will be searched in the project specified in the `.tsconfig`.
-t, --type 'My.Type.Name'
The type the generated schema will represent. If omitted, the generated schema will contain all
types found in the files matching path. The same is true if '*' is specified.
-i, --id 'generatedSchemaId'
The `$id` of the generated schema. If omitted, there will be no `$id`.
-e, --expose <all|none|export>
all: Create shared $ref definitions for all types.
none: Do not create shared $ref definitions.
export (default): Create shared $ref definitions only for exported types (not tagged as `@internal`).
-f, --tsconfig 'my/project/tsconfig.json'
Use a custom tsconfig file for processing typescript (see https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) instead of the default:
{
"compilerOptions": {
"noEmit": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"module": "CommonJS",
"strictNullChecks": false,
}
}
-j, --jsDoc <extended|none|basic>
none: Do not use JsDoc annotations.
basic: Read JsDoc annotations to provide schema properties.
extended (default): Also read @nullable, and @asType annotations.
--unstable
Do not sort properties.
--strict-tuples
Do not allow additional items on tuples.
--no-top-ref
Do not create a top-level $ref definition.
--no-type-check
Skip type checks for better performance.
--no-ref-encode
Do not encode references. According to the standard, references must be valid URIs but some tools do not support encoded references.
--validation-keywords
Provide additional validation keywords to include.
-o, --out
Specify the output file path. Without this option, the generator logs the response in the console.
--additional-properties <true|false>
Controls whether or not to allow additional properties for objects that have no index signature.
true: Additional properties are allowed
false (default): Additional properties are not allowed
--minify
Minify generated schema (default: false)
```

## Current state

- `interface` types
Expand All @@ -272,6 +230,7 @@ fs.writeFile(outputPath, schemaString, (err) => {
- `typeof`
- `keyof`
- conditional types
- functions

## Run locally

Expand Down
9 changes: 7 additions & 2 deletions factory/formatter.ts
@@ -1,15 +1,17 @@
import { ChainTypeFormatter } from "../src/ChainTypeFormatter";
import { CircularReferenceTypeFormatter } from "../src/CircularReferenceTypeFormatter";
import { Config } from "../src/Config";
import { CompletedConfig } from "../src/Config";
import { MutableTypeFormatter } from "../src/MutableTypeFormatter";
import { TypeFormatter } from "../src/TypeFormatter";
import { AliasTypeFormatter } from "../src/TypeFormatter/AliasTypeFormatter";
import { AnnotatedTypeFormatter } from "../src/TypeFormatter/AnnotatedTypeFormatter";
import { AnyTypeFormatter } from "../src/TypeFormatter/AnyTypeFormatter";
import { ArrayTypeFormatter } from "../src/TypeFormatter/ArrayTypeFormatter";
import { BooleanTypeFormatter } from "../src/TypeFormatter/BooleanTypeFormatter";
import { ConstructorTypeFormatter } from "../src/TypeFormatter/ConstructorTypeFormatter";
import { DefinitionTypeFormatter } from "../src/TypeFormatter/DefinitionTypeFormatter";
import { EnumTypeFormatter } from "../src/TypeFormatter/EnumTypeFormatter";
import { FunctionTypeFormatter } from "../src/TypeFormatter/FunctionTypeFormatter";
import { HiddenTypeFormatter } from "../src/TypeFormatter/HiddenTypeFormatter";
import { IntersectionTypeFormatter } from "../src/TypeFormatter/IntersectionTypeFormatter";
import { LiteralTypeFormatter } from "../src/TypeFormatter/LiteralTypeFormatter";
Expand All @@ -35,7 +37,7 @@ export type FormatterAugmentor = (
circularReferenceTypeFormatter: CircularReferenceTypeFormatter
) => void;

export function createFormatter(config: Config, augmentor?: FormatterAugmentor): TypeFormatter {
export function createFormatter(config: CompletedConfig, augmentor?: FormatterAugmentor): TypeFormatter {
const chainTypeFormatter = new ChainTypeFormatter([]);
const circularReferenceTypeFormatter = new CircularReferenceTypeFormatter(chainTypeFormatter);

Expand Down Expand Up @@ -70,6 +72,9 @@ export function createFormatter(config: Config, augmentor?: FormatterAugmentor):
.addTypeFormatter(new PrimitiveUnionTypeFormatter())
.addTypeFormatter(new LiteralUnionTypeFormatter())

.addTypeFormatter(new ConstructorTypeFormatter(circularReferenceTypeFormatter, config.functions))
.addTypeFormatter(new FunctionTypeFormatter(circularReferenceTypeFormatter, config.functions))

.addTypeFormatter(new OptionalTypeFormatter(circularReferenceTypeFormatter))
.addTypeFormatter(new RestTypeFormatter(circularReferenceTypeFormatter))

Expand Down
4 changes: 2 additions & 2 deletions factory/generator.ts
@@ -1,10 +1,10 @@
import { Config } from "../src/Config";
import { CompletedConfig } from "../src/Config";
import { SchemaGenerator } from "../src/SchemaGenerator";
import { createFormatter } from "./formatter";
import { createParser } from "./parser";
import { createProgram } from "./program";

export function createGenerator(config: Config): SchemaGenerator {
export function createGenerator(config: CompletedConfig): SchemaGenerator {
const program = createProgram(config);
const parser = createParser(program, config);
const formatter = createFormatter(config);
Expand Down
38 changes: 17 additions & 21 deletions factory/parser.ts
Expand Up @@ -3,7 +3,7 @@ import { BasicAnnotationsReader } from "../src/AnnotationsReader/BasicAnnotation
import { ExtendedAnnotationsReader } from "../src/AnnotationsReader/ExtendedAnnotationsReader";
import { ChainNodeParser } from "../src/ChainNodeParser";
import { CircularReferenceNodeParser } from "../src/CircularReferenceNodeParser";
import { Config, DEFAULT_CONFIG } from "../src/Config";
import { CompletedConfig } from "../src/Config";
import { ExposeNodeParser } from "../src/ExposeNodeParser";
import { MutableParser } from "../src/MutableParser";
import { NodeParser } from "../src/NodeParser";
Expand All @@ -20,7 +20,6 @@ import { ConstructorNodeParser } from "../src/NodeParser/ConstructorNodeParser";
import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser";
import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser";
import { FunctionNodeParser } from "../src/NodeParser/FunctionNodeParser";
import { FunctionParser } from "../src/NodeParser/FunctionParser";
import { HiddenNodeParser } from "../src/NodeParser/HiddenTypeNodeParser";
import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser";
import { InferTypeNodeParser } from "../src/NodeParser/InferTypeNodeParser";
Expand Down Expand Up @@ -62,26 +61,24 @@ import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser";

export type ParserAugmentor = (parser: MutableParser) => void;

export function createParser(program: ts.Program, config: Config, augmentor?: ParserAugmentor): NodeParser {
export function createParser(program: ts.Program, config: CompletedConfig, augmentor?: ParserAugmentor): NodeParser {
const typeChecker = program.getTypeChecker();
const chainNodeParser = new ChainNodeParser(typeChecker, []);

const mergedConfig = { ...DEFAULT_CONFIG, ...config };

function withExpose(nodeParser: SubNodeParser): SubNodeParser {
return new ExposeNodeParser(typeChecker, nodeParser, mergedConfig.expose, mergedConfig.jsDoc);
return new ExposeNodeParser(typeChecker, nodeParser, config.expose, config.jsDoc);
}
function withTopRef(nodeParser: NodeParser): NodeParser {
return new TopRefNodeParser(chainNodeParser, mergedConfig.type, mergedConfig.topRef);
return new TopRefNodeParser(chainNodeParser, config.type, config.topRef);
}
function withJsDoc(nodeParser: SubNodeParser): SubNodeParser {
const extraTags = new Set(mergedConfig.extraTags);
if (mergedConfig.jsDoc === "extended") {
const extraTags = new Set(config.extraTags);
if (config.jsDoc === "extended") {
return new AnnotatedNodeParser(
nodeParser,
new ExtendedAnnotationsReader(typeChecker, extraTags, mergedConfig.markdownDescription)
new ExtendedAnnotationsReader(typeChecker, extraTags, config.markdownDescription)
);
} else if (mergedConfig.jsDoc === "basic") {
} else if (config.jsDoc === "basic") {
return new AnnotatedNodeParser(nodeParser, new BasicAnnotationsReader(extraTags));
} else {
return nodeParser;
Expand Down Expand Up @@ -109,16 +106,13 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(new ObjectTypeNodeParser())
.addNodeParser(new AsExpressionNodeParser(chainNodeParser))
.addNodeParser(new SatisfiesNodeParser(chainNodeParser))
.addNodeParser(new FunctionParser(chainNodeParser))
.addNodeParser(withJsDoc(new ParameterParser(chainNodeParser)))
.addNodeParser(new StringLiteralNodeParser())
.addNodeParser(new StringTemplateLiteralNodeParser(chainNodeParser))
.addNodeParser(new IntrinsicNodeParser())
.addNodeParser(new NumberLiteralNodeParser())
.addNodeParser(new BooleanLiteralNodeParser())
.addNodeParser(new NullLiteralNodeParser())
.addNodeParser(new FunctionNodeParser())
.addNodeParser(new ConstructorNodeParser())
.addNodeParser(new ObjectLiteralExpressionNodeParser(chainNodeParser))
.addNodeParser(new ArrayLiteralExpressionNodeParser(chainNodeParser))

Expand All @@ -132,7 +126,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
.addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new InferTypeNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new MappedTypeNodeParser(chainNodeParser, mergedConfig.additionalProperties))
.addNodeParser(new MappedTypeNodeParser(chainNodeParser, config.additionalProperties))
.addNodeParser(new ConditionalTypeNodeParser(typeChecker, chainNodeParser))
.addNodeParser(new TypeOperatorNodeParser(chainNodeParser))

Expand All @@ -155,7 +149,7 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
new InterfaceAndClassNodeParser(
typeChecker,
withJsDoc(chainNodeParser),
mergedConfig.additionalProperties
config.additionalProperties
)
)
)
Expand All @@ -165,17 +159,19 @@ export function createParser(program: ts.Program, config: Config, augmentor?: Pa
withCircular(
withExpose(
withJsDoc(
new TypeLiteralNodeParser(
typeChecker,
withJsDoc(chainNodeParser),
mergedConfig.additionalProperties
)
new TypeLiteralNodeParser(typeChecker, withJsDoc(chainNodeParser), config.additionalProperties)
)
)
)
)

.addNodeParser(new ArrayNodeParser(chainNodeParser));

if (config.functions !== "fail") {
chainNodeParser
.addNodeParser(new ConstructorNodeParser(chainNodeParser, config.functions))
.addNodeParser(new FunctionNodeParser(chainNodeParser, config.functions));
}

return withTopRef(chainNodeParser);
}
4 changes: 2 additions & 2 deletions factory/program.ts
Expand Up @@ -3,7 +3,7 @@ import * as path from "path";
import ts from "typescript";
import normalize from "normalize-path";

import { Config } from "../src/Config";
import { CompletedConfig, Config } from "../src/Config";
import { DiagnosticError } from "../src/Error/DiagnosticError";
import { LogicError } from "../src/Error/LogicError";
import { NoRootNamesError } from "../src/Error/NoRootNamesError";
Expand Down Expand Up @@ -59,7 +59,7 @@ function getTsConfig(config: Config) {
};
}

export function createProgram(config: Config): ts.Program {
export function createProgram(config: CompletedConfig): ts.Program {
const rootNamesFromPath = config.path ? glob.sync(normalize(path.resolve(config.path))) : [];
const tsconfig = getTsConfig(config);
const rootNames = rootNamesFromPath.length ? rootNamesFromPath : tsconfig.fileNames;
Expand Down
3 changes: 2 additions & 1 deletion index.ts
Expand Up @@ -102,6 +102,7 @@ export * from "./src/TypeFormatter/UndefinedTypeFormatter";
export * from "./src/TypeFormatter/UnionTypeFormatter";
export * from "./src/TypeFormatter/UnknownTypeFormatter";
export * from "./src/TypeFormatter/VoidTypeFormatter";
export * from "./src/TypeFormatter/FunctionTypeFormatter";

export * from "./src/NodeParser";
export * from "./src/SubNodeParser";
Expand All @@ -121,7 +122,7 @@ export * from "./src/NodeParser/ConditionalTypeNodeParser";
export * from "./src/NodeParser/EnumNodeParser";
export * from "./src/NodeParser/ExpressionWithTypeArgumentsNodeParser";
export * from "./src/NodeParser/FunctionNodeParser";
export * from "./src/NodeParser/FunctionParser";
export * from "./src/NodeParser/ConstructorNodeParser";
export * from "./src/NodeParser/HiddenTypeNodeParser";
export * from "./src/NodeParser/IndexedAccessTypeNodeParser";
export * from "./src/NodeParser/InterfaceAndClassNodeParser";
Expand Down
6 changes: 6 additions & 0 deletions src/Config.ts
Expand Up @@ -15,8 +15,13 @@ export interface Config {
extraTags?: string[];
additionalProperties?: boolean;
discriminatorType?: "json-schema" | "open-api";
functions?: FunctionOptions;
}

export type CompletedConfig = Config & typeof DEFAULT_CONFIG;

export type FunctionOptions = "fail" | "comment" | "hide";

export const DEFAULT_CONFIG: Omit<Required<Config>, "path" | "type" | "schemaId" | "tsconfig"> = {
expose: "export",
topRef: true,
Expand All @@ -30,4 +35,5 @@ export const DEFAULT_CONFIG: Omit<Required<Config>, "path" | "type" | "schemaId"
extraTags: [],
additionalProperties: false,
discriminatorType: "json-schema",
functions: "comment",
};
29 changes: 21 additions & 8 deletions src/NodeParser/ConstructorNodeParser.ts
Expand Up @@ -2,17 +2,30 @@ import ts from "typescript";
import { SubNodeParser } from "../SubNodeParser";
import { BaseType } from "../Type/BaseType";
import { ConstructorType } from "../Type/ConstructorType";
import { FunctionOptions } from "../Config";
import { NeverType } from "../Type/NeverType";
import { Context, NodeParser } from "../NodeParser";
import { DefinitionType } from "../Type/DefinitionType";
import { getNamedArguments, getTypeName } from "./FunctionNodeParser";

/**
* A constructor node parser that creates a constructor type so that mapped
* types can use constructors as values. There is no formatter for constructor
* types.
*/
export class ConstructorNodeParser implements SubNodeParser {
public supportsNode(node: ts.ConstructorTypeNode): boolean {
constructor(
protected childNodeParser: NodeParser,
protected functions: FunctionOptions
) {}

public supportsNode(node: ts.TypeNode): boolean {
return node.kind === ts.SyntaxKind.ConstructorType;
}
public createType(): BaseType {
return new ConstructorType();

public createType(node: ts.ConstructorTypeNode, context: Context): BaseType {
if (this.functions === "hide") {
return new NeverType();
}

const name = getTypeName(node);
const func = new ConstructorType(node, getNamedArguments(this.childNodeParser, node, context));

return name ? new DefinitionType(name, func) : func;
}
}

0 comments on commit 91a0c9f

Please sign in to comment.