Skip to content

Commit 6a57441

Browse files
committed
Add a marshaller and unmarshaller for the AWS JSON-RPC protocol
1 parent ac837b1 commit 6a57441

File tree

10 files changed

+430
-1
lines changed

10 files changed

+430
-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: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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(
45+
'should pass the operation output and HTTP response body to the body parser',
46+
async () => {
47+
const bodyParser = {
48+
parse: jest.fn(() => { return {}; })
49+
};
50+
51+
const parser = new JsonRpcParser(
52+
bodyParser,
53+
jest.fn(),
54+
jest.fn(),
55+
);
56+
const parsed = await parser.parse(operation, response);
57+
expect(parsed).toEqual({$metadata});
58+
expect(bodyParser.parse.mock.calls.length).toBe(1);
59+
expect(bodyParser.parse.mock.calls[0]).toEqual([
60+
operation.input,
61+
'a string body'
62+
]);
63+
}
64+
);
65+
it(
66+
'use an empty string for the body if none is included in the message',
67+
async () => {
68+
const bodyParser = {
69+
parse: jest.fn(() => { return {}; })
70+
};
71+
72+
const parser = new JsonRpcParser(
73+
bodyParser,
74+
jest.fn(),
75+
jest.fn(),
76+
);
77+
const parsed = await parser.parse(operation, {
78+
...response,
79+
body: void 0
80+
});
81+
expect(parsed).toEqual({$metadata});
82+
expect(bodyParser.parse.mock.calls.length).toBe(1);
83+
expect(bodyParser.parse.mock.calls[0]).toEqual([
84+
operation.input,
85+
''
86+
]);
87+
}
88+
);
89+
90+
it('should UTF-8 encode ArrayBuffer bodies', async () => {
91+
const bufferBody = new ArrayBuffer(0);
92+
const bodyParser = {
93+
parse: jest.fn(() => { return {}; })
94+
};
95+
const utf8Encoder = jest.fn(() => 'a string');
96+
97+
const parser = new JsonRpcParser(
98+
bodyParser,
99+
jest.fn(),
100+
utf8Encoder,
101+
);
102+
103+
await parser.parse(operation, {
104+
...response,
105+
body: bufferBody
106+
});
107+
108+
expect(utf8Encoder.mock.calls.length).toBe(1);
109+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody);
110+
expect(bodyParser.parse.mock.calls.length).toBe(1);
111+
expect(bodyParser.parse.mock.calls[0]).toEqual([
112+
operation.input,
113+
'a string'
114+
]);
115+
});
116+
117+
it('should UTF-8 encode ArrayBufferView bodies', async () => {
118+
const bufferBody = new Int32Array(0);
119+
const bodyParser = {
120+
parse: jest.fn(() => { return {}; })
121+
};
122+
const utf8Encoder = jest.fn(() => 'a string');
123+
124+
const parser = new JsonRpcParser(
125+
bodyParser,
126+
jest.fn(),
127+
utf8Encoder,
128+
);
129+
130+
await parser.parse(operation, {
131+
...response,
132+
body: bufferBody
133+
});
134+
135+
expect(utf8Encoder.mock.calls.length).toBe(1);
136+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody.buffer);
137+
expect(bodyParser.parse.mock.calls.length).toBe(1);
138+
expect(bodyParser.parse.mock.calls[0]).toEqual([
139+
operation.input,
140+
'a string'
141+
]);
142+
});
143+
144+
it('should collect and UTF-8 encode stream bodies', async () => {
145+
const streamBody = {chunks: [
146+
new Uint8Array([0xde, 0xad]),
147+
new Uint8Array([0xbe, 0xef]),
148+
]};
149+
const collectedStream = new Uint8Array(0);
150+
const bodyParser = {
151+
parse: jest.fn(() => { return {}; })
152+
};
153+
const utf8Encoder = jest.fn(() => 'a string');
154+
const streamCollector = jest.fn(() => Promise.resolve(collectedStream));
155+
156+
const parser = new JsonRpcParser<any>(
157+
bodyParser,
158+
streamCollector,
159+
utf8Encoder,
160+
);
161+
162+
await parser.parse(operation, {
163+
...response,
164+
body: streamBody
165+
});
166+
167+
expect(streamCollector.mock.calls.length).toBe(1);
168+
expect(streamCollector.mock.calls[0][0]).toBe(streamBody);
169+
170+
expect(utf8Encoder.mock.calls.length).toBe(1);
171+
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(collectedStream.buffer);
172+
173+
expect(bodyParser.parse.mock.calls.length).toBe(1);
174+
expect(bodyParser.parse.mock.calls[0]).toEqual([
175+
operation.input,
176+
'a string'
177+
]);
178+
});
179+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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(
45+
'should use the injected body serializer to build the HTTP request body',
46+
() => {
47+
const bodySerializer = {
48+
build: jest.fn(() => 'serialized'),
49+
};
50+
const input = {foo: 'bar'};
51+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
52+
expect(serializer.serialize(operation, input).body)
53+
.toBe('serialized');
54+
55+
expect(bodySerializer.build.mock.calls.length).toBe(1);
56+
expect(bodySerializer.build.mock.calls[0]).toEqual([
57+
operation.input,
58+
input,
59+
]);
60+
}
61+
);
62+
63+
it('should use the operation HTTP trait to build the request', () => {
64+
const bodySerializer = {
65+
build: jest.fn(() => 'serialized'),
66+
};
67+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
68+
const serialized = serializer.serialize(operation, {foo: 'bar'});
69+
70+
expect(serialized.method).toBe(operation.http.method);
71+
expect(serialized.path).toBe(operation.http.requestUri);
72+
});
73+
74+
it(
75+
'should construct a Content-Type header using the service JSON version',
76+
() => {
77+
const bodySerializer = {
78+
build: jest.fn(() => 'serialized'),
79+
};
80+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
81+
const serialized = serializer.serialize(operation, {foo: 'bar'});
82+
83+
expect(serialized.headers['Content-Type']).toBe(
84+
`application/x-amz-json-${operation.metadata.jsonVersion}`
85+
);
86+
}
87+
);
88+
89+
it(
90+
'should construct an X-Amz-Target header using the service target prefix and the operation name',
91+
() => {
92+
const bodySerializer = {
93+
build: jest.fn(() => 'serialized'),
94+
};
95+
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
96+
const serialized = serializer.serialize(operation, {foo: 'bar'});
97+
98+
expect(serialized.headers['X-Amz-Target'])
99+
.toBe(`${operation.metadata.targetPrefix}.${operation.name}`);
100+
}
101+
);
102+
});

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 => this.bodyParser.parse<Partial<OutputType>>(
25+
operation.output,
26+
body
27+
)).then(partialOutput => {
28+
partialOutput.$metadata = extractMetadata(input);
29+
return partialOutput as OutputType;
30+
});
31+
}
32+
33+
private resolveBodyString(
34+
input: HttpResponse<StreamType>
35+
): Promise<string> {
36+
const {body = ''} = input;
37+
if (typeof body === 'string') {
38+
return Promise.resolve(body);
39+
}
40+
41+
let bufferPromise: Promise<Uint8Array>;
42+
if (ArrayBuffer.isView(body)) {
43+
bufferPromise = Promise.resolve(new Uint8Array(
44+
body.buffer,
45+
body.byteLength,
46+
body.byteOffset
47+
));
48+
} else if (isArrayBuffer(body)) {
49+
bufferPromise = Promise.resolve(new Uint8Array(
50+
body,
51+
0,
52+
body.byteLength
53+
));
54+
} else {
55+
bufferPromise = this.bodyCollector(body);
56+
}
57+
58+
return bufferPromise.then(buffer => this.utf8Encoder(buffer));
59+
}
60+
}
61+
62+
function isArrayBuffer(arg: any): arg is ArrayBuffer {
63+
return arg instanceof ArrayBuffer ||
64+
Object.prototype.toString.call(arg) === '[object ArrayBuffer]'
65+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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: {
20+
jsonVersion,
21+
targetPrefix,
22+
},
23+
name,
24+
} = operation;
25+
26+
return {
27+
...this.endpoint,
28+
headers: {
29+
'X-Amz-Target': `${targetPrefix}.${name}`,
30+
'Content-Type': `application/x-amz-json-${jsonVersion}`,
31+
},
32+
body: this.bodySerializer.build(inputShape, input),
33+
path: httpTrait.requestUri,
34+
method: httpTrait.method,
35+
};
36+
}
37+
}

0 commit comments

Comments
 (0)