diff --git a/packages/@ama-sdk/core/src/fwk/param-deserialization.spec.ts b/packages/@ama-sdk/core/src/fwk/param-deserialization.spec.ts new file mode 100644 index 0000000000..edb2099b2f --- /dev/null +++ b/packages/@ama-sdk/core/src/fwk/param-deserialization.spec.ts @@ -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' } }); + }); +}); diff --git a/packages/@ama-sdk/core/src/fwk/param-deserialization.ts b/packages/@ama-sdk/core/src/fwk/param-deserialization.ts new file mode 100644 index 0000000000..c31d87cf9b --- /dev/null +++ b/packages/@ama-sdk/core/src/fwk/param-deserialization.ts @@ -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( + 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 }); +} + +/** + * 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( + 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 }); +} diff --git a/packages/@ama-sdk/core/src/fwk/param-serialization.ts b/packages/@ama-sdk/core/src/fwk/param-serialization.ts index e2bc12bf92..9e6fef06cf 100644 --- a/packages/@ama-sdk/core/src/fwk/param-serialization.ts +++ b/packages/@ama-sdk/core/src/fwk/param-serialization.ts @@ -16,13 +16,13 @@ export type PrimitiveType = string | number | boolean | Date | utils.Date | util export type SupportedParamType = PrimitiveType | PrimitiveType[] | { [key: string]: PrimitiveType }; /** URL encoding of space character, delimiter for spaceDelimited style */ -const SPACE_URL_CODE = encodeURIComponent(' '); +export const SPACE_URL_CODE = encodeURIComponent(' '); /** URL encoding of pipe character, delimiter for pipeDelimited style */ -const PIPE_URL_CODE = encodeURIComponent('|'); +export const PIPE_URL_CODE = encodeURIComponent('|'); /** URL encoding of opening square bracket, used in deepObject style */ -const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('['); +export const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('['); /** URL encoding of closing square bracket, used in deepObject style */ -const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']'); +export const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']'); /** * Verify if property is of type utils.Date or utils.DateTime diff --git a/packages/@ama-sdk/schematics/README.md b/packages/@ama-sdk/schematics/README.md index 82715b2e95..19a278289f 100644 --- a/packages/@ama-sdk/schematics/README.md +++ b/packages/@ama-sdk/schematics/README.md @@ -137,9 +137,12 @@ your own serialization methods if the ones provided do not meet your requirement > API to prepare the URL. You can do so by using the serialization method that we provide (`serializeQueryParams`) or your own serialization method. The value of the query > parameters returned by the `RequestPlugin` will be forwarded to the next plugin and the last value will be directly added to the URL. +We also provide the methods `deserializeQueryParams` and `deserializePathParams` to deserialize the values of query and path parameters based on their serialization +(`explode` and `style`) and type (`primitive`, `array`, or `object`). This method can be used as a tool to better visualize the values of the parameters during development. + > [!NOTE] > It is important to note that special characters have to be encoded, as required by RFC6570 and RFC3986. Please take this into account if you choose to use your own -> serialization methods. +> serialization or deserialization methods. #### Light SDK