diff --git a/sdk/core/core-client/review/core-client.api.md b/sdk/core/core-client/review/core-client.api.md index 1681e51a5672..e647e4e5be33 100644 --- a/sdk/core/core-client/review/core-client.api.md +++ b/sdk/core/core-client/review/core-client.api.md @@ -17,30 +17,20 @@ import { TransferProgressEvent } from '@azure/core-https'; // @public (undocumented) export interface BaseMapper { - // (undocumented) constraints?: MapperConstraints; - // (undocumented) defaultValue?: any; - // (undocumented) isConstant?: boolean; - // (undocumented) nullable?: boolean; - // (undocumented) readOnly?: boolean; - // (undocumented) required?: boolean; - // (undocumented) serializedName?: string; - // (undocumented) type: MapperType; - // (undocumented) xmlElementName?: string; - // (undocumented) xmlIsAttribute?: boolean; - // (undocumented) xmlIsWrapped?: boolean; - // (undocumented) xmlName?: string; + xmlNamespace?: string; + xmlNamespacePrefix?: string; } // @public (undocumented) diff --git a/sdk/core/core-client/src/interfaces.ts b/sdk/core/core-client/src/interfaces.ts index dd38df47b315..f2e29f0da31c 100644 --- a/sdk/core/core-client/src/interfaces.ts +++ b/sdk/core/core-client/src/interfaces.ts @@ -375,17 +375,61 @@ export interface EnumMapperType { } export interface BaseMapper { + /** + * Name for the xml element + */ xmlName?: string; + /** + * Xml element namespace + */ + xmlNamespace?: string; + /** + * Xml element namespace prefix + */ + xmlNamespacePrefix?: string; + /** + * Determines if the current property should be serialized as an attribute of the parent xml element + */ xmlIsAttribute?: boolean; + /** + * Name for the xml elements when serializing an array + */ xmlElementName?: string; + /** + * Whether or not the current propery should have a wrapping XML element + */ xmlIsWrapped?: boolean; + /** + * Whether or not the current propery is readonly + */ readOnly?: boolean; + /** + * Whether or not the current propery is a constant + */ isConstant?: boolean; + /** + * Whether or not the current propery is required + */ required?: boolean; + /** + * Whether or not the current propery allows mull as a value + */ nullable?: boolean; + /** + * The name to use when serializing + */ serializedName?: string; + /** + * Type of the mapper + */ type: MapperType; + /** + * Default value when one is not explicitly provided + */ defaultValue?: any; + /** + * Constraints to test the current value against + */ constraints?: MapperConstraints; } diff --git a/sdk/core/core-client/src/serializer.ts b/sdk/core/core-client/src/serializer.ts index d63a83efe684..82cb24707674 100644 --- a/sdk/core/core-client/src/serializer.ts +++ b/sdk/core/core-client/src/serializer.ts @@ -153,11 +153,29 @@ class SerializerImpl implements Serializer { } else if (mapperType.match(/^Base64Url$/i) !== null) { payload = serializeBase64UrlType(objectName, object); } else if (mapperType.match(/^Sequence$/i) !== null) { - payload = serializeSequenceType(this, mapper as SequenceMapper, object, objectName); + payload = serializeSequenceType( + this, + mapper as SequenceMapper, + object, + objectName, + Boolean(this.isXML) + ); } else if (mapperType.match(/^Dictionary$/i) !== null) { - payload = serializeDictionaryType(this, mapper as DictionaryMapper, object, objectName); + payload = serializeDictionaryType( + this, + mapper as DictionaryMapper, + object, + objectName, + Boolean(this.isXML) + ); } else if (mapperType.match(/^Composite$/i) !== null) { - payload = serializeCompositeType(this, mapper as CompositeMapper, object, objectName); + payload = serializeCompositeType( + this, + mapper as CompositeMapper, + object, + objectName, + Boolean(this.isXML) + ); } } return payload; @@ -481,7 +499,8 @@ function serializeSequenceType( serializer: Serializer, mapper: SequenceMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): any { if (!Array.isArray(object)) { throw new Error(`${objectName} must be of type Array.`); @@ -495,7 +514,19 @@ function serializeSequenceType( } const tempArray = []; for (let i = 0; i < object.length; i++) { - tempArray[i] = serializer.serialize(elementType, object[i], objectName); + const serializedValue = serializer.serialize(elementType, object[i], objectName); + if (isXml && elementType.xmlNamespace) { + const xmlnsKey = elementType.xmlNamespacePrefix + ? `xmlns:${elementType.xmlNamespacePrefix}` + : "xmlns"; + if (elementType.type.name === "Composite") { + tempArray[i] = { ...serializedValue, $: { [xmlnsKey]: elementType.xmlNamespace } }; + } else { + tempArray[i] = { _: serializedValue, $: { [xmlnsKey]: elementType.xmlNamespace } }; + } + } else { + tempArray[i] = serializedValue; + } } return tempArray; } @@ -504,7 +535,8 @@ function serializeDictionaryType( serializer: Serializer, mapper: DictionaryMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): any { if (typeof object !== "object") { throw new Error(`${objectName} must be of type object.`); @@ -518,8 +550,18 @@ function serializeDictionaryType( } const tempDictionary: { [key: string]: any } = {}; for (const key of Object.keys(object)) { - tempDictionary[key] = serializer.serialize(valueType, object[key], objectName + "." + key); + const serializedValue = serializer.serialize(valueType, object[key], objectName); + // If the element needs an XML namespace we need to add it within the $ property + tempDictionary[key] = getXmlObjectValue(valueType, serializedValue, isXml); } + + // Add the namespace to the root element if needed + if (isXml && mapper.xmlNamespace) { + const xmlnsKey = mapper.xmlNamespacePrefix ? `xmlns:${mapper.xmlNamespacePrefix}` : "xmlns"; + + return { ...tempDictionary, $: { [xmlnsKey]: mapper.xmlNamespace } }; + } + return tempDictionary; } @@ -568,7 +610,8 @@ function serializeCompositeType( serializer: Serializer, mapper: CompositeMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): any { if (getPolymorphicDiscriminatorRecursively(serializer, mapper)) { mapper = getPolymorphicMapper(serializer, mapper, object, "clientName"); @@ -609,6 +652,12 @@ function serializeCompositeType( } if (parentObject !== undefined && parentObject !== null) { + if (isXml && mapper.xmlNamespace) { + const xmlnsKey = mapper.xmlNamespacePrefix + ? `xmlns:${mapper.xmlNamespacePrefix}` + : "xmlns"; + parentObject.$ = { ...parentObject.$, [xmlnsKey]: mapper.xmlNamespace }; + } const propertyObjectName = propertyMapper.serializedName !== "" ? objectName + "." + propertyMapper.serializedName @@ -630,16 +679,17 @@ function serializeCompositeType( propertyObjectName ); if (serializedValue !== undefined && propName !== undefined && propName !== null) { - if (propertyMapper.xmlIsAttribute) { + const value = getXmlObjectValue(propertyMapper, serializedValue, isXml); + if (isXml && propertyMapper.xmlIsAttribute) { // $ is the key attributes are kept under in xml2js. // This keeps things simple while preventing name collision // with names in user documents. parentObject.$ = parentObject.$ || {}; parentObject.$[propName] = serializedValue; - } else if (propertyMapper.xmlIsWrapped) { - parentObject[propName] = { [propertyMapper.xmlElementName!]: serializedValue }; + } else if (isXml && propertyMapper.xmlIsWrapped) { + parentObject[propName] = { [propertyMapper.xmlElementName!]: value }; } else { - parentObject[propName] = serializedValue; + parentObject[propName] = value; } } } @@ -665,6 +715,22 @@ function serializeCompositeType( return object; } +function getXmlObjectValue(propertyMapper: Mapper, serializedValue: any, isXml: boolean) { + if (!isXml || !propertyMapper.xmlNamespace) { + return serializedValue; + } + + const xmlnsKey = propertyMapper.xmlNamespacePrefix + ? `xmlns:${propertyMapper.xmlNamespacePrefix}` + : "xmlns"; + const xmlNamespace = { [xmlnsKey]: propertyMapper.xmlNamespace }; + + if (["Composite"].includes(propertyMapper.type.name)) { + return { $: xmlNamespace, ...serializedValue }; + } + return { _: serializedValue, $: xmlNamespace }; +} + function isSpecialXmlProperty(propertyName: string): boolean { return ["$", "_"].includes(propertyName); } diff --git a/sdk/core/core-client/src/serviceClient.ts b/sdk/core/core-client/src/serviceClient.ts index a8d761d767e7..fba4f29eed04 100644 --- a/sdk/core/core-client/src/serviceClient.ts +++ b/sdk/core/core-client/src/serviceClient.ts @@ -250,7 +250,14 @@ export function serializeRequestBody( ); const bodyMapper = operationSpec.requestBody.mapper; - const { required, serializedName, xmlName, xmlElementName } = bodyMapper; + const { + required, + serializedName, + xmlName, + xmlElementName, + xmlNamespace, + xmlNamespacePrefix + } = bodyMapper; const typeName = bodyMapper.type.name; try { @@ -267,13 +274,21 @@ export function serializeRequestBody( const isStream = typeName === MapperTypeNames.Stream; if (operationSpec.isXML) { + const xmlnsKey = xmlNamespacePrefix ? `xmlns:${xmlNamespacePrefix}` : "xmlns"; + const value = getXmlValueWithNamespace(xmlNamespace, xmlnsKey, typeName, request.body); + if (typeName === MapperTypeNames.Sequence) { request.body = stringifyXML( - prepareXMLRootList(request.body, xmlElementName || xmlName || serializedName!), + prepareXMLRootList( + value, + xmlElementName || xmlName || serializedName!, + xmlnsKey, + xmlNamespace + ), { rootName: xmlName || serializedName } ); } else if (!isStream) { - request.body = stringifyXML(request.body, { + request.body = stringifyXML(value, { rootName: xmlName || serializedName }); } @@ -317,6 +332,24 @@ export function serializeRequestBody( } } +/** + * Adds an xml namespace to the xml serialized object if needed, otherwise it just returns the value itself + */ +function getXmlValueWithNamespace( + xmlNamespace: string | undefined, + xmlnsKey: string, + typeName: string, + serializedValue: any +): any { + // Composite and Sequence schemas already got their root namespace set during serialization + // We just need to add xmlns to the other schema types + if (xmlNamespace && !["Composite", "Sequence", "Dictionary"].includes(typeName)) { + return { _: serializedValue, $: { [xmlnsKey]: xmlNamespace } }; + } + + return serializedValue; +} + function createDefaultPipeline( options: { baseUri?: string; credential?: TokenCredential } = {} ): Pipeline { @@ -338,11 +371,20 @@ function createDefaultPipeline( return pipeline; } -function prepareXMLRootList(obj: any, elementName: string): { [key: string]: any[] } { +function prepareXMLRootList( + obj: any, + elementName: string, + xmlNamespaceKey?: string, + xmlNamespace?: string +): { [key: string]: any[] } { if (!Array.isArray(obj)) { obj = [obj]; } - return { [elementName]: obj }; + if (!xmlNamespaceKey || !xmlNamespace) { + return { [elementName]: obj }; + } + + return { [elementName]: obj, $: { [xmlNamespaceKey]: xmlNamespace } }; } function flattenResponse( diff --git a/sdk/core/core-client/test/serviceClient.spec.ts b/sdk/core/core-client/test/serviceClient.spec.ts index 5eb10b91bfd7..e7c9984aafc1 100644 --- a/sdk/core/core-client/test/serviceClient.spec.ts +++ b/sdk/core/core-client/test/serviceClient.spec.ts @@ -25,6 +25,7 @@ import { stringifyXML } from "@azure/core-xml"; import { serializeRequestBody } from "../src/serviceClient"; import { getOperationArgumentValueFromParameter } from "../src/operationHelpers"; import { deserializationPolicy } from "../src/deserializationPolicy"; +import { Mappers } from "./testMappers"; describe("ServiceClient", function() { it("should serialize headerCollectionPrefix", async function() { @@ -189,7 +190,7 @@ describe("ServiceClient", function() { 200: { bodyMapper: { type: { - name: "Sequence", + name: MapperTypeNames.Sequence, element: { type: { name: "Number" @@ -233,6 +234,34 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, `"body value"`); }); + it("should serialize a JSON String request body with namespace, ignoring namespace", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "https://example.com", + xmlNamespacePrefix: "foo", + serializedName: "bodyArg", + type: { + name: "String" + } + } + }, + responses: { 200: {} }, + serializer: createSerializer() + } + ); + assert.strictEqual(httpRequest.body, `"body value"`); + }); + it("should serialize a JSON ByteArray request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( @@ -259,6 +288,34 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, `"SmF2YXNjcmlwdA=="`); }); + it("should serialize a JSON ByteArray request body, ignoring xml properties", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "https://microsoft.com", + xmlNamespacePrefix: "test", + serializedName: "bodyArg", + type: { + name: MapperTypeNames.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: createSerializer() + } + ); + assert.strictEqual(httpRequest.body, `"SmF2YXNjcmlwdA=="`); + }); + it("should serialize a JSON Stream request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( @@ -286,6 +343,35 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, "body value"); }); + it("should serialize a JSON Stream request body", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "http://microsoft.com", + xmlNamespacePrefix: "test", + serializedName: "bodyArg", + type: { + name: MapperTypeNames.Stream + } + } + }, + responses: { 200: {} }, + serializer: createSerializer() + }, + stringifyXML + ); + assert.strictEqual(httpRequest.body, "body value"); + }); + it("should serialize an XML String request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( @@ -317,6 +403,38 @@ describe("ServiceClient", function() { ); }); + it("should serialize an XML String request body, with namespace", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + type: { + name: MapperTypeNames.String + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `body value` + ); + }); + it("should serialize an XML ByteArray request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( @@ -337,7 +455,7 @@ describe("ServiceClient", function() { } }, responses: { 200: {} }, - serializer: createSerializer(), + serializer: createSerializer(undefined, true /** isXML */), isXML: true }, stringifyXML @@ -368,7 +486,7 @@ describe("ServiceClient", function() { } }, responses: { 200: {} }, - serializer: createSerializer(), + serializer: createSerializer(undefined, true /** isXML */), isXML: true }, stringifyXML @@ -376,6 +494,420 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, "body value"); }); + it("should serialize an XML Stream request body, with namespace", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "https://microsoft.com", + serializedName: "bodyArg", + type: { + name: MapperTypeNames.Stream + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual(httpRequest.body, "body value"); + }); + + it("should serialize an XML ByteArray request body with namespace and prefix", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + xmlNamespace: "https://microsoft.com", + xmlNamespacePrefix: "sample", + required: true, + serializedName: "bodyArg", + type: { + name: MapperTypeNames.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `SmF2YXNjcmlwdA==` + ); + }); + + it("should serialize an XML Composite request body", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + requestBody: { + updated: new Date("2020-08-12T23:36:18.308Z"), + content: { type: "application/xml", queueDescription: { maxDeliveryCount: 15 } } + } + }, + { + httpMethod: "POST", + requestBody: Mappers.requestBody1, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `2020-08-12T23:36:18.308Z15` + ); + }); + + it("should serialize a JSON Composite request body, ignoring XML metadata", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + requestBody: { + updated: new Date("2020-08-12T23:36:18.308Z"), + content: { type: "application/xml", queueDescription: { maxDeliveryCount: 15 } } + } + }, + { + httpMethod: "POST", + requestBody: Mappers.requestBody1, + responses: { 200: {} }, + serializer: createSerializer() + } + ); + + assert.deepEqual( + httpRequest.body, + '{"updated":"2020-08-12T23:36:18.308Z","content":{"type":"application/xml","queueDescription":{"maxDeliveryCount":15}}}' + ); + }); + + it("should serialize an XML Array request body with namespace and prefix", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: ["Foo", "Bar"] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperTypeNames.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { name: "String" } + } + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `FooBar` + ); + }); + + it("should serialize a JSON Array request body, ignoring XML metadata", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: ["Foo", "Bar"] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperTypeNames.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { name: "String" } + } + } + } + }, + responses: { 200: {} }, + serializer: createSerializer() + } + ); + assert.deepEqual(httpRequest.body, JSON.stringify(["Foo", "Bar"])); + }); + + it("should serialize an XML Array of composite elements, namespace and prefix", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: [ + { foo: "Foo1", bar: "Bar1" }, + { foo: "Foo2", bar: "Bar2" }, + { foo: "Foo3", bar: "Bar3" } + ] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperTypeNames.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { + name: "Composite", + modelProperties: { + foo: { + serializedName: "foo", + xmlNamespace: "https://microsoft.com/foo", + xmlName: "Foo", + type: { + name: "String" + } + }, + bar: { + xmlNamespacePrefix: "bar", + xmlNamespace: "https://microsoft.com/bar", + xmlName: "Bar", + serializedName: "bar", + type: { + name: "String" + } + } + } + } + } + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `Foo1Bar1Foo2Bar2Foo3Bar3` + ); + }); + + it("should serialize an XML Composite request body with namespace and prefix", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + bodyArg: { foo: "Foo", bar: "Bar" } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + type: { + name: MapperTypeNames.Composite, + modelProperties: { + foo: { + serializedName: "foo", + xmlNamespace: "https://microsoft.com/foo", + xmlName: "Foo", + type: { + name: "String" + } + }, + bar: { + xmlNamespacePrefix: "bar", + xmlNamespace: "https://microsoft.com/bar", + xmlName: "Bar", + serializedName: "bar", + type: { + name: "String" + } + } + } + } + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `FooBar` + ); + }); + + it("should serialize an XML Dictionary request body", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `helloworld` + ); + }); + + it("should serialize an XML Dictionary request body, with namespace and prefix", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + xmlNamespacePrefix: "sample", + xmlNamespace: "https://microsoft.com", + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + xmlNamespacePrefix: "el", + xmlNamespace: "https://microsoft.com/element", + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: createSerializer(undefined, true /** isXML */), + isXML: true + }, + stringifyXML + ); + assert.strictEqual( + httpRequest.body, + `helloworld` + ); + }); + + it("should serialize a JSON Dictionary request body, ignoring xml metadata", () => { + const httpRequest = createPipelineRequest({ url: "https://example.com" }); + serializeRequestBody( + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + xmlNamespacePrefix: "sample", + xmlNamespace: "https://microsoft.com", + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + xmlNamespacePrefix: "el", + xmlNamespace: "https://microsoft.com/element", + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: createSerializer() + }, + stringifyXML + ); + assert.deepEqual(httpRequest.body, `{"alpha":"hello","beta":"world"}`); + }); + it("should serialize a string send to a text/plain endpoint as just a string", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( diff --git a/sdk/core/core-client/test/testMappers.ts b/sdk/core/core-client/test/testMappers.ts index 1db4895d63a9..88022ffa533e 100644 --- a/sdk/core/core-client/test/testMappers.ts +++ b/sdk/core/core-client/test/testMappers.ts @@ -1,5 +1,268 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. + +import { CompositeMapper } from "../src/interfaces"; + +const QueueDescription: CompositeMapper = { + serializedName: "QueueDescription", + xmlName: "QueueDescription", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Composite", + className: "QueueDescription", + modelProperties: { + lockDuration: { + serializedName: "lockDuration", + xmlName: "LockDuration", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + maxSizeInMegabytes: { + serializedName: "maxSizeInMegabytes", + xmlName: "MaxSizeInMegabytes", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + requiresDuplicateDetection: { + serializedName: "requiresDuplicateDetection", + xmlName: "RequiresDuplicateDetection", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + requiresSession: { + serializedName: "requiresSession", + xmlName: "RequiresSession", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + defaultMessageTimeToLive: { + serializedName: "defaultMessageTimeToLive", + xmlName: "DefaultMessageTimeToLive", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + deadLetteringOnMessageExpiration: { + serializedName: "deadLetteringOnMessageExpiration", + xmlName: "DeadLetteringOnMessageExpiration", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + duplicateDetectionHistoryTimeWindow: { + serializedName: "duplicateDetectionHistoryTimeWindow", + xmlName: "DuplicateDetectionHistoryTimeWindow", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + maxDeliveryCount: { + serializedName: "maxDeliveryCount", + xmlName: "MaxDeliveryCount", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + enableBatchedOperations: { + serializedName: "enableBatchedOperations", + xmlName: "EnableBatchedOperations", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + sizeInBytes: { + serializedName: "sizeInBytes", + xmlName: "SizeInBytes", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + messageCount: { + serializedName: "messageCount", + xmlName: "MessageCount", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + isAnonymousAccessible: { + serializedName: "isAnonymousAccessible", + xmlName: "IsAnonymousAccessible", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + status: { + serializedName: "status", + xmlName: "Status", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + forwardTo: { + serializedName: "forwardTo", + xmlName: "ForwardTo", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + userMetadata: { + serializedName: "userMetadata", + xmlName: "UserMetadata", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + createdAt: { + serializedName: "createdAt", + xmlName: "CreatedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + updatedAt: { + serializedName: "updatedAt", + xmlName: "UpdatedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + accessedAt: { + serializedName: "accessedAt", + xmlName: "AccessedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + supportOrdering: { + serializedName: "supportOrdering", + xmlName: "SupportOrdering", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + autoDeleteOnIdle: { + serializedName: "autoDeleteOnIdle", + xmlName: "AutoDeleteOnIdle", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + enablePartitioning: { + serializedName: "enablePartitioning", + xmlName: "EnablePartitioning", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + entityAvailabilityStatus: { + serializedName: "entityAvailabilityStatus", + xmlName: "EntityAvailabilityStatus", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + enableExpress: { + serializedName: "enableExpress", + xmlName: "EnableExpress", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + forwardDeadLetteredMessagesTo: { + serializedName: "forwardDeadLetteredMessagesTo", + xmlName: "ForwardDeadLetteredMessagesTo", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + } + } + } +}; + +const CreateQueueBodyContent: CompositeMapper = { + serializedName: "CreateQueueBodyContent", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "Composite", + className: "CreateQueueBodyContent", + modelProperties: { + type: { + defaultValue: "application/xml", + serializedName: "type", + xmlName: "type", + xmlIsAttribute: true, + type: { + name: "String" + } + }, + queueDescription: { + serializedName: "queueDescription", + xmlName: "QueueDescription", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + ...QueueDescription.type + } + } + } + } +}; + +const CreateQueueBody: CompositeMapper = { + serializedName: "CreateQueueBody", + xmlName: "entry", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "Composite", + className: "CreateQueueBody", + modelProperties: { + updated: { + serializedName: "updated", + xmlName: "updated", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "DateTime" + } + }, + content: { + serializedName: "content", + xmlName: "content", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + ...CreateQueueBodyContent.type + } + } + } + } +}; + const internalMappers: any = {}; internalMappers.SimpleProduct = { @@ -750,4 +1013,9 @@ internalMappers.discriminators = { "Pet.Dog": internalMappers.Dog }; +internalMappers.requestBody1 = { + parameterPath: "requestBody", + mapper: CreateQueueBody +}; + export const Mappers = internalMappers; diff --git a/sdk/core/core-http/review/core-http.api.md b/sdk/core/core-http/review/core-http.api.md index ec6cd89c7816..0586887a7575 100644 --- a/sdk/core/core-http/review/core-http.api.md +++ b/sdk/core/core-http/review/core-http.api.md @@ -54,30 +54,20 @@ export type Authenticator = (challenge: object) => Promise; // @public (undocumented) export interface BaseMapper { - // (undocumented) constraints?: MapperConstraints; - // (undocumented) defaultValue?: any; - // (undocumented) isConstant?: boolean; - // (undocumented) nullable?: boolean; - // (undocumented) readOnly?: boolean; - // (undocumented) required?: boolean; - // (undocumented) serializedName?: string; - // (undocumented) type: MapperType; - // (undocumented) xmlElementName?: string; - // (undocumented) xmlIsAttribute?: boolean; - // (undocumented) xmlIsWrapped?: boolean; - // (undocumented) xmlName?: string; + xmlNamespace?: string; + xmlNamespacePrefix?: string; } // @public (undocumented) diff --git a/sdk/core/core-http/src/serializer.ts b/sdk/core/core-http/src/serializer.ts index 1b32f489f496..685f4d9f076d 100644 --- a/sdk/core/core-http/src/serializer.ts +++ b/sdk/core/core-http/src/serializer.ts @@ -144,11 +144,29 @@ export class Serializer { } else if (mapperType.match(/^Base64Url$/i) !== null) { payload = serializeBase64UrlType(objectName, object); } else if (mapperType.match(/^Sequence$/i) !== null) { - payload = serializeSequenceType(this, mapper as SequenceMapper, object, objectName); + payload = serializeSequenceType( + this, + mapper as SequenceMapper, + object, + objectName, + Boolean(this.isXML) + ); } else if (mapperType.match(/^Dictionary$/i) !== null) { - payload = serializeDictionaryType(this, mapper as DictionaryMapper, object, objectName); + payload = serializeDictionaryType( + this, + mapper as DictionaryMapper, + object, + objectName, + Boolean(this.isXML) + ); } else if (mapperType.match(/^Composite$/i) !== null) { - payload = serializeCompositeType(this, mapper as CompositeMapper, object, objectName); + payload = serializeCompositeType( + this, + mapper as CompositeMapper, + object, + objectName, + Boolean(this.isXML) + ); } } return payload; @@ -352,6 +370,7 @@ function serializeBasicTypes(typeName: string, objectName: string, value: any): } } } + return value; } @@ -462,7 +481,8 @@ function serializeSequenceType( serializer: Serializer, mapper: SequenceMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): any[] { if (!Array.isArray(object)) { throw new Error(`${objectName} must be of type Array.`); @@ -476,7 +496,20 @@ function serializeSequenceType( } const tempArray = []; for (let i = 0; i < object.length; i++) { - tempArray[i] = serializer.serialize(elementType, object[i], objectName); + const serializedValue = serializer.serialize(elementType, object[i], objectName); + + if (isXml && elementType.xmlNamespace) { + const xmlnsKey = elementType.xmlNamespacePrefix + ? `xmlns:${elementType.xmlNamespacePrefix}` + : "xmlns"; + if (elementType.type.name === "Composite") { + tempArray[i] = { ...serializedValue, $: { [xmlnsKey]: elementType.xmlNamespace } }; + } else { + tempArray[i] = { _: serializedValue, $: { [xmlnsKey]: elementType.xmlNamespace } }; + } + } else { + tempArray[i] = serializedValue; + } } return tempArray; } @@ -485,7 +518,8 @@ function serializeDictionaryType( serializer: Serializer, mapper: DictionaryMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): { [key: string]: any } { if (typeof object !== "object") { throw new Error(`${objectName} must be of type object.`); @@ -499,8 +533,18 @@ function serializeDictionaryType( } const tempDictionary: { [key: string]: any } = {}; for (const key of Object.keys(object)) { - tempDictionary[key] = serializer.serialize(valueType, object[key], objectName + "." + key); + const serializedValue = serializer.serialize(valueType, object[key], objectName); + // If the element needs an XML namespace we need to add it within the $ property + tempDictionary[key] = getXmlObjectValue(valueType, serializedValue, isXml); } + + // Add the namespace to the root element if needed + if (isXml && mapper.xmlNamespace) { + const xmlnsKey = mapper.xmlNamespacePrefix ? `xmlns:${mapper.xmlNamespacePrefix}` : "xmlns"; + + return { ...tempDictionary, $: { [xmlnsKey]: mapper.xmlNamespace } }; + } + return tempDictionary; } @@ -549,7 +593,8 @@ function serializeCompositeType( serializer: Serializer, mapper: CompositeMapper, object: any, - objectName: string + objectName: string, + isXml: boolean ): any { if (getPolymorphicDiscriminatorRecursively(serializer, mapper)) { mapper = getPolymorphicMapper(serializer, mapper, object, "clientName"); @@ -589,6 +634,12 @@ function serializeCompositeType( } if (parentObject != undefined) { + if (isXml && mapper.xmlNamespace) { + const xmlnsKey = mapper.xmlNamespacePrefix + ? `xmlns:${mapper.xmlNamespacePrefix}` + : "xmlns"; + parentObject.$ = { ...parentObject.$, [xmlnsKey]: mapper.xmlNamespace }; + } const propertyObjectName = propertyMapper.serializedName !== "" ? objectName + "." + propertyMapper.serializedName @@ -609,17 +660,19 @@ function serializeCompositeType( toSerialize, propertyObjectName ); + if (serializedValue !== undefined && propName != undefined) { - if (propertyMapper.xmlIsAttribute) { + const value = getXmlObjectValue(propertyMapper, serializedValue, isXml); + if (isXml && propertyMapper.xmlIsAttribute) { // $ is the key attributes are kept under in xml2js. // This keeps things simple while preventing name collision // with names in user documents. parentObject.$ = parentObject.$ || {}; parentObject.$[propName] = serializedValue; - } else if (propertyMapper.xmlIsWrapped) { - parentObject[propName] = { [propertyMapper.xmlElementName!]: serializedValue }; + } else if (isXml && propertyMapper.xmlIsWrapped) { + parentObject[propName] = { [propertyMapper.xmlElementName!]: value }; } else { - parentObject[propName] = serializedValue; + parentObject[propName] = value; } } } @@ -645,6 +698,22 @@ function serializeCompositeType( return object; } +function getXmlObjectValue(propertyMapper: Mapper, serializedValue: any, isXml: boolean) { + if (!isXml || !propertyMapper.xmlNamespace) { + return serializedValue; + } + + const xmlnsKey = propertyMapper.xmlNamespacePrefix + ? `xmlns:${propertyMapper.xmlNamespacePrefix}` + : "xmlns"; + const xmlNamespace = { [xmlnsKey]: propertyMapper.xmlNamespace }; + + if (["Composite"].includes(propertyMapper.type.name)) { + return { $: xmlNamespace, ...serializedValue }; + } + return { _: serializedValue, $: xmlNamespace }; +} + function isSpecialXmlProperty(propertyName: string): boolean { return ["$", "_"].includes(propertyName); } @@ -960,17 +1029,61 @@ export interface EnumMapperType { } export interface BaseMapper { + /** + * Name for the xml element + */ xmlName?: string; + /** + * Xml element namespace + */ + xmlNamespace?: string; + /** + * Xml element namespace prefix + */ + xmlNamespacePrefix?: string; + /** + * Determines if the current property should be serialized as an attribute of the parent xml element + */ xmlIsAttribute?: boolean; + /** + * Name for the xml elements when serializing an array + */ xmlElementName?: string; + /** + * Whether or not the current propery should have a wrapping XML element + */ xmlIsWrapped?: boolean; + /** + * Whether or not the current propery is readonly + */ readOnly?: boolean; + /** + * Whether or not the current propery is a constant + */ isConstant?: boolean; + /** + * Whether or not the current propery is required + */ required?: boolean; + /** + * Whether or not the current propery allows mull as a value + */ nullable?: boolean; + /** + * The name to use when serializing + */ serializedName?: string; + /** + * Type of the mapper + */ type: MapperType; + /** + * Default value when one is not explicitly provided + */ defaultValue?: any; + /** + * Constraints to test the current value against + */ constraints?: MapperConstraints; } diff --git a/sdk/core/core-http/src/serviceClient.ts b/sdk/core/core-http/src/serviceClient.ts index 1bed817c86d4..4f67810a9f6f 100644 --- a/sdk/core/core-http/src/serviceClient.ts +++ b/sdk/core/core-http/src/serviceClient.ts @@ -538,7 +538,14 @@ export function serializeRequestBody( ); const bodyMapper = operationSpec.requestBody.mapper; - const { required, xmlName, xmlElementName, serializedName } = bodyMapper; + const { + required, + xmlName, + xmlElementName, + serializedName, + xmlNamespace, + xmlNamespacePrefix + } = bodyMapper; const typeName = bodyMapper.type.name; try { @@ -555,16 +562,25 @@ export function serializeRequestBody( const isStream = typeName === MapperType.Stream; if (operationSpec.isXML) { + const xmlnsKey = xmlNamespacePrefix ? `xmlns:${xmlNamespacePrefix}` : "xmlns"; + const value = getXmlValueWithNamespace( + xmlNamespace, + xmlnsKey, + typeName, + httpRequest.body + ); if (typeName === MapperType.Sequence) { httpRequest.body = stringifyXML( utils.prepareXMLRootList( - httpRequest.body, - xmlElementName || xmlName || serializedName! + value, + xmlElementName || xmlName || serializedName!, + xmlnsKey, + xmlNamespace ), { rootName: xmlName || serializedName } ); } else if (!isStream) { - httpRequest.body = stringifyXML(httpRequest.body, { + httpRequest.body = stringifyXML(value, { rootName: xmlName || serializedName }); } @@ -610,6 +626,24 @@ export function serializeRequestBody( } } +/** + * Adds an xml namespace to the xml serialized object if needed, otherwise it just returns the value itself + */ +function getXmlValueWithNamespace( + xmlNamespace: string | undefined, + xmlnsKey: string, + typeName: string, + serializedValue: any +): any { + // Composite and Sequence schemas already got their root namespace set during serialization + // We just need to add xmlns to the other schema types + if (xmlNamespace && !["Composite", "Sequence", "Dictionary"].includes(typeName)) { + return { _: serializedValue, $: { [xmlnsKey]: xmlNamespace } }; + } + + return serializedValue; +} + function getValueOrFunctionResult( value: undefined | string | ((defaultValue: string) => string), defaultValueCreator: () => string diff --git a/sdk/core/core-http/src/util/utils.ts b/sdk/core/core-http/src/util/utils.ts index 8c116895286d..10cdc1a92363 100644 --- a/sdk/core/core-http/src/util/utils.ts +++ b/sdk/core/core-http/src/util/utils.ts @@ -189,11 +189,21 @@ export function promiseToServiceCallback(promise: Promise { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "https://example.com", + xmlNamespacePrefix: "foo", + serializedName: "bodyArg", + type: { + name: MapperType.String + } + } + }, + responses: { 200: {} }, + serializer: new Serializer() + } + ); + assert.strictEqual(httpRequest.body, `"body value"`); + }); + it("should serialize a JSON ByteArray request body", () => { const httpRequest = new WebResource(); serializeRequestBody( @@ -509,6 +539,33 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, `"SmF2YXNjcmlwdA=="`); }); + it("should serialize a JSON ByteArray request body with namespace, ignoring xml properties", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + type: { + name: MapperType.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, false /** isXML */) + } + ); + assert.strictEqual(httpRequest.body, `"SmF2YXNjcmlwdA=="`); + }); + it("should serialize a JSON Stream request body", () => { const httpRequest = new WebResource(); serializeRequestBody( @@ -536,6 +593,34 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, "body value"); }); + it("should serialize a JSON Stream request body, ignore namespace", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "http://microsoft.com", + serializedName: "bodyArg", + type: { + name: MapperType.Stream + } + } + }, + responses: { 200: {} }, + serializer: new Serializer() + } + ); + assert.strictEqual(httpRequest.body, "body value"); + }); + it("should serialize an XML String request body", () => { const httpRequest = new WebResource(); serializeRequestBody( @@ -567,6 +652,38 @@ describe("ServiceClient", function() { ); }); + it("should serialize an XML String request body with namespace", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + type: { + name: MapperType.String + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true /** isXML*/), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `body value` + ); + }); + it("should serialize an XML ByteArray request body", () => { const httpRequest = new WebResource(); serializeRequestBody( @@ -588,7 +705,7 @@ describe("ServiceClient", function() { } }, responses: { 200: {} }, - serializer: new Serializer(), + serializer: new Serializer(undefined, true /** isXml */), isXML: true } ); @@ -598,6 +715,453 @@ describe("ServiceClient", function() { ); }); + it("should serialize an XML Dictionary request body", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true /** isXml */), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `helloworld` + ); + }); + + it("should serialize an XML Dictionary request body, with namespace and prefix", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + xmlNamespacePrefix: "sample", + xmlNamespace: "https://microsoft.com", + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + xmlNamespacePrefix: "el", + xmlNamespace: "https://microsoft.com/element", + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true /** isXml */), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `helloworld` + ); + }); + + it("should serialize a JSON Dictionary request body, ignoring xml metadata", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + metadata: { + alpha: "hello", + beta: "world" + } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "metadata", + mapper: { + xmlNamespacePrefix: "sample", + xmlNamespace: "https://microsoft.com", + serializedName: "metadata", + type: { + name: "Dictionary", + value: { + xmlNamespacePrefix: "el", + xmlNamespace: "https://microsoft.com/element", + type: { + name: "String" + } + } + }, + headerCollectionPrefix: "foo-bar-" + } + }, + responses: { 200: {} }, + serializer: new Serializer() + } + ); + assert.deepEqual(httpRequest.body, `{"alpha":"hello","beta":"world"}`); + }); + + it("should serialize a Json ByteArray request body, ignoring namespace", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + xmlNamespace: "https://google.com", + serializedName: "bodyArg", + type: { + name: MapperType.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, false /** isXML */) + } + ); + assert.strictEqual(httpRequest.body, `"SmF2YXNjcmlwdA=="`); + }); + + it("should serialize an XML ByteArray request body with namespace", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + xmlNamespace: "https://microsoft.com", + required: true, + serializedName: "bodyArg", + type: { + name: MapperType.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `SmF2YXNjcmlwdA==` + ); + }); + + it("should serialize an XML ByteArray request body with namespace and prefix", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: stringToByteArray("Javascript") + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + xmlNamespace: "https://microsoft.com", + xmlNamespacePrefix: "sample", + required: true, + serializedName: "bodyArg", + type: { + name: MapperType.ByteArray + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `SmF2YXNjcmlwdA==` + ); + }); + + it("should serialize an XML Composite request body", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + requestBody: { + updated: new Date("2020-08-12T23:36:18.308Z"), + content: { type: "application/xml", queueDescription: { maxDeliveryCount: 15 } } + } + }, + { + httpMethod: "POST", + requestBody: requestBody1, + responses: { 200: {} }, + serializer: new Serializer(undefined, true /** isXML */), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `2020-08-12T23:36:18.308Z15` + ); + }); + + it("should serialize a JSON Composite request body, ignoring XML metadata", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + requestBody: { + updated: new Date("2020-08-12T23:36:18.308Z"), + content: { type: "application/xml", queueDescription: { maxDeliveryCount: 15 } } + } + }, + { + httpMethod: "POST", + requestBody: requestBody1, + responses: { 200: {} }, + serializer: new Serializer() + } + ); + + assert.deepEqual( + httpRequest.body, + '{"updated":"2020-08-12T23:36:18.308Z","content":{"type":"application/xml","queueDescription":{"maxDeliveryCount":15}}}' + ); + }); + + it("should serialize an XML Array request body with namespace and prefix", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: ["Foo", "Bar"] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperType.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { name: "String" } + } + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `FooBar` + ); + }); + + it("should serialize a JSON Array request body, ignoring XML metadata", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: ["Foo", "Bar"] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperType.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { name: "String" } + } + } + } + }, + responses: { 200: {} }, + serializer: new Serializer() + } + ); + assert.deepEqual(httpRequest.body, JSON.stringify(["Foo", "Bar"])); + }); + + it("should serialize an XML Array of composite elements, namespace and prefix", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: [ + { foo: "Foo1", bar: "Bar1" }, + { foo: "Foo2", bar: "Bar2" }, + { foo: "Foo3", bar: "Bar3" } + ] + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + xmlElementName: "testItem", + type: { + name: MapperType.Sequence, + element: { + xmlNamespace: "https://microsoft.com/element", + type: { + name: "Composite", + modelProperties: { + foo: { + serializedName: "foo", + xmlNamespace: "https://microsoft.com/foo", + xmlName: "Foo", + type: { + name: "String" + } + }, + bar: { + xmlNamespacePrefix: "bar", + xmlNamespace: "https://microsoft.com/bar", + xmlName: "Bar", + serializedName: "bar", + type: { + name: "String" + } + } + } + } + } + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `Foo1Bar1Foo2Bar2Foo3Bar3` + ); + }); + + it("should serialize an XML Composite request body with namespace and prefix", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: { foo: "Foo", bar: "Bar" } + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + xmlNamespace: "https://microsoft.com", + type: { + name: MapperType.Composite, + modelProperties: { + foo: { + serializedName: "foo", + xmlNamespace: "https://microsoft.com/foo", + xmlName: "Foo", + type: { + name: "String" + } + }, + bar: { + xmlNamespacePrefix: "bar", + xmlNamespace: "https://microsoft.com/bar", + xmlName: "Bar", + serializedName: "bar", + type: { + name: "String" + } + } + } + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(undefined, true), + isXML: true + } + ); + assert.strictEqual( + httpRequest.body, + `FooBar` + ); + }); + it("should serialize an XML Stream request body", () => { const httpRequest = new WebResource(); serializeRequestBody( @@ -626,6 +1190,34 @@ describe("ServiceClient", function() { assert.strictEqual(httpRequest.body, "body value"); }); + it("should serialize an XML Stream request body, with namespace", () => { + const httpRequest = new WebResource(); + serializeRequestBody( + new ServiceClient(), + httpRequest, + { + bodyArg: "body value" + }, + { + httpMethod: "POST", + requestBody: { + parameterPath: "bodyArg", + mapper: { + required: true, + serializedName: "bodyArg", + type: { + name: MapperType.Stream + } + } + }, + responses: { 200: {} }, + serializer: new Serializer(), + isXML: true + } + ); + assert.strictEqual(httpRequest.body, "body value"); + }); + it("should serialize a string send to a text/plain endpoint as just a string", () => { const httpRequest = new WebResource(); serializeRequestBody( diff --git a/sdk/core/core-http/test/testMappers.ts b/sdk/core/core-http/test/testMappers.ts new file mode 100644 index 000000000000..06053b8e785c --- /dev/null +++ b/sdk/core/core-http/test/testMappers.ts @@ -0,0 +1,266 @@ +import { CompositeMapper, OperationParameter } from "../src/coreHttp"; + +const QueueDescription: CompositeMapper = { + serializedName: "QueueDescription", + xmlName: "QueueDescription", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Composite", + className: "QueueDescription", + modelProperties: { + lockDuration: { + serializedName: "lockDuration", + xmlName: "LockDuration", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + maxSizeInMegabytes: { + serializedName: "maxSizeInMegabytes", + xmlName: "MaxSizeInMegabytes", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + requiresDuplicateDetection: { + serializedName: "requiresDuplicateDetection", + xmlName: "RequiresDuplicateDetection", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + requiresSession: { + serializedName: "requiresSession", + xmlName: "RequiresSession", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + defaultMessageTimeToLive: { + serializedName: "defaultMessageTimeToLive", + xmlName: "DefaultMessageTimeToLive", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + deadLetteringOnMessageExpiration: { + serializedName: "deadLetteringOnMessageExpiration", + xmlName: "DeadLetteringOnMessageExpiration", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + duplicateDetectionHistoryTimeWindow: { + serializedName: "duplicateDetectionHistoryTimeWindow", + xmlName: "DuplicateDetectionHistoryTimeWindow", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + maxDeliveryCount: { + serializedName: "maxDeliveryCount", + xmlName: "MaxDeliveryCount", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + enableBatchedOperations: { + serializedName: "enableBatchedOperations", + xmlName: "EnableBatchedOperations", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + sizeInBytes: { + serializedName: "sizeInBytes", + xmlName: "SizeInBytes", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + messageCount: { + serializedName: "messageCount", + xmlName: "MessageCount", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Number" + } + }, + isAnonymousAccessible: { + serializedName: "isAnonymousAccessible", + xmlName: "IsAnonymousAccessible", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + status: { + serializedName: "status", + xmlName: "Status", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + forwardTo: { + serializedName: "forwardTo", + xmlName: "ForwardTo", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + userMetadata: { + serializedName: "userMetadata", + xmlName: "UserMetadata", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + createdAt: { + serializedName: "createdAt", + xmlName: "CreatedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + updatedAt: { + serializedName: "updatedAt", + xmlName: "UpdatedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + accessedAt: { + serializedName: "accessedAt", + xmlName: "AccessedAt", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "DateTime" + } + }, + supportOrdering: { + serializedName: "supportOrdering", + xmlName: "SupportOrdering", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + autoDeleteOnIdle: { + serializedName: "autoDeleteOnIdle", + xmlName: "AutoDeleteOnIdle", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "TimeSpan" + } + }, + enablePartitioning: { + serializedName: "enablePartitioning", + xmlName: "EnablePartitioning", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + entityAvailabilityStatus: { + serializedName: "entityAvailabilityStatus", + xmlName: "EntityAvailabilityStatus", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + }, + enableExpress: { + serializedName: "enableExpress", + xmlName: "EnableExpress", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "Boolean" + } + }, + forwardDeadLetteredMessagesTo: { + serializedName: "forwardDeadLetteredMessagesTo", + xmlName: "ForwardDeadLetteredMessagesTo", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + name: "String" + } + } + } + } +}; + +const CreateQueueBodyContent: CompositeMapper = { + serializedName: "CreateQueueBodyContent", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "Composite", + className: "CreateQueueBodyContent", + modelProperties: { + type: { + defaultValue: "application/xml", + serializedName: "type", + xmlName: "type", + xmlIsAttribute: true, + type: { + name: "String" + } + }, + queueDescription: { + serializedName: "queueDescription", + xmlName: "QueueDescription", + xmlNamespace: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect", + type: { + ...QueueDescription.type + } + } + } + } +}; + +const CreateQueueBody: CompositeMapper = { + serializedName: "CreateQueueBody", + xmlName: "entry", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "Composite", + className: "CreateQueueBody", + modelProperties: { + updated: { + serializedName: "updated", + xmlName: "updated", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + name: "DateTime" + } + }, + content: { + serializedName: "content", + xmlName: "content", + xmlNamespace: "http://www.w3.org/2005/Atom", + type: { + ...CreateQueueBodyContent.type + } + } + } + } +}; + +export const requestBody1: OperationParameter = { + parameterPath: "requestBody", + mapper: CreateQueueBody +};