Skip to content

JSON-RPC marshaller/unmarshaller #22

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 3, 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
3 changes: 3 additions & 0 deletions packages/protocol-json-rpc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.js
*.js.map
*.d.ts
179 changes: 179 additions & 0 deletions packages/protocol-json-rpc/__tests__/JsonRpcParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {JsonRpcParser} from '../lib/JsonRpcParser';
import {OperationModel, HttpResponse} from '@aws/types';
import {extractMetadata} from '@aws/response-metadata-extractor';

const operation: OperationModel = {
http: {
method: 'GET',
requestUri: '/'
},
name: 'test',
metadata: {
apiVersion: '2017-06-28',
endpointPrefix: 'foo',
protocol: 'json',
serviceFullName: 'AWS Foo Service',
signatureVersion: 'v4',
uid: 'foo-2017-06-28',
},
input: {
shape: {
type: 'structure',
required: [],
members: {},
}
},
output: {
shape: {
type: 'structure',
required: [],
members: {},
}
},
errors: [],
};

const response: HttpResponse = {
statusCode: 200,
headers: {},
body: 'a string body'
};
const $metadata = extractMetadata(response);

describe('JsonRpcParser', () => {
it(
'should pass the operation output and HTTP response body to the body parser',
async () => {
const bodyParser = {
parse: jest.fn(() => { return {}; })
};

const parser = new JsonRpcParser(
bodyParser,
jest.fn(),
jest.fn(),
);
const parsed = await parser.parse(operation, response);
expect(parsed).toEqual({$metadata});
expect(bodyParser.parse.mock.calls.length).toBe(1);
expect(bodyParser.parse.mock.calls[0]).toEqual([
operation.input,
'a string body'
]);
}
);
it(
'use an empty string for the body if none is included in the message',
async () => {
const bodyParser = {
parse: jest.fn(() => { return {}; })
};

const parser = new JsonRpcParser(
bodyParser,
jest.fn(),
jest.fn(),
);
const parsed = await parser.parse(operation, {
...response,
body: void 0
});
expect(parsed).toEqual({$metadata});
expect(bodyParser.parse.mock.calls.length).toBe(1);
expect(bodyParser.parse.mock.calls[0]).toEqual([
operation.input,
''
]);
}
);

it('should UTF-8 encode ArrayBuffer bodies', async () => {
const bufferBody = new ArrayBuffer(0);
const bodyParser = {
parse: jest.fn(() => { return {}; })
};
const utf8Encoder = jest.fn(() => 'a string');

const parser = new JsonRpcParser(
bodyParser,
jest.fn(),
utf8Encoder,
);

await parser.parse(operation, {
...response,
body: bufferBody
});

expect(utf8Encoder.mock.calls.length).toBe(1);
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody);
expect(bodyParser.parse.mock.calls.length).toBe(1);
expect(bodyParser.parse.mock.calls[0]).toEqual([
operation.input,
'a string'
]);
});

it('should UTF-8 encode ArrayBufferView bodies', async () => {
const bufferBody = new Int32Array(0);
const bodyParser = {
parse: jest.fn(() => { return {}; })
};
const utf8Encoder = jest.fn(() => 'a string');

const parser = new JsonRpcParser(
bodyParser,
jest.fn(),
utf8Encoder,
);

await parser.parse(operation, {
...response,
body: bufferBody
});

expect(utf8Encoder.mock.calls.length).toBe(1);
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(bufferBody.buffer);
expect(bodyParser.parse.mock.calls.length).toBe(1);
expect(bodyParser.parse.mock.calls[0]).toEqual([
operation.input,
'a string'
]);
});

it('should collect and UTF-8 encode stream bodies', async () => {
const streamBody = {chunks: [
new Uint8Array([0xde, 0xad]),
new Uint8Array([0xbe, 0xef]),
]};
const collectedStream = new Uint8Array(0);
const bodyParser = {
parse: jest.fn(() => { return {}; })
};
const utf8Encoder = jest.fn(() => 'a string');
const streamCollector = jest.fn(() => Promise.resolve(collectedStream));

const parser = new JsonRpcParser<any>(
bodyParser,
streamCollector,
utf8Encoder,
);

await parser.parse(operation, {
...response,
body: streamBody
});

expect(streamCollector.mock.calls.length).toBe(1);
expect(streamCollector.mock.calls[0][0]).toBe(streamBody);

expect(utf8Encoder.mock.calls.length).toBe(1);
expect(utf8Encoder.mock.calls[0][0].buffer).toBe(collectedStream.buffer);

expect(bodyParser.parse.mock.calls.length).toBe(1);
expect(bodyParser.parse.mock.calls[0]).toEqual([
operation.input,
'a string'
]);
});
});
102 changes: 102 additions & 0 deletions packages/protocol-json-rpc/__tests__/JsonRpcSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {JsonRpcSerializer} from "../lib/JsonRpcSerializer";
import {HttpEndpoint, OperationModel} from "@aws/types";

