Skip to content

Commit dcaf024

Browse files
committed
Add a marshaller and unmarshaller for the AWS JSON-RPC protocol
1 parent 73bd375 commit dcaf024

File tree

10 files changed

+396
-1
lines changed

10 files changed

+396
-1
lines changed

packages/protocol-json-rpc/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.js.map
3+
*.d.ts
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { JsonRpcParser } from "../lib/JsonRpcParser";
2+
import { OperationModel, HttpResponse } from "@aws/types";
3+
import { extractMetadata } from "@aws/response-metadata-extractor";
4+
5+
const operation: OperationModel = {
6+
http: {
7+
method: "GET",
8+
requestUri: "/"
9+
},
10+
name: "test",
11+
metadata: {
12+
apiVersion: "2017-06-28",
13+
endpointPrefix: "foo",
14+
protocol: "json",
15+
serviceFullName: "AWS Foo Service",
16+
signatureVersion: "v4",
17+
uid: "foo-2017-06-28"
18+
},
19+
input: {
20+
shape: {
21+
type: "structure",
22+
required: [],
23+
members: {}
24+
}
25+
},
26+
output: {
27+
shape: {
28+
type: "structure",
29+
required: [],
30+
members: {}
31+
}
32+
},
33+
errors: []
34+
};
35+
36+
const response: HttpResponse = {
37+
statusCode: 200,
38+
headers: {},
39+
body: "a string body"
40+
};
41+
const $metadata = extractMetadata(response);
42+
43+
describe("JsonRpcParser", () => {
44+
it("should pass the operation output and HTTP response body to the body parser", async () => {
45+
const bodyParser = {
46+
parse: jest.fn(() => {
47+
return {};
48+
})
49+
};
50+
51+
const parser = new JsonRpcParser(bodyParser, jest.fn(), jest.fn());
52+
const parsed = await parser.parse(operation, response);
53+
expect(parsed).toEqual({ $metadata });
54+
expect(bodyParser.parse.mock.calls.length).toBe(1);
55+
expect(bodyParser.parse.mock.calls[0]).toEqual([
56+
operation.input,
57+
"a string body"
58+
]);
59+
});
60+
it("use an empty string for the body if none is included in the message", async () => {
61+
const bodyParser = {
62+
parse: jest.fn(() => {
63+
return {};
64+
})
65+
};
66+
67+
const parser = new JsonRpcParser(bodyParser, jest.fn(), jest.fn());
68+
const parsed = await parser.parse(operation, {
69+
...response,
70+
body: void 0
71+
});
72+
expect(parsed).toEqual({ $metadata });
73+
expect(bodyParser.parse.mock.calls.length).toBe(1);
74+
expect(bodyParser.parse.mock.calls[0]).toEqual([operation.input, ""]);
75+
});
76+
77+
it("should UTF-8 encode ArrayBuffer bodies", async () => {
78+
const bufferBody = new ArrayBuffer(0);
79+
const bodyParser = {
80+
parse: jest.fn(() => {
81+
return {};
82+
})
83+
};
84+
const utf8Encoder = jest.fn(() => "a string");
85+
86+
const parser = new JsonRpcParser(bodyParser, jest.fn(), utf8Encoder);
87+
88+
await parser.parse(operation, {
89+
...response,
90+
body: bufferBody
91+
});
92+
93+
expect(utf8Encoder.mock.calls.length).toBe(1);
94+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody);
95+
expect(bodyParser.parse.mock.calls.length).toBe(1);
96+
expect(bodyParser.parse.mock.calls[0]).toEqual([
97+
operation.input,
98+
"a string"
99+
]);
100+
});
101+
102+
it("should UTF-8 encode ArrayBufferView bodies", async () => {
103+
const bufferBody = new Int32Array(0);
104+
const bodyParser = {
105+
parse: jest.fn(() => {
106+
return {};
107+
})
108+
};
109+
const utf8Encoder = jest.fn(() => "a string");
110+
111+
const parser = new JsonRpcParser(bodyParser, jest.fn(), utf8Encoder);
112+
113+
await parser.parse(operation, {
114+
...response,
115+
body: bufferBody
116+
});
117+
118+
expect(utf8Encoder.mock.calls.length).toBe(1);
119+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody.buffer);
120+
expect(bodyParser.parse.mock.calls.length).toBe(1);
121+
expect(bodyParser.parse.mock.calls[0]).toEqual([
122+
operation.input,
123+
"a string"
124+
]);
125+
});
126+
127+
it("should collect and UTF-8 encode stream bodies", async () => {
128+
const streamBody = {
129+
chunks: [new Uint8Array([0xde, 0xad]), new Uint8Array([0xbe, 0xef])]
130+
};
131+
const collectedStream = new Uint8Array(0);
132+
const bodyParser = {
133+
parse: jest.fn(() => {
134+
return {};
135+
})
136+
};
137+
const utf8Encoder = jest.fn(() => "a string");
138+
const streamCollector = jest.fn(() => Promise.resolve(collectedStream));
139+
140+
const parser = new JsonRpcParser<any>(
141+
bodyParser,
142+
streamCollector,
143+
utf8Encoder
144+
);
145+
146+
await parser.parse(operation, {
147+
...response,
148+
body: streamBody
149+
});
150+
151+
expect(streamCollector.mock.calls.length).toBe(1);
152+
expect(streamCollector.mock.calls[0][0]).toBe(streamBody);
153+
154+
expect(utf8Encoder.mock.calls.length).toBe(1);
155+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(collectedStream.buffer);
156+
157+
expect(bodyParser.parse.mock.calls.length).toBe(1);
158+
expect(bodyParser.parse.mock.calls[0]).toEqual([
159+
operation.input,
160+
"a string"
161+
]);
162+
});
163+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { JsonRpcSerializer } from "../lib/JsonRpcSerializer";
2+
import { HttpEndpoint, OperationModel } from "@aws/types";
3+
4+
const operation: OperationModel = {
5+
http: {
6+
method: "GET",
7+
requestUri: "/"
8+
},
9+
name: "test",
10+
metadata: {
11+
apiVersion: "2017-06-28",
12+
endpointPrefix: "foo",
13+
protocol: "json",
14+
serviceFullName: "AWS Foo Service",
15+
signatureVersion: "v4",
16+
uid: "foo-2017-06-28",
17+
targetPrefix: "FooTarget",
18+
jsonVersion: "1.1"
19+
},
20+
input: {
21+
shape: {
22+
type: "structure",
23+
required: [],
24+
members: {}
25+
}
26+
},
27+
output: {
28+
shape: {
29+
type: "structure",
30+
required: [],
31+
members: {}
32+
}
33+
},
34+
errors: []
35+
};
36+
37+
const endpoint: HttpEndpoint = {
38+
protocol: "https:",
39+
hostname: "foo.region.amazonaws.com",
40+
path: "/path"
41+
};
42+
43+
describe("JsonRpcSerializer", () => {
44+
it("should use the injected body serializer to build the HTTP request body", () => {
45+
const bodySerializer = {
46+
build: jest.fn(() => "serialized")
47+
};
48+
const input = { foo: "bar" };
49+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
50+
expect(serializer.serialize(operation, input).body).toBe("serialized");
51+
52+
expect(bodySerializer.build.mock.calls.length).toBe(1);
53+
expect(bodySerializer.build.mock.calls[0]).toEqual([
54+
operation.input,
55+
input
56+
]);
57+
});
58+
59+
it("should use the operation HTTP trait to build the request", () => {
60+
const bodySerializer = {
61+
build: jest.fn(() => "serialized")
62+
};
63+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
64+
const serialized = serializer.serialize(operation, { foo: "bar" });
65+
66+
expect(serialized.method).toBe(operation.http.method);
67+
expect(serialized.path).toBe(operation.http.requestUri);
68+
});
69+
70+
it("should construct a Content-Type header using the service JSON version", () => {
71+
const bodySerializer = {
72+
build: jest.fn(() => "serialized")
73+
};
74+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
75+
const serialized = serializer.serialize(operation, { foo: "bar" });
76+
77+
expect(serialized.headers["Content-Type"]).toBe(
78+
`application/x-amz-json-${operation.metadata.jsonVersion}`
79+
);
80+
});
81+
82+
it("should construct an X-Amz-Target header using the service target prefix and the operation name", () => {
83+
const bodySerializer = {
84+
build: jest.fn(() => "serialized")
85+
};
86+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
87+
const serialized = serializer.serialize(operation, { foo: "bar" });
88+
89+
expect(serialized.headers["X-Amz-Target"]).toBe(
90+
`${operation.metadata.targetPrefix}.${operation.name}`
91+
);
92+
});
93+
});

