Skip to content

Commit cbcd3bc

Browse files
committed
fix: query string is incorrectly serialized for primitive parameters (#103)
1 parent e81acc7 commit cbcd3bc

File tree

6 files changed

+110
-23
lines changed

6 files changed

+110
-23
lines changed

src/language/typescript/2.0/serializers/operation-object.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ export const serializeQueryParameterObject = (
411411
case 'number':
412412
case 'boolean': {
413413
const f = serializedFragment(
414-
`value => encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(value)`,
415-
[],
414+
`value => some(encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(value))`,
415+
[serializedDependency('some', 'fp-ts/lib/Option')],
416416
[],
417417
);
418418
return right(getSerializedOptionCallFragment(!required, f, encoded));
@@ -430,16 +430,16 @@ export const serializeQueryParameterObject = (
430430
case 'pipes': {
431431
const s = getCollectionSeparator(collectionFormat);
432432
const f = serializedFragment(
433-
`value => encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(value.join('${s}'))`,
434-
[],
433+
`value => some(encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(value.join('${s}')))`,
434+
[serializedDependency('some', 'fp-ts/lib/Option')],
435435
[],
436436
);
437437
return right(getSerializedOptionCallFragment(!required, f, encoded));
438438
}
439439
case 'multi': {
440440
const f = serializedFragment(
441-
`value => value.map(item => encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(item)).join('&')`,
442-
[],
441+
`value => some(value.map(item => encodeURIComponent('${parameter.name}') + '=' + encodeURIComponent(item)).join('&'))`,
442+
[serializedDependency('some', 'fp-ts/lib/Option')],
443443
[],
444444
);
445445
return right(getSerializedOptionCallFragment(!required, f, encoded));

src/language/typescript/3.0/bundled/openapi-3-utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ const content = `
1414
export const serializePrimitiveParameter = (style: string, name: string, value: unknown): Either<Error, string> => {
1515
switch (style) {
1616
case 'matrix': {
17-
return right(\`;\${name}=\${value}\`);
17+
return right(\`;\${name}=\${encodeURIComponent(String(value))}\`);
1818
}
1919
case 'label': {
20-
return right(\`.\${value}\`);
20+
return right(\`.\${encodeURIComponent(String(value))}\`);
2121
}
2222
case 'form': {
23-
return right(\`\${name}=\${value}\`);
23+
return right(\`\${name}=\${encodeURIComponent(String(value))}\`);
2424
}
2525
case 'simple': {
26-
return right(\`\${value}\`);
26+
return right(\`\${encodeURIComponent(String(value))}\`);
2727
}
2828
}
2929
return left(new Error(\`Unsupported style "\${style}" for parameter "\${name}"\`));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getParameters as createGetParameters } from '../operation-object';
2+
import { constant } from 'fp-ts/lib/function';
3+
import { left } from 'fp-ts/lib/Either';
4+
import { fromString } from '../../../../../utils/ref';
5+
import { PathItemObjectCodec } from '../../../../../schema/3.0/path-item-object';
6+
import { pipe } from 'fp-ts/lib/pipeable';
7+
import { either, option } from 'fp-ts';
8+
import { OperationObjectCodec } from '../../../../../schema/3.0/operation-object';
9+
import { sequenceTEither } from '@devexperts/utils/dist/adt/either.utils';
10+
11+
describe('OperationObject', () => {
12+
describe('getParameters', () => {
13+
const getParameters = createGetParameters({
14+
resolveRef: constant(left(new Error('Refs not supported'))),
15+
});
16+
17+
it('should correctly handle primitive query parameters', () => {
18+
const operation = pipe(
19+
OperationObjectCodec.decode({
20+
responses: {},
21+
parameters: [
22+
{
23+
in: 'query',
24+
name: 'offset',
25+
required: false,
26+
schema: {
27+
type: 'number',
28+
},
29+
},
30+
{
31+
in: 'query',
32+
name: 'limit',
33+
required: true,
34+
schema: {
35+
type: 'number',
36+
},
37+
},
38+
],
39+
}),
40+
either.mapLeft(constant(new Error())),
41+
);
42+
43+
const pathItem = pipe(PathItemObjectCodec.decode({}), either.mapLeft(constant(new Error())));
44+
45+
const result = pipe(
46+
sequenceTEither(fromString('#/test'), operation, pathItem),
47+
either.chain(([ref, operation, pathItem]) => getParameters(ref, operation, pathItem)),
48+
);
49+
50+
const generated = pipe(
51+
result,
52+
option.fromEither,
53+
option.chain(result => result.serializedQueryString),
54+
option.fold(constant(''), fragment => fragment.value.replace(/\s+/g, ' ')),
55+
);
56+
57+
expect(generated).toEqual(
58+
`compact([pipe(
59+
optionFromNullable(number).encode(parameters.query.offset),
60+
option.fromNullable,
61+
option.chain(value => option.fromEither(serializePrimitiveParameter('form', 'offset', value))),
62+
),pipe(
63+
number.encode(parameters.query.limit),
64+
value => option.fromEither(serializePrimitiveParameter('form', 'limit', value)),
65+
)]).join('&')`
66+
.trim()
67+
.replace(/\s+/g, ' '),
68+
);
69+
});
70+
});
71+
});

src/language/typescript/3.0/serializers/operation-object.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
getParameterObjectSchema,
1313
isRequired,
1414
serializeParameterObject,
15-
serializeParameterToTemplate,
15+
serializeQueryParameterToTemplate,
1616
} from './parameter-object';
1717
import { pipe } from 'fp-ts/lib/pipeable';
1818
import { getSerializedKindDependency, serializedDependency } from '../../common/data/serialized-dependency';
@@ -70,7 +70,7 @@ const eqParameterByNameAndIn: Eq<ParameterObject> = getStructEq({
7070
});
7171
const contains = array.elem(eqParameterByNameAndIn);
7272

73-
const getParameters = combineReader(
73+
export const getParameters = combineReader(
7474
ask<ResolveRefContext>(),
7575
e => (from: Ref, operation: OperationObject, pathItem: PathItemObject): Either<Error, Parameters> => {
7676
const processedParameters: ParameterObject[] = [];
@@ -142,7 +142,7 @@ const getParameters = combineReader(
142142
return resolvedSchema;
143143
}
144144

145-
const queryStringFragment = serializeParameterToTemplate(
145+
const queryStringFragment = serializeQueryParameterToTemplate(
146146
from,
147147
resolved.right,
148148
resolvedSchema.right,
@@ -209,7 +209,7 @@ const getParameters = combineReader(
209209
option.map(f =>
210210
combineFragmentsK(f, c =>
211211
serializedFragment(
212-
`encodeURIComponent(compact([${c}]).join('&'))`,
212+
`compact([${c}]).join('&')`,
213213
[serializedDependency('compact', 'fp-ts/lib/Array')],
214214
[],
215215
),

src/language/typescript/3.0/serializers/parameter-object.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const getParameterExplode = (parameter: ParameterObject): boolean =>
8585
option.getOrElse(() => getParameterObjectStyle(parameter) === 'form'),
8686
);
8787

88-
export const serializeParameterToTemplate = (
88+
export const serializeQueryParameterToTemplate = (
8989
from: Ref,
9090
parameter: ParameterObject,
9191
parameterSchema: SchemaObject,
@@ -121,26 +121,32 @@ const getFn = (
121121
if (PrimitiveSchemaObjectCodec.is(schema)) {
122122
return right(
123123
serializedFragment(
124-
`value => serializePrimitiveParameter('${style}', '${parameter.name}', value)`,
125-
[serializedDependency('serializePrimitiveParameter', pathToUtils)],
124+
`value => option.fromEither(serializePrimitiveParameter('${style}', '${parameter.name}', value))`,
125+
[
126+
serializedDependency('option', 'fp-ts'),
127+
serializedDependency('serializePrimitiveParameter', pathToUtils),
128+
],
126129
[],
127130
),
128131
);
129132
}
130133
if (ArraySchemaObjectCodec.is(schema)) {
131134
return right(
132135
serializedFragment(
133-
`value => serializeArrayParameter('${style}', '${parameter.name}', value, ${explode})`,
134-
[serializedDependency('serializeArrayParameter', pathToUtils)],
136+
`value => option.fromEither(serializeArrayParameter('${style}', '${parameter.name}', value, ${explode}))`,
137+
[serializedDependency('option', 'fp-ts'), serializedDependency('serializeArrayParameter', pathToUtils)],
135138
[],
136139
),
137140
);
138141
}
139142
if (ObjectSchemaObjectCodec.is(schema)) {
140143
return right(
141144
serializedFragment(
142-
`value => serializeObjectParameter('${style}', '${parameter.name}', value, ${explode})`,
143-
[serializedDependency('serializeObjectParameter', pathToUtils)],
145+
`value => option.fromEither(serializeObjectParameter('${style}', '${parameter.name}', value, ${explode}))`,
146+
[
147+
serializedDependency('option', 'fp-ts'),
148+
serializedDependency('serializeObjectParameter', pathToUtils),
149+
],
144150
[],
145151
),
146152
);

src/language/typescript/common/data/serialized-fragment.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ export function combineFragmentsK(
125125
return serializedFragment(fragment.value, dependencies.concat(fragment.dependencies), refs.concat(fragment.refs));
126126
}
127127

128+
/**
129+
* @param f returns an Option
130+
*/
128131
export const getSerializedOptionCallFragment = (
129132
nullable: boolean,
130133
f: SerializedFragment,
@@ -136,12 +139,19 @@ export const getSerializedOptionCallFragment = (
136139
`pipe(
137140
${a},
138141
option.fromNullable,
139-
option.map(${fn}),
142+
option.chain(${fn}),
140143
)`,
141144
[serializedDependency('option', 'fp-ts'), serializedDependency('pipe', 'fp-ts/lib/pipeable')],
142145
[],
143146
)
144-
: serializedFragment(`some((${fn})(${a}))`, [serializedDependency('some', 'fp-ts/lib/Option')], []),
147+
: serializedFragment(
148+
`pipe(
149+
${a},
150+
${fn},
151+
)`,
152+
[serializedDependency('pipe', 'fp-ts/lib/pipeable')],
153+
[],
154+
),
145155
);
146156

147157
export const commaFragment = serializedFragment(', ', [], []);

0 commit comments

Comments
 (0)