@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,318 @@
1
+ import { expect, test } from 'bun:test'
2
+ import type { UrlMapValue } from '@devup-api/core'
3
+ import type { OpenAPIV3_1 } from 'openapi-types'
4
+ import { createUrlMap } from '../create-url-map'
5
+
6
+ test.each([
7
+ [
8
+ 'camel',
9
+ undefined,
10
+ 'get_users',
11
+ {
12
+ getUsers: { method: 'GET', url: '/users' },
13
+ '/users': { method: 'GET', url: '/users' },
14
+ },
15
+ ],
16
+ [
17
+ 'snake',
18
+ { convertCase: 'snake' as const },
19
+ 'getUsers',
20
+ {
21
+ get_users: { method: 'GET', url: '/users' },
22
+ '/users': { method: 'GET', url: '/users' },
23
+ },
24
+ ],
25
+ [
26
+ 'pascal',
27
+ { convertCase: 'pascal' as const },
28
+ 'get_users',
29
+ {
30
+ GetUsers: { method: 'GET', url: '/users' },
31
+ '/users': { method: 'GET', url: '/users' },
32
+ },
33
+ ],
34
+ [
35
+ 'maintain',
36
+ { convertCase: 'maintain' as const },
37
+ 'get_users',
38
+ {
39
+ get_users: { method: 'GET', url: '/users' },
40
+ '/users': { method: 'GET', url: '/users' },
41
+ },
42
+ ],
43
+ ])('creates url map with %s case conversion', (_, options, operationId, expected) => {
44
+ const schema: OpenAPIV3_1.Document = {
45
+ openapi: '3.1.0',
46
+ info: { title: 'Test API', version: '1.0.0' },
47
+ paths: {
48
+ '/users': {
49
+ get: {
50
+ operationId,
51
+ responses: {},
52
+ },
53
+ },
54
+ },
55
+ }
56
+
57
+ const result = createUrlMap(schema, options)
58
+
59
+ expect(result).toEqual(expected as Record<string, UrlMapValue>)
60
+ })
61
+
62
+ test('converts path parameters based on convertCase', () => {
63
+ const schema: OpenAPIV3_1.Document = {
64
+ openapi: '3.1.0',
65
+ info: { title: 'Test API', version: '1.0.0' },
66
+ paths: {
67
+ '/users/{user_id}/posts/{post_id}': {
68
+ get: {
69
+ operationId: 'get_user_post',
70
+ responses: {},
71
+ },
72
+ },
73
+ },
74
+ }
75
+
76
+ const result = createUrlMap(schema, { convertCase: 'camel' })
77
+
78
+ expect(result).toEqual({
79
+ getUserPost: {
80
+ method: 'GET',
81
+ url: '/users/{userId}/posts/{postId}',
82
+ },
83
+ '/users/{userId}/posts/{postId}': {
84
+ method: 'GET',
85
+ url: '/users/{userId}/posts/{postId}',
86
+ },
87
+ })
88
+ })
89
+
90
+ test.each([
91
+ ['get', 'get_users', 'getUsers', 'GET'],
92
+ ['post', 'create_user', 'createUser', 'POST'],
93
+ ['put', 'update_user', 'updateUser', 'PUT'],
94
+ ['delete', 'delete_user', 'deleteUser', 'DELETE'],
95
+ ['patch', 'patch_user', 'patchUser', 'PATCH'],
96
+ ])('handles %s HTTP method', (method, operationId, expectedKey, expectedMethod) => {
97
+ const schema: OpenAPIV3_1.Document = {
98
+ openapi: '3.1.0',
99
+ info: { title: 'Test API', version: '1.0.0' },
100
+ paths: {
101
+ '/users': {
102
+ [method]: {
103
+ operationId,
104
+ responses: {},
105
+ },
106
+ },
107
+ },
108
+ }
109
+
110
+ const result = createUrlMap(schema)
111
+
112
+ expect(result).toHaveProperty(expectedKey)
113
+ expect(result[expectedKey]?.method).toBe(
114
+ expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
115
+ )
116
+ expect(result).toHaveProperty('/users')
117
+ expect(result['/users']?.method).toBe(
118
+ expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
119
+ )
120
+ })
121
+
122
+ test('handles operation without operationId', () => {
123
+ const schema: OpenAPIV3_1.Document = {
124
+ openapi: '3.1.0',
125
+ info: { title: 'Test API', version: '1.0.0' },
126
+ paths: {
127
+ '/users': {
128
+ get: {
129
+ responses: {},
130
+ },
131
+ },
132
+ },
133
+ }
134
+
135
+ const result = createUrlMap(schema)
136
+
137
+ expect(result).toEqual({
138
+ '/users': {
139
+ method: 'GET',
140
+ url: '/users',
141
+ },
142
+ })
143
+ expect(result).not.toHaveProperty('getUsers')
144
+ })
145
+
146
+ test('handles multiple paths', () => {
147
+ const schema: OpenAPIV3_1.Document = {
148
+ openapi: '3.1.0',
149
+ info: { title: 'Test API', version: '1.0.0' },
150
+ paths: {
151
+ '/users': {
152
+ get: {
153
+ operationId: 'get_users',
154
+ responses: {},
155
+ },
156
+ },
157
+ '/posts': {
158
+ get: {
159
+ operationId: 'get_posts',
160
+ responses: {},
161
+ },
162
+ },
163
+ },
164
+ }
165
+
166
+ const result = createUrlMap(schema)
167
+
168
+ expect(result).toHaveProperty('getUsers')
169
+ expect(result).toHaveProperty('getPosts')
170
+ expect(result).toHaveProperty('/users')
171
+ expect(result).toHaveProperty('/posts')
172
+ })
173
+
174
+ test('handles empty paths', () => {
175
+ const schema: OpenAPIV3_1.Document = {
176
+ openapi: '3.1.0',
177
+ info: { title: 'Test API', version: '1.0.0' },
178
+ paths: {},
179
+ }
180
+
181
+ const result = createUrlMap(schema)
182
+
183
+ expect(result).toEqual({})
184
+ })
185
+
186
+ test('handles undefined paths', () => {
187
+ const schema: OpenAPIV3_1.Document = {
188
+ openapi: '3.1.0',
189
+ info: { title: 'Test API', version: '1.0.0' },
190
+ components: {},
191
+ paths: {},
192
+ }
193
+
194
+ const result = createUrlMap(schema)
195
+
196
+ expect(result).toEqual({})
197
+ })
198
+
199
+ test('handles undefined pathItem', () => {
200
+ const schema: OpenAPIV3_1.Document = {
201
+ openapi: '3.1.0',
202
+ info: { title: 'Test API', version: '1.0.0' },
203
+ paths: {
204
+ '/users': undefined,
205
+ },
206
+ }
207
+
208
+ const result = createUrlMap(schema)
209
+
210
+ expect(result).toEqual({})
211
+ })
212
+
213
+ test('skips operations that do not exist', () => {
214
+ const schema: OpenAPIV3_1.Document = {
215
+ openapi: '3.1.0',
216
+ info: { title: 'Test API', version: '1.0.0' },
217
+ paths: {
218
+ '/users': {
219
+ get: {
220
+ operationId: 'get_users',
221
+ responses: {},
222
+ },
223
+ // post, put, delete, patch are not defined
224
+ },
225
+ },
226
+ }
227
+
228
+ const result = createUrlMap(schema)
229
+
230
+ expect(result).toEqual({
231
+ getUsers: {
232
+ method: 'GET',
233
+ url: '/users',
234
+ },
235
+ '/users': {
236
+ method: 'GET',
237
+ url: '/users',
238
+ },
239
+ })
240
+ })
241
+
242
+ test('handles complex path with multiple parameters', () => {
243
+ const schema: OpenAPIV3_1.Document = {
244
+ openapi: '3.1.0',
245
+ info: { title: 'Test API', version: '1.0.0' },
246
+ paths: {
247
+ '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': {
248
+ get: {
249
+ operationId: 'get_user_post_comment',
250
+ responses: {},
251
+ },
252
+ },
253
+ },
254
+ }
255
+
256
+ const result = createUrlMap(schema, { convertCase: 'snake' })
257
+
258
+ expect(result).toEqual({
259
+ get_user_post_comment: {
260
+ method: 'GET',
261
+ url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}',
262
+ },
263
+ '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': {
264
+ method: 'GET',
265
+ url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}',
266
+ },
267
+ })
268
+ })
269
+
270
+ test.each([
271
+ ['camel', '/users/{userId}', '/users/{userId}'],
272
+ ['snake', '/users/{user_id}', '/users/{user_id}'],
273
+ ['pascal', '/users/{UserId}', '/users/{UserId}'],
274
+ ])('converts path parameters with %s case: %s', (caseType, expectedPath, expectedUrl) => {
275
+ const schema: OpenAPIV3_1.Document = {
276
+ openapi: '3.1.0',
277
+ info: { title: 'Test API', version: '1.0.0' },
278
+ paths: {
279
+ '/users/{user_id}': {
280
+ get: {
281
+ operationId: 'get_user',
282
+ responses: {},
283
+ },
284
+ },
285
+ },
286
+ }
287
+
288
+ const result = createUrlMap(schema, {
289
+ convertCase: caseType as 'camel' | 'snake' | 'pascal',
290
+ })
291
+
292
+ expect(result[expectedPath]?.url).toBe(expectedUrl)
293
+ })
294
+
295
+ test.each([
296
+ ['camel', 'getUserList'],
297
+ ['snake', 'get_user_list'],
298
+ ['pascal', 'GetUserList'],
299
+ ])('converts operationId with %s case: %s', (caseType, expectedKey) => {
300
+ const schema: OpenAPIV3_1.Document = {
301
+ openapi: '3.1.0',
302
+ info: { title: 'Test API', version: '1.0.0' },
303
+ paths: {
304
+ '/users': {
305
+ get: {
306
+ operationId: 'get_user_list',
307
+ responses: {},
308
+ },
309
+ },
310
+ },
311
+ }
312
+
313
+ const result = createUrlMap(schema, {
314
+ convertCase: caseType as 'camel' | 'snake' | 'pascal',
315
+ })
316
+
317
+ expect(result).toHaveProperty(expectedKey)
318
+ })
@@ -0,0 +1,9 @@
1
+ import { expect, test } from 'bun:test'
2
+ import * as indexModule from '../index'
3
+
4
+ test('index.ts exports', () => {
5
+ expect({ ...indexModule }).toEqual({
6
+ createUrlMap: expect.any(Function),
7
+ generateInterface: expect.any(Function),
8
+ })
9
+ })
@@ -0,0 +1,42 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { wrapInterfaceKeyGuard } from '../wrap-interface-key-guard'
3
+
4
+ test.each([
5
+ ['getUsers', 'getUsers'],
6
+ ['createUser', 'createUser'],
7
+ ['updateUser', 'updateUser'],
8
+ ['deleteUser', 'deleteUser'],
9
+ ['testKey', 'testKey'],
10
+ ['camelCase', 'camelCase'],
11
+ ['snake_case', 'snake_case'],
12
+ ['PascalCase', 'PascalCase'],
13
+ ] as const)('wrapInterfaceKeyGuard returns key as-is when no slash: %s -> %s', (key, expected) => {
14
+ expect(wrapInterfaceKeyGuard(key)).toBe(expected)
15
+ })
16
+
17
+ test.each([
18
+ ['/users', '[`/users`]'],
19
+ ['/users/{id}', '[`/users/{id}`]'],
20
+ ['/api/v1/users', '[`/api/v1/users`]'],
21
+ ['/users/{userId}/posts/{postId}', '[`/users/{userId}/posts/{postId}`]'],
22
+ ['/api/v1/users/{id}/profile', '[`/api/v1/users/{id}/profile`]'],
23
+ ] as const)('wrapInterfaceKeyGuard wraps key with backticks when slash present: %s -> %s', (key, expected) => {
24
+ expect(wrapInterfaceKeyGuard(key)).toBe(expected)
25
+ })
26
+
27
+ test.each([
28
+ ['', ''],
29
+ ['/', '[`/`]'],
30
+ ['//', '[`//`]'],
31
+ ['///', '[`///`]'],
32
+ ] as const)('wrapInterfaceKeyGuard handles edge cases: %s -> %s', (key, expected) => {
33
+ expect(wrapInterfaceKeyGuard(key)).toBe(expected)
34
+ })
35
+
36
+ test.each([
37
+ ['users/123', '[`users/123`]'],
38
+ ['test/path/here', '[`test/path/here`]'],
39
+ ['a/b/c/d', '[`a/b/c/d`]'],
40
+ ] as const)('wrapInterfaceKeyGuard wraps key with multiple slashes: %s -> %s', (key, expected) => {
41
+ expect(wrapInterfaceKeyGuard(key)).toBe(expected)
42
+ })
@@ -0,0 +1,22 @@
1
+ import { toCamel, toPascal, toSnake } from '@devup-api/utils'
2
+
3
+ /**
4
+ * Convert string based on convertCase option
5
+ */
6
+ export function convertCase(
7
+ str: string,
8
+ caseType: 'snake' | 'camel' | 'pascal' | 'maintain' = 'camel',
9
+ ): string {
10
+ switch (caseType) {
11
+ case 'snake':
12
+ return toSnake(str)
13
+ case 'camel':
14
+ return toCamel(str)
15
+ case 'pascal':
16
+ return toPascal(str)
17
+ case 'maintain':
18
+ return str
19
+ default:
20
+ return str
21
+ }
22
+ }
@@ -0,0 +1,43 @@
1
+ import type { DevupApiTypeGeneratorOptions, UrlMapValue } from '@devup-api/core'
2
+ import type { OpenAPIV3_1 } from 'openapi-types'
3
+ import { convertCase } from './convert-case'
4
+
5
+ export function createUrlMap(
6
+ schema: OpenAPIV3_1.Document,
7
+ options?: DevupApiTypeGeneratorOptions,
8
+ ) {
9
+ const convertCaseType = options?.convertCase ?? 'camel'
10
+ const urlMap: Record<string, UrlMapValue> = {}
11
+ for (const [path, pathItem] of Object.entries(schema.paths ?? {})) {
12
+ if (!pathItem) continue
13
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
14
+ const operation = pathItem[method]
15
+ if (!operation) continue
16
+ const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
17
+ // Convert param name based on case type
18
+ return `{${convertCase(param, convertCaseType)}}`
19
+ })
20
+ if (operation.operationId) {
21
+ urlMap[convertCase(operation.operationId, convertCaseType)] = {
22
+ method: method.toUpperCase() as
23
+ | 'GET'
24
+ | 'POST'
25
+ | 'PUT'
26
+ | 'DELETE'
27
+ | 'PATCH',
28
+ url: normalizedPath,
29
+ }
30
+ }
31
+ urlMap[normalizedPath] = {
32
+ method: method.toUpperCase() as
33
+ | 'GET'
34
+ | 'POST'
35
+ | 'PUT'
36
+ | 'DELETE'
37
+ | 'PATCH',
38
+ url: normalizedPath,
39
+ }
40
+ }
41
+ }
42
+ return urlMap
43
+ }