Skip to content

Commit 91bf1dd

Browse files
authored
Merge pull request #11 from jeskew/feature/json-builder
Feature/json builder
2 parents 46de3c0 + ff06577 commit 91bf1dd

File tree

10 files changed

+661
-0
lines changed

10 files changed

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

packages/json-builder/index.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 { locationName = key, shape: memberShape } = shape.members[key];
52+
data[locationName] = this.format(memberShape, input[key]);
53+
}
54+
55+
return data;
56+
} else if (shape.type === "list") {
57+
if (Array.isArray(input) || isIterable(input)) {
58+
const data: JsonArray = [];
59+
for (let element of input) {
60+
data.push(this.format(shape.member.shape, element));
61+
}
62+
63+
return data;
64+
}
65+
66+
throw new Error(
67+
"Unable to serialize value that is neither an array nor an" +
68+
" iterable as a list"
69+
);
70+
} else if (shape.type === "map") {
71+
const data: Json = {};
72+
// A map input is should be a [key, value] iterable...
73+
if (isIterable(input)) {
74+
for (let [key, value] of input) {
75+
data[key] = this.format(shape.value.shape, value);
76+
}
77+
return data;
78+
}
79+
80+
// ... or an object
81+
if (inputType !== "object" || input === null) {
82+
throw new Error(
83+
"Unable to serialize value that is neither a [key, value]" +
84+
" iterable nor an object as a map"
85+
);
86+
}
87+
88+
for (let key of Object.keys(input)) {
89+
data[key] = this.format(shape.value.shape, input[key]);
90+
}
91+
return data;
92+
} else if (shape.type === "blob") {
93+
if (typeof input === "string") {
94+
input = this.utf8Decoder(input);
95+
} else if (ArrayBuffer.isView(input)) {
96+
input = new Uint8Array(
97+
input.buffer,
98+
input.byteOffset,
99+
input.byteLength
100+
);
101+
} else if (isArrayBuffer(input)) {
102+
input = new Uint8Array(input);
103+
} else {
104+
throw new Error(
105+
"Unable to serialize value that is neither a string nor an" +
106+
" ArrayBuffer nor an ArrayBufferView as a blob"
107+
);
108+
}
109+
110+
return this.base64Encoder(input);
111+
} else if (shape.type === "timestamp") {
112+
if (
113+
["number", "string"].indexOf(typeof input) > -1 ||
114+
Object.prototype.toString.call(input) === "[object Date]"
115+
) {
116+
return epoch(input);
117+
}
118+
119+
throw new Error(
120+
"Unable to serialize value that is neither a string nor a" +
121+
" number nor a Date object as a timestamp"
122+
);
123+
}
124+
125+
return input;
126+
}
127+
}
128+
129+
function isIterable(arg: any): arg is Iterable<any> {
130+
return (
131+
Boolean(arg) &&
132+
typeof Symbol !== "undefined" &&
133+
typeof arg[Symbol.iterator] === "function"
134+
);
135+
}

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)