Skip to content

Commit 56b9086

Browse files
committed
feat: enhance parser functionality with operation transformation and definition patching, add new operations module, and improve type handling in method parsing
1 parent f5eeb16 commit 56b9086

7 files changed

Lines changed: 274 additions & 9 deletions

File tree

packages/parser/src/parses/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { swagger2ToSwagger3 } from '@genapi/transform'
66
* parse OpenAPI info to commits
77
* @param source
88
*/
9-
export function parseHeaderCommits(source: OpenAPISpecificationV2) {
9+
export function parseHeaderCommits(source: OpenAPISpecificationV2): string[] {
1010
const comments = [
1111
`@title ${source.info.title}`,
1212
source.info.description != null && source.info.description !== '' && `@description ${source.info.description}`,
1313
source.swagger && `@swagger ${source.swagger}`,
1414
`@version ${source.info.version}`,
15-
].filter(Boolean)
15+
].filter((comment): comment is string => typeof comment === 'string')
1616
return comments
1717
}
1818

packages/parser/src/parses/method.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { StatementField, StatementInterface } from '@genapi/shared'
22
import type { Parameter } from 'openapi-specification-types'
33
import type { PathMethod } from '../traverse'
44
import type { InSchemas, LiteralField } from '../utils'
5-
import { inject } from '@genapi/shared'
5+
import { inject, provide } from '@genapi/shared'
66
import { camelCase } from '@hairy/utils'
7+
import { transformOperation } from '../transform'
78
import { isRequiredParameter, signAnyInter, toUndefField, varName } from '../utils'
89
import { parseParameterFiled } from './parameter'
910
import { parseSchemaType } from './schema'
@@ -86,6 +87,7 @@ export function parseMethodParameters({ method, parameters, path }: PathMethod,
8687
// fix: for path required parameters, move to the end
8788
config.parameters.sort(a => (a.required ? -1 : 1))
8889

90+
provide(`${method}/${path}`, config)
8991
return config
9092
}
9193

@@ -100,7 +102,7 @@ export function parseMethodMetadata({ method, path, responses, options: meta }:
100102
metaAny.consumes?.length ? `@consumes ${metaAny.consumes.join('; ') || '-'}` : undefined,
101103
].filter((c): c is string => typeof c === 'string')
102104

103-
const name = camelCase(`${method}/${path}`)
105+
let name = camelCase(`${method}/${path}`)
104106

105107
const url = `${path.replace(/(\{)/g, '${paths.')}`
106108
function hasContent(r: unknown): r is { content?: Record<string, { schema?: unknown }> } {
@@ -114,7 +116,7 @@ export function parseMethodMetadata({ method, path, responses, options: meta }:
114116
?? (content200 && typeof content200 === 'object' && 'schema' in content200 ? (content200 as { schema: unknown }).schema : null)
115117
const schemaFromRes200 = res200 && typeof res200 === 'object' && 'schema' in res200 && !('content' in res200) ? (res200 as { schema: unknown }).schema : null
116118
const responseSchema = schemaFromContent ?? schemaFromRes200
117-
const responseType = responseSchema && typeof responseSchema === 'object' ? parseSchemaType(responseSchema as Parameters<typeof parseSchemaType>[0]) : 'void'
119+
let responseType = responseSchema && typeof responseSchema === 'object' ? parseSchemaType(responseSchema as Parameters<typeof parseSchemaType>[0]) : 'void'
118120

119121
if (configRead.config.responseRequired)
120122
deepSignRequired(interfaces.find(v => v.name === responseType)?.properties || [])
@@ -128,5 +130,12 @@ export function parseMethodMetadata({ method, path, responses, options: meta }:
128130
}
129131
}
130132

133+
const config = inject(`${method}/${path}`)
134+
;({ name, responseType } = transformOperation({
135+
configRead,
136+
name,
137+
parameters: config?.parameters,
138+
responseType,
139+
}))
131140
return { description: comments, name, url, responseType, body: [] as string[] }
132141
}

packages/parser/src/transform/definitions.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { StatementInterface } from '@genapi/shared'
1+
import type { ApiPipeline, StatementInterface } from '@genapi/shared'
22
import type { Definitions, Schema } from 'openapi-specification-types'
33
import { inject } from '@genapi/shared'
44
import { parseSchemaType } from '../parses'
@@ -9,17 +9,33 @@ export interface DefinitionTransformOptions {
99
}
1010

