Skip to content

Commit ff4d5dd

Browse files
committed
Fix schema
1 parent 2aa26f0 commit ff4d5dd

File tree

4 files changed

+237
-24
lines changed

4 files changed

+237
-24
lines changed

examples/vite/openapi.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@
155155
"email": {
156156
"type": "string",
157157
"format": "email"
158+
},
159+
"optional": {
160+
"type": "string"
158161
}
159162
},
160163
"required": ["name", "email"]

packages/core/src/api-struct.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export interface DevupDeleteApiStruct {}
1515
// biome-ignore lint/suspicious/noEmptyInterface: empty interface
1616
export interface DevupPatchApiStruct {}
1717

18+
// biome-ignore lint/suspicious/noEmptyInterface: empty interface
19+
export interface DevupRequestComponentStruct {}
20+
21+
// biome-ignore lint/suspicious/noEmptyInterface: empty interface
22+
export interface DevupResponseComponentStruct {}
23+
1824
export type DevupApiStruct = DevupGetApiStruct &
1925
DevupPostApiStruct &
2026
DevupPutApiStruct &

packages/generator/src/generate-interface.ts

Lines changed: 219 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
extractParameters,
88
extractRequestBody,
99
formatTypeValue,
10+
getTypeFromSchema,
1011
} from './generate-schema'
1112
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
1213

@@ -40,6 +41,118 @@ export function generateInterface(
4041
} as const
4142
const convertCaseType = options?.convertCase ?? 'camel'
4243

