Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion packages/openapi-msw/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// fallow-ignore-file code-duplication
import { mkdtemp, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { describe, it, expect, afterEach } from 'vitest'
import { loadConfig } from '../config.js'
import { loadConfig, loadConfigs } from '../config.js'

let tmpDir: string | undefined

Expand Down Expand Up @@ -105,3 +106,64 @@ describe('loadConfig: invalid field values', () => {
await expect(loadConfig(cwd, configPath)).rejects.toThrow('"depth_cap" must be an integer')
})
})

describe('loadConfigs: projects array support', () => {
it('returns a one-element array for a single-spec config', async () => {
const { cwd, configPath } = await writeConfig({
input_openapi: 'spec.json',
output: 'generated',
})
const configs = await loadConfigs(cwd, configPath)
expect(configs).toHaveLength(1)
expect(configs[0]!.input_openapi).toBe('spec.json')
expect(configs[0]!.output).toBe('generated')
})

it('returns N configs for a projects array with N entries', async () => {
const { cwd, configPath } = await writeConfig({
projects: [
{ input_openapi: 'services/users.json', output: 'mocks/users', seed: 1 },
{ input_openapi: 'services/orders.json', output: 'mocks/orders', seed: 2 },
],
})
const configs = await loadConfigs(cwd, configPath)
expect(configs).toHaveLength(2)
expect(configs[0]!.input_openapi).toBe('services/users.json')
expect(configs[0]!.seed).toBe(1)
expect(configs[1]!.input_openapi).toBe('services/orders.json')
expect(configs[1]!.seed).toBe(2)
})

it('throws when both top-level input_openapi and projects are present', async () => {
const { cwd, configPath } = await writeConfig({
input_openapi: 'spec.json',
output: 'generated',
projects: [{ input_openapi: 'services/users.json', output: 'mocks/users' }],
})
await expect(loadConfigs(cwd, configPath)).rejects.toThrow(
'Config cannot have both top-level "input_openapi"/"output" and a "projects" array'
)
})

it('throws when projects is not an array', async () => {
const { cwd, configPath } = await writeConfig({ projects: 'not-an-array' })
await expect(loadConfigs(cwd, configPath)).rejects.toThrow('"projects" must be an array')
})

it('throws when projects is empty', async () => {
const { cwd, configPath } = await writeConfig({ projects: [] })
await expect(loadConfigs(cwd, configPath)).rejects.toThrow(
'"projects" array must contain at least one config entry'
)
})

it('throws with project index when a project entry is invalid', async () => {
const { cwd, configPath } = await writeConfig({
projects: [
{ input_openapi: 'services/users.json', output: 'mocks/users' },
{ input_openapi: 'services/orders.json', output: 'mocks/orders', seed: 1.5 },
],
})
await expect(loadConfigs(cwd, configPath)).rejects.toThrow('projects[1]')
})
})
39 changes: 30 additions & 9 deletions packages/openapi-msw/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
loadConfigFile,
loadConfigsFile,
validateConfigPath,
validateInputPath,
validateOutputPath,
Expand Down Expand Up @@ -38,19 +39,39 @@ function validateMswFields(raw: Record<string, unknown>): void {
expectIntegerAtLeast(raw, 'depth_cap', 1)
}

function parseMswConfig(
raw: Record<string, unknown>,
base: import('openapi-zod-ts/config-core').BaseConfig
): MswConfig {
validateMswFields(raw)
return {
...base,
seed: raw['seed'] as number | undefined,
max_array_items: raw['max_array_items'] as number | undefined,
depth_cap: raw['depth_cap'] as number | undefined,
}
}

export async function loadConfig(cwd: string, configPath?: string): Promise<MswConfig> {
return loadConfigFile<MswConfig>({
cwd,
configPath,
defaultFileName: 'openapi-msw.config.json',
parse: (raw, base) => {
validateMswFields(raw)
return {
...base,
seed: raw['seed'] as number | undefined,
max_array_items: raw['max_array_items'] as number | undefined,
depth_cap: raw['depth_cap'] as number | undefined,
}
},
parse: parseMswConfig,
})
}

