Skip to content

Commit d5c67cb

Browse files
authored
[core-http] Support overriding xml parser char key via options (Azure#12065)
* [core-http] Support overriding xml parser char key via options Currently our xml parser uses hard-coded `_` as key to access parsed xml element content. This causes issue like storage customer getting errors listing blobs when they use `_` as Blob metadata key. This PR add support to allow customizing the xml parser char key via options. Currently `xmlCharKey` option is supported, with room for other xml parser options (e.g., attr key for accessing parsed xml attributes). * Address CR feedback - introduce XmlOptions interface - Make xml options required for private methods - Remove duplicate by introducing locals * Rename XmlOptions to more general SerializerOptions * Make internal options `Required<SerializerOptions>` * - Fix one missing place - update CHANGELOG
1 parent 0c118a7 commit d5c67cb

File tree

14 files changed

+511
-175
lines changed

14 files changed

+511
-175
lines changed

sdk/core/core-http/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
## 1.2.0 (2020-10-19)
44

55
- Explicitly set `manual` redirect handling for node fetch. And fixing redirectPipeline [PR 11863](https://github.com/Azure/azure-sdk-for-js/pull/11863)
6-
- Add support for multiple error response codes.[PR 11841](https://github.com/Azure/azure-sdk-for-js/)
6+
- Add support for multiple error response codes. [PR 11841](https://github.com/Azure/azure-sdk-for-js/)
7+
- Allow customizing serializer behavior via optional `SerialzierOptions` parameters. Particularly allow using a different `XML_CHARKEY` than the default `_` when parsing XML [PR 12065](https://github.com/Azure/azure-sdk-for-js/pull/12065)
78

89
## 1.1.9 (2020-09-30)
910

sdk/core/core-http/review/core-http.api.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,10 @@ export interface DeserializationOptions {
187187
}
188188

189189
// @public
190-
export function deserializationPolicy(deserializationContentTypes?: DeserializationContentTypes): RequestPolicyFactory;
190+
export function deserializationPolicy(deserializationContentTypes?: DeserializationContentTypes, parsingOptions?: SerializerOptions): RequestPolicyFactory;
191191

192192
// @public (undocumented)
193-
export function deserializeResponseBody(jsonContentTypes: string[], xmlContentTypes: string[], response: HttpOperationResponse): Promise<HttpOperationResponse>;
193+
export function deserializeResponseBody(jsonContentTypes: string[], xmlContentTypes: string[], response: HttpOperationResponse, options?: SerializerOptions): Promise<HttpOperationResponse>;
194194

195195
// @public (undocumented)
196196
export interface DictionaryMapper extends BaseMapper {
@@ -514,9 +514,7 @@ export interface ParameterValue {
514514
}
515515

516516
// @public
517-
export function parseXML(str: string, opts?: {
518-
includeRoot?: boolean;
519-
}): Promise<any>;
517+
export function parseXML(str: string, opts?: SerializerOptions): Promise<any>;
520518

521519
// @public
522520
export interface PipelineOptions {
@@ -599,6 +597,7 @@ export interface RequestOptionsBase {
599597
};
600598
onDownloadProgress?: (progress: TransferProgressEvent) => void;
601599
onUploadProgress?: (progress: TransferProgressEvent) => void;
600+
serializerOptions?: SerializerOptions;
602601
shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
603602
spanOptions?: SpanOptions;
604603
timeout?: number;
@@ -728,18 +727,25 @@ export class Serializer {
728727
constructor(modelMappers?: {
729728
[key: string]: any;
730729
}, isXML?: boolean | undefined);
731-
deserialize(mapper: Mapper, responseBody: any, objectName: string): any;
730+
deserialize(mapper: Mapper, responseBody: any, objectName: string, options?: SerializerOptions): any;
732731
// (undocumented)
733732
readonly isXML?: boolean | undefined;
734733
// (undocumented)
735734
readonly modelMappers: {
736735
[key: string]: any;
737736
};
738-
serialize(mapper: Mapper, object: any, objectName?: string): any;
737+
serialize(mapper: Mapper, object: any, objectName?: string, options?: SerializerOptions): any;
739738
// (undocumented)
740739
validateConstraints(mapper: Mapper, value: any, objectName: string): void;
741740
}
742741

742+
// @public
743+
export interface SerializerOptions {
744+
includeRoot?: boolean;
745+
rootName?: string;
746+
xmlCharKey?: string;
747+
}
748+
743749
// @public
744750
export interface ServiceCallback<TResult> {
745751
(err: Error | RestError | null, result?: TResult, request?: WebResourceLike, response?: HttpOperationResponse): void;
@@ -785,9 +791,7 @@ export interface SimpleMapperType {
785791
}
786792

787793
// @public
788-
export function stringifyXML(obj: any, opts?: {
789-
rootName?: string;
790-
}): string;
794+
export function stringifyXML(obj: any, opts?: SerializerOptions): string;
791795

792796
// @public
793797
export function stripRequest(request: WebResourceLike): WebResourceLike;
@@ -954,6 +958,12 @@ export interface WebResourceLike {
954958
withCredentials: boolean;
955959
}
956960

961+
// @public
962+
export const XML_ATTRKEY = "$";
963+
964+
// @public
965+
export const XML_CHARKEY = "_";
966+
957967

958968
// (No @packageDocumentation comment for this package)
959969

sdk/core/core-http/src/coreHttp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,4 @@ export { TopicCredentials } from "./credentials/topicCredentials";
125125
export { Authenticator } from "./credentials/credentials";
126126

127127
export { parseXML, stringifyXML } from "./util/xml";
128+
export { XML_ATTRKEY, XML_CHARKEY, SerializerOptions } from "./util/serializer.common";

sdk/core/core-http/src/policies/deserializationPolicy.ts

Lines changed: 90 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
RequestPolicyFactory,
1515
RequestPolicyOptions
1616
} from "./requestPolicy";
17+
import { XML_CHARKEY, SerializerOptions } from "../util/serializer.common";
1718

1819
/**
1920
* Options to configure API response deserialization.
@@ -49,11 +50,17 @@ export interface DeserializationContentTypes {
4950
* pass through the HTTP pipeline.
5051
*/
5152
export function deserializationPolicy(
52-
deserializationContentTypes?: DeserializationContentTypes
53+
deserializationContentTypes?: DeserializationContentTypes,
54+
parsingOptions?: SerializerOptions
5355
): RequestPolicyFactory {
5456
return {
5557
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => {
56-
return new DeserializationPolicy(nextPolicy, deserializationContentTypes, options);
58+
return new DeserializationPolicy(
59+
nextPolicy,
60+
options,
61+
deserializationContentTypes,
62+
parsingOptions
63+
);
5764
}
5865
};
5966
}
@@ -75,26 +82,29 @@ export const DefaultDeserializationOptions: DeserializationOptions = {
7582
export class DeserializationPolicy extends BaseRequestPolicy {
7683
public readonly jsonContentTypes: string[];
7784
public readonly xmlContentTypes: string[];
85+
public readonly xmlCharKey: string;
7886

7987
constructor(
8088
nextPolicy: RequestPolicy,
81-
deserializationContentTypes: DeserializationContentTypes | undefined,
82-
options: RequestPolicyOptions
89+
requestPolicyOptions: RequestPolicyOptions,
90+
deserializationContentTypes?: DeserializationContentTypes,
91+
parsingOptions: SerializerOptions = {}
8392
) {
84-
super(nextPolicy, options);
93+
super(nextPolicy, requestPolicyOptions);
8594

8695
this.jsonContentTypes =
8796
(deserializationContentTypes && deserializationContentTypes.json) || defaultJsonContentTypes;
8897
this.xmlContentTypes =
8998
(deserializationContentTypes && deserializationContentTypes.xml) || defaultXmlContentTypes;
99+
this.xmlCharKey = parsingOptions.xmlCharKey ?? XML_CHARKEY;
90100
}
91101

92102
public async sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
93-
return this._nextPolicy
94-
.sendRequest(request)
95-
.then((response: HttpOperationResponse) =>
96-
deserializeResponseBody(this.jsonContentTypes, this.xmlContentTypes, response)
97-
);
103+
return this._nextPolicy.sendRequest(request).then((response: HttpOperationResponse) =>
104+
deserializeResponseBody(this.jsonContentTypes, this.xmlContentTypes, response, {
105+
xmlCharKey: this.xmlCharKey
106+
})
107+
);
98108
}
99109
}
100110

@@ -137,74 +147,84 @@ function shouldDeserializeResponse(parsedResponse: HttpOperationResponse): boole
137147
export function deserializeResponseBody(
138148
jsonContentTypes: string[],
139149
xmlContentTypes: string[],
140-
response: HttpOperationResponse
150+
response: HttpOperationResponse,
151+
options: SerializerOptions = {}
141152
): Promise<HttpOperationResponse> {
142-
return parse(jsonContentTypes, xmlContentTypes, response).then((parsedResponse) => {
143-
if (!shouldDeserializeResponse(parsedResponse)) {
144-
return parsedResponse;
145-
}
153+
const updatedOptions: Required<SerializerOptions> = {
154+
rootName: options.rootName ?? "",
155+
includeRoot: options.includeRoot ?? false,
156+
xmlCharKey: options.xmlCharKey ?? XML_CHARKEY
157+
};
158+
return parse(jsonContentTypes, xmlContentTypes, response, updatedOptions).then(
159+
(parsedResponse) => {
160+
if (!shouldDeserializeResponse(parsedResponse)) {
161+
return parsedResponse;
162+
}
146163

147-
const operationSpec = parsedResponse.request.operationSpec;
148-
if (!operationSpec || !operationSpec.responses) {
149-
return parsedResponse;
150-
}
164+
const operationSpec = parsedResponse.request.operationSpec;
165+
if (!operationSpec || !operationSpec.responses) {
166+
return parsedResponse;
167+
}
151168

152-
const responseSpec = getOperationResponse(parsedResponse);
169+
const responseSpec = getOperationResponse(parsedResponse);
153170

154-
const { error, shouldReturnResponse } = handleErrorResponse(
155-
parsedResponse,
156-
operationSpec,
157-
responseSpec
158-
);
159-
if (error) {
160-
throw error;
161-
} else if (shouldReturnResponse) {
162-
return parsedResponse;
163-
}
171+
const { error, shouldReturnResponse } = handleErrorResponse(
172+
parsedResponse,
173+
operationSpec,
174+
responseSpec
175+
);
176+
if (error) {
177+
throw error;
178+
} else if (shouldReturnResponse) {
179+
return parsedResponse;
180+
}
164181

165-
// An operation response spec does exist for current status code, so
166-
// use it to deserialize the response.
167-
if (responseSpec) {
168-
if (responseSpec.bodyMapper) {
169-
let valueToDeserialize: any = parsedResponse.parsedBody;
170-
if (operationSpec.isXML && responseSpec.bodyMapper.type.name === MapperType.Sequence) {
171-
valueToDeserialize =
172-
typeof valueToDeserialize === "object"
173-
? valueToDeserialize[responseSpec.bodyMapper.xmlElementName!]
174-
: [];
182+
// An operation response spec does exist for current status code, so
183+
// use it to deserialize the response.
184+
if (responseSpec) {
185+
if (responseSpec.bodyMapper) {
186+
let valueToDeserialize: any = parsedResponse.parsedBody;
187+
if (operationSpec.isXML && responseSpec.bodyMapper.type.name === MapperType.Sequence) {
188+
valueToDeserialize =
189+
typeof valueToDeserialize === "object"
190+
? valueToDeserialize[responseSpec.bodyMapper.xmlElementName!]
191+
: [];
192+
}
193+
try {
194+
parsedResponse.parsedBody = operationSpec.serializer.deserialize(
195+
responseSpec.bodyMapper,
196+
valueToDeserialize,
197+
"operationRes.parsedBody",
198+
options
199+
);
200+
} catch (error) {
201+
const restError = new RestError(
202+
`Error ${error} occurred in deserializing the responseBody - ${parsedResponse.bodyAsText}`,
203+
undefined,
204+
parsedResponse.status,
205+
parsedResponse.request,
206+
parsedResponse
207+
);
208+
throw restError;
209+
}
210+
} else if (operationSpec.httpMethod === "HEAD") {
211+
// head methods never have a body, but we return a boolean to indicate presence/absence of the resource
212+
parsedResponse.parsedBody = response.status >= 200 && response.status < 300;
175213
}
176-
try {
177-
parsedResponse.parsedBody = operationSpec.serializer.deserialize(
178-
responseSpec.bodyMapper,
179-
valueToDeserialize,
180-
"operationRes.parsedBody"
181-
);
182-
} catch (error) {
183-
const restError = new RestError(
184-
`Error ${error} occurred in deserializing the responseBody - ${parsedResponse.bodyAsText}`,
185-
undefined,
186-
parsedResponse.status,
187-
parsedResponse.request,
188-
parsedResponse
214+
215+
if (responseSpec.headersMapper) {
216+
parsedResponse.parsedHeaders = operationSpec.serializer.deserialize(
217+
responseSpec.headersMapper,
218+
parsedResponse.headers.rawHeaders(),
219+
"operationRes.parsedHeaders",
220+
options
189221
);
190-
throw restError;
191222
}
192-
} else if (operationSpec.httpMethod === "HEAD") {
193-
// head methods never have a body, but we return a boolean to indicate presence/absence of the resource
194-
parsedResponse.parsedBody = response.status >= 200 && response.status < 300;
195223
}
196224

197-
if (responseSpec.headersMapper) {
198-
parsedResponse.parsedHeaders = operationSpec.serializer.deserialize(
199-
responseSpec.headersMapper,
200-
parsedResponse.headers.rawHeaders(),
201-
"operationRes.parsedHeaders"
202-
);
203-
}
225+
return parsedResponse;
204226
}
205-
206-
return parsedResponse;
207-
});
227+
);
208228
}
209229

210230
function isOperationSpecEmpty(operationSpec: OperationSpec): boolean {
@@ -305,7 +325,8 @@ function handleErrorResponse(
305325
function parse(
306326
jsonContentTypes: string[],
307327
xmlContentTypes: string[],
308-
operationResponse: HttpOperationResponse
328+
operationResponse: HttpOperationResponse,
329+
opts: Required<SerializerOptions>
309330
): Promise<HttpOperationResponse> {
310331
const errorHandler = (err: Error & { code: string }): Promise<never> => {
311332
const msg = `Error "${err}" occurred while parsing the response body - ${operationResponse.bodyAsText}.`;
@@ -335,7 +356,7 @@ function parse(
335356
resolve(operationResponse);
336357
}).catch(errorHandler);
337358
} else if (contentComponents.some((component) => xmlContentTypes.indexOf(component) !== -1)) {
338-
return parseXML(text)
359+
return parseXML(text, opts)
339360
.then((body) => {
340361
operationResponse.parsedBody = body;
341362
return operationResponse;

0 commit comments

Comments
 (0)