packages/protocol-json-rpc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./lib/JsonRpcParser";
2+
export * from "./lib/JsonRpcSerializer";
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { extractMetadata } from "@aws/response-metadata-extractor";
2+
import {
3+
BodyParser,
4+
Encoder,
5+
HttpResponse,
6+
MetadataBearer,
7+
OperationModel,
8+
ResponseParser,
9+
StreamCollector
10+
} from "@aws/types";
11+
12+
export class JsonRpcParser<StreamType> implements ResponseParser<StreamType> {
13+
constructor(
14+
private readonly bodyParser: BodyParser,
15+
private readonly bodyCollector: StreamCollector<StreamType>,
16+
private readonly utf8Encoder: Encoder
17+
) {}
18+
19+
parse<OutputType extends MetadataBearer>(
20+
operation: OperationModel,
21+
input: HttpResponse<StreamType>
22+
): Promise<OutputType> {
23+
return this.resolveBodyString(input)
24+
.then(body =>
25+
this.bodyParser.parse<Partial<OutputType>>(operation.output, body)
26+
)
27+
.then(partialOutput => {
28+
partialOutput.$metadata = extractMetadata(input);
29+
return partialOutput as OutputType;
30+
});
31+
}
32+
33+
private resolveBodyString(input: HttpResponse<StreamType>): Promise<string> {
34+
const { body = "" } = input;
35+
if (typeof body === "string") {
36+
return Promise.resolve(body);
37+
}
38+
39+
let bufferPromise: Promise<Uint8Array>;
40+
if (ArrayBuffer.isView(body)) {
41+
bufferPromise = Promise.resolve(
42+
new Uint8Array(body.buffer, body.byteLength, body.byteOffset)
43+
);
44+
} else if (isArrayBuffer(body)) {
45+
bufferPromise = Promise.resolve(new Uint8Array(body, 0, body.byteLength));
46+
} else {
47+
bufferPromise = this.bodyCollector(body);
48+
}
49+
50+
return bufferPromise.then(buffer => this.utf8Encoder(buffer));
51+
}
52+
}
53+
54+
function isArrayBuffer(arg: any): arg is ArrayBuffer {
55+
return (
56+
arg instanceof ArrayBuffer ||
57+
Object.prototype.toString.call(arg) === "[object ArrayBuffer]"
58+
);
59+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
BodySerializer,
3+
HttpEndpoint,
4+
HttpRequest,
5+
OperationModel,
6+
RequestSerializer
7+
} from "@aws/types";
8+
9+
export class JsonRpcSerializer implements RequestSerializer<string> {
10+
constructor(
11+
private readonly endpoint: HttpEndpoint,
12+
private readonly bodySerializer: BodySerializer
13+
) {}
14+
15+
serialize(operation: OperationModel, input: any): HttpRequest<string> {
16+
const {
17+
http: httpTrait,
18+
input: inputShape,
19+
metadata: { jsonVersion, targetPrefix },
20+
name
21+
} = operation;
22+
23+
return {
24+
...this.endpoint,
25+
headers: {
26+
"X-Amz-Target": `${targetPrefix}.${name}`,
27+
"Content-Type": `application/x-amz-json-${jsonVersion}`
28+
},
29+
body: this.bodySerializer.build(inputShape, input),
30+
path: httpTrait.requestUri,
31+
method: httpTrait.method
32+
};
33+
}
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@aws/protocol-json-rpc",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "Provides a marshaller and unmarshaller for the AWS JSON-RPC protocol",
6+
"scripts": {
7+
"pretest": "tsc",
8+
"test": "jest"
9+
},
10+
"author": "[email protected]",
11+
"license": "UNLICENSED",
12+
"main": "index.js",
13+
"dependencies": {
14+
"@aws/response-metadata-extractor": "^0.0.1",
15+
"@aws/types": "^0.0.1"
16+
},
17+
"devDependencies": {
18+
"@types/jest": "^20.0.2",
19+
"jest": "^20.0.4",
20+
"typescript": "^2.3"
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"declaration": true,
6+
"strict": true,
7+
"sourceMap": true,
8+
"importHelpers": true,
9+
"lib": [
10+
"es5",
11+
"es2015.promise"
12+
]
13+
}
14+
}

0 commit comments

Comments
 (0)