@devup-api/generator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +26 -0
  2. package/dist/__tests__/convert-case.test.d.ts +2 -0
  3. package/dist/__tests__/convert-case.test.d.ts.map +1 -0
  4. package/dist/__tests__/create-url-map.test.d.ts +2 -0
  5. package/dist/__tests__/create-url-map.test.d.ts.map +1 -0
  6. package/dist/__tests__/index.test.d.ts +2 -0
  7. package/dist/__tests__/index.test.d.ts.map +1 -0
  8. package/dist/__tests__/wrap-interface-key-guard.test.d.ts +2 -0
  9. package/dist/__tests__/wrap-interface-key-guard.test.d.ts.map +1 -0
  10. package/dist/convert-case.d.ts +5 -0
  11. package/dist/convert-case.d.ts.map +1 -0
  12. package/dist/create-url-map.d.ts +4 -0
  13. package/dist/create-url-map.d.ts.map +1 -0
  14. package/dist/generate-interface.d.ts +15 -0
  15. package/dist/generate-interface.d.ts.map +1 -0
  16. package/dist/generate-schema.d.ts +44 -0
  17. package/dist/generate-schema.d.ts.map +1 -0
  18. package/dist/index.cjs +36 -0
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +36 -0
  22. package/dist/wrap-interface-key-guard.d.ts +2 -0
  23. package/dist/wrap-interface-key-guard.d.ts.map +1 -0
  24. package/package.json +27 -0
  25. package/src/__tests__/convert-case.test.ts +125 -0
  26. package/src/__tests__/create-url-map.test.ts +318 -0
  27. package/src/__tests__/index.test.ts +9 -0
  28. package/src/__tests__/wrap-interface-key-guard.test.ts +42 -0
  29. package/src/convert-case.ts +22 -0
  30. package/src/create-url-map.ts +43 -0
  31. package/src/generate-interface.ts +594 -0
  32. package/src/generate-schema.ts +482 -0
  33. package/src/index.ts +2 -0
  34. package/src/wrap-interface-key-guard.ts +6 -0
  35. package/tsconfig.json +34 -0