/**
* Load a config file and return all configs as a normalized array.
*
* Single-spec config: returns a one-element array.
* Multi-spec config with "projects" key: returns N-element array.
*/
export async function loadConfigs(cwd: string, configPath?: string): Promise<MswConfig[]> {
return loadConfigsFile<MswConfig>({
cwd,
configPath,
defaultFileName: 'openapi-msw.config.json',
parse: parseMswConfig,
})
}
17 changes: 12 additions & 5 deletions packages/openapi-msw/src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// fallow-ignore-file code-duplication
import { mkdir, writeFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { parseSpec } from 'openapi-zod-ts'
import { loadConfig } from './config.js'
import { runProjects } from 'openapi-zod-ts/config-core'
import { loadConfigs, type MswConfig } from './config.js'
import { generateHandlers } from './plugins/handlers.js'

async function formatTs(content: string, filePath: string): Promise<string> {
Expand All @@ -11,10 +11,12 @@ async function formatTs(content: string, filePath: string): Promise<string> {
return format(content, { ...config, parser: 'typescript' })
}

export async function generate(cwd: string, configPath?: string): Promise<void> {
const config = await loadConfig(cwd, configPath)
async function generateOne(cwd: string, config: MswConfig, label?: string): Promise<void> {
Comment thread
benjamineckstein marked this conversation as resolved.
const inputPath = resolve(cwd, config.input_openapi)
const outputDir = resolve(cwd, config.output)
const prefix = label !== undefined ? `[${label}] ` : ''

console.log(`${prefix}Parsing spec: ${inputPath}`)
const spec = await parseSpec(inputPath)

const file = generateHandlers(spec, {
Expand All @@ -26,5 +28,10 @@ export async function generate(cwd: string, configPath?: string): Promise<void>
await mkdir(outputDir, { recursive: true })
const filePath = join(outputDir, file.filename)
await writeFile(filePath, await formatTs(file.content, filePath), 'utf-8')
console.log(`✓ ${file.filename}`)
console.log(`${prefix}✓ ${file.filename}`)
}

export async function generate(cwd: string, configPath?: string): Promise<void> {
const configs = await loadConfigs(cwd, configPath)
await runProjects(configs, (config, label) => generateOne(cwd, config, label))
}
2 changes: 1 addition & 1 deletion packages/openapi-msw/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { generate } from './generator.js'
export { loadConfig } from './config.js'
export { loadConfig, loadConfigs } from './config.js'
export type { MswConfig } from './config.js'
export { generateHandlers } from './plugins/handlers.js'
export type { HandlerGenOptions, GeneratedFile } from './plugins/handlers.js'
75 changes: 74 additions & 1 deletion packages/openapi-react-query/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// fallow-ignore-file code-duplication
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { loadConfig, validateConfigPath, validateOutputPath, validateInputPath } from '../config.js'
import { loadConfig, loadConfigs, validateConfigPath, validateOutputPath, validateInputPath } from '../config.js'

describe('loadConfig', () => {
let tmpDir: string
Expand Down Expand Up @@ -287,3 +288,75 @@ describe('config security validation', () => {
})
})
})

describe('loadConfigs: projects array support', () => {
let tmpDir: string

beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'openapi-rq-multi-config-'))
})

afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true })
})

function writeConfig(content: unknown) {
writeFileSync(join(tmpDir, 'openapi-react-query.config.json'), JSON.stringify(content))
}

it('returns a one-element array for a single-spec config', async () => {
writeConfig({ input_openapi: 'openapi.json', output: 'src/hooks' })
const configs = await loadConfigs(tmpDir)
expect(configs).toHaveLength(1)
expect(configs[0]!.input_openapi).toBe('openapi.json')
expect(configs[0]!.output).toBe('src/hooks')
})

