From 1f08e883eea0ab3118ea2968249b55aa15c336a0 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 6 Mar 2026 21:51:56 +0900 Subject: [PATCH] Add method layer --- .../changepack_log_b2zlSGpeRX5LEJNPy_XI6.json | 1 + packages/core/src/url-map.ts | 8 +- .../fetch/src/__tests__/api-body-type.test.ts | 27 +++-- packages/fetch/src/__tests__/url-map.test.ts | 53 +++++---- packages/fetch/src/url-map.ts | 14 +-- .../src/__tests__/create-url-map.test.ts | 105 +++++++++--------- packages/generator/src/create-url-map.ts | 30 ++--- .../next-plugin/src/__tests__/plugin.test.ts | 6 +- .../src/__tests__/plugin.test.ts | 6 +- .../vite-plugin/src/__tests__/plugin.test.ts | 6 +- .../src/__tests__/plugin.test.ts | 6 +- 11 files changed, 140 insertions(+), 122 deletions(-) create mode 100644 .changepacks/changepack_log_b2zlSGpeRX5LEJNPy_XI6.json diff --git a/.changepacks/changepack_log_b2zlSGpeRX5LEJNPy_XI6.json b/.changepacks/changepack_log_b2zlSGpeRX5LEJNPy_XI6.json new file mode 100644 index 0000000..6d474eb --- /dev/null +++ b/.changepacks/changepack_log_b2zlSGpeRX5LEJNPy_XI6.json @@ -0,0 +1 @@ +{"changes":{"packages/fetch/package.json":"Patch","packages/generator/package.json":"Patch","packages/vite-plugin/package.json":"Patch","packages/core/package.json":"Patch","packages/next-plugin/package.json":"Patch","packages/webpack-plugin/package.json":"Patch","packages/rsbuild-plugin/package.json":"Patch"},"note":"Add method layer","date":"2026-03-06T12:51:48.129089Z"} \ No newline at end of file diff --git a/packages/core/src/url-map.ts b/packages/core/src/url-map.ts index 83136c4..1f6f291 100644 --- a/packages/core/src/url-map.ts +++ b/packages/core/src/url-map.ts @@ -1,5 +1,11 @@ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + export interface UrlMapValue { - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + method: HttpMethod url: string bodyType?: 'json' | 'form' | 'multipart' } + +export type UrlMapStoredValue = Omit + +export type UrlMapEntry = Partial> diff --git a/packages/fetch/src/__tests__/api-body-type.test.ts b/packages/fetch/src/__tests__/api-body-type.test.ts index 494b9e5..6497f76 100644 --- a/packages/fetch/src/__tests__/api-body-type.test.ts +++ b/packages/fetch/src/__tests__/api-body-type.test.ts @@ -3,23 +3,28 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import type { UrlMapValue } from '@devup-api/core' // Mock the url-map module to return custom bodyType values -const mockUrlMap: Record> = { +const mockUrlMap: Record< + string, + Record>>> +> = { 'openapi.json': { - submitForm: { method: 'POST', url: '/submit', bodyType: 'form' }, - uploadFile: { method: 'POST', url: '/upload', bodyType: 'multipart' }, - jsonEndpoint: { method: 'POST', url: '/json', bodyType: 'json' }, + submitForm: { POST: { url: '/submit', bodyType: 'form' } }, + uploadFile: { + POST: { url: '/upload', bodyType: 'multipart' }, + }, + jsonEndpoint: { POST: { url: '/json', bodyType: 'json' } }, }, } mock.module('../url-map', () => ({ DEVUP_API_URL_MAP: mockUrlMap, - getApiEndpointInfo: (key: string, serverName: string): UrlMapValue => { - const result = mockUrlMap[serverName]?.[key] ?? { - method: 'GET' as const, - url: key, - } - result.url ||= key - return result + getApiEndpointInfo: ( + key: string, + serverName: string, + method: string, + ): UrlMapValue => { + const stored = mockUrlMap[serverName]?.[key]?.[method] + return { method: method as 'GET', url: key, ...stored } }, })) diff --git a/packages/fetch/src/__tests__/url-map.test.ts b/packages/fetch/src/__tests__/url-map.test.ts index b3c7501..845924b 100644 --- a/packages/fetch/src/__tests__/url-map.test.ts +++ b/packages/fetch/src/__tests__/url-map.test.ts @@ -2,10 +2,10 @@ import { beforeEach, expect, test } from 'bun:test' const urlMap = { foo: { - getUsers: { method: 'GET' as const, url: '/users' }, - createUser: { method: 'POST' as const, url: '/users' }, - updateUser: { method: 'PUT' as const, url: '/users/{id}' }, - deleteUser: { method: 'DELETE' as const, url: '/users/{id}' }, + getUsers: { GET: { url: '/users' } }, + createUser: { POST: { url: '/users' } }, + updateUser: { PUT: { url: '/users/{id}' } }, + deleteUser: { DELETE: { url: '/users/{id}' } }, }, } @@ -15,41 +15,52 @@ beforeEach(() => { const random = Math.random() test.each([ - ['getUsers', '/users', JSON.stringify(urlMap)], - ['createUser', '/users', JSON.stringify(urlMap)], - ['updateUser', '/users/{id}', JSON.stringify(urlMap)], - ['deleteUser', '/users/{id}', JSON.stringify(urlMap)], -] as const)('getApiEndpointInfo returns url for existing key: %s -> %s', async (key, expected, envValue) => { + ['getUsers', '/users', 'GET', JSON.stringify(urlMap)], + ['createUser', '/users', 'POST', JSON.stringify(urlMap)], + ['updateUser', '/users/{id}', 'PUT', JSON.stringify(urlMap)], + ['deleteUser', '/users/{id}', 'DELETE', JSON.stringify(urlMap)], +] as const)('getApiEndpointInfo returns url for existing key: %s -> %s', async (key, expected, method, envValue) => { process.env.DEVUP_API_URL_MAP = envValue // Add query parameter to bypass module cache and reload const { getApiEndpointInfo } = await import(`../url-map?t=${random}`) - expect(getApiEndpointInfo(key, 'foo')?.url).toBe(expected) + expect(getApiEndpointInfo(key, 'foo', method)?.url).toBe(expected) }) test.each([ - ['nonExistentKey', 'nonExistentKey', JSON.stringify(urlMap)], - ['unknown', 'unknown', JSON.stringify(urlMap)], - ['', '', JSON.stringify(urlMap)], - ['/users', '/users', JSON.stringify(urlMap)], -] as const)('getApiEndpointInfo returns key itself when key does not exist: %s -> %s', async (key, expected, envValue) => { + ['nonExistentKey', 'nonExistentKey', 'GET', JSON.stringify(urlMap)], + ['unknown', 'unknown', 'GET', JSON.stringify(urlMap)], + ['', '', 'GET', JSON.stringify(urlMap)], + ['/users', '/users', 'GET', JSON.stringify(urlMap)], +] as const)('getApiEndpointInfo returns key itself when key does not exist: %s -> %s', async (key, expected, method, envValue) => { process.env.DEVUP_API_URL_MAP = envValue const { getApiEndpointInfo } = await import(`../url-map?t=${random}`) - expect(getApiEndpointInfo(key, 'foo').url).toBe(expected) + expect(getApiEndpointInfo(key, 'foo', method).url).toBe(expected) }) test.each([ - ['getUsers', { method: 'GET', url: '/users' }, JSON.stringify(urlMap)], - ['createUser', { method: 'POST', url: '/users' }, JSON.stringify(urlMap)], - ['updateUser', { method: 'PUT', url: '/users/{id}' }, JSON.stringify(urlMap)], + ['getUsers', { method: 'GET', url: '/users' }, 'GET', JSON.stringify(urlMap)], + [ + 'createUser', + { method: 'POST', url: '/users' }, + 'POST', + JSON.stringify(urlMap), + ], + [ + 'updateUser', + { method: 'PUT', url: '/users/{id}' }, + 'PUT', + JSON.stringify(urlMap), + ], [ 'deleteUser', { method: 'DELETE', url: '/users/{id}' }, + 'DELETE', JSON.stringify(urlMap), ], -] as const)('getApiEndpointInfo returns UrlMapValue for existing key: %s -> %s', async (key, expected, envValue) => { +] as const)('getApiEndpointInfo returns UrlMapValue for existing key: %s -> %s', async (key, expected, method, envValue) => { process.env.DEVUP_API_URL_MAP = envValue const { getApiEndpointInfo } = await import(`../url-map?t=${random}`) - expect(getApiEndpointInfo(key, 'foo')).toEqual(expected) + expect(getApiEndpointInfo(key, 'foo', method)).toEqual(expected) }) test.each([ diff --git a/packages/fetch/src/url-map.ts b/packages/fetch/src/url-map.ts index 4909495..ab9e6fc 100644 --- a/packages/fetch/src/url-map.ts +++ b/packages/fetch/src/url-map.ts @@ -1,19 +1,15 @@ -import type { UrlMapValue } from '@devup-api/core' +import type { HttpMethod, UrlMapEntry, UrlMapValue } from '@devup-api/core' export const DEVUP_API_URL_MAP: Record< string, - Record + Record > = JSON.parse(process.env.DEVUP_API_URL_MAP || '{}') export function getApiEndpointInfo( key: string, serverName: string, - method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', + method: HttpMethod, ): UrlMapValue { - const result = DEVUP_API_URL_MAP[serverName]?.[key] ?? { - method, - url: key, - } - result.url ||= key - return result + const stored = DEVUP_API_URL_MAP[serverName]?.[key]?.[method] + return { method, url: key, ...stored } } diff --git a/packages/generator/src/__tests__/create-url-map.test.ts b/packages/generator/src/__tests__/create-url-map.test.ts index e9a95d0..04f3130 100644 --- a/packages/generator/src/__tests__/create-url-map.test.ts +++ b/packages/generator/src/__tests__/create-url-map.test.ts @@ -10,8 +10,8 @@ test.each([ undefined, 'get_users', { - getUsers: { method: 'GET', url: '/users' }, - '/users': { method: 'GET', url: '/users' }, + getUsers: { GET: { url: '/users' } }, + '/users': { GET: { url: '/users' } }, }, ], [ @@ -19,8 +19,8 @@ test.each([ { convertCase: 'snake' as const }, 'getUsers', { - get_users: { method: 'GET', url: '/users' }, - '/users': { method: 'GET', url: '/users' }, + get_users: { GET: { url: '/users' } }, + '/users': { GET: { url: '/users' } }, }, ], [ @@ -28,8 +28,8 @@ test.each([ { convertCase: 'pascal' as const }, 'get_users', { - GetUsers: { method: 'GET', url: '/users' }, - '/users': { method: 'GET', url: '/users' }, + GetUsers: { GET: { url: '/users' } }, + '/users': { GET: { url: '/users' } }, }, ], [ @@ -37,8 +37,8 @@ test.each([ { convertCase: 'maintain' as const }, 'get_users', { - get_users: { method: 'GET', url: '/users' }, - '/users': { method: 'GET', url: '/users' }, + get_users: { GET: { url: '/users' } }, + '/users': { GET: { url: '/users' } }, }, ], ])('creates url map with %s case conversion', (_, options, operationId, expected) => { @@ -82,12 +82,14 @@ test('converts path parameters based on convertCase', () => { expect(result).toEqual({ '': { getUserPost: { - method: 'GET', - url: '/users/{userId}/posts/{postId}', + GET: { + url: '/users/{userId}/posts/{postId}', + }, }, '/users/{userId}/posts/{postId}': { - method: 'GET', - url: '/users/{userId}/posts/{postId}', + GET: { + url: '/users/{userId}/posts/{postId}', + }, }, }, }) @@ -116,13 +118,17 @@ test.each([ const result = createUrlMap({ '': schema }) expect(result['']).toHaveProperty(expectedKey) - expect(result['']![expectedKey]?.method).toBe( - expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', - ) + expect( + result['']![expectedKey]?.[ + expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + ], + ).toBeDefined() expect(result['']).toHaveProperty('/users') - expect(result['']!['/users']?.method).toBe( - expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', - ) + expect( + result['']!['/users']?.[ + expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + ], + ).toBeDefined() }) test('handles operation without operationId', () => { @@ -143,8 +149,9 @@ test('handles operation without operationId', () => { expect(result).toEqual({ '': { '/users': { - method: 'GET', - url: '/users', + GET: { + url: '/users', + }, }, }, }) @@ -238,12 +245,14 @@ test('skips operations that do not exist', () => { expect(result).toEqual({ '': { getUsers: { - method: 'GET', - url: '/users', + GET: { + url: '/users', + }, }, '/users': { - method: 'GET', - url: '/users', + GET: { + url: '/users', + }, }, }, }) @@ -268,12 +277,14 @@ test('handles complex path with multiple parameters', () => { expect(result).toEqual({ '': { get_user_post_comment: { - method: 'GET', - url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', + GET: { + url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', + }, }, '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': { - method: 'GET', - url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', + GET: { + url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', + }, }, }, }) @@ -304,7 +315,7 @@ test.each([ }, ) - expect(result[''][expectedPath]?.url).toBe(expectedUrl) + expect(result[''][expectedPath]?.GET?.url).toBe(expectedUrl) }) test.each([ @@ -361,13 +372,11 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.subscribe).toEqual({ - method: 'POST', + expect(result['']!.subscribe!.POST).toEqual({ url: '/form', bodyType: 'form', }) - expect(result['']!['/form']).toEqual({ - method: 'POST', + expect(result['']!['/form']!.POST).toEqual({ url: '/form', bodyType: 'form', }) @@ -398,13 +407,11 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.uploadFile).toEqual({ - method: 'POST', + expect(result['']!.uploadFile!.POST).toEqual({ url: '/upload', bodyType: 'multipart', }) - expect(result['']!['/upload']).toEqual({ - method: 'POST', + expect(result['']!['/upload']!.POST).toEqual({ url: '/upload', bodyType: 'multipart', }) @@ -435,12 +442,11 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.createUser).toEqual({ - method: 'POST', + expect(result['']!.createUser!.POST).toEqual({ url: '/users', }) - expect(result['']!.createUser).not.toHaveProperty('bodyType') - expect(result['']!['/users']).not.toHaveProperty('bodyType') + expect(result['']!.createUser!.POST).not.toHaveProperty('bodyType') + expect(result['']!['/users']!.POST).not.toHaveProperty('bodyType') }) test('does NOT emit bodyType for GET endpoint without requestBody', () => { @@ -459,11 +465,10 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.getUsers).toEqual({ - method: 'GET', + expect(result['']!.getUsers!.GET).toEqual({ url: '/users', }) - expect(result['']!.getUsers).not.toHaveProperty('bodyType') + expect(result['']!.getUsers!.GET).not.toHaveProperty('bodyType') }) test('resolves $ref in requestBody to detect content type', () => { @@ -501,8 +506,7 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.submitForm).toEqual({ - method: 'POST', + expect(result['']!.submitForm!.POST).toEqual({ url: '/form', bodyType: 'form', }) @@ -540,8 +544,7 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) - expect(result['']!.uploadFile).toEqual({ - method: 'POST', + expect(result['']!.uploadFile!.POST).toEqual({ url: '/upload', bodyType: 'multipart', }) @@ -597,11 +600,11 @@ describe('bodyType emission', () => { const result = createUrlMap({ '': schema }) // JSON endpoint: no bodyType - expect(result['']!.createUser).not.toHaveProperty('bodyType') + expect(result['']!.createUser!.POST).not.toHaveProperty('bodyType') // Form endpoint: bodyType = 'form' - expect(result['']!.subscribe!.bodyType).toBe('form') + expect(result['']!.subscribe!.POST!.bodyType).toBe('form') // Multipart endpoint: bodyType = 'multipart' - expect(result['']!.uploadFile!.bodyType).toBe('multipart') + expect(result['']!.uploadFile!.POST!.bodyType).toBe('multipart') }) }) diff --git a/packages/generator/src/create-url-map.ts b/packages/generator/src/create-url-map.ts index 14c9574..c23da20 100644 --- a/packages/generator/src/create-url-map.ts +++ b/packages/generator/src/create-url-map.ts @@ -1,4 +1,9 @@ -import type { DevupApiTypeGeneratorOptions, UrlMapValue } from '@devup-api/core' +import type { + DevupApiTypeGeneratorOptions, + HttpMethod, + UrlMapEntry, + UrlMapStoredValue, +} from '@devup-api/core' import type { OpenAPIV3_1 } from 'openapi-types' import { convertCase } from './convert-case' import { resolveRef } from './openapi-utils' @@ -30,12 +35,12 @@ function getBodyType( export function createUrlMap( schemas: Record, options?: DevupApiTypeGeneratorOptions, -): Record> { +): Record> { const convertCaseType = options?.convertCase ?? 'camel' - const urlMaps: Record> = {} + const urlMaps: Record> = {} for (const [serverName, schema] of Object.entries(schemas)) { - const urlMap: Record = {} + const urlMap: Record = {} for (const [path, pathItem] of Object.entries(schema.paths ?? {})) { if (!pathItem) continue for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) { @@ -46,20 +51,19 @@ export function createUrlMap( return `{${convertCase(param, convertCaseType)}}` }) const bodyType = getBodyType(operation, schema) - const value: UrlMapValue = { - method: method.toUpperCase() as - | 'GET' - | 'POST' - | 'PUT' - | 'DELETE' - | 'PATCH', + const methodKey = method.toUpperCase() as HttpMethod + const value: UrlMapStoredValue = { url: normalizedPath, ...(bodyType && { bodyType }), } if (operation.operationId) { - urlMap[convertCase(operation.operationId, convertCaseType)] = value + const opKey = convertCase(operation.operationId, convertCaseType) + urlMap[opKey] = { ...urlMap[opKey], [methodKey]: value } + } + urlMap[normalizedPath] = { + ...urlMap[normalizedPath], + [methodKey]: value, } - urlMap[normalizedPath] = value } } urlMaps[serverName] = urlMap diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index 2e32bbc..01ecab4 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -41,12 +41,10 @@ const mockSchema = { const mockUrlMap = { getUsers: { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, '/users': { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, } diff --git a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts index 9e7f78c..cca7f2e 100644 --- a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts +++ b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts @@ -38,12 +38,10 @@ const mockSchema = { const mockUrlMap = { getUsers: { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, '/users': { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, } diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts index 05c359f..97bbdab 100644 --- a/packages/vite-plugin/src/__tests__/plugin.test.ts +++ b/packages/vite-plugin/src/__tests__/plugin.test.ts @@ -40,12 +40,10 @@ const mockSchema = { const mockUrlMap = { getUsers: { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, '/users': { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, } diff --git a/packages/webpack-plugin/src/__tests__/plugin.test.ts b/packages/webpack-plugin/src/__tests__/plugin.test.ts index f342a05..c9e6fba 100644 --- a/packages/webpack-plugin/src/__tests__/plugin.test.ts +++ b/packages/webpack-plugin/src/__tests__/plugin.test.ts @@ -39,12 +39,10 @@ const mockSchema = { const mockUrlMap = { getUsers: { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, '/users': { - method: 'GET' as const, - url: '/users', + GET: { url: '/users' }, }, }