summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArnaud Bailly <arnaud.bailly@iohk.io>2024-10-10 08:16:44 +0200
committerArnaud Bailly <arnaud.bailly@iohk.io>2024-10-10 08:16:44 +0200
commit492a9e7ae1b50bd2e46daf589a6e52388703ec7e (patch)
treed4eb6370f2bfb3bc57392463c30a196ae910a151
parente3fdf12d472b24a9a91a0bdebe667b86797b2483 (diff)
downloadlambda-nantes-492a9e7ae1b50bd2e46daf589a6e52388703ec7e.tar.gz
Render leaderboard template from clients data
-rw-r--r--rust/Cargo.lock117
-rw-r--r--rust/Cargo.toml1
-rw-r--r--rust/src/web.rs120
-rw-r--r--rust/templates/leaderboard.html16
4 files changed, 252 insertions, 2 deletions
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 0d9e14f..bdb2dd9 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -1026,6 +1026,21 @@ dependencies = [
]
[[package]]
+name = "handlebars"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1521,6 +1536,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
+name = "pest"
+version = "2.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9"
+dependencies = [
+ "memchr",
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1774,6 +1834,7 @@ dependencies = [
"clap",
"env_logger",
"futures",
+ "handlebars",
"log",
"proptest",
"rand",
@@ -1870,6 +1931,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[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 = "schannel"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1969,6 +2039,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2090,6 +2171,26 @@ dependencies = [
]
[[package]]
+name = "thiserror"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2237,6 +2338,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2337,6 +2444,16 @@ dependencies = [
]
[[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"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 1006d29..b2eab9c 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -17,6 +17,7 @@ reqwest = "0.12.8"
tokio = { version = "1.40.0", features = ["rt", "macros", "rt-multi-thread"]}
clap = { version = "4.5.19", features = ["derive"] }
proptest = "1.0.0"
+handlebars = { version = "5", features = ["dir_source"] }
[dependencies.uuid]
version = "1.10.0"
diff --git a/rust/src/web.rs b/rust/src/web.rs
index 2b540c2..1be9ebc 100644
--- a/rust/src/web.rs
+++ b/rust/src/web.rs
@@ -1,6 +1,10 @@
-use actix_web::{middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder};
+use actix_web::{
+ get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder,
+ Result as ActixResult,
+};
use clap::Parser;
use futures::lock::Mutex;
+use handlebars::{DirectorySourceOptions, Handlebars};
use log::info;
use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
use rand::Rng;
@@ -19,10 +23,41 @@ struct Registration {
name: String,
}
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct ClientData {
+ name: String,
+ grade: u8,
+}
+
+impl ClientData {
+ fn from(client: &Client) -> Self {
+ ClientData {
+ name: client.name.clone(),
+ grade: client.grade,
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct Leaderboard {
+ clients: Vec<ClientData>,
+}
+
trait AppState: Send + Sync {
fn register(&mut self, registration: &Registration) -> RegistrationResult;
}
+fn make_leaderboard(clients: Vec<Client>) -> Leaderboard {
+ let clients = clients
+ .iter()
+ .map(|c| ClientData {
+ name: c.name.clone(),
+ grade: c.grade,
+ })
+ .collect();
+ Leaderboard { clients }
+}
+
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
enum RegistrationResult {
RegistrationSuccess { id: String, url: String },
@@ -32,8 +67,8 @@ enum RegistrationResult {
#[derive(Debug, Clone)]
struct Client {
id: Uuid,
- url: String,
name: String,
+ url: String,
grade: u8,
runner: TestRunner,
results: Vec<TestResult>,
@@ -190,6 +225,30 @@ async fn eval(input: String) -> impl Responder {
}
}
+#[get("/leaderboard")]
+async fn leaderboard(
+ app_state: web::Data<Arc<Mutex<State>>>,
+ hb: web::Data<Handlebars<'_>>,
+) -> impl Responder {
+ let clients = &app_state.lock().await.clients;
+ let mut client_data = vec![];
+ for client in clients.values() {
+ let client = client.lock().await;
+ client_data.push(ClientData::from(&client));
+ }
+
+ let body = hb
+ .render(
+ "leaderboard",
+ &Leaderboard {
+ clients: client_data,
+ },
+ )
+ .unwrap();
+
+ web::Html::new(body)
+}
+
#[derive(Parser, Debug)]
struct Options {
/// The port to listen on
@@ -208,12 +267,29 @@ async fn main() -> std::io::Result<()> {
let app_state = Arc::new(Mutex::new(State::new()));
let http_state: Arc<Mutex<dyn AppState>> = app_state;
env_logger::init();
+ // Handlebars uses a repository for the compiled templates. This object must be
+ // shared between the application threads, and is therefore passed to the
+ // Application Builder as an atomic reference-counted pointer.
+ let mut handlebars = Handlebars::new();
+ handlebars
+ .register_templates_directory(
+ "./templates",
+ DirectorySourceOptions {
+ tpl_extension: ".html".to_owned(),
+ hidden: false,
+ temporary: false,
+ },
+ )
+ .unwrap();
+
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(web::Data::new(http_state.clone()))
+ .app_data(web::Data::new(handlebars.clone()))
.service(register)
.service(eval)
+ .service(leaderboard)
})
.bind((options.host, options.port))?
.run()
@@ -255,6 +331,7 @@ async fn send_test(input: &String, url: &String) -> Result<String, TestResult> {
#[cfg(test)]
mod app_tests {
+ use std::str::from_utf8;
use std::sync::Arc;
use actix_web::http::header::TryIntoHeaderValue;
@@ -376,6 +453,45 @@ mod app_tests {
assert_eq!(bytes, "1".to_string().into_bytes());
}
+ #[actix_web::test]
+ async fn get_leaderboard_returns_html_page_listing_clients_state() {
+ let app_state = Arc::new(Mutex::new(State::new()));
+ app_state.lock().await.register(&Registration {
+ url: "http://1.2.3.4".to_string(),
+ name: "client1".to_string(),
+ });
+
+ let mut handlebars = Handlebars::new();
+ handlebars
+ .register_templates_directory(
+ "./templates",
+ DirectorySourceOptions {
+ tpl_extension: ".html".to_owned(),
+ hidden: false,
+ temporary: false,
+ },
+ )
+ .unwrap();
+
+ let app = test::init_service(
+ App::new()
+ .wrap(Logger::default())
+ .app_data(web::Data::new(app_state.clone()))
+ .app_data(web::Data::new(handlebars.clone()))
+ .service(leaderboard),
+ )
+ .await;
+
+ let req = test::TestRequest::get().uri("/leaderboard").to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_success());
+
+ let bytes = body::to_bytes(resp.into_body()).await.unwrap();
+ assert!(from_utf8(&bytes).unwrap().contains("client1"));
+ }
+
#[test]
async fn app_does_not_register_same_url_twice() {
let mut app_state = State::new();
diff --git a/rust/templates/leaderboard.html b/rust/templates/leaderboard.html
new file mode 100644
index 0000000..f062323
--- /dev/null
+++ b/rust/templates/leaderboard.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Leaderboard</title>
+</head>
+
+<body>
+ <table>
+ <tr><th>Name</th><th>Grade</th></tr>
+ {{#each this.clients}}
+ <tr><td>{{this.name}}</td><td>{{this.grade}}</td></tr>
+ {{/each}}
+ </table>
+</body>
+</html>