Skip to content

Commit 75462a7

Browse files
committed
refactor(generator): migrate all generators to use shared openapi-utils
1 parent d7888ac commit 75462a7

File tree

7 files changed

+37
-144
lines changed

7 files changed

+37
-144
lines changed

packages/generator/src/__tests__/generate-schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
extractParameters,
66
extractRequestBody,
77
formatTypeValue,
8-
getRequestBodyContent,
98
getTypeFromSchema,
109
} from '../generate-schema'
10+
import { getRequestBodyContent } from '../openapi-utils'
1111

1212
const createDocument = (
1313
document: Partial<OpenAPIV3_1.Document> = {},

packages/generator/src/__tests__/index.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,16 @@ test('index.ts exports', () => {
2020
parseCrudConfigsFromMultiple: expect.any(Function),
2121
parseDevupOperations: expect.any(Function),
2222
parseDevupTag: expect.any(Function),
23+
// OpenAPI utilities
24+
CONTENT_TYPE_PRIORITY: [
25+
'application/json',
26+
'application/x-www-form-urlencoded',
27+
'multipart/form-data',
28+
],
29+
resolveRef: expect.any(Function),
30+
getRequestBodyContent: expect.any(Function),
31+
extractSchemaNameFromRef: expect.any(Function),
32+
normalizeServerName: expect.any(Function),
33+
isErrorStatusCode: expect.any(Function),
2334
})
2435
})

packages/generator/src/create-url-map.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
11
import type { DevupApiTypeGeneratorOptions, UrlMapValue } from '@devup-api/core'
22
import type { OpenAPIV3_1 } from 'openapi-types'
33
import { convertCase } from './convert-case'
4-
5-
function resolveRequestBodyRef(
6-
ref: string,
7-
document: OpenAPIV3_1.Document,
8-
): OpenAPIV3_1.RequestBodyObject | undefined {
9-
// Expected format: #/components/requestBodies/Name
10-
const parts = ref.replace('#/', '').split('/')
11-
let current: unknown = document
12-
for (const part of parts) {
13-
if (current == null || typeof current !== 'object') return undefined
14-
current = (current as Record<string, unknown>)[part]
15-
}
16-
return current as OpenAPIV3_1.RequestBodyObject | undefined
17-
}
4+
import { resolveRef } from './openapi-utils'
185

