Skip to content

Commit

Permalink
Add initial state of testrestapi
Browse files Browse the repository at this point in the history
  • Loading branch information
iryndin committed Feb 5, 2020
1 parent e8ca379 commit d879583
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 0 deletions.
121 changes: 121 additions & 0 deletions exchangeserver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
## Задача

Есть некий сервис, который отдает нам поток биржевых данных. Мы хотели бы раздавать этот поток другим внутренним клиентам, заодно агрегируя данные в формат, достаточный для отрисовки [candlestick chart](https://en.wikipedia.org/wiki/Candlestick_chart). Нужно реализовать сервер для этого на Scala или Java. Желательно использование фреймворков, облегчающих написание сетевых серверов (Netty, Mina, Akka IO).

Для тестирования можно использовать тестовый upstream.py, запустив его при помощи команды `python upstream.py`. После запуска он будет ждать соединений на `127.0.0.1:5555` и генерировать поток случайных сделок.

## Протокол (сервер/upstream)

Наш сервер соединяется к провайдеру данных по протоколу TCP/IP, после чего тот начинает присылать биржевые сделки в виде сообщений следующего формата:

```
[ LEN:2 ] [ TIMESTAMP:8 ] [ TICKER_LEN:2 ] [ TICKER:TICKER_LEN ] [ PRICE:8 ] [ SIZE:4 ]
```

где поля имеют следующую семантику:

* `LEN`: длина последующего сообщения (целое, 2 байта)
* `TIMESTAMP`: дата и время события (целое, 8 байт, milliseconds since epoch)
* `TICKER_LEN`: длина биржевого тикера (целое, 2 байта)
* `TICKER`: биржевой тикер (ASCII, TICKER_LEN байт)
* `PRICE`: цена сделки (double, 8 байт)
* `SIZE`: объем сделки (целое, 4 байта)

## Протокол (клиент/сервер)

Клиенты подсоединяются к серверу также по TCP/IP. При подключении нового клиента сервер посылает ему минутные свечи за последние 10 минут, после чего в начале каждой новой минуты присылает свечи за предыдущую минуту. Данные сериализуются в JSON-сообщения следующего вида, разделенные символом перевода строки (\n):

```
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:02:00Z", "open": 112.1, "high": 115.2, "low": 110.0, "close": 114.2, "volume": 13000 }
```

## Пример

Сообщения c upstream (в человекочитаемом формате):

```
2016-01-01 15:02:10 AAPL 101.1 200
2016-01-01 15:02:15 AAPL 101.2 100
2016-01-01 15:02:25 AAPL 101.3 300
2016-01-01 15:02:35 MSFT 120.1 500
2016-01-01 15:02:40 AAPL 101.0 700
2016-01-01 15:03:10 AAPL 102.1 1000
2016-01-01 15:03:11 MSFT 120.2 1000
2016-01-01 15:03:30 AAPL 103.2 100
2016-01-01 15:03:31 MSFT 120.0 700
2016-01-01 15:04:21 AAPL 102.1 100
2016-01-01 15:04:21 MSFT 102.1 200
```

Клиент подсоединяется в `15:04:04`, получает свечи за последние 10 минут:

```
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:02:00Z", "open": 101.1, "high": 101.3, "low": 101, "close": 101, "volume": 1300 }
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:02:00Z", "open": 120.1, "high": 120.1, "low": 120.1, "close": 120.1, "volume": 500 }
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:03:00Z", "open": 102.1, "high": 103.2, "low": 102.1, "close": 103.2, "volume": 1100 }
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:03:00Z", "open": 120.2, "high": 120.2, "low": 120, "close": 120, "volume": 1700 }
```

В `15:05:00` клиент получает свечи за предыдущую минуту:

```
{ "ticker": "AAPL", "timestamp": "2016-01-01T15:04:00Z", "open": 102.1, "high": 102.1, "low": 102.1, "close": 102.1, "volume": 100 }
{ "ticker": "MSFT", "timestamp": "2016-01-01T15:04:00Z", "open": 120.1, "high": 120.1, "low": 120.1, "close": 120.1, "volume": 200 }
```

## Upstream

```python
from __future__ import print_function

import threading
import socket
import struct
import time
from datetime import datetime
import random

TICKERS = ["AAPL", "GOOG", "MSFT", "SPY"]
PORT = 5555

def timestamp_millis(timestamp):
return int((timestamp - datetime.utcfromtimestamp(0)).total_seconds() * 1000.0)

def send_trade(client_socket, host, port):
timestamp = datetime.utcnow()
ticker = random.choice(TICKERS)
price = 90 + random.randrange(0, 400) * 0.05
size = random.randrange(100, 10000, 100)
msg = struct.pack("!QH%dsdI" % len(ticker), timestamp_millis(timestamp), len(ticker), ticker.encode("ascii"), price, size)
msg_len = struct.pack("!H", len(msg))
print("[%s:%d] %s: %s %.2f %d" % (host, port, timestamp, ticker, price, size))
client_socket.send(msg_len + msg)

def emit_trades(client_socket, addr):
(host, port) = addr
try:
while True:
time.sleep(random.uniform(0.2, 3))
send_trade(client_socket, host, port)
except Exception as e:
print("[%s:%d] Error: %s" % (host, port, e))
finally:
client_socket.close()

def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", PORT))
server.listen(5)
print("Waiting for connections on port %d" % PORT)
while True:
(client, addr) = server.accept()
print("Incoming connection from %s:%d" % addr)
client_thread = threading.Thread(target=emit_trades, args=(client, addr))
client_thread.daemon = True
client_thread.start()

if __name__ == "__main__":
main()
```

25 changes: 25 additions & 0 deletions testrestapi/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## The task goal
Execution of "gradle build" have to be successfully completed (passing test phase also).

### Description
A user submits a search query to find an available domain name.

The system should check domain availability with in all supported TLD zones (.com/.net/.club) and return the results among with the registration price for each zone.

A domain is available for registration if it cannot be found in `domains` database table.

Prices for each zone are stored in `tlds` database table.

Request format: `/domains/check?search=new-domain`

Response format:
```
[
{"domain": "new-domain.com", tld: "com", "available": false, "price": 8.99},
{"domain": "new-domain.net", tld: "net", "available": true, "price": 9.99},
{"domain": "new-domain.club", tld: "club", "available": true, "price": 15.99}
]
```
Response items have to be sorted by price ascending (tld with low price at first)

.
33 changes: 33 additions & 0 deletions testrestapi/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}

