Skip to content

Commit 2f15545

Browse files
authored
Merge pull request #3531 from hey-api/feat/plugin-typescript-resolvers
feat: add resolvers to typescript plugin
2 parents 8b58945 + 0e47fcb commit 2f15545

22 files changed

Lines changed: 444 additions & 161 deletions

File tree

.changeset/better-spiders-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
**plugin(@hey-api/typescript)**: add Resolvers API

packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
import type { IR } from '@hey-api/shared';
22

33
import { $ } from '../../../ts-dsl';
4-
import type { TypeScriptResult } from './shared/types';
4+
import type { Type } from './shared/types';
55
import type { HeyApiTypeScriptPlugin } from './types';
66
import { createProcessor } from './v1/processor';
77

88
export type IApi = {
9-
schemaToType: (
10-
plugin: HeyApiTypeScriptPlugin['Instance'],
11-
schema: IR.SchemaObject,
12-
) => TypeScriptResult['type'];
9+
schemaToType: (plugin: HeyApiTypeScriptPlugin['Instance'], schema: IR.SchemaObject) => Type;
1310
};
1411

1512
export class Api implements IApi {
16-
schemaToType(
17-
plugin: HeyApiTypeScriptPlugin['Instance'],
18-
schema: IR.SchemaObject,
19-
): TypeScriptResult['type'] {
13+
schemaToType(plugin: HeyApiTypeScriptPlugin['Instance'], schema: IR.SchemaObject): Type {
2014
const processor = createProcessor(plugin);
2115
const result = processor.process({
2216
export: false,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
export { defaultConfig, defineConfig } from './config';
2+
export type {
3+
EnumResolverContext,
4+
NumberResolverContext,
5+
ObjectResolverContext,
6+
Resolvers,
7+
StringResolverContext,
8+
} from './resolvers';
29
export type { HeyApiTypeScriptPlugin } from './types';
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { Plugin, SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared';
2+
3+
import type { $, DollarTsDsl } from '../../../ts-dsl';
4+
import type { HeyApiTypeScriptPlugin, Type, TypeScriptResult } from './shared/types';
5+
6+
export type Resolvers = Plugin.Resolvers<{
7+
/**
8+
* Resolver for enum schemas.
9+
*
10+
* Allows customization of how enum types are rendered.
11+
*
12+
* Returning `undefined` will execute the default resolver logic.
13+
*/
14+
enum?: (ctx: EnumResolverContext) => Type | undefined;
15+
/**
16+
* Resolver for number schemas.
17+
*
18+
* Allows customization of how number types are rendered.
19+
*
20+
* Returning `undefined` will execute the default resolver logic.
21+
*/
22+
number?: (ctx: NumberResolverContext) => Type | undefined;
23+
/**
24+
* Resolver for object schemas.
25+
*
26+
* Allows customization of how object types are rendered.
27+
*
28+
* Returning `undefined` will execute the default resolver logic.
29+
*/
30+
object?: (ctx: ObjectResolverContext) => Type | undefined;
31+
/**
32+
* Resolver for string schemas.
33+
*
34+
* Allows customization of how string types are rendered.
35+
*
36+
* Returning `undefined` will execute the default resolver logic.
37+
*/
38+
string?: (ctx: StringResolverContext) => Type | undefined;
39+
}>;
40+
41+
interface BaseContext extends DollarTsDsl {
42+
/** The plugin instance. */
43+
plugin: HeyApiTypeScriptPlugin['Instance'];
44+
}
45+
46+
export interface EnumResolverContext extends BaseContext {
47+
/**
48+
* Nodes used to build different parts of the result.
49+
*/
50+
nodes: {
51+
/**
52+
* Returns the base enum type expression.
53+
*/
54+
base: (ctx: EnumResolverContext) => Type;
55+
/**
56+
* Returns parsed enum items with metadata about the enum members.
57+
*/
58+
items: (ctx: EnumResolverContext) => {
59+
/**
60+
* String literal values for use with union types.
61+
*/
62+
enumMembers: Array<ReturnType<typeof $.type.literal>>;
63+
/**
64+
* Whether the enum includes a null value.
65+
*/
66+
isNullable: boolean;
67+
};
68+
};
69+
schema: SchemaWithType<'enum'>;
70+
}
71+
72+
export interface NumberResolverContext extends BaseContext {
73+
/**
74+
* Nodes used to build different parts of the result.
75+
*/
76+
nodes: {
77+
/**
78+
* Returns the base number type expression.
79+
*/
80+
base: (ctx: NumberResolverContext) => Type;
81+
/**
82+
* Returns the literal type for const values.
83+
*/
84+
const: (ctx: NumberResolverContext) => Type | undefined;
85+
};
86+
schema: SchemaWithType<'integer' | 'number'>;
87+
}
88+
89+
export interface ObjectResolverContext extends BaseContext {
90+
/**
91+
* Nodes used to build different parts of the result.
92+
*/
93+
nodes: {
94+
/**
95+
* Returns the base object type expression.
96+
*/
97+
base: (ctx: ObjectResolverContext) => Type;
98+
/**
99+
* Returns the shape (properties) of the object type.
100+
*/
101+
shape: (ctx: ObjectResolverContext) => ReturnType<typeof $.type.object>;
102+
};
103+
schema: SchemaWithType<'object'>;
104+
walk: Walker<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']>;
105+
walkerCtx: SchemaVisitorContext<HeyApiTypeScriptPlugin['Instance']>;
106+
}
107+
108+
export interface StringResolverContext extends BaseContext {
109+
/**
110+
* Nodes used to build different parts of the result.
111+
*/
112+
nodes: {
113+
/**
114+
* Returns the base string type expression.
115+
*/
116+
base: (ctx: StringResolverContext) => Type;
117+
/**
118+
* Returns the literal type for const values.
119+
*/
120+
const: (ctx: StringResolverContext) => Type | undefined;
121+
/**
122+
* Returns the format-specific type expression.
123+
*/
124+
format: (ctx: StringResolverContext) => Type | undefined;
125+
};
126+
schema: SchemaWithType<'string'>;
127+
}

packages/openapi-ts/src/plugins/@hey-api/typescript/shared/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { MaybeTsDsl, TypeTsDsl } from '../../../../ts-dsl';
44

55
export type { HeyApiTypeScriptPlugin } from '../types';
66

7+
export type Type = MaybeTsDsl<TypeTsDsl>;
8+
79
/**
810
* Metadata that flows through schema walking.
911
*/
@@ -25,7 +27,7 @@ export interface TypeScriptEnumData {
2527
export interface TypeScriptResult {
2628
enumData?: TypeScriptEnumData;
2729
meta: TypeScriptMeta;
28-
type: MaybeTsDsl<TypeTsDsl>;
30+
type: Type;
2931
}
3032

3133
/**

packages/openapi-ts/src/plugins/@hey-api/typescript/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey
22
import type { DefinePlugin, Plugin } from '@hey-api/shared';
33

44
import type { IApi } from './api';
5+
import type { Resolvers } from './resolvers';
56

67
export type EnumsType = 'javascript' | 'typescript' | 'typescript-const';
78

89
export type UserConfig = Plugin.Name<'@hey-api/typescript'> &
910
Plugin.Hooks &
1011
Plugin.UserComments &
11-
Plugin.UserExports & {
12+
Plugin.UserExports &
13+
Resolvers & {
1214
/**
1315
* Casing convention for generated names.
1416
*
@@ -232,7 +234,8 @@ export type UserConfig = Plugin.Name<'@hey-api/typescript'> &
232234
export type Config = Plugin.Name<'@hey-api/typescript'> &
233235
Plugin.Hooks &
234236
Plugin.Comments &
235-
Plugin.Exports & {
237+
Plugin.Exports &
238+
Resolvers & {
236239
/**
237240
* Casing convention for generated names.
238241
*/

packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/array.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Walker } from '@hey-api/shared';
44
import { deduplicateSchema } from '@hey-api/shared';
55

66
import { $ } from '../../../../../ts-dsl';
7-
import type { HeyApiTypeScriptPlugin } from '../../shared/types';
7+
import type { HeyApiTypeScriptPlugin, Type } from '../../shared/types';
88
import type { TypeScriptResult } from '../../shared/types';
99

1010
export function arrayToAst({
@@ -15,7 +15,7 @@ export function arrayToAst({
1515
plugin: HeyApiTypeScriptPlugin['Instance'];
1616
schema: SchemaWithType<'array'>;
1717
walk: Walker<TypeScriptResult, HeyApiTypeScriptPlugin['Instance']>;
18-
}): TypeScriptResult['type'] {
18+
}): Type {
1919
if (!schema.items) {
2020
return $.type('Array').generic($.type(plugin.config.topType));
2121
}

packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/boolean.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { SchemaWithType } from '@hey-api/shared';
22

33
import { $ } from '../../../../../ts-dsl';
4-
import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types';
4+
import type { HeyApiTypeScriptPlugin, Type } from '../../shared/types';
55

66
export function booleanToAst({
77
schema,
88
}: {
99
plugin: HeyApiTypeScriptPlugin['Instance'];
1010
schema: SchemaWithType<'boolean'>;
11-
}): TypeScriptResult['type'] {
11+
}): Type {
1212
if (schema.const !== undefined) {
1313
return $.type.fromValue(schema.const);
1414
}

packages/openapi-ts/src/plugins/@hey-api/typescript/v1/toAst/enum.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { SchemaWithType } from '@hey-api/shared';
22

33
import { $ } from '../../../../../ts-dsl';
4-
import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types';
4+
import type { EnumResolverContext } from '../../resolvers';
5+
import type { HeyApiTypeScriptPlugin, Type } from '../../shared/types';
56
import type { TypeScriptEnumData } from '../../shared/types';
67

78
function buildEnumData(
@@ -35,6 +36,46 @@ function buildEnumData(
3536
};
3637
}
3738

39+
function itemsNode(ctx: EnumResolverContext): ReturnType<EnumResolverContext['nodes']['items']> {
40+
const { schema } = ctx;
41+
const items = schema.items ?? [];
42+
43+
const enumMembers: Array<ReturnType<typeof $.type.literal>> = [];
44+
let isNullable = false;
45+
46+
for (const item of items) {
47+
if (item.type === 'string' && typeof item.const === 'string') {
48+
enumMembers.push($.type.literal(item.const));
49+
} else if (item.type === 'number' && typeof item.const === 'number') {
50+
enumMembers.push($.type.literal(item.const));
51+
} else if (item.type === 'boolean' && typeof item.const === 'boolean') {
52+
enumMembers.push($.type.literal(item.const));
53+
} else if (item.type === 'null' || item.const === null) {
54+
isNullable = true;
55+
}
56+
}
57+
58+
return { enumMembers, isNullable };
59+
}
60+
61+
function baseNode(ctx: EnumResolverContext): Type {
62+
const { schema } = ctx;
63+
const items = schema.items ?? [];
64+
65+
if (items.length === 0) {
66+
return $.type('never');
67+
}
68+
69+
const literalTypes = items
70+
.filter((item) => item.const !== undefined)
71+
.map((item) => $.type.fromValue(item.const));
72+
return literalTypes.length > 0 ? $.type.or(...literalTypes) : $.type('string');
73+
}
74+
75+
function enumResolver(ctx: EnumResolverContext): Type {
76+
return ctx.nodes.base(ctx);
77+
}
78+
3879
export function enumToAst({
3980
plugin,
4081
schema,
@@ -43,21 +84,22 @@ export function enumToAst({
4384
schema: SchemaWithType<'enum'>;
4485
}): {
4586
enumData?: TypeScriptEnumData;
46-
type: TypeScriptResult['type'];
87+
type: Type;
4788
} {
48-
const items = schema.items ?? [];
4989
const enumData = buildEnumData(plugin, schema);
5090

51-
let type: TypeScriptResult['type'];
91+
const ctx: EnumResolverContext = {
92+
$,
93+
nodes: {
94+
base: baseNode,
95+
items: itemsNode,
96+
},
97+
plugin,
98+
schema,
99+
};
52100

53-
if (items.length === 0) {
54-
type = $.type('never');
55-
} else {
56-
const literalTypes = items
57-
.filter((item) => item.const !== undefined)
58-
.map((item) => $.type.fromValue(item.const));
59-
type = literalTypes.length > 0 ? $.type.or(...literalTypes) : $.type('string');
60-
}
101+
const resolver = plugin.config['~resolvers']?.enum;
102+
const type = resolver?.(ctx) ?? enumResolver(ctx);
61103

62104
return {
63105
enumData,
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { SchemaWithType } from '@hey-api/shared';
22

33
import { $ } from '../../../../../ts-dsl';
4-
import type { HeyApiTypeScriptPlugin, TypeScriptResult } from '../../shared/types';
4+
import type { HeyApiTypeScriptPlugin, Type } from '../../shared/types';
55

66
// eslint-disable-next-line @typescript-eslint/no-unused-vars
77
export function neverToAst(args: {
88
plugin: HeyApiTypeScriptPlugin['Instance'];
99
schema: SchemaWithType<'never'>;
10-
}): TypeScriptResult['type'] {
10+
}): Type {
1111
return $.type('never');
1212
}

0 commit comments

Comments
 (0)