Skip to content

Feature/json builder #11

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

Merged
merged 2 commits into from
Aug 4, 2017
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
4 changes: 4 additions & 0 deletions packages/json-builder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/
*.js
*.js.map
*.d.ts
202 changes: 202 additions & 0 deletions packages/json-builder/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import {JsonBuilder} from "../";
import {Member} from "@aws/types";

describe('JsonBuilder', () => {
describe('structures', () => {
const structure: Member = {
shape: {
type: "structure",
required: [],
members: {
foo: {shape: {type: 'string'}},
bar: {shape: {type: 'string'}},
baz: {
shape: {type: 'string'},
locationName: 'quux',
},
}
}
};
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

it('should serialize known properties of a structure', () => {
const toSerialize = {foo: 'fizz', bar: 'buzz'};
expect(jsonBody.build(structure, toSerialize))
.toBe(JSON.stringify(toSerialize));
});

it('should ignore unknown properties', () => {
const toSerialize = {foo: 'fizz', bar: 'buzz'};
expect(jsonBody.build(structure, {...toSerialize, pop: 'weasel'}))
.toBe(JSON.stringify(toSerialize));
});

it('should serialize properties to the locationNames', () => {
expect(jsonBody.build(structure, {baz: 'value'}))
.toEqual(JSON.stringify({quux: 'value'}));
});

it('should throw if a scalar value is provided', () => {
for (let scalar of ['string', 123, true, null, void 0]) {
expect(() => jsonBody.build(structure, scalar)).toThrow();
}
});
});

describe('lists', () => {
const listShape: Member = {
shape: {
type: 'list',
member: {shape: {type: 'string'}},
}
};
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

it('should serialize arrays', () => {
expect(jsonBody.build(listShape, ['foo', 'bar', 'baz']))
.toEqual(JSON.stringify(['foo', 'bar', 'baz']));
});

it('should serialize iterators', () => {
const iterator = function* () {
yield 'foo';
yield 'bar';
yield 'baz';
};

expect(jsonBody.build(listShape, iterator()))
.toEqual(JSON.stringify(['foo', 'bar', 'baz']));
});

it('should throw if a non-iterable value is provided', () => {
for (let nonIterable of [{}, 123, true, null, void 0]) {
expect(() => jsonBody.build(listShape, nonIterable)).toThrow();
}
});
});

describe('maps', () => {
const mapShape: Member = {
shape: {
type: 'map',
key: {shape: {type: 'string'}},
value: {shape: {type: 'number'}}
}
};
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

it('should serialize objects', () => {
const object = {
foo: 0,
bar: 1,
baz: 2,
};

expect(jsonBody.build(mapShape, object))
.toEqual(JSON.stringify(object));
});

it('should serialize [key, value] iterables (like ES6 maps)', () => {
const iterator = function* () {
yield ['foo', 0];
yield ['bar', 1];
yield ['baz', 2];
};

expect(jsonBody.build(mapShape, iterator()))
.toEqual(JSON.stringify({
foo: 0,
bar: 1,
baz: 2,
}));
});

it('should throw if a non-iterable and non-object value is provided', () => {
for (let nonIterable of [123, true, null, void 0]) {
expect(() => jsonBody.build(mapShape, nonIterable)).toThrow();
}
});
});

describe('blobs', () => {
const blobShape: Member = {shape: {type: 'blob'}};
const base64Encode = jest.fn(arg => arg);
const utf8Decode = jest.fn(arg => arg);
const jsonBody = new JsonBuilder(base64Encode, utf8Decode);

beforeEach(() => {
base64Encode.mockClear();
utf8Decode.mockClear();
});

it('should base64 encode ArrayBuffers', () => {
jsonBody.build(blobShape, new ArrayBuffer(2));

expect(base64Encode.mock.calls.length).toBe(1);
});

it('should base64 encode ArrayBufferViews', () => {
jsonBody.build(blobShape, Uint8Array.from([0]));

expect(base64Encode.mock.calls.length).toBe(1);
});

it('should utf8 decode and base64 encode strings', () => {
jsonBody.build(blobShape, 'foo' as any);

expect(base64Encode.mock.calls.length).toBe(1);
expect(utf8Decode.mock.calls.length).toBe(1);
});

it('should throw if a non-binary value is provided', () => {
for (let nonBinary of [[], {}, 123, true, null, void 0]) {
expect(() => jsonBody.build(blobShape, nonBinary)).toThrow();
}
});
});

describe('timestamps', () => {
const timestampShape: Member = {shape: {type: "timestamp"}};
const date = new Date('2017-05-22T19:33:14.175Z');
const timestamp = 1495481594;
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

it('should convert Date objects to epoch timestamps', () => {
expect(jsonBody.build(timestampShape, date))
.toBe(JSON.stringify(timestamp));
});

it('should convert date strings to epoch timestamps', () => {
expect(jsonBody.build(timestampShape, date.toUTCString() as any))
.toBe(JSON.stringify(timestamp));
});

it('should preserve numbers as epoch timestamps', () => {
expect(jsonBody.build(timestampShape, timestamp as any))
.toBe(JSON.stringify(timestamp));
});

it('should throw if a value that cannot be converted to a time object is provided', () => {
for (let nonTime of [[], {}, true, null, void 0, new ArrayBuffer(0)]) {
expect(() => jsonBody.build(timestampShape, nonTime))
.toThrow();
}
});
});

describe('scalars', () => {
it('should echo back scalars in their JSON-ified form', () => {
const cases: Iterable<[Member, any]> = [
[{shape: {type: 'string'}}, 'string'],
[{shape: {type: 'number'}}, 1],
[{shape: {type: 'boolean'}}, true],
];
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());

for (let [shape, scalar] of cases) {
expect(jsonBody.build(shape, scalar))
.toBe(JSON.stringify(scalar));
}
});
});
});
136 changes: 136 additions & 0 deletions packages/json-builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {isArrayBuffer} from '@aws/is-array-buffer';
import {epoch} from "@aws/protocol-timestamp";
import {
BodySerializer,
Decoder,
Encoder,
Member,
SerializationModel
} from "@aws/types";

