Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@devup-api/generator

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@devup-api/generator - npm Package Compare versions

Comparing version
0.1.0
to
0.1.1
+7
-3
package.json
{
"name": "@devup-api/generator",
"version": "0.1.0",
"version": "0.1.1",
"license": "Apache-2.0",
"type": "module",

@@ -12,2 +13,5 @@ "exports": {

},
"files": [
"dist"
],
"scripts": {

@@ -20,4 +24,4 @@ "build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"

"dependencies": {
"@devup-api/core": "0.1.0",
"@devup-api/utils": "0.1.0"
"@devup-api/core": "0.1.1",
"@devup-api/utils": "0.1.1"
},

@@ -24,0 +28,0 @@ "devDependencies": {

import { expect, test } from 'bun:test'
import { convertCase } from '../convert-case'
test.each([
['hello_world', 'snake', 'hello_world'],
['my_variable_name', 'snake', 'my_variable_name'],
['snake_case_string', 'snake', 'snake_case_string'],
])('converts to snake_case: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'snake')).toBe(expected)
})
test.each([
['HelloWorld', 'snake', 'hello_world'],
['MyVariableName', 'snake', 'my_variable_name'],
['PascalCaseString', 'snake', 'pascal_case_string'],
])('converts PascalCase to snake_case: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'snake')).toBe(expected)
})
test.each([
['helloWorld', 'snake', 'hello_world'],
['myVariableName', 'snake', 'my_variable_name'],
['camelCaseString', 'snake', 'camel_case_string'],
])('converts camelCase to snake_case: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'snake')).toBe(expected)
})
test.each([
['hello_world', 'camel', 'helloWorld'],
['my_variable_name', 'camel', 'myVariableName'],
['snake_case_string', 'camel', 'snakeCaseString'],
])('converts to camelCase: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'camel')).toBe(expected)
})
test.each([
['HelloWorld', 'camel', 'helloWorld'],
['MyVariableName', 'camel', 'myVariableName'],
['PascalCaseString', 'camel', 'pascalCaseString'],
])('converts PascalCase to camelCase: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'camel')).toBe(expected)
})
test.each([
['helloWorld', 'camel', 'helloWorld'],
['myVariableName', 'camel', 'myVariableName'],
['camelCaseString', 'camel', 'camelCaseString'],
])('returns camelCase strings unchanged: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'camel')).toBe(expected)
})
test.each([
['hello_world', 'pascal', 'HelloWorld'],
['my_variable_name', 'pascal', 'MyVariableName'],
['snake_case_string', 'pascal', 'SnakeCaseString'],
])('converts to PascalCase: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'pascal')).toBe(expected)
})
test.each([
['helloWorld', 'pascal', 'HelloWorld'],
['myVariableName', 'pascal', 'MyVariableName'],
['camelCaseString', 'pascal', 'CamelCaseString'],
])('converts camelCase to PascalCase: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'pascal')).toBe(expected)
})
test.each([
['HelloWorld', 'pascal', 'HelloWorld'],
['MyVariableName', 'pascal', 'MyVariableName'],
['PascalCaseString', 'pascal', 'PascalCaseString'],
])('returns PascalCase strings unchanged: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'pascal')).toBe(expected)
})
test.each([
['hello_world', 'maintain', 'hello_world'],
['myVariableName', 'maintain', 'myVariableName'],
['HelloWorld', 'maintain', 'HelloWorld'],
['any_string-here', 'maintain', 'any_string-here'],
])('maintains original case: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'maintain')).toBe(expected)
})
test.each([
['hello_world', undefined, 'helloWorld'],
['my_variable_name', undefined, 'myVariableName'],
['snake_case_string', undefined, 'snakeCaseString'],
])('defaults to camelCase when caseType is undefined: %s -> %s', (input, caseType, expected) => {
// biome-ignore lint/suspicious/noExplicitAny: Testing default behavior with undefined caseType
expect(convertCase(input, caseType as any)).toBe(expected)
})
test.each([
['hello_world', 'helloWorld'],
['my_variable_name', 'myVariableName'],
['snake_case_string', 'snakeCaseString'],
])('defaults to camelCase when caseType is not provided: %s -> %s', (input, expected) => {
expect(convertCase(input)).toBe(expected)
})
test.each([
['', 'camel', ''],
['a', 'camel', 'a'],
['A', 'camel', 'a'],
])('handles empty string and single characters: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'camel')).toBe(expected)
})
test.each([
['hello_world123', 'camel', 'helloWorld123'],
['my_variable2_name', 'camel', 'myVariable2Name'],
['test123_case', 'camel', 'test123Case'],
])('handles strings with numbers: %s -> %s', (input, caseType, expected) => {
expect(convertCase(input, caseType as 'camel')).toBe(expected)
})
test.each([
['hello_world', 'invalid', 'hello_world'],
['myVariableName', 'unknown', 'myVariableName'],
['HelloWorld', 'wrong', 'HelloWorld'],
])('default case returns original string for invalid caseType: %s -> %s', (input, caseType, expected) => {
// biome-ignore lint/suspicious/noExplicitAny: Testing default case with invalid caseType values
expect(convertCase(input, caseType as any)).toBe(expected)
})
import { expect, test } from 'bun:test'
import type { UrlMapValue } from '@devup-api/core'
import type { OpenAPIV3_1 } from 'openapi-types'
import { createUrlMap } from '../create-url-map'
test.each([
[
'camel',
undefined,
'get_users',
{
getUsers: { method: 'GET', url: '/users' },
'/users': { method: 'GET', url: '/users' },
},
],
[
'snake',
{ convertCase: 'snake' as const },
'getUsers',
{
get_users: { method: 'GET', url: '/users' },
'/users': { method: 'GET', url: '/users' },
},
],
[
'pascal',
{ convertCase: 'pascal' as const },
'get_users',
{
GetUsers: { method: 'GET', url: '/users' },
'/users': { method: 'GET', url: '/users' },
},
],
[
'maintain',
{ convertCase: 'maintain' as const },
'get_users',
{
get_users: { method: 'GET', url: '/users' },
'/users': { method: 'GET', url: '/users' },
},
],
])('creates url map with %s case conversion', (_, options, operationId, expected) => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
get: {
operationId,
responses: {},
},
},
},
}
const result = createUrlMap(schema, options)
expect(result).toEqual(expected as Record<string, UrlMapValue>)
})
test('converts path parameters based on convertCase', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users/{user_id}/posts/{post_id}': {
get: {
operationId: 'get_user_post',
responses: {},
},
},
},
}
const result = createUrlMap(schema, { convertCase: 'camel' })
expect(result).toEqual({
getUserPost: {
method: 'GET',
url: '/users/{userId}/posts/{postId}',
},
'/users/{userId}/posts/{postId}': {
method: 'GET',
url: '/users/{userId}/posts/{postId}',
},
})
})
test.each([
['get', 'get_users', 'getUsers', 'GET'],
['post', 'create_user', 'createUser', 'POST'],
['put', 'update_user', 'updateUser', 'PUT'],
['delete', 'delete_user', 'deleteUser', 'DELETE'],
['patch', 'patch_user', 'patchUser', 'PATCH'],
])('handles %s HTTP method', (method, operationId, expectedKey, expectedMethod) => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
[method]: {
operationId,
responses: {},
},
},
},
}
const result = createUrlMap(schema)
expect(result).toHaveProperty(expectedKey)
expect(result[expectedKey]?.method).toBe(
expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
)
expect(result).toHaveProperty('/users')
expect(result['/users']?.method).toBe(
expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
)
})
test('handles operation without operationId', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
get: {
responses: {},
},
},
},
}
const result = createUrlMap(schema)
expect(result).toEqual({
'/users': {
method: 'GET',
url: '/users',
},
})
expect(result).not.toHaveProperty('getUsers')
})
test('handles multiple paths', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
get: {
operationId: 'get_users',
responses: {},
},
},
'/posts': {
get: {
operationId: 'get_posts',
responses: {},
},
},
},
}
const result = createUrlMap(schema)
expect(result).toHaveProperty('getUsers')
expect(result).toHaveProperty('getPosts')
expect(result).toHaveProperty('/users')
expect(result).toHaveProperty('/posts')
})
test('handles empty paths', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {},
}
const result = createUrlMap(schema)
expect(result).toEqual({})
})
test('handles undefined paths', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
components: {},
paths: {},
}
const result = createUrlMap(schema)
expect(result).toEqual({})
})
test('handles undefined pathItem', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': undefined,
},
}
const result = createUrlMap(schema)
expect(result).toEqual({})
})
test('skips operations that do not exist', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
get: {
operationId: 'get_users',
responses: {},
},
// post, put, delete, patch are not defined
},
},
}
const result = createUrlMap(schema)
expect(result).toEqual({
getUsers: {
method: 'GET',
url: '/users',
},
'/users': {
method: 'GET',
url: '/users',
},
})
})
test('handles complex path with multiple parameters', () => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': {
get: {
operationId: 'get_user_post_comment',
responses: {},
},
},
},
}
const result = createUrlMap(schema, { convertCase: 'snake' })
expect(result).toEqual({
get_user_post_comment: {
method: '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}',
},
})
})
test.each([
['camel', '/users/{userId}', '/users/{userId}'],
['snake', '/users/{user_id}', '/users/{user_id}'],
['pascal', '/users/{UserId}', '/users/{UserId}'],
])('converts path parameters with %s case: %s', (caseType, expectedPath, expectedUrl) => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users/{user_id}': {
get: {
operationId: 'get_user',
responses: {},
},
},
},
}
const result = createUrlMap(schema, {
convertCase: caseType as 'camel' | 'snake' | 'pascal',
})
expect(result[expectedPath]?.url).toBe(expectedUrl)
})
test.each([
['camel', 'getUserList'],
['snake', 'get_user_list'],
['pascal', 'GetUserList'],
])('converts operationId with %s case: %s', (caseType, expectedKey) => {
const schema: OpenAPIV3_1.Document = {
openapi: '3.1.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users': {
get: {
operationId: 'get_user_list',
responses: {},
},
},
},
}
const result = createUrlMap(schema, {
convertCase: caseType as 'camel' | 'snake' | 'pascal',
})
expect(result).toHaveProperty(expectedKey)
})
import { expect, test } from 'bun:test'
import * as indexModule from '../index'
test('index.ts exports', () => {
expect({ ...indexModule }).toEqual({
createUrlMap: expect.any(Function),
generateInterface: expect.any(Function),
})
})
import { expect, test } from 'bun:test'
import { wrapInterfaceKeyGuard } from '../wrap-interface-key-guard'
test.each([
['getUsers', 'getUsers'],
['createUser', 'createUser'],
['updateUser', 'updateUser'],
['deleteUser', 'deleteUser'],
['testKey', 'testKey'],
['camelCase', 'camelCase'],
['snake_case', 'snake_case'],
['PascalCase', 'PascalCase'],
] as const)('wrapInterfaceKeyGuard returns key as-is when no slash: %s -> %s', (key, expected) => {
expect(wrapInterfaceKeyGuard(key)).toBe(expected)
})
test.each([
['/users', '[`/users`]'],
['/users/{id}', '[`/users/{id}`]'],
['/api/v1/users', '[`/api/v1/users`]'],
['/users/{userId}/posts/{postId}', '[`/users/{userId}/posts/{postId}`]'],
['/api/v1/users/{id}/profile', '[`/api/v1/users/{id}/profile`]'],
] as const)('wrapInterfaceKeyGuard wraps key with backticks when slash present: %s -> %s', (key, expected) => {
expect(wrapInterfaceKeyGuard(key)).toBe(expected)
})
test.each([
['', ''],
['/', '[`/`]'],
['//', '[`//`]'],
['///', '[`///`]'],
] as const)('wrapInterfaceKeyGuard handles edge cases: %s -> %s', (key, expected) => {
expect(wrapInterfaceKeyGuard(key)).toBe(expected)
})
test.each([
['users/123', '[`users/123`]'],
['test/path/here', '[`test/path/here`]'],
['a/b/c/d', '[`a/b/c/d`]'],
] as const)('wrapInterfaceKeyGuard wraps key with multiple slashes: %s -> %s', (key, expected) => {
expect(wrapInterfaceKeyGuard(key)).toBe(expected)
})
import { toCamel, toPascal, toSnake } from '@devup-api/utils'
/**
* Convert string based on convertCase option
*/
export function convertCase(
str: string,
caseType: 'snake' | 'camel' | 'pascal' | 'maintain' = 'camel',
): string {
switch (caseType) {
case 'snake':
return toSnake(str)
case 'camel':
return toCamel(str)
case 'pascal':
return toPascal(str)
case 'maintain':
return str
default:
return str
}
}
import type { DevupApiTypeGeneratorOptions, UrlMapValue } from '@devup-api/core'
import type { OpenAPIV3_1 } from 'openapi-types'
import { convertCase } from './convert-case'
export function createUrlMap(
schema: OpenAPIV3_1.Document,
options?: DevupApiTypeGeneratorOptions,
) {
const convertCaseType = options?.convertCase ?? 'camel'
const urlMap: Record<string, UrlMapValue> = {}
for (const [path, pathItem] of Object.entries(schema.paths ?? {})) {
if (!pathItem) continue
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
const operation = pathItem[method]
if (!operation) continue
const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
// Convert param name based on case type
return `{${convertCase(param, convertCaseType)}}`
})
if (operation.operationId) {
urlMap[convertCase(operation.operationId, convertCaseType)] = {
method: method.toUpperCase() as
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH',
url: normalizedPath,
}
}
urlMap[normalizedPath] = {
method: method.toUpperCase() as
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH',
url: normalizedPath,
}
}
}
return urlMap
}
import type { DevupApiTypeGeneratorOptions } from '@devup-api/core'
import { toPascal } from '@devup-api/utils'
import type { OpenAPIV3_1 } from 'openapi-types'
import { convertCase } from './convert-case'
import {
extractParameters,
extractRequestBody,
formatTypeValue,
getTypeFromSchema,
} from './generate-schema'
import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard'
export interface ParameterDefinition
extends Omit<OpenAPIV3_1.ParameterObject, 'schema'> {
type: unknown
default?: unknown
}
export interface EndpointDefinition {
params?: Record<string, ParameterDefinition>
body?: unknown
query?: Record<string, ParameterDefinition>
response?: unknown
error?: unknown
}
// Helper function to extract schema names from $ref
function extractSchemaNameFromRef(ref: string): string | null {
if (ref.startsWith('#/components/schemas/')) {
return ref.replace('#/components/schemas/', '')
}
return null
}
export function generateInterface(
schema: OpenAPIV3_1.Document,
options?: DevupApiTypeGeneratorOptions,
): string {
const endpoints: Record<
'get' | 'post' | 'put' | 'delete' | 'patch',
Record<string, EndpointDefinition>
> = {
get: {},
post: {},
put: {},
delete: {},
patch: {},
} as const
const convertCaseType = options?.convertCase ?? 'camel'
// Helper function to collect schema names from a schema object
const collectSchemaNames = (
schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
targetSet: Set<string>,
): void => {
if ('$ref' in schemaObj) {
const schemaName = extractSchemaNameFromRef(schemaObj.$ref)
if (schemaName) {
targetSet.add(schemaName)
}
return
}
const schema = schemaObj as OpenAPIV3_1.SchemaObject
// Check allOf, anyOf, oneOf
if (schema.allOf) {
schema.allOf.forEach((s) => {
collectSchemaNames(s, targetSet)
})
}
if (schema.anyOf) {
schema.anyOf.forEach((s) => {
collectSchemaNames(s, targetSet)
})
}
if (schema.oneOf) {
schema.oneOf.forEach((s) => {
collectSchemaNames(s, targetSet)
})
}
// Check properties
if (schema.properties) {
Object.values(schema.properties).forEach((prop) => {
collectSchemaNames(prop, targetSet)
})
}
// Check items (for arrays)
if (schema.type === 'array' && 'items' in schema && schema.items) {
collectSchemaNames(schema.items, targetSet)
}
}
// Track which schemas are used in request body and responses
const requestSchemaNames = new Set<string>()
const responseSchemaNames = new Set<string>()
const errorSchemaNames = new Set<string>()
// Helper function to check if a status code is an error response
const isErrorStatusCode = (statusCode: string): boolean => {
if (statusCode === 'default') return true
const code = parseInt(statusCode, 10)
return code >= 400 && code < 600
}
// First, collect schema names used in request body and responses
if (schema.paths) {
for (const pathItem of Object.values(schema.paths)) {
if (!pathItem) continue
const methods = ['get', 'post', 'put', 'delete', 'patch'] as const
for (const method of methods) {
const operation = pathItem[method]
if (!operation) continue
// Collect request body schemas
if (operation.requestBody) {
if ('$ref' in operation.requestBody) {
// Extract schema name from $ref if it's a schema reference
const schemaName = extractSchemaNameFromRef(
operation.requestBody.$ref,
)
if (schemaName) {
requestSchemaNames.add(schemaName)
}
} else {
const content = operation.requestBody.content
const jsonContent = content?.['application/json']
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
collectSchemaNames(jsonContent.schema, requestSchemaNames)
}
}
}
// Collect response and error schemas
if (operation.responses) {
for (const [statusCode, response] of Object.entries(
operation.responses,
)) {
const isError = isErrorStatusCode(statusCode)
if ('$ref' in response) {
// Extract schema name from $ref if it's a schema reference
const schemaName = extractSchemaNameFromRef(response.$ref)
if (schemaName) {
if (isError) {
errorSchemaNames.add(schemaName)
} else {
responseSchemaNames.add(schemaName)
}
}
} else if ('content' in response) {
const content = response.content
const jsonContent = content?.['application/json']
if (
jsonContent &&
'schema' in jsonContent &&
jsonContent.schema
) {
if (isError) {
collectSchemaNames(jsonContent.schema, errorSchemaNames)
} else {
collectSchemaNames(jsonContent.schema, responseSchemaNames)
}
}
}
}
}
}
}
}
// Iterate through OpenAPI paths and extract each endpoint
if (schema.paths) {
for (const [path, pathItem] of Object.entries(schema.paths)) {
if (!pathItem) continue
// Process each HTTP method
const methods = ['get', 'post', 'put', 'delete', 'patch'] as const
for (const method of methods) {
const operation = pathItem[method]
if (!operation) continue
const endpoint: EndpointDefinition = {}
// Extract parameters (path, query, header)
const { pathParams, queryParams } = extractParameters(
pathItem,
operation,
schema,
)
// Apply case conversion to parameter names
const convertedPathParams: Record<string, ParameterDefinition> = {}
for (const [key, value] of Object.entries(pathParams)) {
const convertedKey = convertCase(key, convertCaseType)
convertedPathParams[convertedKey] = value
}
const convertedQueryParams: Record<string, ParameterDefinition> = {}
for (const [key, value] of Object.entries(queryParams)) {
const convertedKey = convertCase(key, convertCaseType)
convertedQueryParams[convertedKey] = value
}
if (Object.keys(convertedPathParams).length > 0) {
endpoint.params = convertedPathParams
}
if (Object.keys(convertedQueryParams).length > 0) {
endpoint.query = convertedQueryParams
}
// Extract request body
// Check if request body uses a component schema
let requestBodyType: unknown
if (operation.requestBody) {
if ('$ref' in operation.requestBody) {
// RequestBodyObject reference - skip for now
const requestBody = extractRequestBody(
operation.requestBody,
schema,
)
if (requestBody !== undefined) {
requestBodyType = requestBody
}
} else {
const content = operation.requestBody.content
const jsonContent = content?.['application/json']
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
// Check if schema is a direct reference to components.schemas
if ('$ref' in jsonContent.schema) {
const schemaName = extractSchemaNameFromRef(
jsonContent.schema.$ref,
)
// Check if schema exists in components.schemas and is used in request body
if (
schemaName &&
schema.components?.schemas?.[schemaName] &&
requestSchemaNames.has(schemaName)
) {
// Use component reference
requestBodyType = `DevupRequestComponentStruct['${schemaName}']`
} else {
const requestBody = extractRequestBody(
operation.requestBody,
schema,
)
if (requestBody !== undefined) {
requestBodyType = requestBody
}
}
} else {
const requestBody = extractRequestBody(
operation.requestBody,
schema,
)
if (requestBody !== undefined) {
requestBodyType = requestBody
}
}
}
}
}
if (requestBodyType !== undefined) {
endpoint.body = requestBodyType
}
// Extract response
// Check if response uses a component schema
let responseType: unknown
if (operation.responses) {
// Prefer 200 response, fallback to first available response
const successResponse =
operation.responses['200'] ||
operation.responses['201'] ||
Object.values(operation.responses)[0]
if (successResponse) {
if ('$ref' in successResponse) {
// ResponseObject reference - skip for now
// Could resolve if needed
} else if ('content' in successResponse) {
const content = successResponse.content
const jsonContent = content?.['application/json']
if (
jsonContent &&
'schema' in jsonContent &&
jsonContent.schema
) {
// Check if schema is a direct reference to components.schemas
if ('$ref' in jsonContent.schema) {
const schemaName = extractSchemaNameFromRef(
jsonContent.schema.$ref,
)
// Check if schema exists in components.schemas and is used in response
if (
schemaName &&
schema.components?.schemas?.[schemaName] &&
responseSchemaNames.has(schemaName)
) {
// Use component reference
responseType = `DevupResponseComponentStruct['${schemaName}']`
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
responseType = schemaType
}
} else {
// Check if it's an array with items referencing a component schema
const schemaObj =
jsonContent.schema as OpenAPIV3_1.SchemaObject
if (
schemaObj.type === 'array' &&
schemaObj.items &&
'$ref' in schemaObj.items
) {
const schemaName = extractSchemaNameFromRef(
schemaObj.items.$ref,
)
// Check if schema exists in components.schemas and is used in response
if (
schemaName &&
schema.components?.schemas?.[schemaName] &&
responseSchemaNames.has(schemaName)
) {
// Use component reference for array items
responseType = `Array<DevupResponseComponentStruct['${schemaName}']>`
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
responseType = schemaType
}
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
responseType = schemaType
}
}
}
}
}
}
if (responseType !== undefined) {
endpoint.response = responseType
}
// Extract error
// Check if error uses a component schema
let errorType: unknown
if (operation.responses) {
// Find error responses (4xx, 5xx, or default)
const errorResponse =
operation.responses['400'] ||
operation.responses['401'] ||
operation.responses['403'] ||
operation.responses['404'] ||
operation.responses['422'] ||
operation.responses['500'] ||
operation.responses.default ||
Object.entries(operation.responses).find(([statusCode]) =>
isErrorStatusCode(statusCode),
)?.[1]
if (errorResponse) {
if ('$ref' in errorResponse) {
// ResponseObject reference - skip for now
// Could resolve if needed
} else if ('content' in errorResponse) {
const content = errorResponse.content
const jsonContent = content?.['application/json']
if (
jsonContent &&
'schema' in jsonContent &&
jsonContent.schema
) {
// Check if schema is a direct reference to components.schemas
if ('$ref' in jsonContent.schema) {
const schemaName = extractSchemaNameFromRef(
jsonContent.schema.$ref,
)
// Check if schema exists in components.schemas and is used in error
if (
schemaName &&
schema.components?.schemas?.[schemaName] &&
errorSchemaNames.has(schemaName)
) {
// Use component reference
errorType = `DevupErrorComponentStruct['${schemaName}']`
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
errorType = schemaType
}
} else {
// Check if it's an array with items referencing a component schema
const schemaObj =
jsonContent.schema as OpenAPIV3_1.SchemaObject
if (
schemaObj.type === 'array' &&
schemaObj.items &&
'$ref' in schemaObj.items
) {
const schemaName = extractSchemaNameFromRef(
schemaObj.items.$ref,
)
// Check if schema exists in components.schemas and is used in error
if (
schemaName &&
schema.components?.schemas?.[schemaName] &&
errorSchemaNames.has(schemaName)
) {
// Use component reference for array items
errorType = `Array<DevupErrorComponentStruct['${schemaName}']>`
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
errorType = schemaType
}
} else {
// Extract schema type with response options
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
const { type: schemaType } = getTypeFromSchema(
jsonContent.schema,
schema,
{ defaultNonNullable: responseDefaultNonNullable },
)
errorType = schemaType
}
}
}
}
}
}
if (errorType !== undefined) {
endpoint.error = errorType
}
// Generate path key (normalize path by replacing {param} with converted param and removing slashes)
const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => {
// Convert param name based on case type
return `{${convertCase(param, convertCaseType)}}`
})
endpoints[method][normalizedPath] = endpoint
if (operation.operationId) {
// If operationId exists, create both operationId and path keys
const operationIdKey = convertCase(
operation.operationId,
convertCaseType,
)
endpoints[method][operationIdKey] = endpoint
}
}
}
}
// Extract components schemas
const requestComponents: Record<string, unknown> = {}
const responseComponents: Record<string, unknown> = {}
const errorComponents: Record<string, unknown> = {}
if (schema.components?.schemas) {
for (const [schemaName, schemaObj] of Object.entries(
schema.components.schemas,
)) {
if (schemaObj) {
const requestDefaultNonNullable =
options?.requestDefaultNonNullable ?? false
const responseDefaultNonNullable =
options?.responseDefaultNonNullable ?? true
// Determine which defaultNonNullable to use based on where schema is used
let defaultNonNullable = responseDefaultNonNullable
if (requestSchemaNames.has(schemaName)) {
defaultNonNullable = requestDefaultNonNullable
}
const { type: schemaType } = getTypeFromSchema(
schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
schema,
{ defaultNonNullable },
)
// Keep original schema name as-is
if (requestSchemaNames.has(schemaName)) {
requestComponents[schemaName] = schemaType
}
if (responseSchemaNames.has(schemaName)) {
responseComponents[schemaName] = schemaType
}
if (errorSchemaNames.has(schemaName)) {
errorComponents[schemaName] = schemaType
}
}
}
}
// Generate TypeScript interface string
const interfaceContent = Object.entries(endpoints)
.flatMap(([method, value]) => {
const entries = Object.entries(value)
if (entries.length > 0) {
const interfaceEntries = entries
.map(([key, endpointValue]) => {
const formattedValue = formatTypeValue(endpointValue, 2)
// Top-level keys in ApiStruct should never be optional
// Only params, query, body etc. can be optional if all their properties are optional
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
})
.join(';\n')
return [
` interface Devup${toPascal(method)}ApiStruct {\n${interfaceEntries};\n }`,
]
}
return []
})
.join('\n')
// Generate RequestComponentStruct interface
const requestComponentEntries = Object.entries(requestComponents)
.map(([key, value]) => {
const formattedValue = formatTypeValue(value, 2)
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
})
.join(';\n')
const requestComponentInterface =
requestComponentEntries.length > 0
? ` interface DevupRequestComponentStruct {\n${requestComponentEntries};\n }`
: ' interface DevupRequestComponentStruct {}'
// Generate ResponseComponentStruct interface
const responseComponentEntries = Object.entries(responseComponents)
.map(([key, value]) => {
const formattedValue = formatTypeValue(value, 2)
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
})
.join(';\n')
const responseComponentInterface =
responseComponentEntries.length > 0
? ` interface DevupResponseComponentStruct {\n${responseComponentEntries};\n }`
: ' interface DevupResponseComponentStruct {}'
// Generate ErrorComponentStruct interface
const errorComponentEntries = Object.entries(errorComponents)
.map(([key, value]) => {
const formattedValue = formatTypeValue(value, 2)
return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}`
})
.join(';\n')
const errorComponentInterface =
errorComponentEntries.length > 0
? ` interface DevupErrorComponentStruct {\n${errorComponentEntries};\n }`
: ' interface DevupErrorComponentStruct {}'
const allInterfaces = interfaceContent
? `${interfaceContent}\n\n${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}`
: `${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}`
return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${allInterfaces}\n}`
}
import type { OpenAPIV3_1 } from 'openapi-types'
import type { ParameterDefinition } from './generate-interface'
/**
* Resolve $ref reference in OpenAPI parameter
*/
export function resolveParameterRef(
ref: string,
document: OpenAPIV3_1.Document,
): OpenAPIV3_1.ParameterObject | null {
if (!ref.startsWith('#/')) {
return null
}
const parts = ref.slice(2).split('/')
let current: unknown = document
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = (current as Record<string, unknown>)[part]
} else {
return null
}
}
if (current && typeof current === 'object' && !('$ref' in current)) {
return current as OpenAPIV3_1.ParameterObject
}
return null
}
/**
* Resolve $ref reference in OpenAPI schema
*/
export function resolveSchemaRef(
ref: string,
document: OpenAPIV3_1.Document,
): OpenAPIV3_1.SchemaObject | null {
if (!ref.startsWith('#/')) {
return null
}
const parts = ref.slice(2).split('/')
let current: unknown = document
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = (current as Record<string, unknown>)[part]
} else {
return null
}
}
if (current && typeof current === 'object' && !('$ref' in current)) {
return current as OpenAPIV3_1.SchemaObject
}
return null
}
/**
* Convert OpenAPI schema to TypeScript type representation
*/
export function getTypeFromSchema(
schema: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject,
document: OpenAPIV3_1.Document,
options?: {
defaultNonNullable?: boolean
},
): { type: unknown; default?: unknown } {
const defaultNonNullable = options?.defaultNonNullable ?? false
// Handle $ref
if ('$ref' in schema) {
const resolved = resolveSchemaRef(schema.$ref, document)
if (resolved) {
return getTypeFromSchema(resolved, document, options)
}
return { type: 'unknown', default: undefined }
}
const schemaObj = schema as OpenAPIV3_1.SchemaObject
// Handle allOf, anyOf, oneOf
if (schemaObj.allOf) {
const types = schemaObj.allOf.map((s) =>
getTypeFromSchema(s, document, options),
)
return {
type:
types.length > 0
? types.map((t) => formatTypeValue(t.type)).join(' & ')
: 'unknown',
default: schemaObj.default,
}
}
if (schemaObj.anyOf || schemaObj.oneOf) {
const types = (schemaObj.anyOf || schemaObj.oneOf || []).map((s) =>
getTypeFromSchema(s, document, options),
)
return {
type:
types.length > 0
? `(${types.map((t) => formatTypeValue(t.type)).join(' | ')})`
: 'unknown',
default: schemaObj.default,
}
}
// Handle enum
if (schemaObj.enum) {
return {
type: schemaObj.enum.map((v) => `"${String(v)}"`).join(' | '),
default: schemaObj.default,
}
}
// Handle primitive types
if (schemaObj.type === 'string') {
if (schemaObj.format === 'date' || schemaObj.format === 'date-time') {
return { type: 'string', default: schemaObj.default }
}
return { type: 'string', default: schemaObj.default }
}
if (schemaObj.type === 'number' || schemaObj.type === 'integer') {
return { type: 'number', default: schemaObj.default }
}
if (schemaObj.type === 'boolean') {
return { type: 'boolean', default: schemaObj.default }
}
// Handle array
if (schemaObj.type === 'array') {
const items = schemaObj.items
if (items) {
const itemType = getTypeFromSchema(items, document, options)
return {
type: `Array<${formatTypeValue(itemType.type)}>`,
default: schemaObj.default,
}
}
return { type: 'unknown[]', default: schemaObj.default }
}
// Handle object
if (schemaObj.type === 'object' || schemaObj.properties) {
const props: Record<string, { type: unknown; default?: unknown }> = {}
const required = schemaObj.required || []
if (schemaObj.properties) {
for (const [key, value] of Object.entries(schemaObj.properties)) {
const propType = getTypeFromSchema(value, document, options)
// Check if property has default value
// Need to resolve $ref if present to check for default
let hasDefault = false
if ('$ref' in value) {
const resolved = resolveSchemaRef(value.$ref, document)
if (resolved) {
hasDefault = resolved.default !== undefined
}
} else {
const propSchema = value as OpenAPIV3_1.SchemaObject
hasDefault = propSchema.default !== undefined
}
const isInRequired = required.includes(key)
// If defaultNonNullable is true and has default, treat as required
// Otherwise, mark as optional if not in required array
if (defaultNonNullable && hasDefault && !isInRequired) {
props[key] = propType
} else if (!isInRequired) {
props[`${key}?`] = propType
} else {
props[key] = propType
}
}
}
// Handle additionalProperties
if (schemaObj.additionalProperties) {
if (schemaObj.additionalProperties === true) {
props['[key: string]'] = { type: 'unknown', default: undefined }
} else if (typeof schemaObj.additionalProperties === 'object') {
const additionalType = getTypeFromSchema(
schemaObj.additionalProperties,
document,
options,
)
props['[key: string]'] = {
type: additionalType.type,
default: additionalType.default,
}
}
}
return { type: { ...props }, default: schemaObj.default }
}
// Handle oneOf/anyOf already handled above, but check again for safety
return { type: 'unknown', default: undefined }
}
/**
* Check if a value is a ParameterDefinition
*/
function isParameterDefinition(value: unknown): value is ParameterDefinition {
return (
typeof value === 'object' &&
value !== null &&
'type' in value &&
'in' in value &&
'name' in value
)
}
/**
* Check if all properties in an object are optional
*/
export function areAllPropertiesOptional(
obj: Record<string, unknown>,
): boolean {
const entries = Object.entries(obj)
if (entries.length === 0) {
return true
}
return entries.every(([key, value]) => {
// If key ends with '?', it's optional (from getTypeFromSchema)
if (key.endsWith('?')) {
return true
}
// If it's a ParameterDefinition, check required field
if (isParameterDefinition(value)) {
return value.required === false
}
// If it's a type object, check if the type itself is optional
if (isTypeObject(value)) {
// For type objects, check if the type is an object with all optional properties
if (
typeof value.type === 'object' &&
value.type !== null &&
!Array.isArray(value.type)
) {
return areAllPropertiesOptional(value.type as Record<string, unknown>)
}
return false
}
// For nested objects, recursively check
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return areAllPropertiesOptional(value as Record<string, unknown>)
}
return false
})
}
/**
* Format a type object to TypeScript interface/type string
*/
export function formatType(
obj: Record<string, unknown>,
indent: number = 0,
): string {
const indentStr = ' '.repeat(indent)
const nextIndent = indent + 1
const nextIndentStr = ' '.repeat(nextIndent)
const entries = Object.entries(obj)
.map(([key, value]) => {
// Handle string values (e.g., component references)
if (typeof value === 'string') {
return `${nextIndentStr}${key}: ${value}`
}
// Handle ParameterDefinition for params and query
if (isParameterDefinition(value)) {
const typeStr = formatTypeValue(value.type, nextIndent)
const isOptional = value.required === false
const keyWithOptional = isOptional ? `${key}?` : key
let description = ''
if (value.description) {
description += `${nextIndentStr}/**\n${nextIndentStr} * ${value.description}`
if (typeof value.default !== 'undefined') {
description += `\n${nextIndentStr} * @default {${value.default}}`
}
description = `${description}\n${nextIndentStr} */\n${nextIndentStr}`
} else if (typeof value.default !== 'undefined') {
description += `${nextIndentStr}/** @default {${value.default}} */\n${nextIndentStr}`
} else {
description = nextIndentStr
}
return `${description}${keyWithOptional}: ${typeStr}`
}
// Handle { type: unknown, default?: unknown } structure (from getTypeFromSchema)
if (isTypeObject(value)) {
const formattedValue = formatTypeValue(value.type, nextIndent)
// Key already has '?' if it's optional (from getTypeFromSchema), keep it as is
return `${nextIndentStr}${key}: ${formattedValue}`
}
// Check if value is an object (like params, query) with all optional properties
const valueAllOptional =
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
areAllPropertiesOptional(value as Record<string, unknown>)
const optionalMarker = valueAllOptional ? '?' : ''
const formattedValue = formatTypeValue(value, nextIndent)
return `${nextIndentStr}${key}${optionalMarker}: ${formattedValue}`
})
.join(';\n')
if (entries.length === 0) {
return '{}'
}
return `{\n${entries};\n${indentStr}}`
}
/**
* Check if a value is a type object with { type, default? } structure
*/
function isTypeObject(
value: unknown,
): value is { type: unknown; default?: unknown } {
return (
typeof value === 'object' &&
value !== null &&
'type' in value &&
Object.keys(value).length <= 2 &&
(!('default' in value) || Object.keys(value).length === 2)
)
}
/**
* Format a type value to TypeScript type string
*/
export function formatTypeValue(value: unknown, indent: number = 0): string {
if (typeof value === 'string') {
return value
}
// Handle { type: unknown, default?: unknown } structure
if (isTypeObject(value)) {
return formatTypeValue(value.type, indent)
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return formatType(value as Record<string, unknown>, indent)
}
return String(value)
}
/**
* Extract parameters from OpenAPI operation
*/
export function extractParameters(
pathItem: OpenAPIV3_1.PathItemObject | undefined,
operation: OpenAPIV3_1.OperationObject | undefined,
document: OpenAPIV3_1.Document,
): {
pathParams: Record<string, ParameterDefinition>
queryParams: Record<string, ParameterDefinition>
headerParams: Record<string, ParameterDefinition>
} {
const pathParams: Record<string, ParameterDefinition> = {}
const queryParams: Record<string, ParameterDefinition> = {}
const headerParams: Record<string, ParameterDefinition> = {}
const allParams = [
...(pathItem?.parameters || []),
...(operation?.parameters || []),
]
for (const param of allParams) {
if ('$ref' in param) {
// Resolve $ref parameter
const resolved = resolveParameterRef(param.$ref, document)
if (
resolved &&
'in' in resolved &&
'name' in resolved &&
typeof resolved.in === 'string' &&
typeof resolved.name === 'string'
) {
const paramSchema =
'schema' in resolved && resolved.schema ? resolved.schema : {}
const { type: paramType, default: paramDefault } = getTypeFromSchema(
paramSchema,
document,
{ defaultNonNullable: false },
)
const result = {
...resolved,
type: paramType,
default: paramDefault,
}
if (resolved.in === 'path') {
pathParams[resolved.name] = result
} else if (resolved.in === 'query') {
queryParams[resolved.name] = result
} else if (resolved.in === 'header') {
headerParams[resolved.name] = result
}
}
continue
}
const paramSchema = param.schema || {}
const { type: paramType, default: paramDefault } = getTypeFromSchema(
paramSchema,
document,
{ defaultNonNullable: false },
)
const result = {
...param,
type: paramType,
default: paramDefault,
}
if (param.in === 'path') {
pathParams[param.name] = result
} else if (param.in === 'query') {
queryParams[param.name] = result
} else if (param.in === 'header') {
headerParams[param.name] = result
}
}
return { pathParams, queryParams, headerParams }
}
/**
* Extract request body from OpenAPI operation
*/
export function extractRequestBody(
requestBody:
| OpenAPIV3_1.RequestBodyObject
| OpenAPIV3_1.ReferenceObject
| undefined,
document: OpenAPIV3_1.Document,
): unknown {
if (!requestBody) {
return undefined
}
if ('$ref' in requestBody) {
const resolved = resolveSchemaRef(requestBody.$ref, document)
if (resolved && 'content' in resolved && resolved.content) {
const content =
resolved.content as OpenAPIV3_1.RequestBodyObject['content']
const jsonContent = content['application/json']
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
return getTypeFromSchema(jsonContent.schema, document, {
defaultNonNullable: false,
}).type
}
}
return 'unknown'
}
const content = requestBody.content
if (content) {
const jsonContent = content['application/json']
if (jsonContent && 'schema' in jsonContent && jsonContent.schema) {
return getTypeFromSchema(jsonContent.schema, document, {
defaultNonNullable: false,
}).type
}
}
return undefined
}
export * from './create-url-map'
export * from './generate-interface'
export function wrapInterfaceKeyGuard(key: string): string {
if (key.includes('/')) {
return `[\`${key}\`]`
}
return key
}
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"emitDeclarationOnly": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}