44+
// Helper function to extract schema names from $ref
45+
const extractSchemaNameFromRef = (ref: string): string | null => {
46+
if (ref.startsWith('#/components/schemas/')) {
47+
return ref.replace('#/components/schemas/', '')
48+
}
49+
return null
50+
}
51+
52+
// Helper function to collect schema names from a schema object
53+
const collectSchemaNames = (
54+
schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
55+
targetSet: Set<string>,
56+
): void => {
57+
if ('$ref' in schemaObj) {
58+
const schemaName = extractSchemaNameFromRef(schemaObj.$ref)
59+
if (schemaName) {
60+
targetSet.add(schemaName)
61+
}
62+
return
63+
}
64+
65+
const schema = schemaObj as OpenAPIV3_1.SchemaObject
66+
67+
// Check allOf, anyOf, oneOf
68+
if (schema.allOf) {
69+
schema.allOf.forEach((s) => {
70+
collectSchemaNames(s, targetSet)
71+
})
72+
}
73+
if (schema.anyOf) {
74+
schema.anyOf.forEach((s) => {
75+
collectSchemaNames(s, targetSet)
76+
})
77+
}
78+
if (schema.oneOf) {
79+
schema.oneOf.forEach((s) => {
80+
collectSchemaNames(s, targetSet)
81+
})
82+
}
83+
84+
// Check properties
85+
if (schema.properties) {
86+
Object.values(schema.properties).forEach((prop) => {
87+
collectSchemaNames(prop, targetSet)
88+
})
89+
}
90+
91+
// Check items (for arrays)
92+
if (schema.type === 'array' && 'items' in schema && schema.items) {
93+
collectSchemaNames(schema.items, targetSet)
94+
}
95+
}
96+
97+
// Track which schemas are used in request body and responses
98+
const requestSchemaNames = new Set<string>()
99+
const responseSchemaNames = new Set<string>()
100+
101+
// First, collect schema names used in request body and responses
102+
if (schema.paths) {
103+
for (const pathItem of Object.values(schema.paths)) {
104+
if (!pathItem) continue
105+
106+
const methods = ['get', 'post', 'put', 'delete', 'patch'] as const
107+
for (const method of methods) {
108+
const operation = pathItem[method]
109+
if (!operation) continue
110+
111+
// Collect request body schemas
112+
if (operation.requestBody) {
113+
if ('$ref' in operation.requestBody) {
114+
// Extract schema name from $ref if it's a schema reference
115+
const schemaName = extractSchemaNameFromRef(
116+
operation.requestBody.$ref,
117+
)
118+
if (schemaName) {
119+
requestSchemaNames.add(schemaName)
120+
}
121+
} else {
122+
const content = operation.requestBody.content
123+
const jsonContent = content?.['application/json']
124+
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
125+
collectSchemaNames(jsonContent.schema, requestSchemaNames)
126+
}
127+
}
128+
}
129+
130+
// Collect response schemas
131+
if (operation.responses) {
132+
for (const response of Object.values(operation.responses)) {
133+
if ('$ref' in response) {
134+
// Extract schema name from $ref if it's a schema reference
135+
const schemaName = extractSchemaNameFromRef(response.$ref)
136+
if (schemaName) {
137+
responseSchemaNames.add(schemaName)
138+
}
139+
} else if ('content' in response) {
140+
const content = response.content
141+
const jsonContent = content?.['application/json']
142+
if (
143+
jsonContent &&
144+
'schema' in jsonContent &&
145+
jsonContent.schema
146+
) {
147+
collectSchemaNames(jsonContent.schema, responseSchemaNames)
148+
}
149+
}
150+
}
151+
}
152+
}
153+
}
154+
}
155+
43156
// Iterate through OpenAPI paths and extract each endpoint
44157
if (schema.paths) {
45158
for (const [path, pathItem] of Object.entries(schema.paths)) {
@@ -82,9 +195,58 @@ export function generateInterface(
82195
}
83196

84197
// Extract request body
85-
const requestBody = extractRequestBody(operation.requestBody, schema)
86-
if (requestBody !== undefined) {
87-
endpoint.body = requestBody
198+
// Check if request body uses a component schema
199+
let requestBodyType: unknown
200+
if (operation.requestBody) {
201+
if ('$ref' in operation.requestBody) {
202+
// RequestBodyObject reference - skip for now
203+
const requestBody = extractRequestBody(
204+
operation.requestBody,
205+
schema,
206+
)
207+
if (requestBody !== undefined) {
208+
requestBodyType = requestBody
209+
}
210+
} else {
211+
const content = operation.requestBody.content
212+
const jsonContent = content?.['application/json']
213+
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
214+
// Check if schema is a direct reference to components.schemas
215+
if ('$ref' in jsonContent.schema) {
216+
const schemaName = extractSchemaNameFromRef(
217+
jsonContent.schema.$ref,
218+
)
219+
// Check if schema exists in components.schemas and is used in request body
220+
if (
221+
schemaName &&
222+
schema.components?.schemas?.[schemaName] &&
223+
requestSchemaNames.has(schemaName)
224+
) {
225+
// Use component reference
226+
requestBodyType = `DevupRequestComponentStruct['${schemaName}']`
227+
} else {
228+
const requestBody = extractRequestBody(
229+
operation.requestBody,
230+
schema,
231+
)
232+
if (requestBody !== undefined) {
233+
requestBodyType = requestBody
234+
}
235+
}
236+
} else {
237+
const requestBody = extractRequestBody(
238+
operation.requestBody,
239+
schema,
240+
)
241+
if (requestBody !== undefined) {
242+
requestBodyType = requestBody
243+
}
244+
}
245+
}
246+
}
247+
}
248+
if (requestBodyType !== undefined) {
249+
endpoint.body = requestBodyType
88250
}
89251

90252
// Generate path key (normalize path by replacing {param} with converted param and removing slashes)
@@ -106,6 +268,29 @@ export function generateInterface(
106268
}
107269
}
108270

271+
// Extract components schemas
272+
const requestComponents: Record<string, unknown> = {}
273+
const responseComponents: Record<string, unknown> = {}
274+
if (schema.components?.schemas) {
275+
for (const [schemaName, schemaObj] of Object.entries(
276+
schema.components.schemas,
277+
)) {
278+
if (schemaObj) {
279+
const { type: schemaType } = getTypeFromSchema(
280+
schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
281+
schema,
282+
)
283+
// Keep original schema name as-is
284+
if (requestSchemaNames.has(schemaName)) {
285+
requestComponents[schemaName] = schemaType
286+
}
287+
if (responseSchemaNames.has(schemaName)) {
288+
responseComponents[schemaName] = schemaType
289+
}
290+
}
291+
}
292+
}
293+
109294
// Generate TypeScript interface string
110295
const interfaceContent = Object.entries(endpoints)
111296
.flatMap(([method, value]) => {
@@ -133,5 +318,35 @@ export function generateInterface(
133318
})
134319
.join('\n')
135320

136-
return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${interfaceContent}\n}`
321+
// Generate RequestComponentStruct interface
322+
const requestComponentEntries = Object.entries(requestComponents)
323+
.map(([key, value]) => {
324+
const formattedValue = formatTypeValue(value, 2)
325+
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
326+
})
327+
.join(';\n')
328+
329+
const requestComponentInterface =
330+
requestComponentEntries.length > 0
331+
? ` interface DevupRequestComponentStruct {\n${requestComponentEntries};\n }`
332+
: ' interface DevupRequestComponentStruct {}'
333+
334+
// Generate ResponseComponentStruct interface
335+
const responseComponentEntries = Object.entries(responseComponents)
336+
.map(([key, value]) => {
337+
const formattedValue = formatTypeValue(value, 2)
338+
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
339+
})
340+
.join(';\n')
341+
342+
const responseComponentInterface =
343+
responseComponentEntries.length > 0
344+
? ` interface DevupResponseComponentStruct {\n${responseComponentEntries};\n }`
345+
: ' interface DevupResponseComponentStruct {}'
346+
347+
const allInterfaces = interfaceContent
348+
? `${interfaceContent}\n\n${requestComponentInterface}\n\n${responseComponentInterface}`
349+
: `${requestComponentInterface}\n\n${responseComponentInterface}`
350+
351+
return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${allInterfaces}\n}`
137352
}

