diff --git a/servers/rust-server/Cargo.lock b/servers/rust-server/Cargo.lock index d95a963..3efbc1e 100644 --- a/servers/rust-server/Cargo.lock +++ b/servers/rust-server/Cargo.lock @@ -129,6 +129,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -909,6 +918,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -1104,6 +1124,9 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-axum", + "utoipa-swagger-ui", "uuid", ] @@ -1114,6 +1137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1271,8 +1295,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1282,9 +1308,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.3+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1478,6 +1506,24 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.2", ] [[package]] @@ -1486,14 +1532,22 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1677,6 +1731,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "iri-string" version = "0.7.8" @@ -1770,6 +1830,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1801,6 +1870,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -2046,6 +2121,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2294,6 +2375,61 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2441,6 +2577,46 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -2504,6 +2680,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.106", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust_decimal" version = "1.38.0" @@ -2565,6 +2775,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -2591,6 +2802,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schemars" version = "0.9.0" @@ -2934,6 +3154,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -3261,6 +3487,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3392,6 +3621,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3567,6 +3806,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -3648,6 +3893,62 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap 2.11.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.106", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "reqwest", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.18.1" @@ -3684,6 +3985,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3787,6 +4107,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -3815,6 +4145,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4251,6 +4590,38 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.11.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/servers/rust-server/Cargo.toml b/servers/rust-server/Cargo.toml index de07876..b9513d8 100644 --- a/servers/rust-server/Cargo.toml +++ b/servers/rust-server/Cargo.toml @@ -45,3 +45,6 @@ uuid = "1.18.1" num-traits = "0.2.19" charming = {version = "0.6.0", features = ["html"]} clap = { version = "4.5.51", features = ["derive"] } +utoipa = { version = "5.4.0", features = ["axum_extras"] } +utoipa-axum = "0.2.0" +utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "reqwest"] } diff --git a/servers/rust-server/src/bot.rs b/servers/rust-server/src/bot.rs index c52cdf5..c35449e 100644 --- a/servers/rust-server/src/bot.rs +++ b/servers/rust-server/src/bot.rs @@ -5,6 +5,7 @@ use axum::{ }; use entity::{bot, wallet, orderbook}; use sea_orm::{DatabaseConnection, EntityTrait, ColumnTrait, QueryFilter}; + use crate::{error::AppError, state::AppState}; #[derive(Template)] diff --git a/servers/rust-server/src/clock.rs b/servers/rust-server/src/clock.rs index c71bcdf..2d2e622 100644 --- a/servers/rust-server/src/clock.rs +++ b/servers/rust-server/src/clock.rs @@ -46,8 +46,16 @@ pub fn start_clock(clock: SharedClock) { }); } +#[utoipa::path( + get, + path = "/api/time", + description = "Get the current time of the financial market simulation clock.", + responses( + (status = 200, description = "Succesfully retrieved market time"), + ) +)] pub async fn time(State(state): State) -> Result { let clock = state.clock.read().await; let time_str = clock.current_time().format("%Y-%m-%d %H:%M:%S").to_string(); Ok(time_str) -} \ No newline at end of file +} diff --git a/servers/rust-server/src/enroll.rs b/servers/rust-server/src/enroll.rs index 0e013aa..c2ae390 100644 --- a/servers/rust-server/src/enroll.rs +++ b/servers/rust-server/src/enroll.rs @@ -4,12 +4,24 @@ use sea_orm::{ActiveValue::Set, prelude::Decimal}; use crate::{error::AppError, state::AppState}; use entity::{bot, wallet}; use sea_orm::ActiveModelTrait; +use serde::{Serialize, Deserialize}; +use utoipa::ToSchema; -#[derive(serde::Deserialize)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] pub struct EnrollPayload { pub name: String, } +#[utoipa::path( + post, + path = "/api/enroll", + description = "Enroll a new bot to the market. Each bot must have a unique name.", + request_body(content = EnrollPayload, content_type = "application/json"), + responses( + (status = 200, description = "New bot enrolled successfully", body = String), + (status = 406, description = "Bot with same name already exists", body = String), + ), +)] pub async fn enroll( State(state): State, Json(payload): Json, ) -> Result { diff --git a/servers/rust-server/src/main.rs b/servers/rust-server/src/main.rs index 6794a22..60b8375 100644 --- a/servers/rust-server/src/main.rs +++ b/servers/rust-server/src/main.rs @@ -19,6 +19,8 @@ use std::sync::Arc; use tokio::sync::RwLock; use tower_http::services::ServeDir; use tower_http::trace::TraceLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; use tracing::Level; use tracing::event; @@ -46,6 +48,24 @@ async fn main() -> Result<(), Error> { cli::run().await } +#[derive(OpenApi)] +#[openapi( + info(title = "FinWar Market API"), + paths( + enroll::enroll, + clock::time, + trade::buy, + trade::sell, + trade::price, + ), + components( + schemas(enroll::EnrollPayload), + schemas(trade::BuyOrderInput), + schemas(trade::SellOrderInput), + ), +)] +struct ApiDoc; + /// Start the HTTP server. Separated out so `main` can dispatch to either /// the server or other management subcommands (like `migrate`). pub async fn run_server(db: DatabaseConnection) -> Result<(), Error> { @@ -88,6 +108,7 @@ pub async fn run_server(db: DatabaseConnection) -> Result<(), Error> { .route("/api/sell", post(sell)) .route("/api/price", get(price)) .nest_service("/static", ServeDir::new("static")) + .merge(SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi())) .fallback(|| async { AppError::NotFound }) .with_state(state) .layer(TraceLayer::new_for_http()); diff --git a/servers/rust-server/src/trade.rs b/servers/rust-server/src/trade.rs index 73b28e3..0c60436 100644 --- a/servers/rust-server/src/trade.rs +++ b/servers/rust-server/src/trade.rs @@ -9,19 +9,31 @@ use sea_orm::ColumnTrait; use sea_orm::prelude::*; use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, ActiveModelTrait, Set}; use uuid::Uuid; +use serde::{Serialize, Deserialize}; +use utoipa::ToSchema; -#[derive(serde::Deserialize)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] pub struct BuyOrderInput { pub bot_uuid: String, pub investment: f64, } -#[derive(serde::Deserialize)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] pub struct SellOrderInput { pub bot_uuid: String, pub quantity: i32, } +#[utoipa::path( + post, + path = "/api/buy", + description = "Buy the maximum amount of stock at market price for the given investment.", + request_body(content = BuyOrderInput, content_type = "application/json"), + responses( + (status = 200, description = "Succesfully buy stock", body = BuyOrderInput), + (status = 400, description = "Bad request"), + ) +)] pub async fn buy( State(state): State, Json(payload): Json, ) -> Result { @@ -63,6 +75,16 @@ pub async fn buy( Ok(format!("Bought {} shares for ${:.2}", shares_to_buy, actual_cost)) } +#[utoipa::path( + post, + path = "/api/sell", + request_body(content = SellOrderInput, content_type = "application/json"), + description = "Sell the defined number of stocks at market price.", + responses( + (status = 200, description = "Successfully sold stock", body = String), + (status = 400, description = "Bad request"), + ) +)] pub async fn sell( State(state): State, Json(payload): Json, ) -> Result { @@ -119,6 +141,14 @@ async fn get_current_price(state: &AppState) -> Result { Ok(price_mean) } +#[utoipa::path( + get, + path = "/api/price", + description = "Get the current market price of the tracked stock.", + responses( + (status = 200, description = "Succesfully retrieved stock price", body = String), + ) +)] pub async fn price( State(state): State, ) -> Result { diff --git a/servers/rust-server/templates/home.html b/servers/rust-server/templates/home.html index 2c2ff2a..8794970 100644 --- a/servers/rust-server/templates/home.html +++ b/servers/rust-server/templates/home.html @@ -25,7 +25,7 @@

Finwar


The Leaderboard tracks the best.
- The Documentation helps you get started building your bot. + The Documentation helps you get started building your bot.

Below is some recent market activity that the bots may work with.