Add message parser
This commit is contained in:
parent
dd0b83734d
commit
66044ddae1
@ -1,6 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json-schema"
|
||||
|
||||
require_relative "server/messages"
|
||||
|
||||
require_relative "server/request_schemas"
|
||||
require_relative "server/message_parser"
|
||||
require_relative "server/message_broker"
|
||||
module Rex
|
||||
module Server
|
||||
|
56
lib/rex/server/message_parser.rb
Normal file
56
lib/rex/server/message_parser.rb
Normal file
@ -0,0 +1,56 @@
|
||||
require "json-schema"
|
||||
module Rex
|
||||
module Server
|
||||
# Parses the messages from the wire and returns well formatted ruby objects
|
||||
class MessageParser
|
||||
def parse(raw_message)
|
||||
parsed_message = begin
|
||||
JSON.parse(raw_message)
|
||||
rescue JSON::ParserError
|
||||
return [false, [{error: :json_parse_error}]]
|
||||
end
|
||||
|
||||
# check that it is one of the accepted requests
|
||||
errors = JSON::Validator.fully_validate(
|
||||
RequestSchemas::REQUEST_NAME,
|
||||
parsed_message,
|
||||
errors_as_objects: true
|
||||
)
|
||||
return [false, errors] unless errors.empty?
|
||||
|
||||
# Full validation
|
||||
errors = JSON::Validator.fully_validate(
|
||||
RequestSchemas::SCHEMA_REQUEST_NAME_MAPPING[parsed_message["name"]],
|
||||
parsed_message,
|
||||
errors_as_objects: true
|
||||
)
|
||||
return [false, errors] unless errors.empty?
|
||||
|
||||
case parsed_message["name"]
|
||||
when "order.create"
|
||||
Messages::CreateOrderRequest.new(
|
||||
nil,
|
||||
parsed_message["args"]["side"],
|
||||
parsed_message["args"]["price"],
|
||||
parsed_message["args"]["quantity"]
|
||||
)
|
||||
when "order.cancel"
|
||||
Messages::CancelOrderRequest.new(
|
||||
nil,
|
||||
parsed_message["args"]["id"]
|
||||
)
|
||||
when "orderbook.fetch"
|
||||
Messages::FetchOrderBookRequest.new(nil)
|
||||
when "orders.fetch"
|
||||
Messages::FetchOrdersRequest.new(nil)
|
||||
when "trades.fetch"
|
||||
Messages::FetchTradesRequest.new(nil)
|
||||
when "authenticate"
|
||||
Messages::AuthenticateRequest.new(
|
||||
parsed_message["args"]["user_id"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
120
lib/rex/server/request_schemas.rb
Normal file
120
lib/rex/server/request_schemas.rb
Normal file
@ -0,0 +1,120 @@
|
||||
module Rex
|
||||
module Server
|
||||
module RequestSchemas
|
||||
REQUEST_ENUM = {
|
||||
type: :string,
|
||||
description: "A valid request name",
|
||||
enum: [
|
||||
"order.create",
|
||||
"order.cancel",
|
||||
"orderbook.fetch",
|
||||
"orders.fetch",
|
||||
"authenticate",
|
||||
"trades.fetch"
|
||||
]
|
||||
}
|
||||
|
||||
REQUEST_NAME = {
|
||||
type: :object,
|
||||
required: [
|
||||
"name"
|
||||
],
|
||||
properties: {
|
||||
name: REQUEST_ENUM
|
||||
}
|
||||
}
|
||||
|
||||
def self.request(args)
|
||||
{
|
||||
type: :object,
|
||||
required: [
|
||||
"request_id",
|
||||
"type",
|
||||
"name",
|
||||
"args"
|
||||
],
|
||||
properties: {
|
||||
request_id: {
|
||||
type: :integer
|
||||
},
|
||||
type: {
|
||||
type: :string
|
||||
},
|
||||
name: REQUEST_ENUM,
|
||||
args: args
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
CREATE_ORDER_ARGUMENTS = {
|
||||
type: :object,
|
||||
required: [
|
||||
"quantity",
|
||||
"price",
|
||||
"side"
|
||||
],
|
||||
properties: {
|
||||
quantity: {
|
||||
type: :integer,
|
||||
exclusiveMinimum: 0
|
||||
},
|
||||
price: {
|
||||
type: :integer,
|
||||
minimum: 0
|
||||
},
|
||||
side: {
|
||||
type: :string,
|
||||
enum: ["buy", "sell"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CREATE_ORDER = request(CREATE_ORDER_ARGUMENTS)
|
||||
|
||||
CANCEL_ORDER_ARGUMENTS = {
|
||||
type: :object,
|
||||
required: [
|
||||
"id"
|
||||
],
|
||||
properties: {
|
||||
id: {
|
||||
type: :integer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EMPTY_ARGUMENTS = {
|
||||
type: :object,
|
||||
properties: {}
|
||||
}
|
||||
|
||||
CANCEL_ORDER = request(CANCEL_ORDER_ARGUMENTS)
|
||||
FETCH_ORDERBOOK = request(EMPTY_ARGUMENTS)
|
||||
FETCH_ORDERS = request(EMPTY_ARGUMENTS)
|
||||
FETCH_TRADES = request(EMPTY_ARGUMENTS)
|
||||
|
||||
AUTHENTICATE_ARGUMENTS = {
|
||||
type: :object,
|
||||
required: [
|
||||
"user_id"
|
||||
],
|
||||
properties: {
|
||||
user_id: {
|
||||
type: :string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AUTHENTICATE = request(AUTHENTICATE_ARGUMENTS)
|
||||
|
||||
SCHEMA_REQUEST_NAME_MAPPING = {
|
||||
"order.create" => CREATE_ORDER,
|
||||
"order.cancel" => CANCEL_ORDER,
|
||||
"orders.fetch" => FETCH_ORDERS,
|
||||
"trades.fetch" => FETCH_TRADES,
|
||||
"orderbook.fetch" => FETCH_ORDERBOOK,
|
||||
"authenticate" => AUTHENTICATE
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
154
spec/server/message_parser_spec.rb
Normal file
154
spec/server/message_parser_spec.rb
Normal file
@ -0,0 +1,154 @@
|
||||
# require "eventmachine"
|
||||
RSpec.describe Rex::Server::MessageParser do
|
||||
subject(:instance) { described_class.new }
|
||||
|
||||
describe "#parse" do
|
||||
subject { instance.parse(message) }
|
||||
|
||||
context "when message is not valid json" do
|
||||
let(:message) do
|
||||
"{{}}"
|
||||
end
|
||||
|
||||
it { is_expected.to eq([false, [{error: :json_parse_error}]]) }
|
||||
end
|
||||
|
||||
context "when message is not a valid message type" do
|
||||
let(:message) do
|
||||
{
|
||||
name: "unknown.request_type"
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns an error" do
|
||||
expect(subject[0]).to be(false)
|
||||
expect(subject.dig(1, 0, :fragment)).to eq("#/name")
|
||||
end
|
||||
end
|
||||
|
||||
context "when message does not match the schema" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: "-1",
|
||||
name: "order.create",
|
||||
type: "request",
|
||||
args: "test"
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns an error" do
|
||||
expect(subject[0]).to be(false)
|
||||
expect(subject[1].length).to eq(2)
|
||||
expect(subject[1].map { _1[:fragment] }).to match_array(["#/request_id", "#/args"])
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a order create request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "order.create",
|
||||
type: "request",
|
||||
args: {
|
||||
quantity: 100,
|
||||
price: 10,
|
||||
side: :buy
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a order create request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::CreateOrderRequest)
|
||||
expect(subject.user_id).to eq(nil)
|
||||
expect(subject.side).to eq("buy")
|
||||
expect(subject.price).to eq(10)
|
||||
expect(subject.quantity).to eq(100)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a order cancel request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "order.cancel",
|
||||
type: "request",
|
||||
args: {
|
||||
id: 123
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a order cancel request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::CancelOrderRequest)
|
||||
expect(subject.user_id).to eq(nil)
|
||||
expect(subject.order_id).to eq(123)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a order book request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "orderbook.fetch",
|
||||
type: "request",
|
||||
args: {}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a fetch order book request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::FetchOrderBookRequest)
|
||||
expect(subject.user_id).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a fetch orders request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "orders.fetch",
|
||||
type: "request",
|
||||
args: {}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a fetch orders request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::FetchOrdersRequest)
|
||||
expect(subject.user_id).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a fetch trades request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "trades.fetch",
|
||||
type: "request",
|
||||
args: {}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a fetch trades request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::FetchTradesRequest)
|
||||
expect(subject.user_id).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is an authenticate request" do
|
||||
let(:message) do
|
||||
{
|
||||
request_id: 1,
|
||||
name: "authenticate",
|
||||
type: "request",
|
||||
args: {
|
||||
user_id: "1"
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
|
||||
it "returns a authenticate request" do
|
||||
expect(subject).to be_instance_of(Rex::Server::Messages::AuthenticateRequest)
|
||||
expect(subject.user_id).to eq("1")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user