@@ -0,0 +1,594 @@
1
+ import type { DevupApiTypeGeneratorOptions } from '@devup-api/core'
2
+ import { toPascal } from '@devup-api/utils'
3
+ import type { OpenAPIV3_1 } from 'openapi-types'
4
+ import { convertCase } from './convert-case'
5
+ import {
6
+ extractParameters,
7
+ extractRequestBody,
8
+ formatTypeValue,
9
+ getTypeFromSchema,
10
+ } from './generate-schema'
11
+ import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
12
+
13
+ export interface ParameterDefinition
14
+ extends Omit<OpenAPIV3_1.ParameterObject, 'schema'> {
15
+ type: unknown
16
+ default?: unknown
17
+ }
18
+
19
+ export interface EndpointDefinition {
20
+ params?: Record<string, ParameterDefinition>
21
+ body?: unknown
22
+ query?: Record<string, ParameterDefinition>
23
+ response?: unknown
24
+ error?: unknown
25
+ }
26
+
27
+ // Helper function to extract schema names from $ref
28
+ function extractSchemaNameFromRef(ref: string): string | null {
29
+ if (ref.startsWith('#/components/schemas/')) {
30
+ return ref.replace('#/components/schemas/', '')
31
+ }
32
+ return null
33
+ }
34
+ export function generateInterface(
35
+ schema: OpenAPIV3_1.Document,
36
+ options?: DevupApiTypeGeneratorOptions,
37
+ ): string {
38
+ const endpoints: Record<
39
+ 'get' | 'post' | 'put' | 'delete' | 'patch',
40
+ Record<string, EndpointDefinition>
41
+ > = {
42
+ get: {},
43
+ post: {},
44
+ put: {},
45
+ delete: {},
46
+ patch: {},
47
+ } as const
48
+ const convertCaseType = options?.convertCase ?? 'camel'
49
+
50
+ // Helper function to collect schema names from a schema object
51
+ const collectSchemaNames = (
52
+ schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
53
+ targetSet: Set<string>,
54
+ ): void => {
55
+ if ('$ref' in schemaObj) {
56
+ const schemaName = extractSchemaNameFromRef(schemaObj.$ref)
57
+ if (schemaName) {
58
+ targetSet.add(schemaName)
59
+ }
60
+ return
61
+ }
62
+
63
+ const schema = schemaObj as OpenAPIV3_1.SchemaObject
64
+
65
+ // Check allOf, anyOf, oneOf
66
+ if (schema.allOf) {
67
+ schema.allOf.forEach((s) => {
68
+ collectSchemaNames(s, targetSet)
69
+ })
70
+ }
71
+ if (schema.anyOf) {
72
+ schema.anyOf.forEach((s) => {
73
+ collectSchemaNames(s, targetSet)
74
+ })
75
+ }
76
+ if (schema.oneOf) {
77
+ schema.oneOf.forEach((s) => {
78
+ collectSchemaNames(s, targetSet)
79
+ })
80
+ }
81
+
82
+ // Check properties
83
+ if (schema.properties) {
84
+ Object.values(schema.properties).forEach((prop) => {
85
+ collectSchemaNames(prop, targetSet)
86
+ })
87
+ }
88
+
89
+ // Check items (for arrays)
90
+ if (schema.type === 'array' && 'items' in schema && schema.items) {
91
+ collectSchemaNames(schema.items, targetSet)
92
+ }
93
+ }
94
+
95
+ // Track which schemas are used in request body and responses
96
+ const requestSchemaNames = new Set<string>()
97
+ const responseSchemaNames = new Set<string>()
98
+ const errorSchemaNames = new Set<string>()
99
+
100
+ // Helper function to check if a status code is an error response
101
+ const isErrorStatusCode = (statusCode: string): boolean => {
102
+ if (statusCode === 'default') return true
103
+ const code = parseInt(statusCode, 10)
104
+ return code >= 400 && code < 600
105
+ }
106
+
107
+ // First, collect schema names used in request body and responses
108
+ if (schema.paths) {
109
+ for (const pathItem of Object.values(schema.paths)) {
110
+ if (!pathItem) continue
111
+
112
+ const methods = ['get', 'post', 'put', 'delete', 'patch'] as const
113
+ for (const method of methods) {
114
+ const operation = pathItem[method]
115
+ if (!operation) continue
116
+
117
+ // Collect request body schemas
118
+ if (operation.requestBody) {
119
+ if ('$ref' in operation.requestBody) {
120
+ // Extract schema name from $ref if it's a schema reference
121
+ const schemaName = extractSchemaNameFromRef(
122
+ operation.requestBody.$ref,
123
+ )
124
+ if (schemaName) {
125
+ requestSchemaNames.add(schemaName)
126
+ }
127
+ } else {
128
+ const content = operation.requestBody.content
129
+ const jsonContent = content?.['application/json']
130
+ if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
131
+ collectSchemaNames(jsonContent.schema, requestSchemaNames)
132
+ }
133
+ }
134
+ }
135
+
136
+ // Collect response and error schemas
137
+ if (operation.responses) {
138
+ for (const [statusCode, response] of Object.entries(
139
+ operation.responses,
140
+ )) {
141
+ const isError = isErrorStatusCode(statusCode)
142
+ if ('$ref' in response) {
143
+ // Extract schema name from $ref if it's a schema reference
144
+ const schemaName = extractSchemaNameFromRef(response.$ref)
145
+ if (schemaName) {
146
+ if (isError) {
147
+ errorSchemaNames.add(schemaName)
148
+ } else {
149
+ responseSchemaNames.add(schemaName)
150
+ }
151
+ }
152
+ } else if ('content' in response) {
153
+ const content = response.content
154
+ const jsonContent = content?.['application/json']
155
+ if (
156
+ jsonContent &&
157
+ 'schema' in jsonContent &&
158
+ jsonContent.schema
159
+ ) {
160
+ if (isError) {
161
+ collectSchemaNames(jsonContent.schema, errorSchemaNames)
162
+ } else {
163
+ collectSchemaNames(jsonContent.schema, responseSchemaNames)
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Iterate through OpenAPI paths and extract each endpoint
174
+ if (schema.paths) {
175
+ for (const [path, pathItem] of Object.entries(schema.paths)) {
176
+ if (!pathItem) continue
177
+
178
+ // Process each HTTP method
179
+ const methods = ['get', 'post', 'put', 'delete', 'patch'] as const
180
+
181
+ for (const method of methods) {
182
+ const operation = pathItem[method]
183
+ if (!operation) continue
184
+
185
+ const endpoint: EndpointDefinition = {}
186
+
187
+ // Extract parameters (path, query, header)
188
+ const { pathParams, queryParams } = extractParameters(
189
+ pathItem,
190
+ operation,
191
+ schema,
192
+ )
193
+
194
+ // Apply case conversion to parameter names
195
+ const convertedPathParams: Record<string, ParameterDefinition> = {}
196
+ for (const [key, value] of Object.entries(pathParams)) {
197
+ const convertedKey = convertCase(key, convertCaseType)
198
+ convertedPathParams[convertedKey] = value
199
+ }
200
+
201
+ const convertedQueryParams: Record<string, ParameterDefinition> = {}
202
+ for (const [key, value] of Object.entries(queryParams)) {
203
+ const convertedKey = convertCase(key, convertCaseType)
204
+ convertedQueryParams[convertedKey] = value
205
+ }
206
+
207
+ if (Object.keys(convertedPathParams).length > 0) {
208
+ endpoint.params = convertedPathParams
209
+ }
210
+ if (Object.keys(convertedQueryParams).length > 0) {
211
+ endpoint.query = convertedQueryParams
212
+ }
213
+
214
+ // Extract request body
215
+ // Check if request body uses a component schema
216
+ let requestBodyType: unknown
217
+ if (operation.requestBody) {
218
+ if ('$ref' in operation.requestBody) {
219
+ // RequestBodyObject reference - skip for now
220
+ const requestBody = extractRequestBody(
221
+ operation.requestBody,
222
+ schema,
223
+ )
224
+ if (requestBody !== undefined) {
225
+ requestBodyType = requestBody
226
+ }
227
+ } else {
228
+ const content = operation.requestBody.content
229
+ const jsonContent = content?.['application/json']
230
+ if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
231
+ // Check if schema is a direct reference to components.schemas
232
+ if ('$ref' in jsonContent.schema) {
233
+ const schemaName = extractSchemaNameFromRef(
234
+ jsonContent.schema.$ref,
235
+ )
236
+ // Check if schema exists in components.schemas and is used in request body
237
+ if (
238
+ schemaName &&
239
+ schema.components?.schemas?.[schemaName] &&
240
+ requestSchemaNames.has(schemaName)
241
+ ) {
242
+ // Use component reference
243
+ requestBodyType = `DevupRequestComponentStruct['${schemaName}']`
244
+ } else {
245
+ const requestBody = extractRequestBody(
246
+ operation.requestBody,
247
+ schema,
248
+ )
249
+ if (requestBody !== undefined) {
250
+ requestBodyType = requestBody
251
+ }
252
+ }
253
+ } else {
254
+ const requestBody = extractRequestBody(
255
+ operation.requestBody,
256
+ schema,
257
+ )
258
+ if (requestBody !== undefined) {
259
+ requestBodyType = requestBody
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ if (requestBodyType !== undefined) {
266
+ endpoint.body = requestBodyType
267
+ }
268
+
269
+ // Extract response
270
+ // Check if response uses a component schema
271
+ let responseType: unknown
272
+ if (operation.responses) {
273
+ // Prefer 200 response, fallback to first available response
274
+ const successResponse =
275
+ operation.responses['200'] ||
276
+ operation.responses['201'] ||
277
+ Object.values(operation.responses)[0]
278
+
279
+ if (successResponse) {
280
+ if ('$ref' in successResponse) {
281
+ // ResponseObject reference - skip for now
282
+ // Could resolve if needed
283
+ } else if ('content' in successResponse) {
284
+ const content = successResponse.content
285
+ const jsonContent = content?.['application/json']
286
+ if (
287
+ jsonContent &&
288
+ 'schema' in jsonContent &&
289
+ jsonContent.schema
290
+ ) {
291
+ // Check if schema is a direct reference to components.schemas
292
+ if ('$ref' in jsonContent.schema) {
293
+ const schemaName = extractSchemaNameFromRef(
294
+ jsonContent.schema.$ref,
295
+ )
296
+ // Check if schema exists in components.schemas and is used in response
297
+ if (
298
+ schemaName &&
299
+ schema.components?.schemas?.[schemaName] &&
300
+ responseSchemaNames.has(schemaName)
301
+ ) {
302
+ // Use component reference
303
+ responseType = `DevupResponseComponentStruct['${schemaName}']`
304
+ } else {
305
+ // Extract schema type with response options
306
+ const responseDefaultNonNullable =
307
+ options?.responseDefaultNonNullable ?? true
308
+ const { type: schemaType } = getTypeFromSchema(
309
+ jsonContent.schema,
310
+ schema,
311
+ { defaultNonNullable: responseDefaultNonNullable },
312
+ )
313
+ responseType = schemaType
314
+ }
315
+ } else {
316
+ // Check if it's an array with items referencing a component schema
317
+ const schemaObj =
318
+ jsonContent.schema as OpenAPIV3_1.SchemaObject
319
+ if (
320
+ schemaObj.type === 'array' &&
321
+ schemaObj.items &&
322
+ '$ref' in schemaObj.items
323
+ ) {
324
+ const schemaName = extractSchemaNameFromRef(
325
+ schemaObj.items.$ref,
326
+ )
327
+ // Check if schema exists in components.schemas and is used in response
328
+ if (
329
+ schemaName &&
330
+ schema.components?.schemas?.[schemaName] &&
331
+ responseSchemaNames.has(schemaName)
332
+ ) {
333
+ // Use component reference for array items
334
+ responseType = `Array<DevupResponseComponentStruct['${schemaName}']>`
335
+ } else {
336
+ // Extract schema type with response options
337
+ const responseDefaultNonNullable =
338
+ options?.responseDefaultNonNullable ?? true
339
+ const { type: schemaType } = getTypeFromSchema(
340
+ jsonContent.schema,
341
+ schema,
342
+ { defaultNonNullable: responseDefaultNonNullable },
343
+ )
344
+ responseType = schemaType
345
+ }
346
+ } else {
347
+ // Extract schema type with response options
348
+ const responseDefaultNonNullable =
349
+ options?.responseDefaultNonNullable ?? true
350
+ const { type: schemaType } = getTypeFromSchema(
351
+ jsonContent.schema,
352
+ schema,
353
+ { defaultNonNullable: responseDefaultNonNullable },
354
+ )
355
+ responseType = schemaType
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ if (responseType !== undefined) {
363
+ endpoint.response = responseType
364
+ }
365
+
366
+ // Extract error
367
+ // Check if error uses a component schema
368
+ let errorType: unknown
369
+ if (operation.responses) {
370
+ // Find error responses (4xx, 5xx, or default)
371
+ const errorResponse =
372
+ operation.responses['400'] ||
373
+ operation.responses['401'] ||
374
+ operation.responses['403'] ||
375
+ operation.responses['404'] ||
376
+ operation.responses['422'] ||
377
+ operation.responses['500'] ||
378
+ operation.responses.default ||
379
+ Object.entries(operation.responses).find(([statusCode]) =>
380
+ isErrorStatusCode(statusCode),
381
+ )?.[1]
382
+
383
+ if (errorResponse) {
384
+ if ('$ref' in errorResponse) {
385
+ // ResponseObject reference - skip for now
386
+ // Could resolve if needed
387
+ } else if ('content' in errorResponse) {
388
+ const content = errorResponse.content
389
+ const jsonContent = content?.['application/json']
390
+ if (
391
+ jsonContent &&
392
+ 'schema' in jsonContent &&
393
+ jsonContent.schema
394
+ ) {
395
+ // Check if schema is a direct reference to components.schemas
396
+ if ('$ref' in jsonContent.schema) {
397
+ const schemaName = extractSchemaNameFromRef(
398
+ jsonContent.schema.$ref,
399
+ )
400
+ // Check if schema exists in components.schemas and is used in error
401
+ if (
402
+ schemaName &&
403
+ schema.components?.schemas?.[schemaName] &&
404
+ errorSchemaNames.has(schemaName)
405
+ ) {
406
+ // Use component reference
407
+ errorType = `DevupErrorComponentStruct['${schemaName}']`
408
+ } else {
409
+ // Extract schema type with response options
410
+ const responseDefaultNonNullable =
411
+ options?.responseDefaultNonNullable ?? true
412
+ const { type: schemaType } = getTypeFromSchema(
413
+ jsonContent.schema,
414
+ schema,
415
+ { defaultNonNullable: responseDefaultNonNullable },
416
+ )
417
+ errorType = schemaType
418
+ }
419
+ } else {
420
+ // Check if it's an array with items referencing a component schema
421
+ const schemaObj =
422
+ jsonContent.schema as OpenAPIV3_1.SchemaObject
423
+ if (
424
+ schemaObj.type === 'array' &&
425
+ schemaObj.items &&
426
+ '$ref' in schemaObj.items
427
+ ) {
428
+ const schemaName = extractSchemaNameFromRef(
429
+ schemaObj.items.$ref,
430
+ )
431
+ // Check if schema exists in components.schemas and is used in error
432
+ if (
433
+ schemaName &&
434
+ schema.components?.schemas?.[schemaName] &&
435
+ errorSchemaNames.has(schemaName)
436
+ ) {
437
+ // Use component reference for array items
438
+ errorType = `Array<DevupErrorComponentStruct['${schemaName}']>`
439
+ } else {
440
+ // Extract schema type with response options
441
+ const responseDefaultNonNullable =
442
+ options?.responseDefaultNonNullable ?? true
443
+ const { type: schemaType } = getTypeFromSchema(
444
+ jsonContent.schema,
445
+ schema,
446
+ { defaultNonNullable: responseDefaultNonNullable },
447
+ )
448
+ errorType = schemaType
449
+ }
450
+ } else {
451
+ // Extract schema type with response options
452
+ const responseDefaultNonNullable =
453
+ options?.responseDefaultNonNullable ?? true
454
+ const { type: schemaType } = getTypeFromSchema(
455
+ jsonContent.schema,
456
+ schema,
457
+ { defaultNonNullable: responseDefaultNonNullable },
458
+ )
459
+ errorType = schemaType
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ if (errorType !== undefined) {
467
+ endpoint.error = errorType
468
+ }
469
+
470
+ // Generate path key (normalize path by replacing {param} with converted param and removing slashes)
471
+ const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
472
+ // Convert param name based on case type
473
+ return `{${convertCase(param, convertCaseType)}}`
474
+ })
475
+
476
+ endpoints[method][normalizedPath] = endpoint
477
+ if (operation.operationId) {
478
+ // If operationId exists, create both operationId and path keys
479
+ const operationIdKey = convertCase(
480
+ operation.operationId,
481
+ convertCaseType,
482
+ )
483
+ endpoints[method][operationIdKey] = endpoint
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ // Extract components schemas
490
+ const requestComponents: Record<string, unknown> = {}
491
+ const responseComponents: Record<string, unknown> = {}
492
+ const errorComponents: Record<string, unknown> = {}
493
+ if (schema.components?.schemas) {
494
+ for (const [schemaName, schemaObj] of Object.entries(
495
+ schema.components.schemas,
496
+ )) {
497
+ if (schemaObj) {
498
+ const requestDefaultNonNullable =
499
+ options?.requestDefaultNonNullable ?? false
500
+ const responseDefaultNonNullable =
501
+ options?.responseDefaultNonNullable ?? true
502
+
503
+ // Determine which defaultNonNullable to use based on where schema is used
504
+ let defaultNonNullable = responseDefaultNonNullable
505
+ if (requestSchemaNames.has(schemaName)) {
506
+ defaultNonNullable = requestDefaultNonNullable
507
+ }
508
+
509
+ const { type: schemaType } = getTypeFromSchema(
510
+ schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
511
+ schema,
512
+ { defaultNonNullable },
513
+ )
514
+ // Keep original schema name as-is
515
+ if (requestSchemaNames.has(schemaName)) {
516
+ requestComponents[schemaName] = schemaType
517
+ }
518
+ if (responseSchemaNames.has(schemaName)) {
519
+ responseComponents[schemaName] = schemaType
520
+ }
521
+ if (errorSchemaNames.has(schemaName)) {
522
+ errorComponents[schemaName] = schemaType
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ // Generate TypeScript interface string
529
+ const interfaceContent = Object.entries(endpoints)
530
+ .flatMap(([method, value]) => {
531
+ const entries = Object.entries(value)
532
+ if (entries.length > 0) {
533
+ const interfaceEntries = entries
534
+ .map(([key, endpointValue]) => {
535
+ const formattedValue = formatTypeValue(endpointValue, 2)
536
+ // Top-level keys in ApiStruct should never be optional
537
+ // Only params, query, body etc. can be optional if all their properties are optional
538
+ return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
539
+ })
540
+ .join(';\n')
541
+
542
+ return [
543
+ ` interface Devup${toPascal(method)}ApiStruct {\n${interfaceEntries};\n }`,
544
+ ]
545
+ }
546
+ return []
547
+ })
548
+ .join('\n')
549
+
550
+ // Generate RequestComponentStruct interface
551
+ const requestComponentEntries = Object.entries(requestComponents)
552
+ .map(([key, value]) => {
553
+ const formattedValue = formatTypeValue(value, 2)
554
+ return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
555
+ })
556
+ .join(';\n')
557
+
558
+ const requestComponentInterface =
559
+ requestComponentEntries.length > 0
560
+ ? ` interface DevupRequestComponentStruct {\n${requestComponentEntries};\n }`
561
+ : ' interface DevupRequestComponentStruct {}'
562
+
563
+ // Generate ResponseComponentStruct interface
564
+ const responseComponentEntries = Object.entries(responseComponents)
565
+ .map(([key, value]) => {
566
+ const formattedValue = formatTypeValue(value, 2)
567
+ return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
568
+ })
569
+ .join(';\n')
570
+
571
+ const responseComponentInterface =
572
+ responseComponentEntries.length > 0
573
+ ? ` interface DevupResponseComponentStruct {\n${responseComponentEntries};\n }`
574
+ : ' interface DevupResponseComponentStruct {}'
575
+
576
+ // Generate ErrorComponentStruct interface
577
+ const errorComponentEntries = Object.entries(errorComponents)
578
+ .map(([key, value]) => {
579
+ const formattedValue = formatTypeValue(value, 2)
580
+ return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
581
+ })
582
+ .join(';\n')
583
+
584
+ const errorComponentInterface =
585
+ errorComponentEntries.length > 0
586
+ ? ` interface DevupErrorComponentStruct {\n${errorComponentEntries};\n }`
587
+ : ' interface DevupErrorComponentStruct {}'
588
+
589
+ const allInterfaces = interfaceContent
590
+ ? `${interfaceContent}\n\n${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}`
591
+ : `${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}`
592
+
593
+ return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${allInterfaces}\n}`
594
+ }