diff --git a/.changeset/early-knives-kneel.md b/.changeset/early-knives-kneel.md new file mode 100644 index 0000000000..2d1f4a57ea --- /dev/null +++ b/.changeset/early-knives-kneel.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**client**: use `getBaseUrl()` function to determine default value diff --git a/.changeset/five-pianos-jump.md b/.changeset/five-pianos-jump.md new file mode 100644 index 0000000000..a9d4c5eb2d --- /dev/null +++ b/.changeset/five-pianos-jump.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**plugin(@hey-api/examples)**: initial release diff --git a/.changeset/nasty-planes-cry.md b/.changeset/nasty-planes-cry.md new file mode 100644 index 0000000000..f31edf98c7 --- /dev/null +++ b/.changeset/nasty-planes-cry.md @@ -0,0 +1,5 @@ +--- +"@hey-api/shared": patch +--- + +**utils**: export `getBaseUrl()` function diff --git a/dev/inputs.ts b/dev/inputs.ts index 3c86b4fe04..5e3f5fc19e 100644 --- a/dev/inputs.ts +++ b/dev/inputs.ts @@ -5,12 +5,15 @@ const specsPath = path.join(__dirname, '..', 'specs'); export const inputs = { circular: path.resolve(specsPath, '3.0.x', 'circular.yaml'), full: path.resolve(specsPath, '3.1.x', 'full.yaml'), + heyapi: 'hey-api/backend', local: 'http://localhost:8000/openapi.json', + mockers: path.resolve(specsPath, '3.1.x', 'mockers.yaml'), opencode: path.resolve(specsPath, '3.1.x', 'opencode.yaml'), petstore: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', redfish: 'https://raw.githubusercontent.com/DMTF/Redfish-Publications/refs/heads/main/openapi/openapi.yaml', + rpc: path.resolve(specsPath, '3.1.x', 'rpc.yaml'), scalar: 'scalar:@scalar/access-service', transformers: path.resolve(specsPath, '3.1.x', 'transformers.json'), validators: path.resolve(specsPath, '3.1.x', 'validators.yaml'), diff --git a/packages/openapi-python/src/plugins/types.ts b/packages/openapi-python/src/plugins/types.ts index 1e18293c7a..b3a75ac0e1 100644 --- a/packages/openapi-python/src/plugins/types.ts +++ b/packages/openapi-python/src/plugins/types.ts @@ -4,7 +4,7 @@ export type PluginClientNames = | '@hey-api/client-requests' | '@hey-api/client-urllib3'; -export type PluginMockNames = 'factory_boy' | 'faker' | 'mimesis'; +export type PluginSourceNames = 'factory_boy' | 'faker' | 'mimesis'; export type PluginTransformerNames = never; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/schema-unknown/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/schema-unknown/client.gen.ts index cab3c70195..52dac25543 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/schema-unknown/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/schema-unknown/client.gen.ts @@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig()); +export const client = createClient(createConfig({ baseUrl: 'api.postmarkapp.com/' })); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/servers-host/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/servers-host/client.gen.ts index cab3c70195..684843b24b 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/servers-host/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/servers-host/client.gen.ts @@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig()); +export const client = createClient(createConfig({ baseUrl: 'foo.com' })); diff --git a/packages/openapi-ts-tests/orpc/v1/test/3.0.x.test.ts b/packages/openapi-ts-tests/orpc/v1/test/3.0.x.test.ts index 5e2314b34a..ac5a0fda5a 100644 --- a/packages/openapi-ts-tests/orpc/v1/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/orpc/v1/test/3.0.x.test.ts @@ -17,7 +17,7 @@ describe(`OpenAPI ${version}`, () => { const scenarios = [ { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'default', plugins: ['orpc', 'zod'], }), @@ -25,7 +25,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'custom-names', plugins: [ 'valibot', diff --git a/packages/openapi-ts-tests/orpc/v1/test/3.1.x.test.ts b/packages/openapi-ts-tests/orpc/v1/test/3.1.x.test.ts index 15c14eb532..19773e6d30 100644 --- a/packages/openapi-ts-tests/orpc/v1/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/orpc/v1/test/3.1.x.test.ts @@ -17,7 +17,7 @@ describe(`OpenAPI ${version}`, () => { const scenarios = [ { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'default', plugins: ['orpc', 'zod'], }), @@ -25,7 +25,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'custom-names', plugins: [ 'valibot', @@ -42,7 +42,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'contracts-strategy-by-tags', plugins: [ 'zod', @@ -58,7 +58,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'contracts-strategy-single', plugins: [ 'zod', @@ -75,7 +75,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'contracts-nesting-id', plugins: [ 'zod', @@ -92,7 +92,7 @@ describe(`OpenAPI ${version}`, () => { }, { config: createConfig({ - input: 'orpc.yaml', + input: 'rpc.yaml', output: 'contracts-custom-naming', plugins: [ 'zod', diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index d473eaa6d0..84a25cfc37 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -47,6 +47,7 @@ declare module '@hey-api/codegen-core' { | 'arktype' | 'fastify' | 'json-schema' + | 'msw' | 'sdk' | 'typescript' | 'valibot' @@ -67,6 +68,7 @@ declare module '@hey-api/shared' { '@hey-api/client-next': Plugins.HeyApiClientNext.Types['Types']; '@hey-api/client-nuxt': Plugins.HeyApiClientNuxt.Types['Types']; '@hey-api/client-ofetch': Plugins.HeyApiClientOfetch.Types['Types']; + '@hey-api/examples': Plugins.HeyApiExamples.Types['Types']; '@hey-api/schemas': Plugins.HeyApiSchemas.Types['Types']; '@hey-api/sdk': Plugins.HeyApiSdk.Types['Types']; '@hey-api/transformers': Plugins.HeyApiTransformers.Types['Types']; @@ -126,6 +128,7 @@ import type { HeyApiClientOfetchPlugin, OfetchClient as OfetchClientImp, } from './plugins/@hey-api/client-ofetch'; +import type { HeyApiExamplesPlugin } from './plugins/@hey-api/examples'; import type { HeyApiSchemasPlugin } from './plugins/@hey-api/schemas'; import type { HeyApiSdkPlugin } from './plugins/@hey-api/sdk'; import type { HeyApiTransformersPlugin } from './plugins/@hey-api/transformers'; @@ -247,6 +250,10 @@ export namespace Plugins { export type Types = HeyApiClientOfetchPlugin; } + export namespace HeyApiExamples { + export type Types = HeyApiExamplesPlugin; + } + export namespace HeyApiSchemas { export type Types = HeyApiSchemasPlugin; } diff --git a/packages/openapi-ts/src/plugins/@faker-js/faker/config.ts b/packages/openapi-ts/src/plugins/@faker-js/faker/config.ts index 50ba6576a8..339b0a2dec 100644 --- a/packages/openapi-ts/src/plugins/@faker-js/faker/config.ts +++ b/packages/openapi-ts/src/plugins/@faker-js/faker/config.ts @@ -24,7 +24,7 @@ export const defaultConfig: FakerJsFakerPlugin['Config'] = { value: plugin.config.definitions, }); }, - tags: ['mocker'], + tags: ['source'], }; /** diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts index c2578ab3a3..725f57c10f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts @@ -1,4 +1,4 @@ -import { parseUrl } from '@hey-api/shared'; +import { getBaseUrl } from '@hey-api/shared'; import { getTypedConfig } from '../../../config/utils'; import { clientFolderAbsolutePath } from '../../../generate/client'; @@ -6,26 +6,6 @@ import { $ } from '../../../ts-dsl'; import type { PluginHandler } from './types'; import { getClientBaseUrlKey } from './utils'; -const resolveBaseUrlString = ({ plugin }: Parameters[0]): string | undefined => { - const { baseUrl } = plugin.config; - - if (baseUrl === false) { - return; - } - - if (typeof baseUrl === 'string') { - return baseUrl; - } - - const { servers } = plugin.context.ir; - - if (!servers) { - return; - } - - return servers[typeof baseUrl === 'number' ? baseUrl : 0]?.url; -}; - export const createClient: PluginHandler = ({ plugin }) => { const clientModule = clientFolderAbsolutePath(getTypedConfig(plugin)); const symbolCreateClient = plugin.symbol('createClient', { @@ -47,26 +27,13 @@ export const createClient: PluginHandler = ({ plugin }) => { }) : undefined; - const defaultVals = $.object(); + const baseUrl = getBaseUrl(plugin.config.baseUrl ?? true, plugin.context.ir); - const resolvedBaseUrl = resolveBaseUrlString({ - plugin: plugin as any, - }); - if (resolvedBaseUrl) { - const url = parseUrl(resolvedBaseUrl); - if (url.protocol && url.host && !resolvedBaseUrl.includes('{')) { - defaultVals.prop(getClientBaseUrlKey(getTypedConfig(plugin)), $.literal(resolvedBaseUrl)); - } else if (resolvedBaseUrl !== '/' && resolvedBaseUrl.startsWith('/')) { - const baseUrl = resolvedBaseUrl.endsWith('/') - ? resolvedBaseUrl.slice(0, -1) - : resolvedBaseUrl; - defaultVals.prop(getClientBaseUrlKey(getTypedConfig(plugin)), $.literal(baseUrl)); - } - } - - if ('throwOnError' in plugin.config && plugin.config.throwOnError) { - defaultVals.prop('throwOnError', $.literal(true)); - } + const defaultVals = $.object() + .$if(baseUrl, (o, v) => o.prop(getClientBaseUrlKey(getTypedConfig(plugin)), $.literal(v))) + .$if('throwOnError' in plugin.config && plugin.config.throwOnError, (o) => + o.prop('throwOnError', $.literal(true)), + ); const createConfigParameters = [ $(symbolCreateConfig) diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts index 64bf8de124..e3c748f18c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts @@ -3,7 +3,7 @@ import { clientFolderAbsolutePath } from '../../../generate/client'; import { $ } from '../../../ts-dsl'; import type { PluginHandler } from './types'; -export const createClientConfigType = ({ plugin }: Parameters[0]) => { +export function createClientConfigType({ plugin }: Parameters[0]) { const clientModule = clientFolderAbsolutePath(getTypedConfig(plugin)); const symbolClientOptions = plugin.referenceSymbol({ category: 'type', @@ -47,4 +47,4 @@ export const createClientConfigType = ({ plugin }: Parameters[0]) ), ); plugin.node(typeCreateClientConfig); -}; +} diff --git a/packages/openapi-ts/src/plugins/@hey-api/examples/config.ts b/packages/openapi-ts/src/plugins/@hey-api/examples/config.ts new file mode 100644 index 0000000000..e38563227b --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/examples/config.ts @@ -0,0 +1,25 @@ +import { definePluginConfig } from '@hey-api/shared'; + +import { handler } from './plugin'; +import type { HeyApiExamplesPlugin } from './types'; + +export const defaultConfig: HeyApiExamplesPlugin['Config'] = { + config: { + case: 'camelCase', + includeInEntry: false, + }, + handler, + name: '@hey-api/examples', + resolveConfig: (plugin, context) => { + plugin.config.case = context.valueToObject({ + defaultValue: 'camelCase' as const, + value: plugin.config.case, + }); + }, + tags: ['source'], +}; + +/** + * Type helper for `@hey-api/examples` plugin, returns {@link Plugin.Config} object + */ +export const defineConfig = definePluginConfig(defaultConfig); diff --git a/packages/openapi-ts/src/plugins/@hey-api/examples/index.ts b/packages/openapi-ts/src/plugins/@hey-api/examples/index.ts new file mode 100644 index 0000000000..cabd9fc641 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/examples/index.ts @@ -0,0 +1,2 @@ +export { defaultConfig, defineConfig } from './config'; +export type { HeyApiExamplesPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/examples/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/examples/plugin.ts new file mode 100644 index 0000000000..0eaeb05248 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/examples/plugin.ts @@ -0,0 +1,361 @@ +import { toCase } from '@hey-api/shared'; + +import { $ } from '../../../ts-dsl'; +import type { HeyApiExamplesPlugin } from './types'; + +export const handler: HeyApiExamplesPlugin['Handler'] = ({ plugin }) => { + const spec = plugin.context.spec as { + components?: { + examples?: Record; + schemas?: Record }>; + }; + paths?: Record< + string, + Record< + string, + { + operationId?: string; + responses?: Record< + string, + { + content?: Record }>; + } + >; + } + > + >; + }; + + if (!spec.components?.schemas) { + return; + } + + const schemas = spec.components.schemas; + const examplesComponent = spec.components.examples; + + for (const [name, schema] of Object.entries(schemas)) { + const schemaExample = resolveSchemaExample(schema, examplesComponent); + if (!schemaExample) { + continue; + } + + const hasPluralExamples = schema.examples && Object.keys(schema.examples).length > 0; + const functionName = `${toCase(name, 'camelCase')}Example`; + + if (hasPluralExamples) { + generatePluralSchemaFactory({ + functionName, + name, + plugin, + schemaExample: schemaExample as Record, + }); + } else { + generateSingularSchemaFactory({ + functionName, + name, + plugin, + schemaExample, + }); + } + } + + if (!spec.paths) { + return; + } + + for (const [, pathItem] of Object.entries(spec.paths)) { + for (const [, operation] of Object.entries(pathItem)) { + if ('parameters' in operation || 'summary' in operation || 'description' in operation) { + continue; + } + + if (!('responses' in operation) || !operation.responses) { + continue; + } + + const operationExamples = collectOperationExamples( + operation.responses as Record< + string, + { + content?: Record }>; + } + >, + examplesComponent, + ); + if (operationExamples.statusCodes.size === 0) { + continue; + } + + generateOperationFactory({ + examples: operationExamples, + functionName: `${toCase(operation.operationId!, 'camelCase')}Example`, + plugin, + }); + } + } +}; + +interface ResolvedExample { + example?: unknown; + examples?: Record; +} + +function resolveSchemaExample( + schema: ResolvedExample, + examplesComponent?: Record, +): unknown | Record | null { + if (schema.example !== undefined) { + if (typeof schema.example === 'object' && schema.example !== null && '$ref' in schema.example) { + return resolveRefExample(schema.example.$ref as string, examplesComponent); + } + return schema.example; + } + + if (schema.examples && Object.keys(schema.examples).length > 0) { + const resolved: Record = {}; + for (const [key, exampleRef] of Object.entries(schema.examples)) { + if (typeof exampleRef === 'object' && exampleRef !== null && '$ref' in exampleRef) { + const resolvedValue = resolveRefExample(exampleRef.$ref as string, examplesComponent); + if (resolvedValue !== undefined) { + resolved[key] = resolvedValue; + } + } else if (typeof exampleRef === 'object' && exampleRef !== null && 'value' in exampleRef) { + resolved[key] = exampleRef.value; + } else { + resolved[key] = exampleRef; + } + } + return resolved; + } + + return null; +} + +function resolveRefExample($ref: string, examples?: Record): unknown { + if (!examples) { + return undefined; + } + + const refPath = $ref.split('/'); + const exampleName = refPath[refPath.length - 1]!; + const example = examples[exampleName]; + + if (!example) { + return undefined; + } + + return example.value; +} + +function getJsonExample( + content?: Record }>, +): unknown | Record | null { + if (!content) { + return null; + } + + const jsonContent = content['application/json']; + if (!jsonContent) { + return null; + } + + if (jsonContent.example !== undefined) { + return jsonContent.example; + } + + if (jsonContent.examples && Object.keys(jsonContent.examples).length > 0) { + const resolved: Record = {}; + for (const [key, exampleRef] of Object.entries(jsonContent.examples)) { + if (typeof exampleRef === 'object' && exampleRef !== null && 'value' in exampleRef) { + resolved[key] = exampleRef.value; + } else { + resolved[key] = exampleRef; + } + } + return resolved; + } + + return null; +} + +interface OperationExamples { + defaultStatusCode?: string; + statusCodes: Map< + string, + { + examples: Record; + isPlural: boolean; + } + >; +} + +function collectOperationExamples( + responses: Record< + string, + { + content?: Record }>; + } + >, + examplesComponent?: Record, +): OperationExamples { + const result: OperationExamples = { + statusCodes: new Map(), + }; + + for (const [statusCode, response] of Object.entries(responses)) { + const jsonExample = getJsonExample(response.content); + if (!jsonExample) { + continue; + } + + const isPlural = + typeof jsonExample === 'object' && jsonExample !== null && !Array.isArray(jsonExample); + + let resolvedExamples: Record; + + if (isPlural && typeof jsonExample === 'object') { + const resolved: Record = {}; + for (const [key, value] of Object.entries(jsonExample)) { + if (typeof value === 'object' && value !== null && '$ref' in value) { + const refResolved = resolveRefExample(value.$ref as string, examplesComponent); + if (refResolved !== undefined) { + resolved[key] = refResolved; + } + } else { + resolved[key] = value; + } + } + resolvedExamples = resolved; + } else { + resolvedExamples = { basic: jsonExample as unknown }; + } + + if (Object.keys(resolvedExamples).length === 0) { + continue; + } + + result.statusCodes.set(statusCode, { + examples: resolvedExamples, + isPlural, + }); + + if (!result.defaultStatusCode) { + const codeNum = parseInt(statusCode.replace('X', '0').replace('default', '999')); + const defaultNum = result.defaultStatusCode + ? parseInt(result.defaultStatusCode.replace('X', '0').replace('default', '999')) + : 999; + + if (statusCode.startsWith('2') && !result.defaultStatusCode) { + result.defaultStatusCode = statusCode; + } else if (codeNum < defaultNum && statusCode !== 'default') { + result.defaultStatusCode = statusCode; + } + } + } + + if (!result.defaultStatusCode && result.statusCodes.size > 0) { + result.defaultStatusCode = Array.from(result.statusCodes.keys())[0]!; + } + + return result; +} + +function generateSingularSchemaFactory({ + functionName, + name, + plugin, + schemaExample, +}: { + functionName: string; + name: string; + plugin: HeyApiExamplesPlugin['Instance']; + schemaExample: unknown; +}): void { + const symbol = plugin.symbol(functionName, { + meta: { + category: 'example', + resource: 'schema', + resourceId: name, + }, + }); + + const func = $.func(symbol).decl(); + // @ts-expect-error TODO + func.$do($.return($.fromValue(schemaExample, { layout: 'pretty' }))); + + plugin.node(func); +} + +function generatePluralSchemaFactory({ + functionName, + name, + plugin, + schemaExample, +}: { + functionName: string; + name: string; + plugin: HeyApiExamplesPlugin['Instance']; + schemaExample: Record; +}): void { + const exampleKeys = Object.keys(schemaExample); + const firstKey = exampleKeys[0]!; + + const symbol = plugin.symbol(functionName, { + meta: { + category: 'example', + resource: 'schema', + resourceId: name, + }, + }); + + const func = $.func(symbol).decl(); + // @ts-expect-error TODO + func.param($.param('options')); + + // @ts-expect-error TODO + func.$do($.return($.fromValue(schemaExample[firstKey]!, { layout: 'pretty' }))); + + plugin.node(func); +} + +function generateOperationFactory({ + examples: operationExamples, + functionName, + plugin, +}: { + examples: OperationExamples; + functionName: string; + plugin: HeyApiExamplesPlugin['Instance']; +}): void { + const statusCodes = Array.from(operationExamples.statusCodes.entries()); + + const defaultStatusCode = operationExamples.defaultStatusCode || statusCodes[0]?.[0]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const defaultCodeNum = defaultStatusCode ? parseInt(defaultStatusCode.replace('X', '0')) : 200; + + const symbol = plugin.symbol(functionName, { + meta: { + category: 'example', + resource: 'operation', + resourceId: functionName.replace('Example', ''), + }, + }); + + const func = $.func(symbol).decl(); + // @ts-expect-error TODO + func.param($.param('options')); + + // @ts-expect-error TODO + func.$do( + $.return( + $.fromValue( + statusCodes[0]?.[1]?.examples.basic ?? + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + statusCodes[0]?.[1]?.examples[Object.keys(statusCodes[0]?.[1]?.examples ?? {})[0]!]!, + { layout: 'pretty' }, + ), + ), + ); + + plugin.node(func); +} diff --git a/packages/openapi-ts/src/plugins/@hey-api/examples/types.ts b/packages/openapi-ts/src/plugins/@hey-api/examples/types.ts new file mode 100644 index 0000000000..0857154143 --- /dev/null +++ b/packages/openapi-ts/src/plugins/@hey-api/examples/types.ts @@ -0,0 +1,22 @@ +import type { Casing } from '@hey-api/shared'; +import type { DefinePlugin, Plugin } from '@hey-api/shared'; + +export type UserConfig = Plugin.Name<'@hey-api/examples'> & + Plugin.Hooks & + Plugin.UserExports & { + /** + * Casing convention for generated names. + * + * @default 'camelCase' + */ + case?: Casing; + }; + +export type Config = Plugin.Name<'@hey-api/examples'> & + Plugin.Hooks & + Plugin.Exports & { + /** Casing convention for generated names. */ + case: Casing; + }; + +export type HeyApiExamplesPlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/shared/clientOptions.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/shared/clientOptions.ts index 4965148361..ce9e48d401 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/shared/clientOptions.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/shared/clientOptions.ts @@ -10,7 +10,7 @@ import type { TypeTsDsl } from '../../../../ts-dsl'; import { $ } from '../../../../ts-dsl'; import type { HeyApiTypeScriptPlugin } from '../types'; -const serverToBaseUrlType = ({ server }: { server: IR.ServerObject }) => { +function serverToBaseUrlType({ server }: { server: IR.ServerObject }): TypeTsDsl { const url = parseUrl(server.url); if (url.protocol && url.host) { @@ -24,9 +24,9 @@ const serverToBaseUrlType = ({ server }: { server: IR.ServerObject }) => { .add(url.host || $.type('string')) .add(url.port ? `:${url.port}` : '') .add(url.path || ''); -}; +} -export const createClientOptions = ({ +export function createClientOptions({ nodeIndex, plugin, servers, @@ -34,7 +34,7 @@ export const createClientOptions = ({ nodeIndex: number; plugin: HeyApiTypeScriptPlugin['Instance']; servers: ReadonlyArray; -}) => { +}) { const client = getClientPlugin(getTypedConfig(plugin)); const types: Array = servers.map((server) => serverToBaseUrlType({ server })); @@ -70,4 +70,4 @@ export const createClientOptions = ({ .prop(getClientBaseUrlKey(getTypedConfig(plugin)), (p) => p.type($.type.or(...types))), ); plugin.node(node, nodeIndex); -}; +} diff --git a/packages/openapi-ts/src/plugins/config.ts b/packages/openapi-ts/src/plugins/config.ts index 4d0abc5795..27c3732500 100644 --- a/packages/openapi-ts/src/plugins/config.ts +++ b/packages/openapi-ts/src/plugins/config.ts @@ -9,6 +9,7 @@ import { defaultConfig as heyApiClientKy } from '../plugins/@hey-api/client-ky'; import { defaultConfig as heyApiClientNext } from '../plugins/@hey-api/client-next'; import { defaultConfig as heyApiClientNuxt } from '../plugins/@hey-api/client-nuxt'; import { defaultConfig as heyApiClientOfetch } from '../plugins/@hey-api/client-ofetch'; +import { defaultConfig as heyApiExamples } from '../plugins/@hey-api/examples'; import { defaultConfig as heyApiSchemas } from '../plugins/@hey-api/schemas'; import { defaultConfig as heyApiSdk } from '../plugins/@hey-api/sdk'; import { defaultConfig as heyApiTransformers } from '../plugins/@hey-api/transformers'; @@ -40,6 +41,7 @@ export const defaultPluginConfigs: { '@hey-api/client-next': heyApiClientNext, '@hey-api/client-nuxt': heyApiClientNuxt, '@hey-api/client-ofetch': heyApiClientOfetch, + '@hey-api/examples': heyApiExamples, '@hey-api/schemas': heyApiSchemas, '@hey-api/sdk': heyApiSdk, '@hey-api/transformers': heyApiTransformers, diff --git a/packages/openapi-ts/src/plugins/types.ts b/packages/openapi-ts/src/plugins/types.ts index 4da1edded1..f4f7e2eaa8 100644 --- a/packages/openapi-ts/src/plugins/types.ts +++ b/packages/openapi-ts/src/plugins/types.ts @@ -7,7 +7,7 @@ export type PluginClientNames = | '@hey-api/client-nuxt' | '@hey-api/client-ofetch'; -export type PluginMockNames = '@faker-js/faker'; +export type PluginSourceNames = '@faker-js/faker' | '@hey-api/examples'; export type PluginTransformerNames = '@hey-api/transformers'; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index dc6eecc088..1ec513a9b5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -144,4 +144,4 @@ export { refToName, resolveRef, } from './utils/ref'; -export { parseUrl } from './utils/url'; +export { getBaseUrl, parseUrl } from './utils/url'; diff --git a/packages/shared/src/plugins/types.ts b/packages/shared/src/plugins/types.ts index 31d7199b09..1944d26525 100644 --- a/packages/shared/src/plugins/types.ts +++ b/packages/shared/src/plugins/types.ts @@ -19,7 +19,7 @@ export type PluginNames = keyof PluginConfigMap extends never ? string : keyof P export type AnyPluginName = PluginNames | AnyString; -type PluginTag = 'client' | 'mocker' | 'sdk' | 'transformer' | 'validator'; +type PluginTag = 'client' | 'handler' | 'sdk' | 'source' | 'transformer' | 'validator'; export type PluginContext = { package: Dependency; diff --git a/packages/shared/src/utils/url.ts b/packages/shared/src/utils/url.ts index 6eb335b799..e3bab22d0c 100644 --- a/packages/shared/src/utils/url.ts +++ b/packages/shared/src/utils/url.ts @@ -1,3 +1,5 @@ +import type { IR } from '../ir/types'; + const parseUrlRegExp = /^(([^:/?#]+):)?((\/\/)?([^:/?#]*)(:?([^/?#]*)))?([^?#]*)(\?([^#]*))?(#(.*))?/; @@ -8,6 +10,36 @@ interface Url { protocol: string; } +/** + * Resolve the base URL value based on the plugin configuration. + * + * The `baseUrl` config option can be: + * - `false` to disable using the base URL + * - a string to use as the base URL + * - a number to pick a server from the IR `servers` array + */ +function resolveBaseUrl(baseUrl: string | number | boolean, ir: IR.Model): string | undefined { + if (baseUrl === false) return; + if (typeof baseUrl === 'string') return baseUrl; + const servers = ir.servers ?? []; + return servers[typeof baseUrl === 'number' ? baseUrl : 0]?.url; +} + +/** + * Resolve the base URL string if it's a valid URL or path. + */ +export function getBaseUrl(config: string | number | boolean, ir: IR.Model): string | undefined { + const baseUrl = resolveBaseUrl(config, ir); + if (baseUrl === undefined) return; + if (baseUrl.includes('{')) return; + const url = parseUrl(baseUrl); + if (url.protocol && url.host) return baseUrl; + if (baseUrl !== '/' && baseUrl.startsWith('/')) { + return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + } + return baseUrl; +} + export function parseUrl(value: string): Url { const errorResponse: Url = { host: '', diff --git a/specs/3.0.x/orpc.yaml b/specs/3.0.x/rpc.yaml similarity index 99% rename from specs/3.0.x/orpc.yaml rename to specs/3.0.x/rpc.yaml index 3a4f792c63..214f36d9c4 100644 --- a/specs/3.0.x/orpc.yaml +++ b/specs/3.0.x/rpc.yaml @@ -1,6 +1,6 @@ openapi: 3.0.4 info: - title: OpenAPI 3.0.4 oRPC example + title: OpenAPI 3.0.4 RPC example version: 1 paths: /users: diff --git a/specs/3.1.x/examples.yaml b/specs/3.1.x/examples.yaml new file mode 100644 index 0000000000..9d9b7c5ce3 --- /dev/null +++ b/specs/3.1.x/examples.yaml @@ -0,0 +1,84 @@ +openapi: 3.1.0 +info: + title: Examples API + version: 1.0.0 +paths: + /users/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: User found + content: + application/json: + example: + id: 1 + name: John + '404': + description: User not found + content: + application/json: + examples: + basic: + value: + code: NOT_FOUND + message: User not found + detailed: + value: + code: NOT_FOUND + message: User not found + timestamp: '2024-01-01T00:00:00Z' + /users: + post: + operationId: createUser + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + responses: + '201': + description: User created + content: + application/json: + example: + id: 2 + name: Jane +components: + schemas: + User: + type: object + properties: + id: + type: integer + name: + type: string + example: + id: 1 + name: John + Organization: + type: object + properties: + id: + type: integer + name: + type: string + examples: + basic: + value: + id: 1 + name: Acme Corp + detailed: + value: + id: 1 + name: Acme Corporation + founded: '2000-01-01' diff --git a/specs/3.1.x/mockers.yaml b/specs/3.1.x/mockers.yaml new file mode 100644 index 0000000000..8a8f6b62ef --- /dev/null +++ b/specs/3.1.x/mockers.yaml @@ -0,0 +1,73 @@ +openapi: 3.1.1 +info: + title: OpenAPI 3.1.1 mockers example + version: 1 +paths: + /foo: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + post: + requestBody: + content: + 'text/plain': + schema: + type: string + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + fullName: + type: string + age: + type: number + example: + fullName: 'John Doe' + age: 34 + '204': + description: SUCCESSFUL + put: + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: + name: 'Alice' + text/plain: + schema: + type: string + application/octet-stream: + schema: + type: string + format: binary +components: + schemas: + Person: + type: object + properties: + firstName: + type: string + lastName: + type: string + age: + type: number + example: + firstName: 'Marry' + lastName: 'Jane' + age: 30 diff --git a/specs/3.1.x/orpc.yaml b/specs/3.1.x/rpc.yaml similarity index 99% rename from specs/3.1.x/orpc.yaml rename to specs/3.1.x/rpc.yaml index 29163b0b07..9002a1f30a 100644 --- a/specs/3.1.x/orpc.yaml +++ b/specs/3.1.x/rpc.yaml @@ -1,6 +1,6 @@ openapi: 3.1.1 info: - title: OpenAPI 3.1.1 oRPC example + title: OpenAPI 3.1.1 RPC example version: 1 paths: /users: