Skip to content

Commit

Permalink
Replaces XMLBuilder dependency with a light-weight implementation (#2045
Browse files Browse the repository at this point in the history
)

* Replaces XMLBuilder dependency with a light-weight implementation

* Pins @types/node devDependency

* Adds escaping apostrophes in XML attributes
  • Loading branch information
chrisradek committed May 11, 2018
1 parent eef6e5c commit b5df631
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changes/next-release/feature-XML-443ac7b0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "XML",
"description": "Replaces XMLBuilder dependency with a light-weight XML builder."
}
43 changes: 28 additions & 15 deletions lib/xml/builder.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
var util = require('../util');
var builder = require('xmlbuilder');
var XmlNode = require('./xml-node').XmlNode;
var XmlText = require('./xml-text').XmlText;

function XmlBuilder() { }

XmlBuilder.prototype.toXML = function(params, shape, rootElement, noEmpty) {
var xml = builder.create(rootElement);
applyNamespaces(xml, shape);
var xml = new XmlNode(rootElement);
applyNamespaces(xml, shape, true);
serialize(xml, params, shape);
return xml.children.length > 0 || noEmpty ? xml.root().toString() : '';
return xml.children.length > 0 || noEmpty ? xml.toString() : '';
};

function serialize(xml, value, shape) {
Expand All @@ -28,11 +29,12 @@ function serializeStructure(xml, params, shape) {
var name = memberShape.name;
if (value !== undefined && value !== null) {
if (memberShape.isXmlAttribute) {
xml.att(name, value);
xml.addAttribute(name, value);
} else if (memberShape.flattened) {
serialize(xml, value, memberShape);
} else {
var element = xml.ele(name);
var element = new XmlNode(name);
xml.addChildNode(element);
applyNamespaces(element, memberShape);
serialize(element, value, memberShape);
}
Expand All @@ -45,42 +47,53 @@ function serializeMap(xml, map, shape) {
var xmlValue = shape.value.name || 'value';

util.each(map, function(key, value) {
var entry = xml.ele(shape.flattened ? shape.name : 'entry');
serialize(entry.ele(xmlKey), key, shape.key);
serialize(entry.ele(xmlValue), value, shape.value);
var entry = new XmlNode(shape.flattened ? shape.name : 'entry');
xml.addChildNode(entry);

var entryKey = new XmlNode(xmlKey);
var entryValue = new XmlNode(xmlValue);
entry.addChildNode(entryKey)
entry.addChildNode(entryValue)

serialize(entryKey, key, shape.key);
serialize(entryValue, value, shape.value);
});
}

function serializeList(xml, list, shape) {
if (shape.flattened) {
util.arrayEach(list, function(value) {
var name = shape.member.name || shape.name;
var element = xml.ele(name);
var element = new XmlNode(name);
xml.addChildNode(element);
serialize(element, value, shape.member);
});
} else {
util.arrayEach(list, function(value) {
var name = shape.member.name || 'member';
var element = xml.ele(name);
var element = new XmlNode(name);
xml.addChildNode(element);
serialize(element, value, shape.member);
});
}
}

function serializeScalar(xml, value, shape) {
xml.txt(shape.toWireFormat(value));
xml.addChildNode(
new XmlText(shape.toWireFormat(value))
);
}

function applyNamespaces(xml, shape) {
function applyNamespaces(xml, shape, isRoot) {
var uri, prefix = 'xmlns';
if (shape.xmlNamespaceUri) {
uri = shape.xmlNamespaceUri;
if (shape.xmlNamespacePrefix) prefix += ':' + shape.xmlNamespacePrefix;
} else if (xml.isRoot && shape.api.xmlNamespaceUri) {
} else if (isRoot && shape.api.xmlNamespaceUri) {
uri = shape.api.xmlNamespaceUri;
}

if (uri) xml.att(prefix, uri);
if (uri) xml.addAttribute(prefix, uri);
}

module.exports = XmlBuilder;
13 changes: 13 additions & 0 deletions lib/xml/escape-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Escapes characters that can not be in an XML attribute.
*/
function escapeAttribute(value) {
return value.replace(/&/g, '&amp;').replace(/'/g, '&apos;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

/**
* @api private
*/
module.exports = {
escapeAttribute: escapeAttribute
};
13 changes: 13 additions & 0 deletions lib/xml/escape-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Escapes characters that can not be in an XML element.
*/
function escapeElement(value) {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

/**
* @api private
*/
module.exports = {
escapeElement: escapeElement
};
45 changes: 45 additions & 0 deletions lib/xml/xml-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
var escapeAttribute = require('./escape-attribute').escapeAttribute;

/**
* Represents an XML node.
* @api private
*/
function XmlNode(name, children) {
if (children === void 0) { children = []; }
this.name = name;
this.children = children;
this.attributes = {};
}
XmlNode.prototype.addAttribute = function (name, value) {
this.attributes[name] = value;
return this;
};
XmlNode.prototype.addChildNode = function (child) {
this.children.push(child);
return this;
};
XmlNode.prototype.removeAttribute = function (name) {
delete this.attributes[name];
return this;
};
XmlNode.prototype.toString = function () {
var hasChildren = Boolean(this.children.length);
var xmlText = '<' + this.name;
// add attributes
var attributes = this.attributes;
for (var i = 0, attributeNames = Object.keys(attributes); i < attributeNames.length; i++) {
var attributeName = attributeNames[i];
var attribute = attributes[attributeName];
if (typeof attribute !== 'undefined' && attribute !== null) {
xmlText += ' ' + attributeName + '=\"' + escapeAttribute('' + attribute) + '\"';
}
}
return xmlText += !hasChildren ? '/>' : '>' + this.children.map(function (c) { return c.toString(); }).join('') + '</' + this.name + '>';
};

/**
* @api private
*/
module.exports = {
XmlNode: XmlNode
};
20 changes: 20 additions & 0 deletions lib/xml/xml-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
var escapeElement = require('./escape-element').escapeElement;

/**
* Represents an XML text value.
* @api private
*/
function XmlText(value) {
this.value = value;
}

XmlText.prototype.toString = function () {
return escapeElement('' + this.value);
};

/**
* @api private
*/
module.exports = {
XmlText: XmlText
};
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"Trevor Rowe <trevrowe@amazon.com>"
],
"devDependencies": {
"@types/node": "^6.0.46",
"@types/node": "6.0.92",
"browserify": "13.1.0",
"chai": "^3.0",
"codecov": "^1.0.1",
Expand Down Expand Up @@ -47,8 +47,7 @@
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.1.0",
"xml2js": "0.4.17",
"xmlbuilder": "4.2.1"
"xml2js": "0.4.17"
},
"main": "lib/aws.js",
"browser": {
Expand Down Expand Up @@ -141,4 +140,4 @@
"region-check": "node ./scripts/region-checker/index.js",
"test-remove-event-stream": "mocha scripts/lib/remove-event-stream-ops.spec.js"
}
}
}
8 changes: 8 additions & 0 deletions test/xml/escape-attribute.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var escapeAttribute = require('../../lib/xml/escape-attribute').escapeAttribute;

describe('escape-attribute', function() {
it('escapes: & < > "\'', function() {
var value = 'abc 123 &<>"%\'';
expect(escapeAttribute(value)).to.equal('abc 123 &amp;&lt;&gt;&quot;%&apos;');
});
});
8 changes: 8 additions & 0 deletions test/xml/escape-element.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var escapeElement = require('../../lib/xml/escape-element').escapeElement;

describe('escape-element', function() {
it('escapes: & < >', function() {
var value = 'abc 123 &<>"%';
expect(escapeElement(value)).to.equal('abc 123 &amp;&lt;&gt;"%');
});
});
134 changes: 134 additions & 0 deletions test/xml/xml-node.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
var XmlNode = require('../../lib/xml/xml-node').XmlNode;
var XmlText = require('../../lib/xml/xml-text').XmlText;

describe('XmlNode', function() {
it('creates empty xml documents', function() {
var node = new XmlNode('Xml');
expect(node.toString()).to.equal('<Xml/>');
});

it('nests elements', function() {
var node = new XmlNode('xml', [
new XmlNode('element')
]);
expect(node.toString()).to.equal('<xml><element/></xml>');
});

it('nests elements deeply', function() {
var node = new XmlNode('xml', [
new XmlNode('a', [
new XmlNode('b', [
new XmlNode('c')
])
])
]);
expect(node.toString()).to.equal('<xml><a><b><c/></b></a></xml>');
});

it('supports flat elements with nested elements', function() {
var node = new XmlNode('xml', [
new XmlNode('a', [
new XmlNode('b')
]),
new XmlNode('c')
]);
expect(node.toString()).to.equal('<xml><a><b/></a><c/></xml>');
});

it('accepts element values', function() {
var node = new XmlNode('xml', [
new XmlNode('element', [
new XmlText('value')
])
]);
expect(node.toString()).to.equal('<xml><element>value</element></xml>');
});

it('accepts element attributes', function() {
var node = new XmlNode('xml', [
new XmlNode('el')
.addAttribute('abc', 123)
.addAttribute('mno', 'xyz')
]);
expect(node.toString()).to.equal('<xml><el abc="123" mno="xyz"/></xml>');
});

it('accepts element values and attributes at the same time', function() {
var node = new XmlNode('xml', [
new XmlNode('el', [
new XmlText('value')
]).addAttribute('abc', 'xyz')
]);
expect(node.toString()).to.equal('<xml><el abc="xyz">value</el></xml>');
});

it('accepts attributes on outer elements', function() {
var node = new XmlNode('xml', [
new XmlNode('out', [
new XmlNode('c')
]).addAttribute('a', 'b')
]).addAttribute('xmlns', 'abc');
expect(node.toString()).to.equal('<xml xmlns="abc"><out a="b"><c/></out></xml>');
});

it('ignores null and undefined attributes', function() {
var node = new XmlNode('xml');
expect(Object.keys(node.attributes).length).to.equal(0);
node.addAttribute('foo', null);
node.addAttribute('bar', undefined);
node.addAttribute('baz', 123);
node.addAttribute('bingo', 'bongo');
expect(node.toString()).to.equal('<xml baz="123" bingo="bongo"/>');
});

it('escapes attribute values and element text', function() {
var node = new XmlNode('xml', [
new XmlNode('this & that')
]).addAttribute('xmlns', 'a"b');
expect(node.toString()).to.equal('<xml xmlns="a&quot;b"><this & that/></xml>');
});

describe('addAttribute', function() {
it('adds an attribute to the XmlNode', function() {
var node = new XmlNode('xml');
expect(node.attributes['foo']).to.be.undefined;
node.addAttribute('foo', 'bar');
expect(node.attributes['foo']).to.equal('bar');
});

it('returns a reference to the XmlNode', function() {
var node = new XmlNode('xml');
expect(node.addAttribute('foo', 'bar')).to.equal(node);
});
});

describe('addChildNode', function() {
it('adds a child to the XmlNode', function() {
var node = new XmlNode('xml');
expect(node.children.length === 0);
node.addChildNode(new XmlNode('foo'));
expect(node.children.length === 1);
expect(node.toString()).to.equal('<xml><foo/></xml>');
});

it('returns a reference to the XmlNode', function() {
var node = new XmlNode('xml');
expect(node.addChildNode(new XmlNode('foo'))).to.equal(node);
});
});

describe('removeAttribute', function() {
it('removes an attribute from the XmlNode', function() {
var node = new XmlNode('xml');
node.addAttribute('foo', 'bar');
expect(node.attributes['foo']).to.equal('bar');
node.removeAttribute('foo');
expect(node.attributes['foo']).to.be.undefined;
});

it('returns a reference to the XmlNode', function() {
var node = new XmlNode('xml');
expect(node.removeAttribute('foo')).to.equal(node);
});
});
});
8 changes: 8 additions & 0 deletions test/xml/xml-text.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var XmlText = require('../../lib/xml/xml-text').XmlText;

describe('XmlText', function() {
it('escapes element text', function() {
var text = new XmlText('this & that are < or > "most"');
expect(text.toString()).to.equal('this &amp; that are &lt; or &gt; "most"');
});
});

0 comments on commit b5df631

Please sign in to comment.