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
4 changes: 3 additions & 1 deletion docs/content/docs/reference/configuration/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1694,7 +1694,9 @@ Make all parameters optional except path parameters.
**Type:** `Boolean`
**Default:** `false`

Enable URL encoding of path/query parameters.
Wrap each path parameter with `encodeURIComponent(String(...))` in generated URL helpers. This option only affects path parameters; query parameters are typically encoded by the underlying client (`URLSearchParams`, `axios`, etc.).

Path parameters are stringified via `String(value)` before encoding, so array (`style: simple|matrix|label`) and object path parameters are not serialized according to their OpenAPI `style` — they fall back to the default `String(value)` representation.

### optionsParamRequired

Expand Down
56 changes: 56 additions & 0 deletions packages/core/src/getters/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
getFullRoute,
getRoute,
getRouteAsArray,
makeRouteSafe,
wrapRouteParameters,
} from './route';

describe('getRoute getter', () => {
Expand Down Expand Up @@ -267,3 +269,57 @@ describe('getRouteAsArray getter', () => {
expect(getRouteAsArray(input)).toEqual(output);
});
});

describe('wrapRouteParameters', () => {
it('wraps parameters correctly', () => {
const result = wrapRouteParameters(
'/user/${id}/profile',
'prefix-',
'-suffix',
);
expect(result).toBe('/user/${prefix-id-suffix}/profile');
});

it('handles no parameters gracefully', () => {
const result = wrapRouteParameters('/user/profile', 'prefix-', '-suffix');
expect(result).toBe('/user/profile');
});

it('handles empty route', () => {
const result = wrapRouteParameters('', 'prefix-', '-suffix');
expect(result).toBe('');
});
});

describe('makeRouteSafe', () => {
it('encodes URI components in parameters', () => {
const result = makeRouteSafe('/search/${query}/bla/${something}');
expect(result).toBe(
'/search/${encodeURIComponent(String(query))}/bla/${encodeURIComponent(String(something))}',
);
});

it('encodes adjacent parameters separately', () => {
const result = makeRouteSafe('/x/${a}${b}');
expect(result).toBe(
'/x/${encodeURIComponent(String(a))}${encodeURIComponent(String(b))}',
);
});

it('encodes parameters mixed with literal separators', () => {
const result = makeRouteSafe('/files/${name}.${ext}');
expect(result).toBe(
'/files/${encodeURIComponent(String(name))}.${encodeURIComponent(String(ext))}',
);
});

it('handles no special characters gracefully', () => {
const result = makeRouteSafe('/search/query');
expect(result).toBe('/search/query');
});

it('handles empty route', () => {
const result = makeRouteSafe('');
expect(result).toBe('');
});
});
12 changes: 12 additions & 0 deletions packages/core/src/getters/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TEMPLATE_TAG_REGEX } from '../constants';
import type {
BaseUrlFromConstant,
BaseUrlFromSpec,
Expand Down Expand Up @@ -145,6 +146,17 @@ export function getBaseUrlRuntimeImports(
}));
}

// Emits a codegen string: wraps each `${param}` segment of a template-literal
// route so the generated client encodes path parameters at request time.
export const wrapRouteParameters = (
route: string,
prepend: string,
append: string,
): string => route.replaceAll(TEMPLATE_TAG_REGEX, `\${${prepend}$1${append}}`);

export const makeRouteSafe = (route: string): string =>
wrapRouteParameters(route, 'encodeURIComponent(String(', '))');