type Scalar = string|number|boolean|null;

interface Json {
[index: string]: Scalar|Json|JsonArray;
}

interface JsonArray extends Array<Scalar|Json|JsonArray> {}

type JsonValue = Json|Scalar|JsonArray;

export class JsonBuilder implements BodySerializer {
constructor(
private readonly base64Encoder: Encoder,
private readonly utf8Decoder: Decoder
) {}

public build(shape: Member, input: any): string {
return JSON.stringify(this.format(shape.shape, input));
}

private format(shape: SerializationModel, input: any): JsonValue {
const inputType = typeof input;
if (shape.type === 'structure') {
if (inputType !== 'object' || input === null) {
throw new Error(
`Unable to serialize value of type ${typeof input} as a`
+ ' structure'
);
}

const data: Json = {};
for (let key of Object.keys(input)) {
if (
input[key] === undefined ||
input[key] === null ||
!(key in shape.members)
) {
continue;
}

const {
locationName = key,
shape: memberShape
} = shape.members[key];
data[locationName] = this.format(memberShape, input[key]);
}

return data;
} else if (shape.type === 'list') {
if (Array.isArray(input) || isIterable(input)) {
const data: JsonArray = [];
for (let element of input) {
data.push(this.format(shape.member.shape, element));
}

return data;
}

throw new Error(
'Unable to serialize value that is neither an array nor an'
+ ' iterable as a list'
);
} else if (shape.type === 'map') {
const data: Json = {};
// A map input is should be a [key, value] iterable...
if (isIterable(input)) {
for (let [key, value] of input) {
data[key] = this.format(shape.value.shape, value);
}
return data;
}

// ... or an object
if (inputType !== 'object' || input === null) {
throw new Error(
'Unable to serialize value that is neither a [key, value]'
+ ' iterable nor an object as a map'
);
}

for (let key of Object.keys(input)) {
data[key] = this.format(shape.value.shape, input[key]);
}
return data;
} else if (shape.type === 'blob') {
if (typeof input === 'string') {
input = this.utf8Decoder(input);
} else if (ArrayBuffer.isView(input)) {
input = new Uint8Array(
input.buffer,
input.byteOffset,
input.byteLength
);
} else if (isArrayBuffer(input)) {
input = new Uint8Array(input);
} else {
throw new Error(
'Unable to serialize value that is neither a string nor an'
+ ' ArrayBuffer nor an ArrayBufferView as a blob'
);
}

return this.base64Encoder(input);
} else if (shape.type === 'timestamp') {
if (
['number', 'string'].indexOf(typeof input) > -1
|| Object.prototype.toString.call(input) === '[object Date]'
) {
return epoch(input);
}

throw new Error(
'Unable to serialize value that is neither a string nor a'
+ ' number nor a Date object as a timestamp'
);
}

return input;
}
}

function isIterable(arg: any): arg is Iterable<any> {
return Boolean(arg)
&& typeof Symbol !== 'undefined'
&& typeof arg[Symbol.iterator] === 'function';
}
25 changes: 25 additions & 0 deletions packages/json-builder/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@aws/json-builder",
"private": true,
"version": "0.0.1",
"description": "A marshaller for the body portion of AWS's JSON and REST-JSON protocols",
"main": "lib/index.js",
"scripts": {
"prepublishOnly": "tsc",
"pretest": "tsc",
"test": "jest"
},
"author": "[email protected]",
"license": "UNLICENSED",
"dependencies": {
"@aws/is-array-buffer": "^0.0.1",
"@aws/protocol-timestamp": "^0.0.1",
"@aws/types": "^0.0.1",
"tslib": "^1.7.1"
},
"devDependencies": {
"@types/jest": "^20.0.2",
"jest": "^20.0.4",
"typescript": "^2.3"
}
}
16 changes: 16 additions & 0 deletions packages/json-builder/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"strict": true,
"downlevelIteration": true,
"importHelpers": true,
"lib": [
"es5",
"es2015.symbol",
"es2015.iterable"
]
}
}
4 changes: 4 additions & 0 deletions packages/json-parser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/
*.js
*.js.map
*.d.ts
Loading