Refactor JSON parsing in HttpClient
This commit is contained in:
parent
9f3acf8e28
commit
8fe81c9d09
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
42
packages/connect/src/utils/relayer_response_json_parsers.ts
Normal file
42
packages/connect/src/utils/relayer_response_json_parsers.ts
Normal 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']);
|
||||||
|
},
|
||||||
|
};
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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): {
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user