196
export function getBodyType(
207
operation: OpenAPIV3_1.OperationObject,
@@ -25,7 +12,10 @@ export function getBodyType(
2512

2613
let content: OpenAPIV3_1.RequestBodyObject['content'] | undefined
2714
if ('$ref' in requestBody) {
28-
const resolved = resolveRequestBodyRef(requestBody.$ref, document)
15+
const resolved = resolveRef<OpenAPIV3_1.RequestBodyObject>(
16+
requestBody.$ref,
17+
document,
18+
)
2919
content = resolved?.content
3020
} else {
3121
content = requestBody.content

packages/generator/src/generate-interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
extractParameters,
99
extractRequestBody,
1010
formatTypeValue,
11-
getRequestBodyContent,
1211
getTypeFromSchema,
1312
} from './generate-schema'
13+
import { getRequestBodyContent } from './openapi-utils'
1414
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
1515

1616
export interface ParameterDefinition

packages/generator/src/generate-schema.ts

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { OpenAPIV3_1 } from 'openapi-types'
22
import type { ParameterDefinition } from './generate-interface'
3+
import { getRequestBodyContent, resolveRef } from './openapi-utils'
34
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
45

56
/**
@@ -111,34 +112,6 @@ function getNonNullType(
111112
return types.find((t) => t !== 'null')
112113
}
113114

114-
/**
115-
* Resolve $ref reference in OpenAPI schema
116-
*/
117-
function resolveSchemaRef<
118-
T extends OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ParameterObject,
119-
>(ref: string, document: OpenAPIV3_1.Document): T | null {
120-
if (!ref.startsWith('#/')) {
121-
return null
122-
}
123-
124-
const parts = ref.slice(2).split('/')
125-
let current: unknown = document
126-
127-
for (const part of parts) {
128-
if (current && typeof current === 'object' && part in current) {
129-
current = (current as Record<string, unknown>)[part]
130-
} else {
131-
return null
132-
}
133-
}
134-
135-
if (current && typeof current === 'object' && !('$ref' in current)) {
136-
return current as T
137-
}
138-
139-
return null
140-
}
141-
142115
/**
143116
* Options for component reference handling
144117
*/
@@ -179,7 +152,7 @@ export function getTypeFromSchema(
179152
? schema.$ref.replace('#/components/schemas/', '')
180153
: undefined
181154

182-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
155+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(
183156
schema.$ref,
184157
document,
185158
)
@@ -369,7 +342,7 @@ export function getTypeFromSchema(
369342
// Need to resolve $ref if present to check for default
370343
let hasDefault = false
371344
if ('$ref' in value) {
372-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
345+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(
373346
value.$ref,
374347
document,
375348
)
@@ -672,7 +645,7 @@ export function extractParameters(
672645
for (const param of allParams) {
673646
if ('$ref' in param) {
674647
// Resolve $ref parameter
675-
const resolved = resolveSchemaRef<OpenAPIV3_1.ParameterObject>(
648+
const resolved = resolveRef<OpenAPIV3_1.ParameterObject>(
676649
param.$ref,
677650
document,
678651
)
@@ -730,23 +703,6 @@ export function extractParameters(
730703
return { pathParams, queryParams, headerParams }
731704
}
732705

733-
// Priority order: json > urlencoded > multipart
734-
const CONTENT_TYPE_PRIORITY = [
735-
'application/json',
736-
'application/x-www-form-urlencoded',
737-
'multipart/form-data',
738-
] as const
739-
740-
export function getRequestBodyContent(
741-
content: OpenAPIV3_1.RequestBodyObject['content'] | undefined,
742-
): OpenAPIV3_1.MediaTypeObject | undefined {
743-
if (!content) return undefined
744-
for (const ct of CONTENT_TYPE_PRIORITY) {
745-
if (content[ct]) return content[ct]
746-
}
747-
return undefined
748-
}
749-
750706
/**
751707
* Extract request body from OpenAPI operation
752708
*/
@@ -762,7 +718,7 @@ export function extractRequestBody(
762718
}
763719

764720
if ('$ref' in requestBody) {
765-
const resolved = resolveSchemaRef(requestBody.$ref, document)
721+
const resolved = resolveRef(requestBody.$ref, document)
766722
if (resolved && 'content' in resolved && resolved.content) {
767723
const content =
768724
resolved.content as OpenAPIV3_1.RequestBodyObject['content']

packages/generator/src/generate-zod.ts

Lines changed: 13 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,15 @@
11
import type { DevupApiTypeGeneratorOptions } from '@devup-api/core'
22
import type { OpenAPIV3_1 } from 'openapi-types'
33
import { convertCase } from './convert-case'
4+
import {
5+
CONTENT_TYPE_PRIORITY,
6+
extractSchemaNameFromRef,
7+
isErrorStatusCode,
8+
normalizeServerName,
9+
resolveRef,
10+
} from './openapi-utils'
411
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
512

6-
// =============================================================================
7-
// Helper Functions
8-
// =============================================================================
9-
10-
/**
11-
* Normalize server name by removing ./ prefix
12-
*/
13-
function normalizeServerName(serverName: string): string {
14-
return serverName.replace(/^\.\//, '')
15-
}
16-
17-
/**
18-
* Resolve $ref reference in OpenAPI schema
19-
*/
20-
function resolveSchemaRef<
21-
T extends OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ParameterObject,
22-
>(ref: string, document: OpenAPIV3_1.Document): T | null {
23-
if (!ref.startsWith('#/')) {
24-
return null
25-
}
26-
27-
const parts = ref.slice(2).split('/')
28-
let current: unknown = document
29-
30-
for (const part of parts) {
31-
if (current && typeof current === 'object' && part in current) {
32-
current = (current as Record<string, unknown>)[part]
33-
} else {
34-
return null
35-
}
36-
}
37-
38-
if (current && typeof current === 'object' && !('$ref' in current)) {
39-
return current as T
40-
}
41-
42-
return null
43-
}
44-
45-
/**
46-
* Extract schema name from $ref
47-
*/
48-
function extractSchemaNameFromRef(ref: string): string | null {
49-
if (ref.startsWith('#/components/schemas/')) {
50-
return ref.replace('#/components/schemas/', '')
51-
}
52-
return null
53-
}
54-
55-
/**
56-
* Check if status code is an error response
57-
*/
58-
function isErrorStatusCode(statusCode: string): boolean {
59-
if (statusCode === 'default') return true
60-
const code = parseInt(statusCode, 10)
61-
return code >= 400 && code < 600
62-
}
63-
6413
// =============================================================================
6514
// OpenAPI to Zod Conversion
6615
// =============================================================================
@@ -83,10 +32,7 @@ function schemaToZod(
8332
// Return lazy reference for circular dependencies
8433
return `z.lazy(() => ${schemaRefs.get(schemaName)})`
8534
}
86-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
87-
schema.$ref,
88-
document,
89-
)
35+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(schema.$ref, document)
9036
if (resolved) {
9137
return schemaToZod(resolved, document, schemaRefs, options)
9238
}
@@ -262,7 +208,7 @@ function schemaToZod(
262208
// Check for default value
263209
let hasDefault = false
264210
if ('$ref' in value) {
265-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
211+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(
266212
value.$ref,
267213
document,
268214
)
@@ -328,10 +274,7 @@ function schemaToZodType(
328274
// Return a lazy type reference
329275
return `z.ZodLazy<z.ZodTypeAny>`
330276
}
331-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
332-
schema.$ref,
333-
document,
334-
)
277+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(schema.$ref, document)
335278
if (resolved) {
336279
return schemaToZodType(resolved, document, options)
337280
}
@@ -442,7 +385,7 @@ function schemaToZodType(
442385
// Check for default value
443386
let hasDefault = false
444387
if ('$ref' in value) {
445-
const resolved = resolveSchemaRef<OpenAPIV3_1.SchemaObject>(
388+
const resolved = resolveRef<OpenAPIV3_1.SchemaObject>(
446389
value.$ref,
447390
document,
448391
)
@@ -572,11 +515,7 @@ function collectSchemaUsage(
572515
return extractSchemaNameFromRef(requestBody.$ref)
573516
}
574517
const content = requestBody.content
575-
for (const ct of [
576-
'application/json',
577-
'application/x-www-form-urlencoded',
578-
'multipart/form-data',
579-
]) {
518+
for (const ct of CONTENT_TYPE_PRIORITY) {
580519
const bodyContent = content?.[ct]
581520
if (bodyContent?.schema && '$ref' in bodyContent.schema) {
582521
return extractSchemaNameFromRef(bodyContent.schema.$ref)
@@ -618,11 +557,7 @@ function collectSchemaUsage(
618557
}
619558
} else {
620559
const content = operation.requestBody.content
621-
for (const ct of [
622-
'application/json',
623-
'application/x-www-form-urlencoded',
624-
'multipart/form-data',
625-
]) {
560+
for (const ct of CONTENT_TYPE_PRIORITY) {
626561
const bodyContent = content?.[ct]
627562
if (bodyContent?.schema) {
628563
collectSchemaNames(bodyContent.schema, requestSchemaNames)

packages/generator/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './crud-types'
33
export * from './generate-crud-config'
44
export * from './generate-interface'
55
export * from './generate-zod'
6+
export * from './openapi-utils'
67
export * from './parse-crud-tags'

0 commit comments

Comments
 (0)