Skip to content

Commit

Permalink
fix: generate response zod schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Dec 13, 2024
1 parent 8095505 commit e926028
Show file tree
Hide file tree
Showing 31 changed files with 1,173 additions and 338 deletions.
10 changes: 8 additions & 2 deletions packages/client-axios/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,14 @@ export const createClient = (config: Config): Client => {

let { data } = response;

if (opts.responseType === 'json' && opts.responseTransformer) {
data = await opts.responseTransformer(data);
if (opts.responseType === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}

if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}

return {
Expand Down
13 changes: 9 additions & 4 deletions packages/client-axios/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,16 @@ export interface Config<ThrowOnError extends boolean = boolean>
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function for transforming response data before it's returned to the
* caller function. This is an ideal place to post-process server data,
* e.g. convert date ISO strings into native Date objects.
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
/**
* Throw an error instead of returning it in the response?
*
Expand All @@ -97,7 +102,7 @@ export interface Config<ThrowOnError extends boolean = boolean>
}

export interface RequestOptions<
ThrowOnError extends boolean = false,
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<ThrowOnError> {
/**
Expand Down
10 changes: 8 additions & 2 deletions packages/client-fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ export const createClient = (config: Config = {}): Client => {
: opts.parseAs) ?? 'json';

let data = await response[parseAs]();
if (parseAs === 'json' && opts.responseTransformer) {
data = await opts.responseTransformer(data);
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}

if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}

return {
Expand Down
11 changes: 8 additions & 3 deletions packages/client-fetch/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,16 @@ export interface Config<ThrowOnError extends boolean = boolean>
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function for transforming response data before it's returned to the
* caller function. This is an ideal place to post-process server data,
* e.g. convert date ISO strings into native Date objects.
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
/**
* Throw an error instead of returning it in the response?
*
Expand Down
111 changes: 67 additions & 44 deletions packages/openapi-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import type { IRContext } from './ir/context';
import { parseExperimental, parseLegacy } from './openApi';
import type { ClientPlugins, UserPlugins } from './plugins';
import { defaultPluginConfigs } from './plugins';
import type { DefaultPluginConfigs, PluginNames } from './plugins/types';
import type {
DefaultPluginConfigs,
PluginContext,
PluginNames,
} from './plugins/types';
import type { Client } from './types/client';
import type {
ClientConfig,
Expand Down Expand Up @@ -180,52 +184,99 @@ const getOutput = (userConfig: ClientConfig): Config['output'] => {
return output;
};

const getPluginOrder = ({
const getPluginsConfig = ({
pluginConfigs,
userPlugins,
userPluginsConfig,
}: {
pluginConfigs: DefaultPluginConfigs<ClientPlugins>;
userPlugins: ReadonlyArray<PluginNames>;
}): Config['pluginOrder'] => {
userPluginsConfig: Config['plugins'];
}): Pick<Config, 'plugins' | 'pluginOrder'> => {
const circularReferenceTracker = new Set<PluginNames>();
const visitedNodes = new Set<PluginNames>();
const pluginOrder = new Set<PluginNames>();
const plugins: Config['plugins'] = {};

const dfs = (name: PluginNames) => {
if (circularReferenceTracker.has(name)) {
throw new Error(`Circular reference detected at '${name}'`);
}

if (!visitedNodes.has(name)) {
if (!pluginOrder.has(name)) {
circularReferenceTracker.add(name);

const pluginConfig = pluginConfigs[name];

if (!pluginConfig) {
throw new Error(
`🚫 unknown plugin dependency "${name}" - do you need to register a custom plugin with this name?`,
);
}

for (const dependency of pluginConfig._dependencies || []) {
dfs(dependency);
const defaultOptions = defaultPluginConfigs[name];
const userOptions = userPluginsConfig[name];
if (userOptions && defaultOptions) {
const nativePluginOption = Object.keys(userOptions).find((key) =>
key.startsWith('_'),
);
if (nativePluginOption) {
throw new Error(
`🚫 cannot register plugin "${name}" - attempting to override a native plugin option "${nativePluginOption}"`,
);
}

Check warning on line 225 in packages/openapi-ts/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/index.ts#L222-L225

Added lines #L222 - L225 were not covered by tests
}

for (const dependency of pluginConfig._optionalDependencies || []) {
if (userPlugins.includes(dependency)) {
dfs(dependency);
}
const config = {
_dependencies: [],
...defaultOptions,
...userOptions,
};

if (config._infer) {
const context: PluginContext = {
ensureDependency: (dependency) => {
if (
typeof dependency === 'string' &&
!config._dependencies.includes(dependency)
) {
config._dependencies = [...config._dependencies, dependency];
}
},

Check warning on line 243 in packages/openapi-ts/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/index.ts#L237-L243

Added lines #L237 - L243 were not covered by tests
pluginByTag: (tag) => {
for (const userPlugin of userPlugins) {
const defaultConfig = defaultPluginConfigs[userPlugin];
if (
defaultConfig &&
defaultConfig._tags?.includes(tag) &&
userPlugin !== name
) {
return userPlugin;
}
}
},

Check warning on line 255 in packages/openapi-ts/src/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/index.ts#L245-L255

Added lines #L245 - L255 were not covered by tests
};
config._infer(config, context);
}

for (const dependency of config._dependencies) {
dfs(dependency);
}

circularReferenceTracker.delete(name);
visitedNodes.add(name);
pluginOrder.add(name);

// @ts-expect-error
plugins[name] = config;
}
};

for (const name of userPlugins) {
dfs(name);
}

return Array.from(visitedNodes);
return {
pluginOrder: Array.from(pluginOrder),
plugins,
};
};

const getPlugins = (
Expand All @@ -248,42 +299,14 @@ const getPlugins = (
})
.filter(Boolean);

const pluginOrder = getPluginOrder({
return getPluginsConfig({
pluginConfigs: {
...userPluginsConfig,
...defaultPluginConfigs,
},
userPlugins,
userPluginsConfig,
});

const plugins = pluginOrder.reduce(
(result, name) => {
const defaultOptions = defaultPluginConfigs[name];
const userOptions = userPluginsConfig[name];
if (userOptions && defaultOptions) {
const nativePluginOption = Object.keys(userOptions).find((key) =>
key.startsWith('_'),
);
if (nativePluginOption) {
throw new Error(
`🚫 cannot register plugin "${userOptions.name}" - attempting to override a native plugin option "${nativePluginOption}"`,
);
}
}
// @ts-expect-error
result[name] = {
...defaultOptions,
...userOptions,
};
return result;
},
{} as Config['plugins'],
);

return {
pluginOrder,
plugins,
};
};

const getSpec = async ({ config }: { config: Config }) => {
Expand Down
12 changes: 12 additions & 0 deletions packages/openapi-ts/src/ir/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,21 @@ export const statusCodeToGroup = ({
};

interface OperationResponsesMap {
/**
* A deduplicated union of all error types. Unknown types are omitted.
*/
error?: IRSchemaObject;
/**
* An object containing a map of status codes for each error type.
*/
errors?: IRSchemaObject;
/**
* A deduplicated union of all response types. Unknown types are omitted.
*/
response?: IRSchemaObject;
/**
* An object containing a map of status codes for each response type.
*/
responses?: IRSchemaObject;
}

Expand Down
22 changes: 21 additions & 1 deletion packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,27 @@ export const defaultConfig: Plugin.Config<Config> = {
_dependencies: ['@hey-api/typescript'],
_handler: handler,
_handlerLegacy: handlerLegacy,
_optionalDependencies: ['@hey-api/transformers'],
_infer: (config, context) => {
if (config.transformer) {
if (typeof config.transformer === 'boolean') {
config.transformer = context.pluginByTag(
'transformer',
) as unknown as typeof config.transformer;
}

context.ensureDependency(config.transformer);
}

Check warning on line 19 in packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts#L12-L19

Added lines #L12 - L19 were not covered by tests

if (config.validator) {
if (typeof config.validator === 'boolean') {
config.validator = context.pluginByTag(
'validator',
) as unknown as typeof config.validator;
}

context.ensureDependency(config.validator);
}

Check warning on line 29 in packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts#L22-L29

Added lines #L22 - L29 were not covered by tests
},
asClass: false,
auth: true,
name: '@hey-api/sdk',
Expand Down
Loading

0 comments on commit e926028

Please sign in to comment.