Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions packages/xml-builder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/*.js
**/*.js.map
**/*.d.ts
11 changes: 11 additions & 0 deletions packages/xml-builder/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as pkg from '../index';

describe('package index', () => {
it('should define XmlNode', () => {
expect(pkg.XmlNode).toBeDefined();
});

it('should define XmlText', () => {
expect(pkg.XmlText).toBeDefined();
});
});
134 changes: 134 additions & 0 deletions packages/xml-builder/__tests__/lib/XmlNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {XmlNode} from '../../lib/XmlNode';
import {XmlText} from '../../lib/XmlText';

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

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

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

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

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

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

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

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

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

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

describe('addAttribute', () => {
it('adds an attribute to the XmlNode', () => {
const node:any = new XmlNode('xml');
expect(node.attributes['foo']).toBeUndefined();
node.addAttribute('foo', 'bar');
expect(node.attributes['foo']).toBe('bar');
});

it('returns a reference to the XmlNode', () => {
const node = new XmlNode('xml');
expect(node.addAttribute('foo', 'bar')).toBe(node);
});
});

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

it('returns a reference to the XmlNode', () => {
const node = new XmlNode('xml');
expect(node.addChildNode(new XmlNode('foo'))).toBe(node);
});
});

describe('removeAttribute', () => {
it('removes an attribute from the XmlNode', () => {
const node:any = new XmlNode('xml');
node.addAttribute('foo', 'bar');
expect(node.attributes['foo']).toBe('bar');
node.removeAttribute('foo');
expect(node.attributes['foo']).toBeUndefined();
});

it('returns a reference to the XmlNode', () => {
const node = new XmlNode('xml');
expect(node.removeAttribute('foo')).toBe(node);
});
});
});
8 changes: 8 additions & 0 deletions packages/xml-builder/__tests__/lib/XmlText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {XmlText} from '../../lib/XmlText';

describe('XmlText', () => {
it('escapes element text', () => {
const text = new XmlText('this & that are < or > "most"');
expect(text.toString()).toBe('this &amp; that are &lt; or &gt; "most"');
});
});
8 changes: 8 additions & 0 deletions packages/xml-builder/__tests__/lib/escape-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {escapeAttribute} from '../../lib/escape-attribute';

describe('escape-attribute', () => {
it('escapes: & < > "', () => {
const value = 'abc 123 &<>"%';
expect(escapeAttribute(value)).toBe('abc 123 &amp;&lt;&gt;&quot;%');
});
});
8 changes: 8 additions & 0 deletions packages/xml-builder/__tests__/lib/escape-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {escapeElement} from '../../lib/escape-element';

describe('escape-element', () => {
it('escapes: & < >', () => {
const value = 'abc 123 &<>"%';
expect(escapeElement(value)).toBe('abc 123 &amp;&lt;&gt;"%');
});
});
2 changes: 2 additions & 0 deletions packages/xml-builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/XmlNode';
export * from './lib/XmlText';
43 changes: 43 additions & 0 deletions packages/xml-builder/lib/XmlNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {escapeAttribute} from './escape-attribute';
import {Stringable} from './stringable';

/**
* Represents an XML node.
*/
export class XmlNode {

private attributes: {[name: string]: any} = {};

constructor(private name: string, private children: Stringable[] = []) {}

addAttribute(name: string, value: any): XmlNode {
this.attributes[name] = value;
return this;
}

addChildNode(child: Stringable): XmlNode {
this.children.push(child);
return this;
}

removeAttribute(name: string): XmlNode {
delete this.attributes[name];
return this;
}

toString(): string {
const hasChildren = Boolean(this.children.length);
let xmlText = `<${this.name}`;
// add attributes
const attributes = this.attributes;
for (let attributeName of Object.keys(attributes)) {
let attribute = attributes[attributeName];
if (typeof attribute !== 'undefined' && attribute !== null) {
xmlText += ` ${attributeName}="${escapeAttribute('' + attribute)}"`;
}
}

return xmlText += !hasChildren ? '/>' : `>${this.children.map(c => c.toString()).join('')}</${this.name}>`;
}
}

12 changes: 12 additions & 0 deletions packages/xml-builder/lib/XmlText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {escapeElement} from './escape-element';
import {Stringable} from './stringable';
/**
* Represents an XML text value.
*/
export class XmlText implements Stringable {
constructor(private value: string) {}

toString(): string {
return escapeElement('' + this.value);
}
}
6 changes: 6 additions & 0 deletions packages/xml-builder/lib/escape-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Escapes characters that can not be in an XML attribute.
*/
export function escapeAttribute(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
6 changes: 6 additions & 0 deletions packages/xml-builder/lib/escape-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Escapes characters that can not be in an XML element.
*/
export function escapeElement(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
3 changes: 3 additions & 0 deletions packages/xml-builder/lib/stringable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Stringable {
toString(): string;
}
22 changes: 22 additions & 0 deletions packages/xml-builder/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@aws/xml-builder",
"private": true,
"version": "0.0.1",
"description": "XML builder for the AWS SDK",
"devDependencies": {
"@types/jest": "^19.2.3",
"jest": "^20.0.3",
"typescript": "^2.3"
},
"dependencies": {
"@aws/service-model": "^0.0.1",
"@aws/types": "^0.0.1"
},
"scripts": {
"prepublishOnly": "tsc",
"pretest": "tsc",
"test": "jest"
},
"author": "aws-javascript-sdk-team@amazon.com",
"license": "UNLICENSED"
}
11 changes: 11 additions & 0 deletions packages/xml-builder/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"strict": true,
"sourceMap": true,
"declaration": true
},
"exclude": ["node_modules"]
}