diff --git a/packages/xml-builder/.gitignore b/packages/xml-builder/.gitignore
new file mode 100644
index 000000000000..d1a786583bc8
--- /dev/null
+++ b/packages/xml-builder/.gitignore
@@ -0,0 +1,3 @@
+**/*.js
+**/*.js.map
+**/*.d.ts
\ No newline at end of file
diff --git a/packages/xml-builder/__tests__/index.ts b/packages/xml-builder/__tests__/index.ts
new file mode 100644
index 000000000000..13609e9fb15b
--- /dev/null
+++ b/packages/xml-builder/__tests__/index.ts
@@ -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();
+ });
+});
\ No newline at end of file
diff --git a/packages/xml-builder/__tests__/lib/XmlNode.ts b/packages/xml-builder/__tests__/lib/XmlNode.ts
new file mode 100644
index 000000000000..fb4cc05b761e
--- /dev/null
+++ b/packages/xml-builder/__tests__/lib/XmlNode.ts
@@ -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('');
+ });
+
+ it('nests elements', () => {
+ const node = new XmlNode('xml', [
+ new XmlNode('element')
+ ]);
+ expect(node.toString()).toBe('');
+ });
+
+ it('nests elements deeply', () => {
+ const node = new XmlNode('xml', [
+ new XmlNode('a', [
+ new XmlNode('b', [
+ new XmlNode('c')
+ ])
+ ])
+ ]);
+ expect(node.toString()).toBe('');
+ });
+
+ 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('');
+ });
+
+ it('accepts element values', () => {
+ const node = new XmlNode('xml', [
+ new XmlNode('element', [
+ new XmlText('value')
+ ])
+ ]);
+ expect(node.toString()).toBe('value');
+ });
+
+ it('accepts element attributes', () => {
+ const node = new XmlNode('xml', [
+ new XmlNode('el')
+ .addAttribute('abc', 123)
+ .addAttribute('mno', 'xyz')
+ ]);
+ expect(node.toString()).toBe('');
+ });
+
+ 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('value');
+ });
+
+ 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('');
+ });
+
+ 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('');
+ });
+
+ it('escapes attribute values and element text', () => {
+ const node = new XmlNode('xml', [
+ new XmlNode('this & that')
+ ]).addAttribute('xmlns', 'a"b');
+ expect(node.toString()).toBe('');
+ });
+
+ 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('');
+ });
+
+ 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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/xml-builder/__tests__/lib/XmlText.ts b/packages/xml-builder/__tests__/lib/XmlText.ts
new file mode 100644
index 000000000000..1ee0547a9e16
--- /dev/null
+++ b/packages/xml-builder/__tests__/lib/XmlText.ts
@@ -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 & that are < or > "most"');
+ });
+});
\ No newline at end of file
diff --git a/packages/xml-builder/__tests__/lib/escape-attribute.ts b/packages/xml-builder/__tests__/lib/escape-attribute.ts
new file mode 100644
index 000000000000..d880f8a9f94c
--- /dev/null
+++ b/packages/xml-builder/__tests__/lib/escape-attribute.ts
@@ -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 &<>"%');
+ });
+});
\ No newline at end of file
diff --git a/packages/xml-builder/__tests__/lib/escape-element.ts b/packages/xml-builder/__tests__/lib/escape-element.ts
new file mode 100644
index 000000000000..337b6a98bfd3
--- /dev/null
+++ b/packages/xml-builder/__tests__/lib/escape-element.ts
@@ -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 &<>"%');
+ });
+});
\ No newline at end of file
diff --git a/packages/xml-builder/index.ts b/packages/xml-builder/index.ts
new file mode 100644
index 000000000000..938f56437fac
--- /dev/null
+++ b/packages/xml-builder/index.ts
@@ -0,0 +1,2 @@
+export * from './lib/XmlNode';
+export * from './lib/XmlText';
\ No newline at end of file
diff --git a/packages/xml-builder/lib/XmlNode.ts b/packages/xml-builder/lib/XmlNode.ts
new file mode 100644
index 000000000000..d8aad4dd864f
--- /dev/null
+++ b/packages/xml-builder/lib/XmlNode.ts
@@ -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}>`;
+ }
+}
+
diff --git a/packages/xml-builder/lib/XmlText.ts b/packages/xml-builder/lib/XmlText.ts
new file mode 100644
index 000000000000..f5bf8e660c2d
--- /dev/null
+++ b/packages/xml-builder/lib/XmlText.ts
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/packages/xml-builder/lib/escape-attribute.ts b/packages/xml-builder/lib/escape-attribute.ts
new file mode 100644
index 000000000000..22677c30d922
--- /dev/null
+++ b/packages/xml-builder/lib/escape-attribute.ts
@@ -0,0 +1,6 @@
+/**
+ * Escapes characters that can not be in an XML attribute.
+ */
+export function escapeAttribute(value: string): string {
+ return value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+}
\ No newline at end of file
diff --git a/packages/xml-builder/lib/escape-element.ts b/packages/xml-builder/lib/escape-element.ts
new file mode 100644
index 000000000000..a8c808f5e51f
--- /dev/null
+++ b/packages/xml-builder/lib/escape-element.ts
@@ -0,0 +1,6 @@
+/**
+ * Escapes characters that can not be in an XML element.
+ */
+export function escapeElement(value: string): string {
+ return value.replace(/&/g, '&').replace(//g, '>');
+}
\ No newline at end of file
diff --git a/packages/xml-builder/lib/stringable.ts b/packages/xml-builder/lib/stringable.ts
new file mode 100644
index 000000000000..ea5228679be8
--- /dev/null
+++ b/packages/xml-builder/lib/stringable.ts
@@ -0,0 +1,3 @@
+export interface Stringable {
+ toString(): string;
+}
\ No newline at end of file
diff --git a/packages/xml-builder/package.json b/packages/xml-builder/package.json
new file mode 100644
index 000000000000..bb57ddd9a28b
--- /dev/null
+++ b/packages/xml-builder/package.json
@@ -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"
+}
diff --git a/packages/xml-builder/tsconfig.json b/packages/xml-builder/tsconfig.json
new file mode 100644
index 000000000000..52e3cbb7a6e7
--- /dev/null
+++ b/packages/xml-builder/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "strict": true,
+ "sourceMap": true,
+ "declaration": true
+ },
+ "exclude": ["node_modules"]
+}