-
Notifications
You must be signed in to change notification settings - Fork 43
feat: parameter deserialization #3116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { | ||
deserializePathParams, | ||
deserializeQueryParams, | ||
} from './param-deserialization'; | ||
|
||
describe('Deserialize parameters', () => { | ||
it('should correctly deserialize query parameters of type primitive', () => { | ||
expect(deserializeQueryParams({ idPrimitive: 'idPrimitive=5' }, { idPrimitive: { explode: true, style: 'form', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializeQueryParams({ idPrimitive: 'idPrimitive=5' }, { idPrimitive: { explode: false, style: 'form', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
}); | ||
|
||
it('should correctly deserialize query parameters of type array', () => { | ||
expect(deserializeQueryParams({ idArray: 'idArray=3&idArray=4&idArray=5' }, { idArray: { explode: true, style: 'form', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializeQueryParams({ idArray: 'idArray=3,4,5' }, { idArray: { explode: false, style: 'form', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializeQueryParams({ idArray: 'should-not-work' }, { idArray: { explode: true, style: 'spaceDelimited', paramType: 'array' } })).toEqual({ idArray: undefined }); | ||
expect(deserializeQueryParams({ idArray: 'idArray=3%204%205' }, { idArray: { explode: false, style: 'spaceDelimited', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializeQueryParams({ idArray: 'should-not-work' }, { idArray: { explode: true, style: 'pipeDelimited', paramType: 'array' } })).toEqual({ idArray: undefined }); | ||
expect(deserializeQueryParams({ idArray: 'idArray=3%7C4%7C5' }, { idArray: { explode: false, style: 'pipeDelimited', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
}); | ||
|
||
it('should correctly deserialize query parameters of type object', () => { | ||
expect(deserializeQueryParams({ idObject: 'role=admin&firstName=Alex' }, { idObject: { explode: true, style: 'form', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializeQueryParams({ idObject: 'idObject=role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'form', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: true, style: 'spaceDelimited', paramType: 'object' } })).toEqual({ idObject: undefined }); | ||
expect(deserializeQueryParams({ idObject: 'idObject=role%20admin%20firstName%20Alex' }, { idObject: { explode: false, style: 'spaceDelimited', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: true, style: 'pipeDelimited', paramType: 'object' } })).toEqual({ idObject: undefined }); | ||
expect(deserializeQueryParams({ idObject: 'idObject=role%7Cadmin%7CfirstName%7CAlex' }, { idObject: { explode: false, style: 'pipeDelimited', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializeQueryParams({ idObject: 'idObject%5Brole%5D=admin&idObject%5BfirstName%5D=Alex' }, { idObject: { explode: true, style: 'deepObject', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializeQueryParams({ idObject: 'should-not-work' }, { idObject: { explode: false, style: 'deepObject', paramType: 'object' } })).toEqual({ idObject: undefined }); | ||
}); | ||
|
||
it('should correctly deserialize path parameters of type primitive', () => { | ||
expect(deserializePathParams({ idPrimitive: '5' }, { idPrimitive: { explode: true, style: 'simple', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializePathParams({ idPrimitive: '5' }, { idPrimitive: { explode: false, style: 'simple', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializePathParams({ idPrimitive: '.5' }, { idPrimitive: { explode: true, style: 'label', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializePathParams({ idPrimitive: '.5' }, { idPrimitive: { explode: false, style: 'label', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializePathParams({ idPrimitive: ';idPrimitive=5' }, { idPrimitive: { explode: true, style: 'matrix', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
expect(deserializePathParams({ idPrimitive: ';idPrimitive=5' }, { idPrimitive: { explode: false, style: 'matrix', paramType: 'primitive' } })).toEqual({ idPrimitive: '5' }); | ||
}); | ||
|
||
it('should correctly deserialize path parameters of type array', () => { | ||
expect(deserializePathParams({ idArray: '3,4,5' }, { idArray: { explode: true, style: 'simple', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializePathParams({ idArray: '3,4,5' }, { idArray: { explode: false, style: 'simple', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializePathParams({ idArray: '.3.4.5' }, { idArray: { explode: true, style: 'label', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializePathParams({ idArray: '.3,4,5' }, { idArray: { explode: false, style: 'label', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializePathParams({ idArray: ';idArray=3;idArray=4;idArray=5' }, { idArray: { explode: true, style: 'matrix', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
expect(deserializePathParams({ idArray: ';idArray=3,4,5' }, { idArray: { explode: false, style: 'matrix', paramType: 'array' } })).toEqual({ idArray: ['3', '4', '5'] }); | ||
}); | ||
|
||
it('should correctly deserialize path parameters of type object', () => { | ||
expect(deserializePathParams({ idObject: 'role=admin,firstName=Alex' }, { idObject: { explode: true, style: 'simple', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializePathParams({ idObject: 'role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'simple', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializePathParams({ idObject: '.role=admin.firstName=Alex' }, { idObject: { explode: true, style: 'label', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializePathParams({ idObject: '.role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'label', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializePathParams({ idObject: ';role=admin;firstName=Alex' }, { idObject: { explode: true, style: 'matrix', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
expect(deserializePathParams({ idObject: ';idObject=role,admin,firstName,Alex' }, { idObject: { explode: false, style: 'matrix', paramType: 'object' } })) | ||
.toEqual({ idObject: { role: 'admin', firstName: 'Alex' } }); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,212 @@ | ||||||
import { | ||||||
CLOSING_SQUARE_BRACKET_URL_CODE, | ||||||
OPENING_SQUARE_BRACKET_URL_CODE, | ||||||
type ParamSerialization, | ||||||
PIPE_URL_CODE, | ||||||
type PrimitiveType, | ||||||
SPACE_URL_CODE, | ||||||
type SupportedParamType, | ||||||
} from './param-serialization'; | ||||||
|
||||||
/** Specification of the parameter type for parameter deserialization */ | ||||||
export type ParamTypeForDeserialization = 'primitive' | 'array' | 'object'; | ||||||
|
||||||
/** | ||||||
* Split parameter elements by delimiter based on the serialization style (with removal of the prefix) | ||||||
* @param serializedParamValue serialized parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function splitParamElements(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
switch (paramSerialization.style) { | ||||||
case 'simple': { | ||||||
return serializedParamValue.split(',').map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
case 'label': { | ||||||
// NOTE: Path parameters of style label are prefixed with a '.' | ||||||
return serializedParamValue.substring(1).split(paramSerialization.explode ? '.' : ',').map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
case 'matrix': { | ||||||
// NOTE: Path parameters of style matrix and exploded true are prefixed with a ';paramName=' | ||||||
// NOTE: Path parameters of style matrix and exploded false are written like this: ';paramName=value1;paramName=value2' | ||||||
return paramSerialization.explode | ||||||
? serializedParamValue.substring(1).split(';').map((value) => decodeURIComponent(value.split('=')[1])) | ||||||
: serializedParamValue.split('=')[1].split(',').map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
case 'form': { | ||||||
// NOTE: Query parameters of style form and explode true are written like this: 'paramName=value1¶mName=value2¶mName=value3' | ||||||
// NOTE: Query parameters of style form and explode false are prefixed with the parameter name and delimited by a ',' | ||||||
return paramSerialization.explode | ||||||
? serializedParamValue.split('&').map((value) => decodeURIComponent(value.split('=')[1])) | ||||||
: serializedParamValue.split('=')[1].split(',').map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
case 'spaceDelimited': { | ||||||
// NOTE: Query parameters of style spaceDelimited and explode false are prefixed with the parameter name and delimited by the encoded space character | ||||||
// Here is an example of the format: 'paramName=value1%20value2%20value3' | ||||||
return paramSerialization.explode ? undefined : serializedParamValue.split('=')[1].split(SPACE_URL_CODE).map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
case 'pipeDelimited': { | ||||||
// NOTE: Query parameters of style spaceDelimited and explode false are prefixed with the parameter name and delimited by the encoded pipe character | ||||||
// Here is an example of the format: 'paramName=value1%7Cvalue2%7Cvalue3' | ||||||
return paramSerialization.explode ? undefined : serializedParamValue.split('=')[1].split(PIPE_URL_CODE).map((value) => decodeURIComponent(value)); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
function objectFromPairwiseArray(splitObject: string[]) { | ||||||
return splitObject.reduce((obj: { [key: string]: PrimitiveType }, currentValue, index, array) => { | ||||||
// NOTE: Every other item of the array is a key and the following item is the corresponding value | ||||||
if (index % 2 === 0) { | ||||||
obj[decodeURIComponent(currentValue)] = decodeURIComponent(array[index + 1]); | ||||||
} | ||||||
return obj; | ||||||
}, {} as { [key: string]: PrimitiveType }); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize query parameters of type array | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedParamValue serialized query parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function deserializeArrayQueryParams(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
return splitParamElements(serializedParamValue, paramSerialization); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize query parameters of type object | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedParamValue serialized query parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function deserializeObjectQueryParams(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
// NOTE: Applies to the exploded styles 'form' and 'deepObject' | ||||||
if (paramSerialization.explode && (paramSerialization.style === 'form' || paramSerialization.style === 'deepObject')) { | ||||||
return serializedParamValue.split('&').reduce((obj: { [key: string]: PrimitiveType }, serializedProperty) => { | ||||||
const [key, value] = serializedProperty.split('='); | ||||||
// NOTE: The key of an object in deepObject style is surrounded by opening and closing square brackets | ||||||
const objKey = paramSerialization.style === 'deepObject' ? key.split(OPENING_SQUARE_BRACKET_URL_CODE)[1].split(CLOSING_SQUARE_BRACKET_URL_CODE)[0] : key; | ||||||
obj[decodeURIComponent(objKey)] = decodeURIComponent(value); | ||||||
return obj; | ||||||
}, {} as { [key: string]: PrimitiveType }); | ||||||
} | ||||||
|
||||||
// NOTE: Applies to the non-exploded styles 'form', 'spaceDelimited', and 'pipeDelimited' | ||||||
if (paramSerialization.style !== 'deepObject' && !paramSerialization.explode) { | ||||||
// NOTE: The splitParamElements function is called since object query parameters can be split by delimiters based on the serialization style | ||||||
// NOTE: The deserialized value will exist since these exploded styles are supported | ||||||
const splitObject = splitParamElements(serializedParamValue, paramSerialization); | ||||||
return objectFromPairwiseArray(splitObject!); | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize query parameters based on the values of exploded and style and the parameter type | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedQueryParams serialized query parameters | ||||||
* @param queryParamSerialization query parameter serialization | ||||||
*/ | ||||||
export function deserializeQueryParams<T extends { [key: string]: string }>( | ||||||
serializedQueryParams: T, | ||||||
queryParamSerialization: { [p in keyof T]: ParamSerialization & { paramType: ParamTypeForDeserialization } } | ||||||
): { [p in keyof T]: SupportedParamType } { | ||||||
return Object.entries(serializedQueryParams).reduce((acc, [queryParamName, serializedParamValue]) => { | ||||||
const paramSerialization = queryParamSerialization[queryParamName]; | ||||||
let deserializedValue: SupportedParamType; | ||||||
if (paramSerialization.paramType === 'array') { | ||||||
deserializedValue = deserializeArrayQueryParams(serializedParamValue, paramSerialization); | ||||||
} else if (paramSerialization.paramType === 'object') { | ||||||
deserializedValue = deserializeObjectQueryParams(serializedParamValue, paramSerialization); | ||||||
} else { | ||||||
// NOTE: Query parameters of type primitive are prefixed with the parameter name like this: 'paramName=value' | ||||||
deserializedValue = decodeURIComponent(serializedParamValue.split('=')[1]); | ||||||
} | ||||||
if (deserializedValue) { | ||||||
acc[queryParamName as keyof T] = deserializedValue; | ||||||
} | ||||||
return acc; | ||||||
}, {} as { [p in keyof T]: SupportedParamType }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the type is a bit incorrect as this stage (accept if
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it fine to accept There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The purpose is not to change the return type (which remains |
||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize path parameters of type primitive | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedParamValue serialized path parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function deserializePrimitivePathParams(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
switch (paramSerialization.style) { | ||||||
case 'simple': { | ||||||
return decodeURIComponent(serializedParamValue); | ||||||
} | ||||||
case 'label': { | ||||||
// NOTE: Path parameters of style label are prefixed with a '.' | ||||||
return decodeURIComponent(serializedParamValue.substring(1)); | ||||||
} | ||||||
case 'matrix': { | ||||||
return decodeURIComponent(serializedParamValue.substring(1).split('=')[1]); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize path parameters of type array | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedParamValue serialized path parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function deserializeArrayPathParams(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
return splitParamElements(serializedParamValue, paramSerialization); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize path parameters of type object | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedParamValue serialized path parameter value | ||||||
* @param paramSerialization parameter serialization | ||||||
*/ | ||||||
function deserializeObjectPathParams(serializedParamValue: string, paramSerialization: ParamSerialization) { | ||||||
// NOTE: The splitParamElements function is called since object path parameters can be split by delimiters based on the serialization style | ||||||
// NOTE: There is an exception for path parameters of exploded style 'matrix' which is not serialized like its corresponding array | ||||||
// This exception is serialized like this (prefixed by a ';'): ';prop1=value1;prop2=value2' | ||||||
const splitObject: string[] = paramSerialization.style === 'matrix' && paramSerialization.explode | ||||||
? serializedParamValue.substring(1).split(';').map((value) => decodeURIComponent(value)) | ||||||
: splitParamElements(serializedParamValue, paramSerialization)!; | ||||||
|
||||||
// NOTE: Object path parameters that are exploded are serialized as 'prop=value' | ||||||
if (paramSerialization.explode) { | ||||||
return splitObject.reduce((obj: { [key: string]: PrimitiveType }, serializedProperty) => { | ||||||
const [key, value] = serializedProperty.split('=').map((v) => decodeURIComponent(v)); | ||||||
obj[key] = value; | ||||||
return obj; | ||||||
}, {} as { [key: string]: PrimitiveType }); | ||||||
} | ||||||
|
||||||
return objectFromPairwiseArray(splitObject); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Deserialize path parameters based on the values of exploded and style and the parameter type | ||||||
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation} | ||||||
* @param serializedPathParams serialized path parameters | ||||||
* @param pathParamSerialization path parameter serialization | ||||||
*/ | ||||||
export function deserializePathParams<T extends { [key: string]: string }>( | ||||||
serializedPathParams: T, | ||||||
pathParamSerialization: { [p in keyof T]: ParamSerialization & { paramType: ParamTypeForDeserialization } } | ||||||
): { [p in keyof T]: SupportedParamType } { | ||||||
return Object.entries(serializedPathParams).reduce((acc, [pathParamName, serializedParamValue]) => { | ||||||
const paramSerialization = pathParamSerialization[pathParamName]; | ||||||
let deserializedValue: SupportedParamType; | ||||||
if (paramSerialization.paramType === 'array') { | ||||||
deserializedValue = deserializeArrayPathParams(serializedParamValue, paramSerialization); | ||||||
} else if (paramSerialization.paramType === 'object') { | ||||||
deserializedValue = deserializeObjectPathParams(serializedParamValue, paramSerialization); | ||||||
} else { | ||||||
deserializedValue = deserializePrimitivePathParams(serializedParamValue, paramSerialization); | ||||||
} | ||||||
if (deserializedValue) { | ||||||
acc[pathParamName as keyof T] = deserializedValue; | ||||||
} | ||||||
return acc; | ||||||
}, {} as { [p in keyof T]: SupportedParamType }); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above:
Suggested change
|
||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this return
undefined
or throw an error?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should return
undefined
, we don't throw errors for non-supported use casesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect a function like this to throw when the format is not recognized, but I'm ok to return
undefined