Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core-http] Support xml namespaces #11201

Merged
merged 9 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/core/core-client/review/core-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface BaseMapper {
xmlIsWrapped?: boolean;
// (undocumented)
xmlName?: string;
// (undocumented)
xmlNamespace?: string;
// (undocumented)
xmlNamespacePrefix?: string;
}

// @public (undocumented)
Expand Down
44 changes: 44 additions & 0 deletions sdk/core/core-client/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
104 changes: 92 additions & 12 deletions sdk/core/core-client/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.`);
Expand All @@ -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;
}
Expand All @@ -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.`);
Expand All @@ -518,8 +550,32 @@ 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
if (isXml && valueType.xmlNamespace) {
const xmlnsKey = valueType.xmlNamespacePrefix
? `xmlns:${valueType.xmlNamespacePrefix}`
: "xmlns";
// If the value is an object the object's properties need to be siblings of the $ property
if (valueType.type.name === "Composite") {
tempDictionary[key] = { ...serializedValue, $: { [xmlnsKey]: valueType.xmlNamespace } };
} else {
// When the value is not an object, it has to go under _
tempDictionary[key] = { _: serializedValue, $: { [xmlnsKey]: valueType.xmlNamespace } };
}
} else {
// Add the serialized value when we are not serializing XML or it doesn't need a namespace
tempDictionary[key] = serializedValue;
}
}

// 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;
}

Expand Down Expand Up @@ -568,7 +624,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");
Expand Down Expand Up @@ -609,6 +666,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
Expand All @@ -630,16 +693,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;
}
}
}
Expand All @@ -665,6 +729,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);
}
Expand Down
52 changes: 47 additions & 5 deletions sdk/core/core-client/src/serviceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
});
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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(
Expand Down
Loading