Skip to content

Commit 8a7ca39

Browse files
committed
feat: parameter deserialization
1 parent 2fa4a61 commit 8a7ca39

File tree

4 files changed

+289
-5
lines changed

4 files changed

+289
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
deserializePathParams,
3+
deserializeQueryParams,
4+
} from './param-deserialization';
5+
6+
describe('Deserialize parameters', () => {
7+
it('should correctly deserialize query parameters of type primitive', () => {
8+
expect(deserializeQueryParams({ idPrimitive: 'idPrimitive=5' }, { idPrimitive: { explode: true, style: 'form', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
9+
expect(deserializeQueryParams({ idPrimitive: 'idPrimitive=5' }, { idPrimitive: { explode: false, style: 'form', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
10+
});
11+
12+
it('should correctly deserialize query parameters of type array', () => {
13+
expect(deserializeQueryParams({ idArray: 'idArray=3&idArray=4&idArray=5' }, { idArray: { explode: true, style: 'form', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
14+
expect(deserializeQueryParams({ idArray: 'idArray=3,4,5' }, { idArray: { explode: false, style: 'form', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
15+
expect(deserializeQueryParams({ idArray: 'should-not-work' }, { idArray: { explode: true, style: 'spaceDelimited', paramType: 'array' } })).toEqual({ idArray: undefined });
16+
expect(deserializeQueryParams({ idArray: 'idArray=3%204%205' }, { idArray: { explode: false, style: 'spaceDelimited', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
17+
expect(deserializeQueryParams({ idArray: 'should-not-work' }, { idArray: { explode: true, style: 'pipeDelimited', paramType: 'array' } })).toEqual({ idArray: undefined });
18+
expect(deserializeQueryParams({ idArray: 'idArray=3%7C4%7C5' }, { idArray: { explode: false, style: 'pipeDelimited', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
19+
});
20+
21+
it('should correctly deserialize query parameters of type object', () => {
22+
expect(deserializeQueryParams({ idObject: 'role=admin&firstName=Alex' }, { idObject: { explode: true, style: 'form', paramType: 'object' } }))
23+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
24+
expect(deserializeQueryParams({ idObject: 'idObject=role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'form', paramType: 'object' } }))
25+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
26+
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: true, style: 'spaceDelimited', paramType: 'object' } })).toEqual({ idObject: undefined });
27+
expect(deserializeQueryParams({ idObject: 'idObject=role%20admin%20firstName%20Alex' }, { idObject: { explode: false, style: 'spaceDelimited', paramType: 'object' } }))
28+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
29+
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: true, style: 'pipeDelimited', paramType: 'object' } })).toEqual({ idObject: undefined });
30+
expect(deserializeQueryParams({ idObject: 'idObject=role%7Cadmin%7CfirstName%7CAlex' }, { idObject: { explode: false, style: 'pipeDelimited', paramType: 'object' } }))
31+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
32+
expect(deserializeQueryParams({ idObject: 'idObject%5Brole%5D=admin&idObject%5BfirstName%5D=Alex' }, { idObject: { explode: true, style: 'deepObject', paramType: 'object' } }))
33+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
34+
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: false, style: 'deepObject', paramType: 'object' } })).toEqual({ idObject: undefined });
35+
});
36+
37+
it('should correctly deserialize path parameters of type primitive', () => {
38+
expect(deserializePathParams({ idPrimitive: '5' }, { idPrimitive: { explode: true, style: 'simple', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
39+
expect(deserializePathParams({ idPrimitive: '5' }, { idPrimitive: { explode: false, style: 'simple', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
40+
expect(deserializePathParams({ idPrimitive: '.5' }, { idPrimitive: { explode: true, style: 'label', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
41+
expect(deserializePathParams({ idPrimitive: '.5' }, { idPrimitive: { explode: false, style: 'label', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
42+
expect(deserializePathParams({ idPrimitive: ';idPrimitive=5' }, { idPrimitive: { explode: true, style: 'matrix', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
43+
expect(deserializePathParams({ idPrimitive: ';idPrimitive=5' }, { idPrimitive: { explode: false, style: 'matrix', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' });
44+
});
45+
46+
it('should correctly deserialize path parameters of type array', () => {
47+
expect(deserializePathParams({ idArray: '3,4,5' }, { idArray: { explode: true, style: 'simple', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
48+
expect(deserializePathParams({ idArray: '3,4,5' }, { idArray: { explode: false, style: 'simple', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
49+
expect(deserializePathParams({ idArray: '.3.4.5' }, { idArray: { explode: true, style: 'label', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
50+
expect(deserializePathParams({ idArray: '.3,4,5' }, { idArray: { explode: false, style: 'label', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
51+
expect(deserializePathParams({ idArray: ';idArray=3;idArray=4;idArray=5' }, { idArray: { explode: true, style: 'matrix', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
52+
expect(deserializePathParams({ idArray: ';idArray=3,4,5' }, { idArray: { explode: false, style: 'matrix', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] });
53+
});
54+
55+
it('should correctly deserialize path parameters of type object', () => {
56+
expect(deserializePathParams({ idObject: 'role=admin,firstName=Alex' }, { idObject: { explode: true, style: 'simple', paramType: 'object' } }))
57+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
58+
expect(deserializePathParams({ idObject: 'role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'simple', paramType: 'object' } }))
59+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
60+
expect(deserializePathParams({ idObject: '.role=admin.firstName=Alex' }, { idObject: { explode: true, style: 'label', paramType: 'object' } }))
61+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
62+
expect(deserializePathParams({ idObject: '.role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'label', paramType: 'object' } }))
63+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
64+
expect(deserializePathParams({ idObject: ';role=admin;firstName=Alex' }, { idObject: { explode: true, style: 'matrix', paramType: 'object' } }))
65+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
66+
expect(deserializePathParams({ idObject: ';idObject=role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'matrix', paramType: 'object' } }))
67+
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } });
68+
});
69+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {
2+
CLOSING_SQUARE_BRACKET_URL_CODE,
3+
OPENING_SQUARE_BRACKET_URL_CODE,
4+
ParamSerialization,
5+
PIPE_URL_CODE,
6+
PrimitiveType,
7+
SPACE_URL_CODE,
8+
SupportedParamType,
9+
} from './param-serialization';
10+
11+
/** Specification of the parameter type for parameter deserialization */
12+
export type ParamTypeForDeserialization = 'primitive' | 'array' | 'object';
13+
14+
/**
15+
* Split parameter elements by delimiter based on the serialization style (with removal of the prefix)
16+
* @param serializedParamValue serialized parameter value
17+
* @param paramSerialization parameter serialization
18+
*/
19+
function splitParamElements(serializedParamValue: string, paramSerialization: ParamSerialization) {
20+
switch (paramSerialization.style) {
21+
case 'simple': {
22+
return serializedParamValue.split(',').map((value) => decodeURIComponent(value));
23+
}
24+
case 'label': {
25+
// NOTE: Path parameters of style label are prefixed with a '.'
26+
return serializedParamValue.substring(1).split(paramSerialization.explode ? '.' : ',').map((value) => decodeURIComponent(value));
27+
}
28+
case 'matrix': {
29+
// NOTE: Path parameters of style matrix and exploded true are prefixed with a ';paramName='
30+
// NOTE: Path parameters of style matrix and exploded false are written like this: ';paramName=value1;paramName=value2'
31+
return paramSerialization.explode
32+
? serializedParamValue.substring(1).split(';').map((value) => decodeURIComponent(value.split('=')[1]))
33+
: serializedParamValue.split('=')[1].split(',').map((value) => decodeURIComponent(value));
34+
}
35+
case 'form': {
36+
// NOTE: Query parameters of style form and explode true are written like this: 'paramName=value1&paramName=value2&paramName=value3'
37+
// NOTE: Query parameters of style form and explode false are prefixed with the parameter name and delimited by a ','
38+
return paramSerialization.explode
39+
? serializedParamValue.split('&').map((value) => decodeURIComponent(value.split('=')[1]))
40+
: serializedParamValue.split('=')[1].split(',').map((value) => decodeURIComponent(value));
41+
}
42+
case 'spaceDelimited': {
43+
// NOTE: Query parameters of style spaceDelimited and explode false are prefixed with the parameter name and delimited by the encoded space character
44+
// Here is an example of the format: 'paramName=value1%20value2%20value3'
45+
return paramSerialization.explode ? undefined : serializedParamValue.split('=')[1].split(SPACE_URL_CODE).map((value) => decodeURIComponent(value));
46+
}
47+
case 'pipeDelimited': {
48+
// NOTE: Query parameters of style spaceDelimited and explode false are prefixed with the parameter name and delimited by the encoded pipe character
49+
// Here is an example of the format: 'paramName=value1%7Cvalue2%7Cvalue3'
50+
return paramSerialization.explode ? undefined : serializedParamValue.split('=')[1].split(PIPE_URL_CODE).map((value) => decodeURIComponent(value));
51+
}
52+
}
53+
}
54+
55+
function objectFromPairwiseArray(splitObject: string[]) {
56+
return splitObject.reduce((obj: { [key: string]: PrimitiveType }, currentValue, index, array) => {
57+
// NOTE: Every other item of the array is a key and the following item is the corresponding value
58+
if (index % 2 === 0) {
59+
obj[decodeURIComponent(currentValue)] = decodeURIComponent(array[index + 1]);
60+
}
61+
return obj;
62+
}, {} as { [key: string]: PrimitiveType });
63+
}
64+
65+
/**
66+
* Deserialize query parameters of type array
67+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
68+
* @param serializedParamValue serialized query parameter value
69+
* @param paramSerialization parameter serialization
70+
*/
71+
function deserializeArrayQueryParams(serializedParamValue: string, paramSerialization: ParamSerialization) {
72+
return splitParamElements(serializedParamValue, paramSerialization);
73+
}
74+
75+
/**
76+
* Deserialize query parameters of type object
77+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
78+
* @param serializedParamValue serialized query parameter value
79+
* @param paramSerialization parameter serialization
80+
*/
81+
function deserializeObjectQueryParams(serializedParamValue: string, paramSerialization: ParamSerialization) {
82+
// NOTE: Applies to the exploded styles 'form' and 'deepObject'
83+
if (paramSerialization.explode && (paramSerialization.style === 'form' || paramSerialization.style === 'deepObject')) {
84+
return serializedParamValue.split('&').reduce((obj: { [key: string]: PrimitiveType }, serializedProperty) => {
85+
const [key, value] = serializedProperty.split('=');
86+
// NOTE: The key of an object in deepObject style is surrounded by opening and closing square brackets
87+
const objKey = paramSerialization.style === 'deepObject' ? key.split(OPENING_SQUARE_BRACKET_URL_CODE)[1].split(CLOSING_SQUARE_BRACKET_URL_CODE)[0] : key;
88+
obj[decodeURIComponent(objKey)] = decodeURIComponent(value);
89+
return obj;
90+
}, {} as { [key: string]: PrimitiveType });
91+
}
92+
93+
// NOTE: Applies to the non-exploded styles 'form', 'spaceDelimited', and 'pipeDelimited'
94+
if (paramSerialization.style !== 'deepObject' && !paramSerialization.explode) {
95+
// NOTE: The splitParamElements function is called since object query parameters can be split by delimiters based on the serialization style
96+
// NOTE: The deserialized value will exist since these exploded styles are supported
97+
const splitObject = splitParamElements(serializedParamValue, paramSerialization);
98+
return objectFromPairwiseArray(splitObject!);
99+
}
100+
}
101+
102+
/**
103+
* Deserialize query parameters based on the values of exploded and style and the parameter type
104+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
105+
* @param serializedQueryParams serialized query parameters
106+
* @param queryParamSerialization query parameter serialization
107+
*/
108+
export function deserializeQueryParams<T extends { [key: string]: string }>(
109+
serializedQueryParams: T,
110+
queryParamSerialization: { [p in keyof T]: ParamSerialization & { paramType: ParamTypeForDeserialization } }
111+
): { [p in keyof T]: SupportedParamType } {
112+
return Object.entries(serializedQueryParams).reduce((acc, [queryParamName, serializedParamValue]) => {
113+
const paramSerialization = queryParamSerialization[queryParamName];
114+
let deserializedValue: SupportedParamType;
115+
if (paramSerialization.paramType === 'array') {
116+
deserializedValue = deserializeArrayQueryParams(serializedParamValue, paramSerialization);
117+
} else if (paramSerialization.paramType === 'object') {
118+
deserializedValue = deserializeObjectQueryParams(serializedParamValue, paramSerialization);
119+
} else {
120+
// NOTE: Query parameters of type primitive are prefixed with the parameter name like this: 'paramName=value'
121+
deserializedValue = decodeURIComponent(serializedParamValue.split('=')[1]);
122+
}
123+
if (deserializedValue) {
124+
acc[queryParamName as keyof T] = deserializedValue;
125+
}
126+
return acc;
127+
}, {} as { [p in keyof T]: SupportedParamType });
128+
}
129+
130+
/**
131+
* Deserialize path parameters of type primitive
132+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
133+
* @param serializedParamValue serialized path parameter value
134+
* @param paramSerialization parameter serialization
135+
*/
136+
function deserializePrimitivePathParams(serializedParamValue: string, paramSerialization: ParamSerialization) {
137+
switch (paramSerialization.style) {
138+
case 'simple': {
139+
return decodeURIComponent(serializedParamValue);
140+
}
141+
case 'label': {
142+
// NOTE: Path parameters of style label are prefixed with a '.'
143+
return decodeURIComponent(serializedParamValue.substring(1));
144+
}
145+
case 'matrix': {
146+
return decodeURIComponent(serializedParamValue.substring(1).split('=')[1]);
147+
}
148+
}
149+
}
150+
151+
/**
152+
* Deserialize path parameters of type array
153+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
154+
* @param serializedParamValue serialized path parameter value
155+
* @param paramSerialization parameter serialization
156+
*/
157+
function deserializeArrayPathParams(serializedParamValue: string, paramSerialization: ParamSerialization) {
158+
return splitParamElements(serializedParamValue, paramSerialization);
159+
}
160+
161+
/**
162+
* Deserialize path parameters of type object
163+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
164+
* @param serializedParamValue serialized path parameter value
165+
* @param paramSerialization parameter serialization
166+
*/
167+
function deserializeObjectPathParams(serializedParamValue: string, paramSerialization: ParamSerialization) {
168+
// NOTE: The splitParamElements function is called since object path parameters can be split by delimiters based on the serialization style
169+
// NOTE: There is an exception for path parameters of exploded style 'matrix' which is not serialized like its corresponding array
170+
// This exception is serialized like this (prefixed by a ';'): ';prop1=value1;prop2=value2'
171+
const splitObject: string[] = paramSerialization.style === 'matrix' && paramSerialization.explode
172+
? serializedParamValue.substring(1).split(';').map((value) => decodeURIComponent(value))
173+
: splitParamElements(serializedParamValue, paramSerialization)!;
174+
175+
// NOTE: Object path parameters that are exploded are serialized as 'prop=value'
176+
if (paramSerialization.explode) {
177+
return splitObject.reduce((obj: { [key: string]: PrimitiveType }, serializedProperty) => {
178+
const [key, value] = serializedProperty.split('=').map((v) => decodeURIComponent(v));
179+
obj[key] = value;
180+
return obj;
181+
}, {} as { [key: string]: PrimitiveType });
182+
}
183+
184+
return objectFromPairwiseArray(splitObject);
185+
}
186+
187+
/**
188+
* Deserialize path parameters based on the values of exploded and style and the parameter type
189+
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
190+
* @param serializedPathParams serialized path parameters
191+
* @param pathParamSerialization path parameter serialization
192+
*/
193+
export function deserializePathParams<T extends { [key: string]: string }>(
194+
serializedPathParams: T,
195+
pathParamSerialization: { [p in keyof T]: ParamSerialization & { paramType: ParamTypeForDeserialization } }
196+
): { [p in keyof T]: SupportedParamType } {
197+
return Object.entries(serializedPathParams).reduce((acc, [pathParamName, serializedParamValue]) => {
198+
const paramSerialization = pathParamSerialization[pathParamName];
199+
let deserializedValue: SupportedParamType;
200+
if (paramSerialization.paramType === 'array') {
201+
deserializedValue = deserializeArrayPathParams(serializedParamValue, paramSerialization);
202+
} else if (paramSerialization.paramType === 'object') {
203+
deserializedValue = deserializeObjectPathParams(serializedParamValue, paramSerialization);
204+
} else {
205+
deserializedValue = deserializePrimitivePathParams(serializedParamValue, paramSerialization);
206+
}
207+
if (deserializedValue) {
208+
acc[pathParamName as keyof T] = deserializedValue;
209+
}
210+
return acc;
211+
}, {} as { [p in keyof T]: SupportedParamType });
212+
}

packages/@ama-sdk/core/src/fwk/param-serialization.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ export type PrimitiveType = string | number | boolean | Date | utils.Date | util
1616
export type SupportedParamType = PrimitiveType | PrimitiveType[] | { [key: string]: PrimitiveType };
1717

1818
/** URL encoding of space character, delimiter for spaceDelimited style */
19-
const SPACE_URL_CODE = encodeURIComponent(' ');
19+
export const SPACE_URL_CODE = encodeURIComponent(' ');
2020
/** URL encoding of pipe character, delimiter for pipeDelimited style */
21-
const PIPE_URL_CODE = encodeURIComponent('|');
21+
export const PIPE_URL_CODE = encodeURIComponent('|');
2222
/** URL encoding of opening square bracket, used in deepObject style */
23-
const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('[');
23+
export const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('[');
2424
/** URL encoding of closing square bracket, used in deepObject style */
25-
const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']');
25+
export const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']');
2626

2727
/**
2828
* Verify if property is of type utils.Date or utils.DateTime

0 commit comments

Comments
 (0)