Skip to content

Commit 2aa26f0

Browse files
committed
Improve gen interface
1 parent 82eab42 commit 2aa26f0

File tree

6 files changed

+337
-79
lines changed

6 files changed

+337
-79
lines changed

examples/vite/openapi.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,48 @@
5353
}
5454
}
5555
},
56+
"/users/{user_id}/posts": {
57+
"get": {
58+
"summary": "Get user posts by ID",
59+
"operationId": "getUserPostsById",
60+
"parameters": [
61+
{
62+
"name": "user_id",
63+
"in": "path",
64+
"required": true,
65+
"schema": {
66+
"type": "integer",
67+
"default": 1
68+
},
69+
"description": "User ID"
70+
},
71+
{
72+
"name": "post_id",
73+
"in": "query",
74+
"required": false,
75+
"schema": {
76+
"type": "integer"
77+
},
78+
"description": "Post ID"
79+
}
80+
],
81+
"responses": {
82+
"200": {
83+
"description": "User details",
84+
"content": {
85+
"application/json": {
86+
"schema": {
87+
"type": "array",
88+
"items": {
89+
"$ref": "#/components/schemas/Post"
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}
97+
},
5698
"/users/{id}": {
5799
"get": {
58100
"summary": "Get user by ID",

examples/vite/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ function App() {
1414
})
1515

1616
api
17-
.get('getUserById', {
18-
params: { id: 1 },
17+
.get('/users/{userId}/posts', {
18+
params: { userId: 1 },
1919
})
2020
.then((res) => {
2121
console.log(res)

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { convertCase } from './convert-case'
44

55
export function createUrlMap(
66
schema: OpenAPIV3_1.Document,
7-
87
options?: DevupApiTypeGeneratorOptions,
98
) {
109
const convertCaseType = options?.convertCase ?? 'camel'
@@ -14,6 +13,10 @@ export function createUrlMap(
1413
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
1514
const operation = pathItem[method]
1615
if (!operation) continue
16+
const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
17+
// Convert param name based on case type
18+
return `{${convertCase(param, convertCaseType)}}`
19+
})
1720
if (operation.operationId) {
1821
urlMap[convertCase(operation.operationId, convertCaseType)] = {
1922
method: method.toUpperCase() as
@@ -22,17 +25,17 @@ export function createUrlMap(
2225
| 'PUT'
2326
| 'DELETE'
2427
| 'PATCH',
25-
url: path,
28+
url: normalizedPath,
2629
}
2730
}
28-
urlMap[path] = {
31+
urlMap[normalizedPath] = {
2932
method: method.toUpperCase() as
3033
| 'GET'
3134
| 'POST'
3235
| 'PUT'
3336
| 'DELETE'
3437
| 'PATCH',
35-
url: path,
38+
url: normalizedPath,
3639
}
3740
}
3841
}

packages/generator/src/generate-interface.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ import { toPascal } from '@devup-api/utils'
33
import type { OpenAPIV3_1 } from 'openapi-types'
44
import { convertCase } from './convert-case'
55
import {
6+
areAllPropertiesOptional,
67
extractParameters,
78
extractRequestBody,
89
formatTypeValue,
910
} from './generate-schema'
11+
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
1012

11-
interface EndpointDefinition {
12-
params?: Record<string, unknown>
13+
export type ParameterDefinition = Omit<
14+
OpenAPIV3_1.ParameterObject,
15+
'schema'
16+
> & {
17+
type: unknown
18+
default?: unknown
19+
}
20+
21+
export interface EndpointDefinition {
22+
params?: Record<string, ParameterDefinition>
1323
body?: unknown
14-
query?: Record<string, unknown>
24+
query?: Record<string, ParameterDefinition>
1525
}
1626

1727
export function generateInterface(
@@ -52,13 +62,13 @@ export function generateInterface(
5262
)
5363

5464
// Apply case conversion to parameter names
55-
const convertedPathParams: Record<string, unknown> = {}
65+
const convertedPathParams: Record<string, ParameterDefinition> = {}
5666
for (const [key, value] of Object.entries(pathParams)) {
5767
const convertedKey = convertCase(key, convertCaseType)
5868
convertedPathParams[convertedKey] = value
5969
}
6070

61-
const convertedQueryParams: Record<string, unknown> = {}
71+
const convertedQueryParams: Record<string, ParameterDefinition> = {}
6272
for (const [key, value] of Object.entries(queryParams)) {
6373
const convertedKey = convertCase(key, convertCaseType)
6474
convertedQueryParams[convertedKey] = value
@@ -78,32 +88,19 @@ export function generateInterface(
7888
}
7989

8090
// Generate path key (normalize path by replacing {param} with converted param and removing slashes)
81-
const normalizedPath = path
82-
.replace(/\{([^}]+)\}/g, (_, param) => {
83-
// Convert param name based on case type
84-
return convertCase(param, convertCaseType)
85-
})
86-
.replace(/^\//, '')
87-
.replace(/\//g, '')
88-
const pathKey = convertCase(normalizedPath, convertCaseType)
91+
const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
92+
// Convert param name based on case type
93+
return `{${convertCase(param, convertCaseType)}}`
94+
})
8995

96+
endpoints[method][normalizedPath] = endpoint
9097
if (operation.operationId) {
9198
// If operationId exists, create both operationId and path keys
9299
const operationIdKey = convertCase(
93100
operation.operationId,
94101
convertCaseType,
95102
)
96103
endpoints[method][operationIdKey] = endpoint
97-
98-
// Add path key if different from operationId key
99-
if (pathKey && pathKey !== operationIdKey) {
100-
endpoints[method][pathKey] = endpoint
101-
}
102-
} else {
103-
// If operationId doesn't exist, only use path key
104-
if (pathKey) {
105-
endpoints[method][pathKey] = endpoint
106-
}
107104
}
108105
}
109106
}
@@ -114,16 +111,27 @@ export function generateInterface(
114111
.flatMap(([method, value]) => {
115112
const entries = Object.entries(value)
116113
if (entries.length > 0) {
114+
const interfaceEntries = entries
115+
.map(([key, endpointValue]) => {
116+
const formattedValue = formatTypeValue(endpointValue, 2)
117+
// Check if all properties in endpointValue are optional
118+
const allOptional =
119+
typeof endpointValue === 'object' &&
120+
endpointValue !== null &&
121+
!Array.isArray(endpointValue) &&
122+
areAllPropertiesOptional(endpointValue as Record<string, unknown>)
123+
const optionalMarker = allOptional ? '?' : ''
124+
return ` ${wrapInterfaceKeyGuard(key)}${optionalMarker}: ${formattedValue}`
125+
})
126+
.join(';\n')
127+
117128
return [
118-
`interface Devup${toPascal(method)}ApiStruct{${entries
119-
.map(([key, value]) => {
120-
return `${key}:${formatTypeValue(value)}`
121-
})
122-
.join(';')}}`,
129+
` interface Devup${toPascal(method)}ApiStruct {\n${interfaceEntries};\n }`,
123130
]
124131
}
125132
return []
126133
})
127-
.flat()
128-
return `import "@devup-api/fetch";declare module "@devup-api/fetch"{${interfaceContent.join('')}}`
134+
.join('\n')
135+
136+
return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${interfaceContent}\n}`
129137
}

0 commit comments

Comments
 (0)