packages/generator/src/generate-schema.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ export function formatType(
249249

250250
const entries = Object.entries(obj)
251251
.map(([key, value]) => {
252+
// Handle string values (e.g., component references)
253+
if (typeof value === 'string') {
254+
return `${nextIndentStr}${key}: ${value}`
255+
}
256+
252257
// Handle ParameterDefinition for params and query
253258
if (isParameterDefinition(value)) {
254259
const typeStr = formatTypeValue(value.type, nextIndent)
@@ -272,29 +277,13 @@ export function formatType(
272277
// Handle { type: unknown, default?: unknown } structure (from getTypeFromSchema)
273278
if (isTypeObject(value)) {
274279
const formattedValue = formatTypeValue(value.type, nextIndent)
275-
// Remove '?' from key if it exists (from getTypeFromSchema)
276-
const cleanKey = key.endsWith('?') ? key.slice(0, -1) : key
277-
// Check if the type object represents an object with all optional properties
278-
const valueAllOptional =
279-
typeof value.type === 'object' &&
280-
value.type !== null &&
281-
!Array.isArray(value.type) &&
282-
areAllPropertiesOptional(value.type as Record<string, unknown>)
283-
const optionalMarker = valueAllOptional ? '?' : ''
284-
return `${nextIndentStr}${cleanKey}${optionalMarker}: ${formattedValue}`
280+
// Key already has '?' if it's optional (from getTypeFromSchema), keep it as is
281+
return `${nextIndentStr}${key}: ${formattedValue}`
285282
}
286283

287284
const formattedValue = formatTypeValue(value, nextIndent)
288-
// Remove '?' from key if it exists (from getTypeFromSchema)
289-
const cleanKey = key.endsWith('?') ? key.slice(0, -1) : key
290-
// Check if value is an object with all optional properties
291-
const valueAllOptional =
292-
typeof value === 'object' &&
293-
value !== null &&
294-
!Array.isArray(value) &&
295-
areAllPropertiesOptional(value as Record<string, unknown>)
296-
const optionalMarker = valueAllOptional ? '?' : ''
297-
return `${nextIndentStr}${cleanKey}${optionalMarker}: ${formattedValue}`
285+
// Key already has '?' if it's optional (from getTypeFromSchema), keep it as is
286+
return `${nextIndentStr}${key}: ${formattedValue}`
298287
})
299288
.join(';\n')
300289

0 commit comments

Comments
 (0)