1111
export function transformDefinitions(definitions: Definitions) {
12-
const { interfaces } = inject()
12+
const { interfaces, configRead } = inject()
13+
const config = (configRead?.config || {}) as ApiPipeline.Config
14+
const transformDef = config.transform?.definition
15+
const patchDefinitions = config.patch?.definitions || {}
1316

1417
for (const [name, definition] of Object.entries(definitions)) {
1518
const { properties = {} } = definition
1619

20+
const interfaceName = varName(name)
21+
1722
interfaces.push({
1823
export: true,
19-
name: varName(name),
24+
name: interfaceName,
2025
properties: Object.keys(properties).map(name => defToFields(name, properties[name])),
2126
})
2227

28+
// Build a structural type string for this definition so `transform.definition`
29+
// and `patch.definitions` can operate on it.
30+
const baseType = buildDefinitionType(interfaceName, properties)
31+
applyDefinitionTransformsAndPatches({
32+
baseName: interfaceName,
33+
baseType,
34+
configRead,
35+
transformDef,
36+
patchDefinitions,
37+
})
38+
2339
function defToFields(name: string, propertie: Schema) {
2440
const required = Array.isArray(definition?.required) ? definition.required.includes(name) : undefined
2541
const description = propertie.description ? `@description ${propertie.description}` : undefined
@@ -32,3 +48,90 @@ export function transformDefinitions(definitions: Definitions) {
3248
}
3349
}
3450
}
51+
52+
function buildDefinitionType(interfaceName: string, properties: Definitions[string]['properties'] = {}) {
53+
const entries = Object.entries(properties)
54+
if (entries.length === 0)
55+
return 'any'
56+
57+
const fields = entries.map(([propName, schema]) => {
58+
const type = parseSchemaType(schema as Schema)
59+
return `${varFiled(propName)}: ${type}`
60+
})
61+
62+
return `{ ${fields.join(', ')} }`
63+
}
64+
65+
interface DefinitionPatchContext {
66+
baseName: string
67+
baseType: string
68+
configRead: ApiPipeline.ConfigRead
69+
transformDef?: (name: string, type: string) => ApiPipeline.Definition
70+
patchDefinitions: Record<string, ApiPipeline.Definition>
71+
}
72+
73+
/**
74+
* Applies `config.transform.definition` (global) and `config.patch.definitions` (static)
75+
* to a single Swagger/OpenAPI definition.
76+
*
77+
* Semantics:
78+
* - Rename only: `'UserDto': 'User'` → `export type User = UserDto`
79+
* - Rename + override type:
80+
* `'SessionDto': { name: 'Session', type: '{ name: string }' }`
81+
* → `export type Session = { name: string }`
82+
*
83+
* The original interface (e.g. `UserDto`) is preserved so existing references
84+
* from schemas remain valid; patches add friendly alias types on top.
85+
*/
86+
function applyDefinitionTransformsAndPatches(ctx: DefinitionPatchContext) {
87+
const {
88+
baseName,
89+
baseType,
90+
configRead,
91+
transformDef,
92+
patchDefinitions,
93+
} = ctx
94+
95+
let aliasName = baseName
96+
let aliasType = baseType
97+
98+
function applyPatch(patch?: ApiPipeline.Definition) {
99+
if (!patch)
100+
return
101+
102+
if (typeof patch === 'string') {
103+
aliasName = patch
104+
return
105+
}
106+
107+
if (patch.name)
108+
aliasName = patch.name
109+
if (patch.type)
110+
aliasType = patch.type
111+
}
112+
113+
// Global transform first.
114+
if (transformDef) {
115+
const patch = transformDef(baseName, baseType)
116+
applyPatch(patch)
117+
}
118+
119+
// Then static patch; allow matching by original or transformed name.
120+
const staticPatch = patchDefinitions[baseName] ?? patchDefinitions[aliasName]
121+
applyPatch(staticPatch)
122+
123+
const hasNameChange = aliasName !== baseName
124+
const hasTypeChange = aliasType !== baseType
125+
126+
if (!hasNameChange && !hasTypeChange)
127+
return
128+
129+
const aliasValue = hasTypeChange ? aliasType : baseName
130+
131+
configRead.graphs.typings = configRead.graphs.typings || []
132+
configRead.graphs.typings.push({
133+
export: true,
134+
name: aliasName,
135+
value: aliasValue,
136+
})
137+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './body'
22
export * from './definitions'
33
export * from './headers'
4+
export * from './operations'
45
export * from './parameters'
56
export * from './urls'
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { ApiPipeline, StatementField } from '@genapi/shared'
2+
3+
export interface OperationTransformOptions {
4+
/**
5+
* Current pipeline config + graphs.
6+
*/
7+
configRead: ApiPipeline.ConfigRead
8+
/**
9+
* Generated operation name.
10+
*/
11+
name: string
12+
/**
13+
* Generated parameters (will be mutated in place if overridden).
14+
*/
15+
parameters: StatementField[]
16+
/**
17+
* Inferred response type for this operation.
18+
*/
19+
responseType: string
20+
}
21+
22+
/**
23+
* Applies `config.transform.operation` (global) and `config.patch.operations` (static)
24+
* to a single operation.
25+
*
26+
* The returned object contains the final `name`, `parameters`, and `responseType`.
27+
* Note: `parameters` are always the same array instance passed in; when overridden
28+
* by a patch, the array is mutated in place (cleared and re-filled) so callers
29+
* using a `const` reference remain valid.
30+
*
31+
* @group Transform
32+
*/
33+
export function transformOperation(options: OperationTransformOptions) {
34+
const {
35+
configRead,
36+
name: originalName,
37+
parameters,
38+
responseType: originalResponseType,
39+
} = options
40+
41+
const config = configRead.config || {}
42+
43+
let name = originalName
44+
let responseType = originalResponseType
45+
46+
function applyPatch(patch?: ApiPipeline.Operation) {
47+
if (!patch)
48+
return
49+
50+
if (typeof patch === 'string') {
51+
name = patch
52+
return
53+
}
54+
55+
if (patch.name)
56+
name = patch.name
57+
58+
if (patch.parameters) {
59+
// Replace parameters in place so existing references stay valid.
60+
parameters.length = 0
61+
parameters.push(...patch.parameters)
62+
}
63+
64+
if (patch.responseType != null)
65+
responseType = patch.responseType
66+
}
67+
68+
// Global transform first so static patches can target the final name.
69+
const transformFn = config.transform?.operation
70+
if (transformFn) {
71+
const patch = transformFn(name, parameters, responseType)
72+
applyPatch(patch)
73+
}
74+
75+
const staticPatchMap = config.patch?.operations
76+
if (staticPatchMap) {
77+
const staticPatch = staticPatchMap[name]
78+
applyPatch(staticPatch)
79+
}
80+
81+
return {
82+
name,
83+
parameters,
84+
responseType,
85+
}
86+
}

packages/presets/src/got/ts/parser/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
export const parser = createParser((config, { configRead, functions, interfaces }) => {
1313
const { parameters, interfaces: attachInters, options } = parseMethodParameters(config)
1414
let { name, description, url, responseType } = parseMethodMetadata(config)
15-
1615
interfaces.push(...attachInters)
1716
parameters.push({
1817
name: 'config',

packages/shared/src/types/config.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OptionsOfJSONResponseBody } from 'got'
22
import type {
3+
StatementField,
34
StatementFunction,
45
StatementImported,
56
StatementInterface,
@@ -89,7 +90,73 @@ export namespace ApiPipeline {
8990
onlyDeclaration?: boolean
9091
}
9192

93+
export type Operation = string | {
94+
name?: string
95+
parameters?: StatementField[]
96+
responseType?: string
97+
}
98+
export type Definition = string | {
99+
name?: string
100+
type?: string
101+
}
102+
103+
export interface Patch {
104+
/**
105+
* @example
106+
* {
107+
* // change name to updateUserInfo
108+
* postUpdateUserUsingPOST: 'updateUserInfo',
109+
* // change parameters and name
110+
* 'getUserUsingGET_1': {
111+
* name: 'getUser',
112+
* parameters: [
113+
* { name: 'id', type: 'string' }
114+
* ]
115+
* }
116+
* }
117+
*/
118+
operations?: Record<string, Operation>
119+
/**
120+
* @example
121+
* {
122+
* // rename type
123+
* 'UserDto': 'User',
124+
* 'OrderDto': 'Order',
125+
* // change type
126+
* 'SessionDto': {
127+
* name: 'Session',
128+
* type: '{ name: string, age: number }',
129+
* }
130+
* }
131+
*/
132+
definitions?: Record<string, Definition>
133+
}
134+
135+
export interface Transform {
136+
/**
137+
* @description
138+
* Transform the operation
139+
*/
140+
operation: (name: string, parameters: StatementField[], responseType: string) => Operation
141+
/**
142+
* @description
143+
* Transform the definition type
144+
*/
145+
definition: (name: string, type: string) => Definition
146+
}
147+
92148
export interface Config extends PreInputs, PreOutput, Meta {
149+
/**
150+
* @description
151+
* Static patches: exact match modification
152+
*/
153+
patch?: Patch
154+
155+
/**
156+
* @description
157+
* Transform the operation and definition
158+
*/
159+
transform?: Transform
93160
/**
94161
* The compilation pipeline used supports npm package (add the prefix @genapi/ or genapi-) | local path
95162
* @default 'swag-axios-ts'

0 commit comments

Comments
 (0)