Skip to content

Commit 5cd1821

Browse files
authored
Merge pull request #11 from jeskew/feature/json-builder
Feature/json builder
2 parents 2137d17 + 1297089 commit 5cd1821

File tree

10 files changed

+666
-0
lines changed

10 files changed

+666
-0
lines changed

packages/json-builder/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import {JsonBuilder} from "../";
2+
import {Member} from "@aws/types";
3+
4+
describe('JsonBuilder', () => {
5+
describe('structures', () => {
6+
const structure: Member = {
7+
shape: {
8+
type: "structure",
9+
required: [],
10+
members: {
11+
foo: {shape: {type: 'string'}},
12+
bar: {shape: {type: 'string'}},
13+
baz: {
14+
shape: {type: 'string'},
15+
locationName: 'quux',
16+
},
17+
}
18+
}
19+
};
20+
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
21+
22+
it('should serialize known properties of a structure', () => {
23+
const toSerialize = {foo: 'fizz', bar: 'buzz'};
24+
expect(jsonBody.build(structure, toSerialize))
25+
.toBe(JSON.stringify(toSerialize));
26+
});
27+
28+
it('should ignore unknown properties', () => {
29+
const toSerialize = {foo: 'fizz', bar: 'buzz'};
30+
expect(jsonBody.build(structure, {...toSerialize, pop: 'weasel'}))
31+
.toBe(JSON.stringify(toSerialize));
32+
});
33+
34+
it('should serialize properties to the locationNames', () => {
35+
expect(jsonBody.build(structure, {baz: 'value'}))
36+
.toEqual(JSON.stringify({quux: 'value'}));
37+
});
38+
39+
it('should throw if a scalar value is provided', () => {
40+
for (let scalar of ['string', 123, true, null, void 0]) {
41+
expect(() => jsonBody.build(structure, scalar)).toThrow();
42+
}
43+
});
44+
});
45+
46+
describe('lists', () => {
47+
const listShape: Member = {
48+
shape: {
49+
type: 'list',
50+
member: {shape: {type: 'string'}},
51+
}
52+
};
53+
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
54+
55+
it('should serialize arrays', () => {
56+
expect(jsonBody.build(listShape, ['foo', 'bar', 'baz']))
57+
.toEqual(JSON.stringify(['foo', 'bar', 'baz']));
58+
});
59+
60+
it('should serialize iterators', () => {
61+
const iterator = function* () {
62+
yield 'foo';
63+
yield 'bar';
64+
yield 'baz';
65+
};
66+
67+
expect(jsonBody.build(listShape, iterator()))
68+
.toEqual(JSON.stringify(['foo', 'bar', 'baz']));
69+
});
70+
71+
it('should throw if a non-iterable value is provided', () => {
72+
for (let nonIterable of [{}, 123, true, null, void 0]) {
73+
expect(() => jsonBody.build(listShape, nonIterable)).toThrow();
74+
}
75+
});
76+
});
77+
78+
describe('maps', () => {
79+
const mapShape: Member = {
80+
shape: {
81+
type: 'map',
82+
key: {shape: {type: 'string'}},
83+
value: {shape: {type: 'number'}}
84+
}
85+
};
86+
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
87+
88+
it('should serialize objects', () => {
89+
const object = {
90+
foo: 0,
91+
bar: 1,
92+
baz: 2,
93+
};
94+
95+
expect(jsonBody.build(mapShape, object))
96+
.toEqual(JSON.stringify(object));
97+
});
98+
99+
it('should serialize [key, value] iterables (like ES6 maps)', () => {
100+
const iterator = function* () {
101+
yield ['foo', 0];
102+
yield ['bar', 1];
103+
yield ['baz', 2];
104+
};
105+
106+
expect(jsonBody.build(mapShape, iterator()))
107+
.toEqual(JSON.stringify({
108+
foo: 0,
109+
bar: 1,
110+
baz: 2,
111+
}));
112+
});
113+
114+
it('should throw if a non-iterable and non-object value is provided', () => {
115+
for (let nonIterable of [123, true, null, void 0]) {
116+
expect(() => jsonBody.build(mapShape, nonIterable)).toThrow();
117+
}
118+
});
119+
});
120+
121+
describe('blobs', () => {
122+
const blobShape: Member = {shape: {type: 'blob'}};
123+
const base64Encode = jest.fn(arg => arg);
124+
const utf8Decode = jest.fn(arg => arg);
125+
const jsonBody = new JsonBuilder(base64Encode, utf8Decode);
126+
127+
beforeEach(() => {
128+
base64Encode.mockClear();
129+
utf8Decode.mockClear();
130+
});
131+
132+
it('should base64 encode ArrayBuffers', () => {
133+
jsonBody.build(blobShape, new ArrayBuffer(2));
134+
135+
expect(base64Encode.mock.calls.length).toBe(1);
136+
});
137+
138+
it('should base64 encode ArrayBufferViews', () => {
139+
jsonBody.build(blobShape, Uint8Array.from([0]));
140+
141+
expect(base64Encode.mock.calls.length).toBe(1);
142+
});
143+
144+
it('should utf8 decode and base64 encode strings', () => {
145+
jsonBody.build(blobShape, 'foo' as any);
146+
147+
expect(base64Encode.mock.calls.length).toBe(1);
148+
expect(utf8Decode.mock.calls.length).toBe(1);
149+
});
150+
151+
it('should throw if a non-binary value is provided', () => {
152+
for (let nonBinary of [[], {}, 123, true, null, void 0]) {
153+
expect(() => jsonBody.build(blobShape, nonBinary)).toThrow();
154+
}
155+
});
156+
});
157+
158+
describe('timestamps', () => {
159+
const timestampShape: Member = {shape: {type: "timestamp"}};
160+
const date = new Date('2017-05-22T19:33:14.175Z');
161+
const timestamp = 1495481594;
162+
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
163+
164+
it('should convert Date objects to epoch timestamps', () => {
165+
expect(jsonBody.build(timestampShape, date))
166+
.toBe(JSON.stringify(timestamp));
167+
});
168+
169+
it('should convert date strings to epoch timestamps', () => {
170+
expect(jsonBody.build(timestampShape, date.toUTCString() as any))
171+
.toBe(JSON.stringify(timestamp));
172+
});
173+
174+
it('should preserve numbers as epoch timestamps', () => {
175+
expect(jsonBody.build(timestampShape, timestamp as any))
176+
.toBe(JSON.stringify(timestamp));
177+
});
178+
179+
it('should throw if a value that cannot be converted to a time object is provided', () => {
180+
for (let nonTime of [[], {}, true, null, void 0, new ArrayBuffer(0)]) {
181+
expect(() => jsonBody.build(timestampShape, nonTime))
182+
.toThrow();
183+
}
184+
});
185+
});
186+
187+
describe('scalars', () => {
188+
it('should echo back scalars in their JSON-ified form', () => {
189+
const cases: Iterable<[Member, any]> = [
190+
[{shape: {type: 'string'}}, 'string'],
191+
[{shape: {type: 'number'}}, 1],
192+
[{shape: {type: 'boolean'}}, true],
193+
];
194+
const jsonBody = new JsonBuilder(jest.fn(), jest.fn());
195+
196+
for (let [shape, scalar] of cases) {
197+
expect(jsonBody.build(shape, scalar))
198+
.toBe(JSON.stringify(scalar));
199+
}
200+
});
201+
});
202+
});

