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('')}`; + } +} + 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"] +}