Skip to content

Commit

Permalink
fix: handle non-exploded array query parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Nov 9, 2024
1 parent 37970e0 commit 691cdc2
Show file tree
Hide file tree
Showing 36 changed files with 498 additions and 234 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-poets-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: handle non-exploded array query parameters
9 changes: 6 additions & 3 deletions packages/openapi-ts/src/compiler/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ export interface ImportExportItemObject {
name: string;
}

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
});

export const createSourceFile = (sourceText: string) =>
ts.createSourceFile(
'',
sourceText,
ts.ScriptTarget.ES2015,
undefined,
ts.ScriptTarget.ESNext,
false,
ts.ScriptKind.TS,
);

Expand Down
21 changes: 13 additions & 8 deletions packages/openapi-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,20 @@ const getPlugins = (
'@hey-api/schemas',
'@hey-api/services',
]
).map((plugin) => {
if (typeof plugin === 'string') {
return plugin;
}
)
.map((plugin) => {
if (typeof plugin === 'string') {
return plugin;
}

// @ts-expect-error
userPluginsConfig[plugin.name] = plugin;
return plugin.name;
});
if (plugin.name) {
// @ts-expect-error
userPluginsConfig[plugin.name] = plugin;
}

return plugin.name;
})
.filter(Boolean);

const pluginOrder = getPluginOrder({
pluginConfigs: {
Expand Down
19 changes: 19 additions & 0 deletions packages/openapi-ts/src/ir/ir.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export interface IRParametersObject {
}

export interface IRParameterObject {
/**
* Determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 `:/?#[]@!$&'()*+,;=` to be included without percent-encoding. The default value is `false`. This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded` or `multipart/form-data`. If a value is explicitly defined, then the value of `contentType` (implicit or explicit) SHALL be ignored.
*/
allowReserved?: boolean;
/**
* When this is true, property values of type `array` or `object` generate separate parameters for each value of the array, or key-value-pair of the map. For other types of properties this property has no effect. When `style` is `form`, the default value is `true`. For all other styles, the default value is `false`. This property SHALL be ignored if the request body media type is not `application/x-www-form-urlencoded` or `multipart/form-data`. If a value is explicitly defined, then the value of `contentType` (implicit or explicit) SHALL be ignored.
*/
explode: boolean;
/**
* Endpoint parameters must specify their location.
*/
Expand All @@ -72,6 +80,17 @@ export interface IRParameterObject {
pagination?: boolean | string;
required?: boolean;
schema: IRSchemaObject;
/**
* Describes how the parameter value will be serialized depending on the type of the parameter value. Default values (based on value of `in`): for `query` - `form`; for `path` - `simple`; for `header` - `simple`; for `cookie` - `form`.
*/
style:
| 'deepObject'
| 'form'
| 'label'
| 'matrix'
| 'pipeDelimited'
| 'simple'
| 'spaceDelimited';
}

export interface IRResponsesObject {
Expand Down
57 changes: 57 additions & 0 deletions packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ import { mediaTypeObject } from './mediaType';
import { paginationField } from './pagination';
import { schemaToIrSchema } from './schema';

/**
* Returns default parameter `allowReserved` based on value of `in`.
*/
const defaultAllowReserved = (
_in: ParameterObject['in'],
): boolean | undefined => {
switch (_in) {
// this keyword only applies to parameters with an `in` value of `query`
case 'query':
return false;
default:
return;
}
};

Check warning on line 25 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L16-L25

Added lines #L16 - L25 were not covered by tests

/**
* Returns default parameter `explode` based on value of `style`.
*/
const defaultExplode = (style: Required<ParameterObject>['style']): boolean => {
switch (style) {
// default value for `deepObject` is `false`, but that behavior is undefined
// so we use `true` to make this work with the `client-fetch` package
case 'deepObject':
case 'form':
return true;
default:
return false;
}
};

Check warning on line 40 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L31-L40

Added lines #L31 - L40 were not covered by tests

/**
* Returns default parameter `style` based on value of `in`.
*/
const defaultStyle = (
_in: ParameterObject['in'],
): Required<ParameterObject>['style'] => {
switch (_in) {
case 'header':
case 'path':
return 'simple';
case 'cookie':
case 'query':
return 'form';
}
};

Check warning on line 56 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L46-L56

Added lines #L46 - L56 were not covered by tests

export const parametersArrayToObject = ({
context,
parameters,
Expand Down Expand Up @@ -141,13 +187,24 @@ const parameterToIrParameter = ({
schema: finalSchema,
});

const style = parameter.style || defaultStyle(parameter.in);
const explode =
parameter.explode !== undefined ? parameter.explode : defaultExplode(style);
const allowReserved =
parameter.allowReserved !== undefined
? parameter.allowReserved
: defaultAllowReserved(parameter.in);

Check warning on line 197 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L190-L197

Added lines #L190 - L197 were not covered by tests
const irParameter: IRParameterObject = {
allowReserved,
explode,

Check warning on line 200 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L199-L200

Added lines #L199 - L200 were not covered by tests
location: parameter.in,
name: parameter.name,
schema: schemaToIrSchema({
context,
schema: finalSchema,
}),
style,

Check warning on line 207 in packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts#L207

Added line #L207 was not covered by tests
};

if (pagination) {
Expand Down
57 changes: 57 additions & 0 deletions packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,52 @@ import { mediaTypeObject } from './mediaType';
import { paginationField } from './pagination';
import { schemaToIrSchema } from './schema';

/**
* Returns default parameter `allowReserved` based on value of `in`.
*/
const defaultAllowReserved = (
_in: ParameterObject['in'],
): boolean | undefined => {
switch (_in) {
// this keyword only applies to parameters with an `in` value of `query`
case 'query':
return false;
default:
return;
}
};

/**
* Returns default parameter `explode` based on value of `style`.
*/
const defaultExplode = (style: Required<ParameterObject>['style']): boolean => {
switch (style) {
// default value for `deepObject` is `false`, but that behavior is undefined
// so we use `true` to make this work with the `client-fetch` package
case 'deepObject':
case 'form':
return true;
default:
return false;
}
};

/**
* Returns default parameter `style` based on value of `in`.
*/
const defaultStyle = (
_in: ParameterObject['in'],
): Required<ParameterObject>['style'] => {
switch (_in) {
case 'header':
case 'path':
return 'simple';
case 'cookie':
case 'query':
return 'form';
}
};

export const parametersArrayToObject = ({
context,
parameters,
Expand Down Expand Up @@ -134,13 +180,24 @@ const parameterToIrParameter = ({
schema: finalSchema,
});

const style = parameter.style || defaultStyle(parameter.in);
const explode =
parameter.explode !== undefined ? parameter.explode : defaultExplode(style);
const allowReserved =
parameter.allowReserved !== undefined
? parameter.allowReserved

Check warning on line 188 in packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts#L188

Added line #L188 was not covered by tests
: defaultAllowReserved(parameter.in);

const irParameter: IRParameterObject = {
allowReserved,
explode,
location: parameter.in,
name: parameter.name,
schema: schemaToIrSchema({
context,
schema: finalSchema,
}),
style,
};

if (pagination) {
Expand Down
32 changes: 32 additions & 0 deletions packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,38 @@ const requestOptions = ({
}
}

for (const name in operation.parameters?.query) {
const parameter = operation.parameters.query[name];
if (
(parameter.schema.type === 'array' ||
parameter.schema.type === 'tuple') &&
(parameter.style !== 'form' || !parameter.explode)

Check warning on line 161 in packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts#L161

Added line #L161 was not covered by tests
) {
// override the default settings for `querySerializer`
if (context.config.client.name === '@hey-api/client-fetch') {
obj.push({
key: 'querySerializer',
value: [
{
key: 'array',
value: [
{
key: 'explode',
value: false,
},
{
key: 'style',
value: 'form',
},
],
},
],
});
}
break;
}

Check warning on line 185 in packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts#L163-L185

Added lines #L163 - L185 were not covered by tests
}

return compiler.objectExpression({
identifiers: ['responseTransformer'],
obj,
Expand Down
8 changes: 8 additions & 0 deletions packages/openapi-ts/test/3.0.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ describe(`OpenAPI ${VERSION}`, () => {
}),
description: 'handles empty response status codes',
},
{
config: createConfig({
input: 'parameter-explode-false.json',
output: 'parameter-explode-false',
plugins: ['@hey-api/services'],
}),
description: 'handles non-exploded array query parameters',
},
];

it.each(scenarios)('$description', async ({ config }) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/openapi-ts/test/3.1.x.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ describe(`OpenAPI ${VERSION}`, () => {
}),
description: 'handles empty response status codes',
},
{
config: createConfig({
input: 'parameter-explode-false.json',
output: 'parameter-explode-false',
plugins: ['@hey-api/services'],
}),
description: 'handles non-exploded array query parameters',
},
{
config: createConfig({
input: 'required-all-of-ref.json',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './services.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file is auto-generated by @hey-api/openapi-ts

import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
import type { PostFooData } from './types.gen';

export const client = createClient(createConfig());

export const postFoo = <ThrowOnError extends boolean = false>(options?: Options<PostFooData, ThrowOnError>) => {
return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({
...options,
url: '/foo',
querySerializer: {
array: {
explode: false,
style: 'form'
}
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

export type PostFooData = {
body?: never;
path?: never;
query?: {
foo?: Array<string>;
};
};
Loading

0 comments on commit 691cdc2

Please sign in to comment.