From 66044ddae1e2c753548e958a94db99486a75f11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20K=C3=A4chele?= Date: Wed, 14 Feb 2024 19:33:05 +0100 Subject: [PATCH] Add message parser --- lib/rex/server.rb | 5 + lib/rex/server/message_parser.rb | 56 +++++++++++ lib/rex/server/request_schemas.rb | 120 ++++++++++++++++++++++ spec/server/message_parser_spec.rb | 154 +++++++++++++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 lib/rex/server/message_parser.rb create mode 100644 lib/rex/server/request_schemas.rb create mode 100644 spec/server/message_parser_spec.rb diff --git a/lib/rex/server.rb b/lib/rex/server.rb index 4b14eeb..e72b590 100644 --- a/lib/rex/server.rb +++ b/lib/rex/server.rb @@ -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 diff --git a/lib/rex/server/message_parser.rb b/lib/rex/server/message_parser.rb new file mode 100644 index 0000000..ee4541e --- /dev/null +++ b/lib/rex/server/message_parser.rb @@ -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 diff --git a/lib/rex/server/request_schemas.rb b/lib/rex/server/request_schemas.rb new file mode 100644 index 0000000..ff340fe --- /dev/null +++ b/lib/rex/server/request_schemas.rb @@ -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 diff --git a/spec/server/message_parser_spec.rb b/spec/server/message_parser_spec.rb new file mode 100644 index 0000000..69c379d --- /dev/null +++ b/spec/server/message_parser_spec.rb @@ -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