group = 'com.test'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'
implementation 'org.flywaydb:flyway-core'
compile 'org.projectlombok:lombok'

testCompile 'com.jayway.jsonpath:json-path'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions testrestapi/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'rest-service'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.test.restservice;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DomainController {


@GetMapping("/domains/check")
public Object check(SearchDto searchDto) {
// todo start point for implementation
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.test.restservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication(scanBasePackages = {"com.test"})
@EnableJpaRepositories
public class RestServiceApplication {

public static void main(String[] args) {
SpringApplication.run(RestServiceApplication.class, args);
}

}
8 changes: 8 additions & 0 deletions testrestapi/src/main/java/com/test/restservice/SearchDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.test.restservice;

import lombok.Data;

@Data
public class SearchDto {
private String search;
}
24 changes: 24 additions & 0 deletions testrestapi/src/main/java/com/test/restservice/model/Domain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.test.restservice.model;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;

@Getter
@Setter
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "domains")
public class Domain implements Serializable {
@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
}
24 changes: 24 additions & 0 deletions testrestapi/src/main/java/com/test/restservice/model/Tld.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.test.restservice.model;

import lombok.*;

import javax.persistence.*;
import java.io.Serializable;

@Getter
@Setter
@Entity
@EqualsAndHashCode(callSuper = false)
@Table(name = "tlds")
public class Tld implements Serializable {
@Id
@Column
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@Column
private Double price;


}
20 changes: 20 additions & 0 deletions testrestapi/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: password

jpa:
database: h2
generate-ddl: false
hibernate.ddl-auto: none
open-in-view: false
database-platform: org.hibernate.dialect.H2Dialect
show-sql: true

flyway:
enabled: true
locations: classpath:db
validate-on-migrate: false

19 changes: 19 additions & 0 deletions testrestapi/src/main/resources/db/V0000__initial.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE tlds (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(16) NOT NULL,
price DOUBLE NOT NULL

);

INSERT INTO tlds (name, price) VALUES
('com', 8.99),
('club', 15.99),
('net', 9.99);

CREATE TABLE domains (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

INSERT INTO domains (name) VALUES
('existing');
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.test.restservice;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class DomainControllerTests {

@Autowired
private MockMvc mockMvc;

@Test
public void testExisting() throws Exception {

this.mockMvc.perform(get("/domains/check?search=existing"))
.andDo(print()).andExpect(status().isOk())
.andExpect(jsonPath("$[0].tld").value("com"))
.andExpect(jsonPath("$[0].price").value(8.99))
.andExpect(jsonPath("$[0].available").value(false))
.andExpect(jsonPath("$[1].tld").value("net"))
.andExpect(jsonPath("$[1].price").value(9.99))
.andExpect(jsonPath("$[1].available").value(true))
.andExpect(jsonPath("$[2].tld").value("club"))
.andExpect(jsonPath("$[2].price").value(15.99))
.andExpect(jsonPath("$[2].available").value(true));
}

@Test
public void testNotExisting() throws Exception {

this.mockMvc.perform(get("/domains/check?search=new"))
.andDo(print()).andExpect(status().isOk())
.andExpect(jsonPath("$[0].tld").value("com"))
.andExpect(jsonPath("$[0].price").value(8.99))
.andExpect(jsonPath("$[0].available").value(true))
.andExpect(jsonPath("$[1].tld").value("net"))
.andExpect(jsonPath("$[1].price").value(9.99))
.andExpect(jsonPath("$[1].available").value(true))
.andExpect(jsonPath("$[2].tld").value("club"))
.andExpect(jsonPath("$[2].price").value(15.99))
.andExpect(jsonPath("$[2].available").value(true));
}
}

0 comments on commit d879583

Please sign in to comment.