Refactor JSON parsing in HttpClient

This commit is contained in:
Brandon Millman 2017-12-19 17:22:38 -05:00
parent 9f3acf8e28
commit 8fe81c9d09
6 changed files with 121 additions and 63 deletions

View File

@ -18,7 +18,7 @@ import {
TokenPairsItem, TokenPairsItem,
TokenPairsRequest, TokenPairsRequest,
} from './types'; } from './types';
import {typeConverters} from './utils/type_converters'; import {relayerResponseJsonParsers} from './utils/relayer_response_json_parsers';
/** /**
* This class includes all the functionality related to interacting with a set of HTTP endpoints * This class includes all the functionality related to interacting with a set of HTTP endpoints
@ -48,18 +48,13 @@ export class HttpClient implements Client {
const requestOpts = { const requestOpts = {
params: request, params: request,
}; };
const tokenPairs = await this._requestAsync('/token_pairs', HttpRequestType.Get, requestOpts); const result = await this._requestAsync(
assert.doesConformToSchema( '/token_pairs',
'tokenPairs', tokenPairs, schemas.relayerApiTokenPairsResponseSchema); HttpRequestType.Get,
_.each(tokenPairs, (tokenPair: object) => { relayerResponseJsonParsers.parseTokenPairsJson,
typeConverters.convertStringsFieldsToBigNumbers(tokenPair, [ requestOpts,
'tokenA.minAmount', );
'tokenA.maxAmount', return result;
'tokenB.minAmount',
'tokenB.maxAmount',
]);
});
return tokenPairs;
} }
/** /**
* Retrieve orders from the API * Retrieve orders from the API
@ -73,10 +68,13 @@ export class HttpClient implements Client {
const requestOpts = { const requestOpts = {
params: request, params: request,
}; };
const orders = await this._requestAsync(`/orders`, HttpRequestType.Get, requestOpts); const result = await this._requestAsync(
assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); `/orders`,
_.each(orders, (order: object) => typeConverters.convertOrderStringFieldsToBigNumber(order)); HttpRequestType.Get,
return orders; relayerResponseJsonParsers.parseOrdersJson,
requestOpts,
);
return result;
} }
/** /**
* Retrieve a specific order from the API * Retrieve a specific order from the API
@ -85,10 +83,12 @@ export class HttpClient implements Client {
*/ */
public async getOrderAsync(orderHash: string): Promise<SignedOrder> { public async getOrderAsync(orderHash: string): Promise<SignedOrder> {
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
const order = await this._requestAsync(`/order/${orderHash}`, HttpRequestType.Get); const result = await this._requestAsync(
assert.doesConformToSchema('order', order, schemas.signedOrderSchema); `/order/${orderHash}`,
typeConverters.convertOrderStringFieldsToBigNumber(order); HttpRequestType.Get,
return order; relayerResponseJsonParsers.parseOrderJson,
);
return result;
} }
/** /**
* Retrieve an orderbook from the API * Retrieve an orderbook from the API
@ -100,10 +100,13 @@ export class HttpClient implements Client {
const requestOpts = { const requestOpts = {
params: request, params: request,
}; };
const orderBook = await this._requestAsync('/orderbook', HttpRequestType.Get, requestOpts); const result = await this._requestAsync(
assert.doesConformToSchema('orderBook', orderBook, schemas.relayerApiOrderBookResponseSchema); '/orderbook',
typeConverters.convertOrderbookStringFieldsToBigNumber(orderBook); HttpRequestType.Get,
return orderBook; relayerResponseJsonParsers.parseOrderbookResponseJson,
requestOpts,
);
return result;
} }
/** /**
* Retrieve fee information from the API * Retrieve fee information from the API
@ -115,10 +118,13 @@ export class HttpClient implements Client {
const requestOpts = { const requestOpts = {
payload: request, payload: request,
}; };
const fees = await this._requestAsync('/fees', HttpRequestType.Post, requestOpts); const result = await this._requestAsync(
assert.doesConformToSchema('fees', fees, schemas.relayerApiFeesResponseSchema); '/fees',
typeConverters.convertStringsFieldsToBigNumbers(fees, ['makerFee', 'takerFee']); HttpRequestType.Post,
return fees; relayerResponseJsonParsers.parseFeesResponseJson,
requestOpts,
);
return result;
} }
/** /**
* Submit a signed order to the API * Submit a signed order to the API
@ -129,10 +135,16 @@ export class HttpClient implements Client {
const requestOpts = { const requestOpts = {
payload: signedOrder, payload: signedOrder,
}; };
await this._requestAsync('/order', HttpRequestType.Post, requestOpts); await this._requestAsync(
'/order',
HttpRequestType.Post,
_.noop,
requestOpts,
);
} }
private async _requestAsync(path: string, requestType: HttpRequestType, private async _requestAsync<T>(path: string, requestType: HttpRequestType,
requestOptions?: HttpRequestOptions): Promise<any> { jsonParser: (json: any) => T,
requestOptions?: HttpRequestOptions): Promise<T> {
const params = _.get(requestOptions, 'params'); const params = _.get(requestOptions, 'params');
const payload = _.get(requestOptions, 'payload'); const payload = _.get(requestOptions, 'payload');
let query = ''; let query = '';
@ -154,6 +166,6 @@ export class HttpClient implements Client {
throw Error(response.statusText); throw Error(response.statusText);
} }
const json = await response.json(); const json = await response.json();
return json; return jsonParser(json);
} }
} }

View File

@ -7,10 +7,10 @@ import {
OrderbookChannelMessageTypes, OrderbookChannelMessageTypes,
} from '../types'; } from '../types';
import {typeConverters} from './type_converters'; import {relayerResponseJsonParsers} from './relayer_response_json_parsers';
export const orderbookChannelMessageParsers = { export const orderbookChannelMessageParser = {
parser(utf8Data: string): OrderbookChannelMessage { parse(utf8Data: string): OrderbookChannelMessage {
const messageObj = JSON.parse(utf8Data); const messageObj = JSON.parse(utf8Data);
const type: string = _.get(messageObj, 'type'); const type: string = _.get(messageObj, 'type');
assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`); assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`);
@ -18,15 +18,15 @@ export const orderbookChannelMessageParsers = {
switch (type) { switch (type) {
case (OrderbookChannelMessageTypes.Snapshot): { case (OrderbookChannelMessageTypes.Snapshot): {
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema); assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema);
const orderbook = messageObj.payload; const orderbookJson = messageObj.payload;
typeConverters.convertOrderbookStringFieldsToBigNumber(orderbook); const orderbook = relayerResponseJsonParsers.parseOrderbookResponseJson(orderbookJson);
return messageObj; return _.assign(messageObj, {payload: orderbook});
} }
case (OrderbookChannelMessageTypes.Update): { case (OrderbookChannelMessageTypes.Update): {
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelUpdateSchema); assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelUpdateSchema);
const order = messageObj.payload; const orderJson = messageObj.payload;
typeConverters.convertOrderStringFieldsToBigNumber(order); const order = relayerResponseJsonParsers.parseOrderJson(orderJson);
return messageObj; return _.assign(messageObj, {payload: order});
} }
default: { default: {
return { return {

View File

@ -0,0 +1,42 @@
import {assert} from '@0xproject/assert';
import {schemas} from '@0xproject/json-schemas';
import * as _ from 'lodash';
import {
FeesResponse,
OrderbookResponse,
SignedOrder,
TokenPairsItem,
} from '../types';
import {typeConverters} from './type_converters';
export const relayerResponseJsonParsers = {
parseTokenPairsJson(json: any): TokenPairsItem[] {
assert.doesConformToSchema('tokenPairs', json, schemas.relayerApiTokenPairsResponseSchema);
return json.map((tokenPair: any) => {
return typeConverters.convertStringsFieldsToBigNumbers(tokenPair, [
'tokenA.minAmount',
'tokenA.maxAmount',
'tokenB.minAmount',
'tokenB.maxAmount',
]);
});
},
parseOrdersJson(json: any): SignedOrder[] {
assert.doesConformToSchema('orders', json, schemas.signedOrdersSchema);
return json.map((order: object) => typeConverters.convertOrderStringFieldsToBigNumber(order));
},
parseOrderJson(json: any): SignedOrder {
assert.doesConformToSchema('order', json, schemas.signedOrderSchema);
return typeConverters.convertOrderStringFieldsToBigNumber(json);
},
parseOrderbookResponseJson(json: any): OrderbookResponse {
assert.doesConformToSchema('orderBook', json, schemas.relayerApiOrderBookResponseSchema);
return typeConverters.convertOrderbookStringFieldsToBigNumber(json);
},
parseFeesResponseJson(json: any): FeesResponse {
assert.doesConformToSchema('fees', json, schemas.relayerApiFeesResponseSchema);
return typeConverters.convertStringsFieldsToBigNumbers(json, ['makerFee', 'takerFee']);
},
};

View File

@ -1,15 +1,17 @@
import {BigNumber} from 'bignumber.js'; import {BigNumber} from 'bignumber.js';
import * as _ from 'lodash'; import * as _ from 'lodash';
// TODO: convert all of these to non-mutating, pure functions
export const typeConverters = { export const typeConverters = {
convertOrderbookStringFieldsToBigNumber(orderbook: object): void { convertOrderbookStringFieldsToBigNumber(orderbook: any): any {
_.each(orderbook, (orders: object[]) => { const bids = _.get(orderbook, 'bids', []);
_.each(orders, (order: object) => this.convertOrderStringFieldsToBigNumber(order)); const asks = _.get(orderbook, 'asks', []);
}); return {
bids: bids.map((order: any) => this.convertOrderStringFieldsToBigNumber(order)),
asks: asks.map((order: any) => this.convertOrderStringFieldsToBigNumber(order)),
};
}, },
convertOrderStringFieldsToBigNumber(order: object): void { convertOrderStringFieldsToBigNumber(order: any): any {
this.convertStringsFieldsToBigNumbers(order, [ return this.convertStringsFieldsToBigNumbers(order, [
'makerTokenAmount', 'makerTokenAmount',
'takerTokenAmount', 'takerTokenAmount',
'makerFee', 'makerFee',
@ -18,9 +20,11 @@ export const typeConverters = {
'salt', 'salt',
]); ]);
}, },
convertStringsFieldsToBigNumbers(obj: object, fields: string[]): void { convertStringsFieldsToBigNumbers(obj: any, fields: string[]): any {
const result = _.assign({}, obj);
_.each(fields, field => { _.each(fields, field => {
_.update(obj, field, (value: string) => new BigNumber(value)); _.update(result, field, (value: string) => new BigNumber(value));
}); });
return result;
}, },
}; };

View File

@ -11,7 +11,7 @@ import {
WebsocketClientEventType, WebsocketClientEventType,
WebsocketConnectionEventType, WebsocketConnectionEventType,
} from './types'; } from './types';
import {orderbookChannelMessageParsers} from './utils/orderbook_channel_message_parsers'; import {orderbookChannelMessageParser} from './utils/orderbook_channel_message_parser';
/** /**
* This class includes all the functionality related to interacting with a websocket endpoint * This class includes all the functionality related to interacting with a websocket endpoint
@ -97,7 +97,7 @@ export class WebSocketOrderbookChannel implements OrderbookChannel {
if (!_.isUndefined(message.utf8Data)) { if (!_.isUndefined(message.utf8Data)) {
try { try {
const utf8Data = message.utf8Data; const utf8Data = message.utf8Data;
const parserResult = orderbookChannelMessageParsers.parser(utf8Data); const parserResult = orderbookChannelMessageParser.parse(utf8Data);
if (parserResult.requestId === requestId) { if (parserResult.requestId === requestId) {
switch (parserResult.type) { switch (parserResult.type) {
case (OrderbookChannelMessageTypes.Snapshot): { case (OrderbookChannelMessageTypes.Snapshot): {

View File

@ -2,7 +2,7 @@ import * as chai from 'chai';
import * as dirtyChai from 'dirty-chai'; import * as dirtyChai from 'dirty-chai';
import 'mocha'; import 'mocha';
import {orderbookChannelMessageParsers} from '../src/utils/orderbook_channel_message_parsers'; import {orderbookChannelMessageParser} from '../src/utils/orderbook_channel_message_parser';
// tslint:disable-next-line:max-line-length // tslint:disable-next-line:max-line-length
import {orderResponse} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; import {orderResponse} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f';
@ -21,20 +21,20 @@ chai.config.includeStack = true;
chai.use(dirtyChai); chai.use(dirtyChai);
const expect = chai.expect; const expect = chai.expect;
describe('orderbookChannelMessageParsers', () => { describe('orderbookChannelMessageParser', () => {
describe('#parser', () => { describe('#parser', () => {
it('parses snapshot messages', () => { it('parses snapshot messages', () => {
const snapshotMessage = orderbookChannelMessageParsers.parser(snapshotOrderbookChannelMessage); const snapshotMessage = orderbookChannelMessageParser.parse(snapshotOrderbookChannelMessage);
expect(snapshotMessage.type).to.be.equal('snapshot'); expect(snapshotMessage.type).to.be.equal('snapshot');
expect(snapshotMessage.payload).to.be.deep.equal(orderbookResponse); expect(snapshotMessage.payload).to.be.deep.equal(orderbookResponse);
}); });
it('parses update messages', () => { it('parses update messages', () => {
const updateMessage = orderbookChannelMessageParsers.parser(updateOrderbookChannelMessage); const updateMessage = orderbookChannelMessageParser.parse(updateOrderbookChannelMessage);
expect(updateMessage.type).to.be.equal('update'); expect(updateMessage.type).to.be.equal('update');
expect(updateMessage.payload).to.be.deep.equal(orderResponse); expect(updateMessage.payload).to.be.deep.equal(orderResponse);
}); });
it('returns unknown message for messages with unsupported types', () => { it('returns unknown message for messages with unsupported types', () => {
const unknownMessage = orderbookChannelMessageParsers.parser(unknownOrderbookChannelMessage); const unknownMessage = orderbookChannelMessageParser.parse(unknownOrderbookChannelMessage);
expect(unknownMessage.type).to.be.equal('unknown'); expect(unknownMessage.type).to.be.equal('unknown');
expect(unknownMessage.payload).to.be.undefined(); expect(unknownMessage.payload).to.be.undefined();
}); });
@ -44,7 +44,7 @@ describe('orderbookChannelMessageParsers', () => {
"requestId": 1, "requestId": 1,
"payload": {} "payload": {}
}`; }`;
const badCall = () => orderbookChannelMessageParsers.parser(typelessMessage); const badCall = () => orderbookChannelMessageParser.parse(typelessMessage);
expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`); expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`);
}); });
it('throws when type is not a string', () => { it('throws when type is not a string', () => {
@ -54,24 +54,24 @@ describe('orderbookChannelMessageParsers', () => {
"requestId": 1, "requestId": 1,
"payload": {} "payload": {}
}`; }`;
const badCall = () => orderbookChannelMessageParsers.parser(messageWithBadType); const badCall = () => orderbookChannelMessageParser.parse(messageWithBadType);
expect(badCall).throws('Expected type to be of type string, encountered: 1'); expect(badCall).throws('Expected type to be of type string, encountered: 1');
}); });
it('throws when snapshot message has malformed payload', () => { it('throws when snapshot message has malformed payload', () => {
const badCall = () => const badCall = () =>
orderbookChannelMessageParsers.parser(malformedSnapshotOrderbookChannelMessage); orderbookChannelMessageParser.parse(malformedSnapshotOrderbookChannelMessage);
// tslint:disable-next-line:max-line-length // tslint:disable-next-line:max-line-length
const errMsg = 'Validation errors: instance.payload requires property "bids", instance.payload requires property "asks"'; const errMsg = 'Validation errors: instance.payload requires property "bids", instance.payload requires property "asks"';
expect(badCall).throws(errMsg); expect(badCall).throws(errMsg);
}); });
it('throws when update message has malformed payload', () => { it('throws when update message has malformed payload', () => {
const badCall = () => const badCall = () =>
orderbookChannelMessageParsers.parser(malformedUpdateOrderbookChannelMessage); orderbookChannelMessageParser.parse(malformedUpdateOrderbookChannelMessage);
expect(badCall).throws(/^Expected message to conform to schema/); expect(badCall).throws(/^Expected message to conform to schema/);
}); });
it('throws when input message is not valid JSON', () => { it('throws when input message is not valid JSON', () => {
const nonJsonString = 'h93b{sdfs9fsd f'; const nonJsonString = 'h93b{sdfs9fsd f';
const badCall = () => orderbookChannelMessageParsers.parser(nonJsonString); const badCall = () => orderbookChannelMessageParser.parse(nonJsonString);
expect(badCall).throws('Unexpected token h in JSON at position 0'); expect(badCall).throws('Unexpected token h in JSON at position 0');
}); });
}); });