From 23d00550cd4ddcd1eb8c2d31c80baef068299f9f Mon Sep 17 00:00:00 2001 From: Enola Knezevic Date: Wed, 3 Apr 2024 13:07:45 +0200 Subject: [PATCH] WIP: Added new endpoint /catalogue to return catalogue defined in CATALOGUE_URL env var, extended with counts. --- Cargo.toml | 4 +- src/banner.rs | 2 +- src/catalogue.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 4 ++ src/main.rs | 23 +++++++++-- 5 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/catalogue.rs diff --git a/Cargo.toml b/Cargo.toml index 6639818..9887ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spot" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "Apache-2.0" documentation = "https://github.com/samply/spot" @@ -20,7 +20,7 @@ once_cell = "1" # Logging tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -reqwest = { version = "0.11.20", default-features = false, features = ["stream"] } +reqwest = { version = "0.11.20", default-features = false, features = ["stream", "default-tls"] } tower-http = { version = "0.4.4", features = ["cors"] } [build-dependencies] diff --git a/src/banner.rs b/src/banner.rs index 3068a92..da63103 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -10,7 +10,7 @@ pub(crate) fn print_banner() { _ => "SNAPSHOT", }; info!( - "🌈 Samply.Spot v{} (built {} {}, {}) starting up ...", + "🌈 Samply.Spot v{} (built {} {}, {}) ready to take requests.", env!("CARGO_PKG_VERSION"), env!("BUILD_DATE"), env!("BUILD_TIME"), diff --git a/src/catalogue.rs b/src/catalogue.rs new file mode 100644 index 0000000..bb7e075 --- /dev/null +++ b/src/catalogue.rs @@ -0,0 +1,100 @@ +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tracing::{debug, info}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +enum ChildCategoryType { + Equals, + SomethingElse +} + +#[derive(Serialize, Deserialize)] +struct ChildCategoryCriterion { + key: String, + count: Option +} + +#[derive(Serialize, Deserialize)] +struct ChildCategory { + key: String, + type_: ChildCategoryType, + criteria: Vec +} + +pub async fn get_extended_json(catalogue_url: Url) -> Value { + debug!("Fetching catalogue from {catalogue_url} ..."); + let resp = reqwest::get(catalogue_url).await + .expect("Unable to fetch catalogue from upstream; please check URL specified in config."); + + let mut json: Value = resp.json().await + .expect("Unable to parse catalogue from upstream; please check URL specified in config."); + + // TODO: Query prism for counts here. + + recurse(&mut json); + + // println!("{}", serde_json::to_string_pretty(&json).unwrap()); + + info!("Catalogue built successfully."); + + json +} + +/// Key order: group key (e.g. patient) +/// \-- stratifier key (e.g. admin_gender) +/// \-- stratum key (e.g. male, other) +fn recurse(json: &mut Value) { + match json { + Value::Null => (), + Value::Bool(_) => (), + Value::Number(_) => (), + Value::String(_) => (), + Value::Array(arr) => { + for ele in arr { + recurse(ele); + } + }, + Value::Object(obj) => { + if ! obj.contains_key("childCategories") { + for (_key, child_val) in obj.iter_mut() { + recurse(child_val); + } + } else { + let group_key = obj.get("key").expect("Got JSON element with childCategories but without (group) key. Please check json."); + + let children_cats = obj + .get_mut("childCategories") + .unwrap() + .as_array_mut() + .unwrap() + .iter_mut() + .filter(|item| item.get("type").unwrap_or(&Value::Null) == "EQUALS"); + + for child_cat in children_cats { + let stratifier_key = child_cat.get("key").expect("Got JSON element with childCategory that does not contain a (stratifier) key. Please check json."); + let criteria = child_cat + .get_mut("criteria") + .expect("Got JSON element with childCategory that does not contain a criteria array. Please check json.") + .as_array_mut() + .expect("Got JSON element with childCategory with criteria that are not an array. Please check json."); + + for criterion in criteria { + let criterion = criterion.as_object_mut() + .expect("Got JSON where a criterion was not an object. Please check json."); + let stratum_key = criterion.get("key") + .expect("Got JSON where a criterion did not have a key. Please check json.") + .as_str() + .expect("Got JSON where a criterion key was not a string. Please check json."); + + // fetch from Prism output + let count_from_prism = 10; + + criterion.insert("count".into(), json!(count_from_prism)); + } + } + } + }, + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 5e4d6cb..cdf1dbf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,10 @@ pub struct Config { /// The socket address this server will bind to #[clap(long, env, default_value = "0.0.0.0:8080")] pub bind_addr: SocketAddr, + + /// URL to catalogue.json file + #[clap(long, env)] + pub catalogue_url: Url } fn parse_cors(v: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index 9e14876..5b72eea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{Json, Path, Query}, + extract::{Json, Path, Query, State}, http::HeaderValue, response::{IntoResponse, Response}, routing::{get, post}, @@ -12,12 +12,14 @@ use config::Config; use once_cell::sync::Lazy; use reqwest::{header, Method, StatusCode}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use tower_http::cors::CorsLayer; use tracing::{info, warn, Level}; use tracing_subscriber::{EnvFilter, util::SubscriberInitExt}; mod banner; mod beam; +mod catalogue; mod config; static CONFIG: Lazy = Lazy::new(Config::parse); @@ -30,6 +32,11 @@ static BEAM_CLIENT: Lazy = Lazy::new(|| { ) }); +#[derive(Clone)] +struct SharedState { + extended_json: Value +} + #[tokio::main] async fn main() { tracing_subscriber::FmtSubscriber::builder() @@ -40,9 +47,9 @@ async fn main() { info!("{:#?}", Lazy::force(&CONFIG)); - + let extended_json = catalogue::get_extended_json(CONFIG.catalogue_url.clone()).await; + let state = SharedState { extended_json }; - banner::print_banner(); // TODO: Add check for reachability of beam-proxy let cors = CorsLayer::new() @@ -53,9 +60,13 @@ async fn main() { let app = Router::new() .route("/beam", post(handle_create_beam_task)) .route("/beam/:task_id", get(handle_listen_to_beam_tasks)) + .route("/catalogue", get(handle_get_catalogue)) + .with_state(state) .layer(axum::middleware::map_response(banner::set_server_header)) .layer(cors); + banner::print_banner(); + axum::Server::bind(&CONFIG.bind_addr) .serve(app.into_make_service()) .await @@ -133,3 +144,9 @@ fn convert_response(response: reqwest::Response) -> axum::response::Response { .unwrap() .into_response() } + +async fn handle_get_catalogue( + State(state): State +) -> Json { + Json(state.extended_json) +} \ No newline at end of file