it('returns N configs for a projects array with N entries', async () => {
writeConfig({
projects: [
{ input_openapi: 'services/users.json', output: 'src/users-hooks' },
{ input_openapi: 'services/orders.json', output: 'src/orders-hooks', stale_time: 5000 },
],
})
const configs = await loadConfigs(tmpDir)
expect(configs).toHaveLength(2)
expect(configs[0]!.input_openapi).toBe('services/users.json')
expect(configs[0]!.stale_time).toBeUndefined()
expect(configs[1]!.input_openapi).toBe('services/orders.json')
expect(configs[1]!.stale_time).toBe(5000)
})

it('throws when both top-level input_openapi and projects are present', async () => {
writeConfig({
input_openapi: 'openapi.json',
output: 'src/hooks',
projects: [{ input_openapi: 'services/users.json', output: 'src/users-hooks' }],
})
await expect(loadConfigs(tmpDir)).rejects.toThrow(
'Config cannot have both top-level "input_openapi"/"output" and a "projects" array'
)
})

it('throws when projects is not an array', async () => {
writeConfig({ projects: 'not-an-array' })
await expect(loadConfigs(tmpDir)).rejects.toThrow('"projects" must be an array')
})

it('throws when projects is empty', async () => {
writeConfig({ projects: [] })
await expect(loadConfigs(tmpDir)).rejects.toThrow(
'"projects" array must contain at least one config entry'
)
})

it('throws with project index when a project entry is invalid', async () => {
writeConfig({
projects: [
{ input_openapi: 'services/users.json', output: 'src/users-hooks' },
{ input_openapi: 'services/orders.json', output: 'src/orders-hooks', stale_time: 'fast' },
],
})
await expect(loadConfigs(tmpDir)).rejects.toThrow('projects[1]')
})
})
52 changes: 38 additions & 14 deletions packages/openapi-react-query/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
loadConfigFile,
loadConfigsFile,
validateConfigPath,
validateInputPath,
validateOutputPath,
Expand Down Expand Up @@ -76,24 +77,47 @@ function validateReactQueryFields(raw: Record<string, unknown>): void {
if (raw['overrides'] !== undefined) validateOverrides(raw['overrides'])
}

function parseReactQueryConfig(
raw: Record<string, unknown>,
base: import('openapi-zod-ts/config-core').BaseConfig
): ReactQueryConfig {
validateReactQueryFields(raw)
return {
...base,
stale_time: raw['stale_time'] as number | undefined,
gc_time: raw['gc_time'] as number | undefined,
suspense: raw['suspense'] as boolean | undefined,
overrides: raw['overrides'] as
| Record<string, { stale_time?: number; gc_time?: number }>
| undefined,
auto_invalidate: raw['auto_invalidate'] as boolean | undefined,
infinite_query: raw['infinite_query'] as boolean | 'auto' | undefined,
}
}

export async function loadConfig(cwd: string, configPath?: string): Promise<ReactQueryConfig> {
return loadConfigFile<ReactQueryConfig>({
cwd,
configPath,
defaultFileName: 'openapi-react-query.config.json',
parse: (raw, base) => {
validateReactQueryFields(raw)
return {
...base,
stale_time: raw['stale_time'] as number | undefined,
gc_time: raw['gc_time'] as number | undefined,
suspense: raw['suspense'] as boolean | undefined,
overrides: raw['overrides'] as
| Record<string, { stale_time?: number; gc_time?: number }>
| undefined,
auto_invalidate: raw['auto_invalidate'] as boolean | undefined,
infinite_query: raw['infinite_query'] as boolean | 'auto' | undefined,
}
},
parse: parseReactQueryConfig,
})
}

/**
* Load a config file and return all configs as a normalized array.
*
* Single-spec config: returns a one-element array.
* Multi-spec config with "projects" key: returns N-element array.
*/
export async function loadConfigs(
cwd: string,
configPath?: string
): Promise<ReactQueryConfig[]> {
return loadConfigsFile<ReactQueryConfig>({
cwd,
configPath,
defaultFileName: 'openapi-react-query.config.json',
parse: parseReactQueryConfig,
})
}
Loading
Loading