Add message parser

This commit is contained in:
Tim Kächele 2024-02-14 19:33:05 +01:00
parent dd0b83734d
commit 66044ddae1
4 changed files with 335 additions and 0 deletions

View File

@ -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

View 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

View 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

View 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