const operation: OperationModel = {
http: {
method: 'GET',
requestUri: '/'
},
name: 'test',
metadata: {
apiVersion: '2017-06-28',
endpointPrefix: 'foo',
protocol: 'json',
serviceFullName: 'AWS Foo Service',
signatureVersion: 'v4',
uid: 'foo-2017-06-28',
targetPrefix: 'FooTarget',
jsonVersion: '1.1',
},
input: {
shape: {
type: 'structure',
required: [],
members: {},
}
},
output: {
shape: {
type: 'structure',
required: [],
members: {},
}
},
errors: [],
};

const endpoint: HttpEndpoint = {
protocol: 'https:',
hostname: 'foo.region.amazonaws.com',
path: '/path',
};

describe('JsonRpcSerializer', () => {
it(
'should use the injected body serializer to build the HTTP request body',
() => {
const bodySerializer = {
build: jest.fn(() => 'serialized'),
};
const input = {foo: 'bar'};
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
expect(serializer.serialize(operation, input).body)
.toBe('serialized');

expect(bodySerializer.build.mock.calls.length).toBe(1);
expect(bodySerializer.build.mock.calls[0]).toEqual([
operation.input,
input,
]);
}
);

it('should use the operation HTTP trait to build the request', () => {
const bodySerializer = {
build: jest.fn(() => 'serialized'),
};
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
const serialized = serializer.serialize(operation, {foo: 'bar'});

expect(serialized.method).toBe(operation.http.method);
expect(serialized.path).toBe(operation.http.requestUri);
});

it(
'should construct a Content-Type header using the service JSON version',
() => {
const bodySerializer = {
build: jest.fn(() => 'serialized'),
};
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
const serialized = serializer.serialize(operation, {foo: 'bar'});

expect(serialized.headers['Content-Type']).toBe(
`application/x-amz-json-${operation.metadata.jsonVersion}`
);
}
);

it(
'should construct an X-Amz-Target header using the service target prefix and the operation name',
() => {
const bodySerializer = {
build: jest.fn(() => 'serialized'),
};
const serializer = new JsonRpcSerializer(endpoint, bodySerializer);
const serialized = serializer.serialize(operation, {foo: 'bar'});

expect(serialized.headers['X-Amz-Target'])
.toBe(`${operation.metadata.targetPrefix}.${operation.name}`);
}
);
});
2 changes: 2 additions & 0 deletions packages/protocol-json-rpc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/JsonRpcParser';
export * from './lib/JsonRpcSerializer';
65 changes: 65 additions & 0 deletions packages/protocol-json-rpc/lib/JsonRpcParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {extractMetadata} from '@aws/response-metadata-extractor';
import {
BodyParser,
Encoder,
HttpResponse,
MetadataBearer,
OperationModel,
ResponseParser,
StreamCollector,
} from '@aws/types';

export class JsonRpcParser<StreamType> implements ResponseParser<StreamType> {
constructor(
private readonly bodyParser: BodyParser,
private readonly bodyCollector: StreamCollector<StreamType>,
private readonly utf8Encoder: Encoder
) {}

parse<OutputType extends MetadataBearer>(
operation: OperationModel,
input: HttpResponse<StreamType>
): Promise<OutputType> {
return this.resolveBodyString(input)
.then(body => this.bodyParser.parse<Partial<OutputType>>(
operation.output,
body
)).then(partialOutput => {
partialOutput.$metadata = extractMetadata(input);
return partialOutput as OutputType;
});
}

private resolveBodyString(
input: HttpResponse<StreamType>
): Promise<string> {
const {body = ''} = input;
if (typeof body === 'string') {
return Promise.resolve(body);
}

let bufferPromise: Promise<Uint8Array>;
if (ArrayBuffer.isView(body)) {
bufferPromise = Promise.resolve(new Uint8Array(
body.buffer,
body.byteLength,
body.byteOffset
));
} else if (isArrayBuffer(body)) {
bufferPromise = Promise.resolve(new Uint8Array(
body,
0,
body.byteLength
));
} else {
bufferPromise = this.bodyCollector(body);
}

return bufferPromise.then(buffer => this.utf8Encoder(buffer));
}
}

function isArrayBuffer(arg: any): arg is ArrayBuffer {
return arg instanceof ArrayBuffer ||
Object.prototype.toString.call(arg) === '[object ArrayBuffer]'
}
37 changes: 37 additions & 0 deletions packages/protocol-json-rpc/lib/JsonRpcSerializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
BodySerializer,
HttpEndpoint,
HttpRequest,
OperationModel,
RequestSerializer,
} from '@aws/types';

export class JsonRpcSerializer implements RequestSerializer<string> {
constructor(
private readonly endpoint: HttpEndpoint,
private readonly bodySerializer: BodySerializer
) {}

serialize(operation: OperationModel, input: any): HttpRequest<string> {
const {
http: httpTrait,
input: inputShape,
metadata: {
jsonVersion,
targetPrefix,
},
name,
} = operation;

return {
...this.endpoint,
headers: {
'X-Amz-Target': `${targetPrefix}.${name}`,
'Content-Type': `application/x-amz-json-${jsonVersion}`,
},
body: this.bodySerializer.build(inputShape, input),
path: httpTrait.requestUri,
method: httpTrait.method,
};
}
}
Loading