// Creates a mixed use array with path variables and string from template string route
export function getRouteAsArray(route: string): string {
return route
Expand Down
9 changes: 8 additions & 1 deletion packages/fetch/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type GeneratorVerbOptions,
GetterPropType,
isObject,
makeRouteSafe,
type OpenApiParameterObject,
type OpenApiReferenceObject,
type OpenApiSchemaObject,
Expand Down Expand Up @@ -68,8 +69,14 @@ export const generateRequestFunction = (
override,
doc,
}: GeneratorVerbOptions,
{ route, context, pathRoute }: GeneratorOptions,
{ route: _route, context, pathRoute }: GeneratorOptions,
) => {
let route = _route;

if (context.output.urlEncodeParameters) {
route = makeRouteSafe(route);
}

const isRequestOptions = override.requestOptions !== false;
const isFormData = !override.formData.disabled;
const isFormUrlEncoded = override.formUrlEncoded !== false;
Expand Down
8 changes: 2 additions & 6 deletions packages/query/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isObject,
isSyntheticDefaultImportsAllow,
kebab,
makeRouteSafe,
OutputHttpClient,
pascal,
toObjectString,
Expand All @@ -24,12 +25,7 @@ import {
generateRequestFunction as generateFetchRequestFunction,
} from '@orval/fetch';

import {
getHasSignal,
makeRouteSafe,
vueUnRefParams,
vueWrapTypeWithMaybeRef,
} from './utils';
import { getHasSignal, vueUnRefParams, vueWrapTypeWithMaybeRef } from './utils';

export const AXIOS_DEPENDENCIES = [
{
Expand Down
46 changes: 1 addition & 45 deletions packages/query/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,6 @@
import { describe, expect, it } from 'vitest';

import {
makeRouteSafe,
normalizeQueryOptions,
wrapRouteParameters,
} from './utils';

describe('wrapRouteParameters', () => {
it('wraps parameters correctly', () => {
const result = wrapRouteParameters(
'/user/${id}/profile',
'prefix-',
'-suffix',
);
expect(result).toBe('/user/${prefix-id-suffix}/profile');
});

it('handles no parameters gracefully', () => {
const result = wrapRouteParameters('/user/profile', 'prefix-', '-suffix');
expect(result).toBe('/user/profile');
});

it('handles empty route', () => {
const result = wrapRouteParameters('', 'prefix-', '-suffix');
expect(result).toBe('');
});
});

describe('makeRouteSafe', () => {
it('encodes URI components in parameters', () => {
const result = makeRouteSafe('/search/${query}/bla/${something}');
expect(result).toBe(
'/search/${encodeURIComponent(String(query))}/bla/${encodeURIComponent(String(something))}',
);
});

it('handles no special characters gracefully', () => {
const result = makeRouteSafe('/search/query');
expect(result).toBe('/search/query');
});

it('handles empty route', () => {
const result = makeRouteSafe('');
expect(result).toBe('');
});
});
import { normalizeQueryOptions } from './utils';

describe('normalizeQueryOptions', () => {
it('should include useOperationIdAsQueryKey when provided', () => {
Expand Down
10 changes: 0 additions & 10 deletions packages/query/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
OutputClient,
type OutputClientFunc,
type QueryOptions,
TEMPLATE_TAG_REGEX,
} from '@orval/core';

export const normalizeQueryOptions = (
Expand Down Expand Up @@ -137,15 +136,6 @@ export const vueUnRefParams = (props: GetterProps): string => {
.join('\n');
};

export const wrapRouteParameters = (
route: string,
prepend: string,
append: string,
): string => route.replaceAll(TEMPLATE_TAG_REGEX, `\${${prepend}$1${append}}`);

export const makeRouteSafe = (route: string): string =>
wrapRouteParameters(route, 'encodeURIComponent(String(', '))');

export const isVue = (client: OutputClient | OutputClientFunc) =>
OutputClient.VUE_QUERY === client;

Expand Down
8 changes: 4 additions & 4 deletions tests/__snapshots__/fetch/dateParams/pets/pets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export const getListPetsByCountryUrl = (
const stringifiedParams = normalizedParams.toString();

return stringifiedParams.length > 0
? `/pets-by-country/${country}?${stringifiedParams}`
: `/pets-by-country/${country}`;
? `/pets-by-country/${encodeURIComponent(String(country))}?${stringifiedParams}`
: `/pets-by-country/${encodeURIComponent(String(country))}`;
};

/**
Expand Down Expand Up @@ -170,8 +170,8 @@ export const getListPetsByAgeUrl = (
const stringifiedParams = normalizedParams.toString();

return stringifiedParams.length > 0
? `/pets-by-age/${age}?${stringifiedParams}`
: `/pets-by-age/${age}`;
? `/pets-by-age/${encodeURIComponent(String(age))}?${stringifiedParams}`
: `/pets-by-age/${encodeURIComponent(String(age))}`;
};

/**
Expand Down
Loading
Loading