packages/json-builder/index.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import {isArrayBuffer} from '@aws/is-array-buffer';
2+
import {epoch} from "@aws/protocol-timestamp";
3+
import {
4+
BodySerializer,
5+
Decoder,
6+
Encoder,
7+
Member,
8+
SerializationModel
9+
} from "@aws/types";
10+
11+
type Scalar = string|number|boolean|null;
12+
13+
interface Json {
14+
[index: string]: Scalar|Json|JsonArray;
15+
}
16+
17+
interface JsonArray extends Array<Scalar|Json|JsonArray> {}
18+
19+
type JsonValue = Json|Scalar|JsonArray;
20+
21+
export class JsonBuilder implements BodySerializer {
22+
constructor(
23+
private readonly base64Encoder: Encoder,
24+
private readonly utf8Decoder: Decoder
25+
) {}
26+
27+
public build(shape: Member, input: any): string {
28+
return JSON.stringify(this.format(shape.shape, input));
29+
}
30+
31+
private format(shape: SerializationModel, input: any): JsonValue {
32+
const inputType = typeof input;
33+
if (shape.type === 'structure') {
34+
if (inputType !== 'object' || input === null) {
35+
throw new Error(
36+
`Unable to serialize value of type ${typeof input} as a`
37+
+ ' structure'
38+
);
39+
}
40+
41+
const data: Json = {};
42+
for (let key of Object.keys(input)) {
43+
if (
44+
input[key] === undefined ||
45+
input[key] === null ||
46+
!(key in shape.members)
47+
) {
48+
continue;
49+
}
50+
51+
const {
52+
locationName = key,
53+
shape: memberShape
54+
} = shape.members[key];
55+
data[locationName] = this.format(memberShape, input[key]);
56+
}
57+
58+
return data;
59+
} else if (shape.type === 'list') {
60+
if (Array.isArray(input) || isIterable(input)) {
61+
const data: JsonArray = [];
62+
for (let element of input) {
63+
data.push(this.format(shape.member.shape, element));
64+
}
65+
66+
return data;
67+
}
68+
69+
throw new Error(
70+
'Unable to serialize value that is neither an array nor an'
71+
+ ' iterable as a list'
72+
);
73+
} else if (shape.type === 'map') {
74+
const data: Json = {};
75+
// A map input is should be a [key, value] iterable...
76+
if (isIterable(input)) {
77+
for (let [key, value] of input) {
78+
data[key] = this.format(shape.value.shape, value);
79+
}
80+
return data;
81+
}
82+
83+
// ... or an object
84+
if (inputType !== 'object' || input === null) {
85+
throw new Error(
86+
'Unable to serialize value that is neither a [key, value]'
87+
+ ' iterable nor an object as a map'
88+
);
89+
}
90+
91+
for (let key of Object.keys(input)) {
92+
data[key] = this.format(shape.value.shape, input[key]);
93+
}
94+
return data;
95+
} else if (shape.type === 'blob') {
96+
if (typeof input === 'string') {
97+
input = this.utf8Decoder(input);
98+
} else if (ArrayBuffer.isView(input)) {
99+
input = new Uint8Array(
100+
input.buffer,
101+
input.byteOffset,
102+
input.byteLength
103+
);
104+
} else if (isArrayBuffer(input)) {
105+
input = new Uint8Array(input);
106+
} else {
107+
throw new Error(
108+
'Unable to serialize value that is neither a string nor an'
109+
+ ' ArrayBuffer nor an ArrayBufferView as a blob'
110+
);
111+
}
112+
113+
return this.base64Encoder(input);
114+
} else if (shape.type === 'timestamp') {
115+
if (
116+
['number', 'string'].indexOf(typeof input) > -1
117+
|| Object.prototype.toString.call(input) === '[object Date]'
118+
) {
119+
return epoch(input);
120+
}
121+
122+
throw new Error(
123+
'Unable to serialize value that is neither a string nor a'
124+
+ ' number nor a Date object as a timestamp'
125+
);
126+
}
127+
128+
return input;
129+
}
130+
}
131+
132+
function isIterable(arg: any): arg is Iterable<any> {
133+
return Boolean(arg)
134+
&& typeof Symbol !== 'undefined'
135+
&& typeof arg[Symbol.iterator] === 'function';
136+
}

packages/json-builder/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@aws/json-builder",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "A marshaller for the body portion of AWS's JSON and REST-JSON protocols",
6+
"main": "lib/index.js",
7+
"scripts": {
8+
"prepublishOnly": "tsc",
9+
"pretest": "tsc",
10+
"test": "jest"
11+
},
12+
"author": "[email protected]",
13+
"license": "UNLICENSED",
14+
"dependencies": {
15+
"@aws/is-array-buffer": "^0.0.1",
16+
"@aws/protocol-timestamp": "^0.0.1",
17+
"@aws/types": "^0.0.1",
18+
"tslib": "^1.7.1"
19+
},
20+
"devDependencies": {
21+
"@types/jest": "^20.0.2",
22+
"jest": "^20.0.4",
23+
"typescript": "^2.3"
24+
}
25+
}

packages/json-builder/tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"declaration": true,
6+
"sourceMap": true,
7+
"strict": true,
8+
"downlevelIteration": true,
9+
"importHelpers": true,
10+
"lib": [
11+
"es5",
12+
"es2015.symbol",
13+
"es2015.iterable"
14+
]
15+
}
16+
}

packages/json-parser/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts

0 commit comments

Comments
 (0)