diff --git a/Cargo.toml b/Cargo.toml index 53f91bb..fedd55b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ chrono = { version = "0.4.37", default-features = false, features = ["serde", "n clap = { version = "4.5.3", features = ["env", "derive"] } fhir-sdk = { version = "0.14.1", default-features = false, features = ["builders", "r4b"] } futures-util = { version = "0.3", default-features = false } -once_cell = "1.19.0" reqwest = { version = "0.12.2", features = ["json", "rustls-tls"], default-features = false } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1" diff --git a/Dockerfile b/Dockerfile index be014f4..fa28e1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,5 @@ RUN chmod +x /app/* FROM gcr.io/distroless/cc-debian12 ARG COMPONENT COPY --from=chmodder /app/$COMPONENT /usr/local/bin/samply -ENTRYPOINT [ "/usr/local/bin/samply" ] \ No newline at end of file +ENTRYPOINT [ "/usr/local/bin/samply" ] +CMD [ "dic" ] \ No newline at end of file diff --git a/dev/test b/dev/test index d5d4f51..8f7771a 100755 --- a/dev/test +++ b/dev/test @@ -40,7 +40,7 @@ function start_bg() { done done chmod +x artifacts/binaries-amd64/transfair - artifacts/binaries-amd64/transfair & + artifacts/binaries-amd64/transfair dic mainzelliste & sleep 10 } diff --git a/src/config.rs b/src/config.rs index 279b50b..72ad1b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,60 @@ use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::LazyLock, time::{Duration, Instant}}; -use clap::{Args, CommandFactory, FromArgMatches, Parser}; +use clap::Parser; use reqwest::{Certificate, Client, Url}; use anyhow::anyhow; use tokio::sync::RwLock; use tracing::info; -use crate::{ttp::{self, greifswald::GreifswaldConfig, mainzelliste::MlConfig, Ttp}, CONFIG}; +use crate::{ttp::Ttp, CLIENT}; -#[derive(Parser, Clone, Debug)] +#[derive(Debug, Parser)] #[clap(author, version, about, long_about = None)] -pub struct Config { +pub struct CliArgs { + #[clap(subcommand)] + pub subcommand: SubCommand, + + /// Trusted tls root certificates + #[clap(long, env)] + pub tls_ca_certificates_dir: Option, + /// Disable TLS verification + #[clap(long, env, default_value_t = false)] + pub tls_disable: bool, +} + +impl CliArgs { + pub fn build_client(&self) -> Client { + let mut client_builder = Client::builder(); + client_builder = client_builder + .danger_accept_invalid_hostnames(self.tls_disable) + .danger_accept_invalid_certs(self.tls_disable); + if let Some(tls_ca_dir) = &self.tls_ca_certificates_dir { + info!("Loading available custom ca certificates from {:?}", self.tls_ca_certificates_dir); + for path_res in tls_ca_dir.read_dir().expect(&format!("Unable to read {:?}", self.tls_ca_certificates_dir)) { + if let Ok(path_buf) = path_res { + info!("Adding custom ca certificate {:?}", path_buf.path()); + client_builder = client_builder.add_root_certificate( + Certificate::from_pem( + &fs::read(path_buf.path()).expect(&format!("Unable to read file provided: {:?}", path_buf.path())) + ).expect(&format!("Unable to convert {:?} to a certificate. Please verify it is a valid pem file", path_buf.path())) + ); + } + } + } + + client_builder.build().expect("Unable to initially build reqwest client") + } +} + +#[derive(Debug, clap::Subcommand)] +pub enum SubCommand { + Dic(DicConfig) +} + +#[derive(Parser, Clone, Debug)] +pub struct DicConfig { // Definition of necessary parameters for communicating with a ttp - #[clap(skip)] + #[clap(subcommand)] pub ttp: Option, // Either an id well-known to both, project and dic, or a temporary identifier created by the ttp #[clap(long, env, default_value = "TOKEN")] @@ -35,55 +77,6 @@ pub struct Config { pub fhir_output_url: Url, #[clap(long, env, default_value = "")] pub fhir_output_credentials: Auth, - /// Trusted tls root certificates - #[clap(long, env)] - pub tls_ca_certificates_dir: Option, - /// Disable TLS verification - #[clap(long, env, default_value_t = false)] - pub tls_disable: bool, - - #[clap(skip)] - pub client: Client, -} - -impl Config { - pub fn parse() -> Self { - let cmd = Config::command(); - let ttp_cmd = ttp::Ttp::augment_args(cmd.clone()); - let args_matches = cmd.get_matches(); - let mut this = Self::from_arg_matches(&args_matches).map_err(|e| e.exit()).unwrap(); - let ca_client = build_client(&this.tls_ca_certificates_dir, this.tls_disable); - this.client = ca_client.clone(); - let mut ttp = ttp_cmd.try_get_matches().ok().and_then(|matches| Ttp::from_arg_matches(&matches).ok()); - if let Some(ref mut ttp) = ttp { - let (Ttp::Mainzelliste(MlConfig {base, ..}) | Ttp::Greifswald(GreifswaldConfig {base, ..})) = ttp; - base.client = ca_client.clone(); - } - this.ttp = ttp; - this - } -} - -fn build_client(tls_ca_certificates_dir: &Option, disable_tls: bool) -> Client { - let mut client_builder = Client::builder(); - client_builder = client_builder - .danger_accept_invalid_hostnames(disable_tls) - .danger_accept_invalid_certs(disable_tls); - if let Some(tls_ca_dir) = tls_ca_certificates_dir { - info!("Loading available custom ca certificates from {:?}", tls_ca_certificates_dir); - for path_res in tls_ca_dir.read_dir().expect(&format!("Unable to read {:?}", tls_ca_certificates_dir)) { - if let Ok(path_buf) = path_res { - info!("Adding custom ca certificate {:?}", path_buf.path()); - client_builder = client_builder.add_root_certificate( - Certificate::from_pem( - &fs::read(path_buf.path()).expect(&format!("Unable to read file provided: {:?}", path_buf.path())) - ).expect(&format!("Unable to convert {:?} to a certificate. Please verify it is a valid pem file", path_buf.path())) - ); - } - } - } - - client_builder.build().expect("Unable to initially build reqwest client") } #[derive(Debug, Clone)] @@ -144,7 +137,7 @@ impl ClientBuilderExt for reqwest::RequestBuilder { expires_in: u64, access_token: String, } - let TokenRes { expires_in, access_token } = CONFIG.client + let TokenRes { expires_in, access_token } = CLIENT .post(token_url.clone()) .form(&serde_json::json!({ "grant_type": "client_credentials", diff --git a/src/fhir.rs b/src/fhir.rs index a6cc567..8b8828a 100644 --- a/src/fhir.rs +++ b/src/fhir.rs @@ -5,21 +5,20 @@ use fhir_sdk::r4b::{ resources::{Bundle, BundleEntry, BundleEntryRequest, Patient, Resource}, types::Identifier, }; -use reqwest::{header, Client, StatusCode, Url}; +use reqwest::{header, StatusCode, Url}; use tracing::debug; -use crate::{config::{Auth, ClientBuilderExt}, requests::DataRequestPayload, CONFIG}; +use crate::{config::{Auth, ClientBuilderExt}, requests::DataRequestPayload, CLIENT}; #[derive(Clone, Debug)] pub struct FhirServer { - url: Url, + pub url: Url, auth: Auth, - client: Client, } impl FhirServer { pub fn new(url: Url, auth: Auth) -> Self { - Self { url, auth, client: CONFIG.client.clone()} + Self { url, auth } } pub async fn post_data_request( @@ -31,7 +30,7 @@ impl FhirServer { let bundle: Bundle = payload.into(); - let response = self.client + let response = CLIENT .post(bundle_endpoint) .add_auth(&self.auth) .await? @@ -58,7 +57,7 @@ impl FhirServer { let bundle_endpoint = format!("{}fhir/Bundle", self.url); debug!("Fetching new data from: {}", bundle_endpoint); let query = vec![("_lastUpdated", format!("gt{}", last_update.format("%Y-%m-%dT%H:%M:%S").to_string()))]; - let response = self.client + let response = CLIENT .get(bundle_endpoint) .add_auth(&self.auth) .await? @@ -77,7 +76,7 @@ impl FhirServer { pub async fn post_data(&self, bundle: &Bundle) -> anyhow::Result { let bundle_endpoint = format!("{}fhir", self.url); debug!("Posting data to output fhir server: {}", bundle_endpoint); - self.client + CLIENT .post(bundle_endpoint) .add_auth(&self.auth) .await? @@ -89,7 +88,7 @@ impl FhirServer { } pub trait PatientExt: Sized { - fn pseudonymize(self) -> axum::response::Result; + fn pseudonymize(self, exchange_id_system: &str) -> axum::response::Result; fn add_id_request(self, id: String) -> Self; fn get_identifier(&self, id_system: &str) -> Option<&Identifier>; fn get_identifier_mut(&mut self, id_system: &str) -> Option<&mut Identifier>; @@ -120,16 +119,16 @@ impl PatientExt for Patient { .find(|x| x.system.as_deref() == Some(id_system)) } - fn pseudonymize(self) -> axum::response::Result { + fn pseudonymize(self, exchange_id_system: &str) -> axum::response::Result { let Some(exchange_identifier) = self .identifier .iter() .find(|x| { - x.as_ref().is_some_and(|y| y.system.as_deref() == Some(&CONFIG.exchange_id_system)) + x.as_ref().is_some_and(|y| y.system.as_deref() == Some(exchange_id_system)) }) else { return Err(( StatusCode::BAD_REQUEST, - format!("Request did not contain identifier of system {}", &CONFIG.exchange_id_system) + format!("Request did not contain identifier of system {exchange_id_system}") ).into()); }; let pseudonymized_patient = Patient::builder() diff --git a/src/main.rs b/src/main.rs index 8157a42..15d3f83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ -use std::{process::ExitCode, time::Duration}; +use std::{process::ExitCode, sync::{LazyLock, OnceLock}, time::Duration}; use axum::{routing::{get, post}, Router}; use chrono::{DateTime, Utc}; -use config::Config; +use clap::Parser; +use config::DicConfig; use fhir::FhirServer; use fhir_sdk::r4b::resources::{Bundle, Resource, ResourceType}; -use once_cell::sync::Lazy; use requests::update_data_request; +use reqwest::Client; use sqlx::{Pool, Sqlite, SqlitePool}; use futures_util::future::TryJoinAll; use tracing::{debug, error, info, trace, warn, Level}; use tracing_subscriber::{EnvFilter, util::SubscriberInitExt}; use ttp::Ttp; -use crate::{fhir::PatientExt, requests::{create_data_request, list_data_requests, get_data_request}}; +use crate::{config::CliArgs, fhir::PatientExt, requests::{create_data_request, get_data_request, list_data_requests}}; mod banner; mod config; @@ -21,7 +22,8 @@ mod fhir; mod requests; mod ttp; -static CONFIG: Lazy = Lazy::new(Config::parse); +pub static CLIENT: LazyLock<&Client> = LazyLock::new(|| INNER_CLIENT.get().expect("Client should be initialized before use")); +static INNER_CLIENT: OnceLock = OnceLock::new(); static SERVER_ADDRESS: &str = "0.0.0.0:8080"; #[tokio::main] @@ -31,18 +33,51 @@ async fn main() -> ExitCode { .with_env_filter(EnvFilter::from_default_env()) .finish() .init(); - banner::print_banner(); - trace!("{:#?}", Lazy::force(&CONFIG)); - let database_pool = SqlitePool::connect(CONFIG.database_url.as_str()) + let args = CliArgs::parse(); + INNER_CLIENT.set(args.build_client()).unwrap(); + match args.subcommand { + config::SubCommand::Dic(config) => { + dic_main(config).await + } + } +} + +#[derive(Debug, Clone)] +pub struct DicAppState { + pub database_pool: Pool, + pub config: &'static DicConfig, + pub request_server: &'static FhirServer, +} + +impl DicAppState { + pub fn new(database_pool: Pool, config: &'static DicConfig) -> Self { + let request_server = FhirServer::new( + config.fhir_request_url.clone(), + config.fhir_request_credentials.clone() + ); + let request_server = Box::leak(Box::new(request_server)); + Self { + database_pool, + config, + request_server, + } + } +} + +async fn dic_main(config: DicConfig) -> ExitCode { + banner::print_banner(); + trace!("{config:#?}"); + let config: &'static _ = Box::leak(Box::new(config)); + let database_pool = SqlitePool::connect(config.database_url.as_str()) .await.map_err(|e| { - error!("Unable to connect to database file {}. Error is: {}", CONFIG.database_url.as_str(), e); + error!("Unable to connect to database file {}. Error is: {}", config.database_url.as_str(), e); return }).unwrap(); let _ = sqlx::migrate!().run(&database_pool).await; - if let Some(ttp) = &CONFIG.ttp { + if let Some(ttp) = &config.ttp { const RETRY_COUNT: i32 = 30; let mut failures = 0; while !(ttp.check_availability().await) { @@ -62,29 +97,28 @@ async fn main() -> ExitCode { } info!("Connected to ttp {}", ttp.url); // verify that both, the exchange id system and project id system are configured in the ttp - for idtype in [&CONFIG.exchange_id_system, &ttp.project_id_system] { + for idtype in [&config.exchange_id_system, &ttp.project_id_system] { if !(ttp.check_idtype_available(&idtype).await) { - error!("Configured exchange id system is not available in TTP: expected {}", &idtype); + error!("Configured exchange id system '{idtype}' is not available in TTP."); return ExitCode::from(1) } } } - - let database_pool_for_axum = database_pool.clone(); - + let state = DicAppState::new(database_pool, config); + let state_for_fetch = state.clone(); tokio::spawn(async move { const RETRY_PERIOD: Duration = Duration::from_secs(60); let input_fhir_server = FhirServer::new( - CONFIG.fhir_input_url.clone(), - CONFIG.fhir_input_credentials.clone() + config.fhir_input_url.clone(), + config.fhir_input_credentials.clone() ); - let output_fhir_server = FhirServer::new ( - CONFIG.fhir_output_url.clone(), - CONFIG.fhir_output_credentials.clone() + let output_fhir_server = FhirServer::new( + config.fhir_output_url.clone(), + config.fhir_output_credentials.clone() ); loop { // TODO: Persist the updated data in the database - match fetch_data(&input_fhir_server, &output_fhir_server, &database_pool).await { + match fetch_data(&input_fhir_server, &output_fhir_server, &state_for_fetch).await { Ok(status) => info!("{}", status), Err(error) => warn!("Failed to fetch project data: {error:#}. Will try again in {}s", RETRY_PERIOD.as_secs()) } @@ -97,7 +131,7 @@ async fn main() -> ExitCode { .route("/", post(create_data_request)) .route("/", get(list_data_requests)) .route("/{request_id}", get(get_data_request)) - .with_state(database_pool_for_axum); + .with_state(state); let app = Router::new() .nest("/requests", request_routes); @@ -112,14 +146,14 @@ async fn main() -> ExitCode { // Pull data from input_fhir_server and push it to output_fhir_server -async fn fetch_data(input_fhir_server: &FhirServer, output_fhir_server: &FhirServer, database_pool: &Pool) -> anyhow::Result { - let fetch_start_date = extract_execution_time(database_pool).await; +async fn fetch_data(input_fhir_server: &FhirServer, output_fhir_server: &FhirServer, state: &DicAppState) -> anyhow::Result { + let fetch_start_date = extract_execution_time(&state.database_pool).await; let mut new_data = input_fhir_server.pull_new_data( fetch_start_date.naive_local().into() ).await?; let fetch_finish_date = chrono::prelude::Utc::now(); if new_data.entry.is_empty() { - debug!("Received empty bundle from mdat server ({}). No update necessary", CONFIG.fhir_input_url); + debug!("Received empty bundle from mdat server ({}). No update necessary", input_fhir_server.url); } else { for entry in new_data.entry.iter_mut().flatten() { let Some(resource) = &mut entry.resource else { @@ -153,8 +187,8 @@ async fn fetch_data(input_fhir_server: &FhirServer, output_fhir_server: &FhirSer }; let mut linkage_results = None; - if let Some(ttp) = &CONFIG.ttp { - linkage_results = Some(replace_exchange_identifiers(bundle_id_value, entry_bundle, ttp, &database_pool).await?); + if let Some(ttp) = &state.config.ttp { + linkage_results = Some(replace_exchange_identifiers(bundle_id_value, entry_bundle, ttp, &state).await?); }; // TODO: integrate transformation using transfair-batch here @@ -164,15 +198,15 @@ async fn fetch_data(input_fhir_server: &FhirServer, output_fhir_server: &FhirSer Err(error) => error!("Received the following error: {error:#}"), }; - update_data_request(bundle_id_value, linkage_results, &database_pool).await.unwrap(); + update_data_request(bundle_id_value, linkage_results, &state.database_pool).await.unwrap(); } } let finish_as_timestamp = fetch_finish_date.timestamp_millis(); - let _ = sqlx::query!( + sqlx::query!( "UPDATE last_request SET execution_time = $1 WHERE id = 1", finish_as_timestamp - ).fetch_optional(database_pool).await; + ).fetch_optional(&state.database_pool).await?; Ok(format!("Last fetch for new data executed at {:?}", fetch_finish_date)) } @@ -212,7 +246,7 @@ enum LinkageError { IdentifierNotLinkable(ResourceType) } -async fn replace_exchange_identifiers(data_request_identifier: &str, new_data: &mut Bundle, ttp: &Ttp, database_connection: &Pool) -> sqlx::Result>> { +async fn replace_exchange_identifiers(data_request_identifier: &str, new_data: &mut Bundle, ttp: &Ttp, state: &DicAppState) -> sqlx::Result>> { new_data.entry.iter_mut().flatten().map(|entry| { let Some(resource) = &mut entry.resource else { return Err(LinkageError::EntryWithoutResource) @@ -220,7 +254,7 @@ async fn replace_exchange_identifiers(data_request_identifier: &str, new_data: & let rt = resource.resource_type(); let identifier = match resource { - Resource::Patient(patient) => patient.get_identifier_mut(&CONFIG.exchange_id_system), + Resource::Patient(patient) => patient.get_identifier_mut(&state.config.exchange_id_system), Resource::Consent(consent) => match consent.patient.as_mut() { Some(patient) => patient.identifier.as_mut(), None => return Err(LinkageError::NoReference(rt)) @@ -246,7 +280,7 @@ async fn replace_exchange_identifiers(data_request_identifier: &str, new_data: & return Err(LinkageError::IdentifierWithoutSystem(rt)) }; - if system != &CONFIG.exchange_id_system { + if system != &state.config.exchange_id_system { Err(LinkageError::WrongIdentifierType(rt)) } else { Ok((identifier, rt)) @@ -260,7 +294,7 @@ async fn replace_exchange_identifiers(data_request_identifier: &str, new_data: & let result = sqlx::query!( "SELECT project_id FROM data_requests WHERE id = $1", data_request_identifier - ).fetch_optional(database_connection).await?; + ).fetch_optional(&state.database_pool).await?; if let Some(patient_identifier) = result { let Some(project_id) = patient_identifier.project_id else { return Ok(Err(LinkageError::MissingIdentifierValue(rt))); diff --git a/src/requests.rs b/src/requests.rs index 89336a0..84e1510 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -1,17 +1,12 @@ use axum::{extract::{Path, State}, Json}; use fhir_sdk::r4b::{resources::{Consent, Patient, ResourceType}, types::Reference}; -use once_cell::sync::Lazy; use reqwest::StatusCode; use serde::{Serialize, Deserialize}; use sqlx::{Pool, Sqlite}; use tracing::{trace, debug, error}; -use crate::{fhir::{FhirServer, PatientExt}, LinkageError, CONFIG}; - -static REQUEST_SERVER: Lazy = Lazy::new(|| { - FhirServer::new(CONFIG.fhir_request_url.clone(), CONFIG.fhir_request_credentials.clone()) -}); +use crate::{fhir::PatientExt, DicAppState, LinkageError}; #[derive(Serialize, Deserialize, sqlx::Type)] pub enum RequestStatus { @@ -38,7 +33,7 @@ pub struct DataRequestPayload { // POST /requests; Creates a new Data Request pub async fn create_data_request( - State(database_pool): State>, + State(DicAppState { database_pool, config, request_server }): State, Json(payload): Json ) -> axum::response::Result<(StatusCode, Json)> { let consent = payload.consent; @@ -46,9 +41,9 @@ pub async fn create_data_request( let mut project_identifier = None; - if let Some(ttp) = &CONFIG.ttp { + if let Some(ttp) = &config.ttp { // pseudonymize the patient - patient = ttp.request_project_pseudonym(patient).await?; + patient = ttp.request_project_pseudonym(patient, &config.exchange_id_system).await?; // now, the patient should have project1id data (which can be stored in the DB) trace!("TTP Returned these patient with project pseudonym {:#?}", &patient); if let Some(ref consent) = consent { @@ -60,23 +55,23 @@ pub async fn create_data_request( } // ensure that we have at least one identifier with which we can link - let Some(exchange_identifier) = patient.get_identifier(&CONFIG.exchange_id_system).cloned() else { + let Some(exchange_identifier) = patient.get_identifier(&config.exchange_id_system).cloned() else { return Err( - (StatusCode::BAD_REQUEST, format!("Couldn't identify a valid identifier with system {}!", &CONFIG.exchange_id_system)).into() + (StatusCode::BAD_REQUEST, format!("Couldn't identify a valid identifier with system {}!", &config.exchange_id_system)).into() ); }; let Some(ref exchange_identifier) = exchange_identifier.value else { return Err( - (StatusCode::BAD_REQUEST, format!("No valid value for identifier {}", &CONFIG.exchange_id_system)).into() + (StatusCode::BAD_REQUEST, format!("No valid value for identifier {}", &config.exchange_id_system)).into() ) }; - patient = patient.pseudonymize()?; + patient = patient.pseudonymize(&config.exchange_id_system)?; - let linked_consent = consent.map(|c| link_patient_consent(c, &patient)).transpose()?; + let linked_consent = consent.map(|c| link_patient_consent(c, &patient, &config.exchange_id_system)).transpose()?; // und in beiden fällen anschließend die Anfrage beim Datenintegrationszentrum abgelegt werden - let data_request_id = REQUEST_SERVER.post_data_request(DataRequestPayload { + let data_request_id = request_server.post_data_request(DataRequestPayload { patient, consent: linked_consent }).await.map_err(|e| { @@ -109,7 +104,7 @@ pub async fn create_data_request( // GET /requests; Lists all running Data Requests pub async fn list_data_requests( - State(database_pool): State> + State(DicAppState { database_pool, .. }): State ) -> Result>, (StatusCode, &'static str)> { let data_requests = sqlx::query_as!( DataRequest, @@ -123,7 +118,7 @@ pub async fn list_data_requests( // GET /requests/; Gets the Request specified by id in Path pub async fn get_data_request( - State(database_pool): State>, + State(DicAppState { database_pool, .. }): State, Path(request_id): Path ) -> Result, (StatusCode, &'static str)> { debug!("Information on data request {} requested.", request_id); @@ -141,8 +136,8 @@ pub async fn get_data_request( } } -fn link_patient_consent(mut consent: Consent, patient: &Patient) -> Result { - let exchange_identifier= patient.get_identifier(&CONFIG.exchange_id_system); +fn link_patient_consent(mut consent: Consent, patient: &Patient, exchange_id_system: &str) -> Result { + let exchange_identifier= patient.get_identifier(exchange_id_system); let Some(exchange_identifier) = exchange_identifier else { return Err((StatusCode::INTERNAL_SERVER_ERROR, "Unable to generate exchange identifier")); }; diff --git a/src/ttp.rs b/src/ttp.rs index 725d700..75eb995 100644 --- a/src/ttp.rs +++ b/src/ttp.rs @@ -4,14 +4,13 @@ pub mod greifswald; use std::ops::Deref; use axum::response::IntoResponse; -use clap::{FromArgMatches, Parser, ValueEnum}; use fhir_sdk::r4b::resources::{Consent, Patient}; -use reqwest::{Client, StatusCode, Url}; +use reqwest::{StatusCode, Url}; use thiserror::Error; use crate::config::Auth; -#[derive(Parser, Debug, Clone)] +#[derive(clap::Args, Debug, Clone)] pub struct TtpInner { #[clap( long = "ttp-url", @@ -29,59 +28,14 @@ pub struct TtpInner { default_value = "" )] pub ttp_auth: Auth, - - #[clap(skip)] - pub client: Client, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, clap::Subcommand)] pub enum Ttp { Mainzelliste(mainzelliste::MlConfig), Greifswald(greifswald::GreifswaldConfig) } -#[derive(ValueEnum, Clone, Copy)] -enum TtpType { - Mainzelliste, - Greifswald, -} - -#[derive(clap::Args)] -struct TtpTypeParser { - #[clap(long, env, default_value = "mainzelliste")] - ttp_type: TtpType, -} - -impl FromArgMatches for Ttp { - fn from_arg_matches(matches: &clap::ArgMatches) -> Result { - let ttp = match TtpTypeParser::from_arg_matches(matches)?.ttp_type { - TtpType::Mainzelliste => Ttp::Mainzelliste(mainzelliste::MlConfig::from_arg_matches(matches)?), - TtpType::Greifswald => Ttp::Greifswald(greifswald::GreifswaldConfig::from_arg_matches(matches)?), - }; - Ok(ttp) - } - - fn update_from_arg_matches(&mut self, _matches: &clap::ArgMatches) -> Result<(), clap::Error> { - Ok(()) - } -} - -impl clap::Args for Ttp { - fn augment_args(cmd: clap::Command) -> clap::Command { - let cmd = TtpTypeParser::augment_args(cmd); - cmd.defer(|cmd| { - match TtpTypeParser::from_arg_matches(&cmd.clone().get_matches()).unwrap().ttp_type { - TtpType::Mainzelliste => mainzelliste::MlConfig::augment_args(cmd), - TtpType::Greifswald => greifswald::GreifswaldConfig::augment_args(cmd), - } - }) - } - - fn augment_args_for_update(cmd: clap::Command) -> clap::Command { - cmd - } -} - impl Deref for Ttp { type Target = TtpInner; @@ -122,9 +76,10 @@ impl Ttp { pub async fn request_project_pseudonym( &self, patient: Patient, + exchange_id_system: &str, ) -> axum::response::Result { match self { - Ttp::Mainzelliste(config) => config.request_project_pseudonym(patient).await.map_err(Into::into), + Ttp::Mainzelliste(config) => config.request_project_pseudonym(patient, exchange_id_system).await.map_err(Into::into), Ttp::Greifswald(config) => config.request_project_pseudonym(patient).await.map_err(Into::into), } } diff --git a/src/ttp/greifswald.rs b/src/ttp/greifswald.rs index 80e7406..37f4104 100644 --- a/src/ttp/greifswald.rs +++ b/src/ttp/greifswald.rs @@ -1,7 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use clap::Parser; use fhir_sdk::r4b::codes::AdministrativeGender; use fhir_sdk::r4b::resources::{Bundle, ParametersParameterValue, Resource}; use fhir_sdk::r4b::resources::{ @@ -10,11 +9,11 @@ use fhir_sdk::r4b::resources::{ use fhir_sdk::r4b::types::Identifier; use crate::config::ClientBuilderExt; -use crate::ttp_bail; +use crate::{ttp_bail, CLIENT}; use super::TtpError; -#[derive(Debug, Parser, Clone)] +#[derive(Debug, clap::Args, Clone)] pub struct GreifswaldConfig { #[clap(flatten)] pub base: super::TtpInner, @@ -39,7 +38,7 @@ impl std::ops::Deref for GreifswaldConfig { impl GreifswaldConfig { pub async fn check_availability(&self) -> bool { - self.client.get(self.url.clone()).send().await.is_ok() + CLIENT.get(self.url.clone()).send().await.is_ok() } pub async fn check_idtype_available(&self, idtype: &str) -> bool { @@ -95,8 +94,7 @@ impl GreifswaldConfig { ]) .build() .unwrap(); - let res = self - .client + let res = CLIENT .post(url) .json(¶ms) .add_auth(&self.ttp_auth) @@ -153,8 +151,7 @@ impl GreifswaldConfig { "#); - let res = self - .client + let res = CLIENT .post(url) .body(soap_body) .add_auth(&self.ttp_auth) @@ -177,9 +174,7 @@ impl GreifswaldConfig { .value(psn) .build() .unwrap(), - )]) - .build() - .unwrap(); + )]).build().unwrap(); Ok(patient) } @@ -201,8 +196,7 @@ impl GreifswaldConfig { "#); - let res = self - .client + let res = CLIENT .post(url) .body(xml_body) .add_auth(&self.ttp_auth) @@ -342,7 +336,6 @@ mod tests { }, time::Date, }; - use reqwest::Client; #[tokio::test] #[ignore = "Unclear how we proceed here as it does not seem to accept a Consent resource"] @@ -351,7 +344,6 @@ mod tests { base: TtpInner { url: "https://demo.ths-greifswald.de".parse().unwrap(), project_id_system: "MII".into(), - client: Client::new(), ttp_auth: Auth::None, }, source: "dummy_safe_source".into(), @@ -377,7 +369,6 @@ mod tests { base: TtpInner { url: "https://demo.ths-greifswald.de".parse().unwrap(), project_id_system: "Transferstelle A".into(), - client: Client::new(), ttp_auth: Auth::None, }, source: "dummy_safe_source".into(), diff --git a/src/ttp/mainzelliste.rs b/src/ttp/mainzelliste.rs index 8cd129d..a9a9888 100644 --- a/src/ttp/mainzelliste.rs +++ b/src/ttp/mainzelliste.rs @@ -1,15 +1,14 @@ //! Client implementation for Mainzelliste TTP -use clap::Parser; use fhir_sdk::r4b::resources::{Consent, IdentifiableResource, Patient}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use tracing::{debug, trace, warn}; -use crate::{fhir::PatientExt, ttp_bail, CONFIG}; +use crate::{fhir::PatientExt, ttp_bail, CLIENT}; use super::TtpError; -#[derive(Debug, Parser, Clone)] +#[derive(Debug, clap::Args, Clone)] pub struct MlConfig { #[clap(flatten)] pub base: super::TtpInner, @@ -31,7 +30,7 @@ impl std::ops::Deref for MlConfig { impl MlConfig { pub(super) async fn check_availability(&self) -> bool { - let response = match self.client + let response = match CLIENT .get(self.url.clone()) .header( "Accept", "application/json") .send() @@ -67,7 +66,7 @@ impl MlConfig { pub async fn get_supported_ids(&self) -> Result, (StatusCode, &'static str)> { let idtypes_endpoint = self.url.join("configuration/idTypes").unwrap(); - let supported_ids = self.client + let supported_ids = CLIENT .get(idtypes_endpoint) .header("mainzellisteApiKey", &self.api_key) .send() @@ -97,14 +96,15 @@ impl MlConfig { pub(super) async fn request_project_pseudonym( &self, patient: Patient, + exchange_id_system: &str, ) -> Result { let patient = patient - .add_id_request(CONFIG.exchange_id_system.clone()) + .add_id_request(exchange_id_system.to_owned()) .add_id_request(self.project_id_system.clone()); // TODO: Need to ensure request for project pseudonym is included let patients_endpoint = self.url.join("fhir/Patient").unwrap(); - let response = self.client + let response = CLIENT .post(patients_endpoint) .header("mainzellisteApiKey", &self.api_key) .json(&patient) @@ -125,7 +125,7 @@ impl MlConfig { let sessions_endpoint = self.url.join("sessions").unwrap(); debug!("Requesting Session from Mainzelliste: {}", sessions_endpoint); - self.client + CLIENT .post(sessions_endpoint) .header("mainzellisteApiKey", &self.api_key) .send() @@ -144,7 +144,7 @@ impl MlConfig { let token_request = TokenRequest { token_type }; - self.client + CLIENT .post(tokens_endpoint) .header("mainzellisteApiKey", &self.api_key) .json(&token_request) @@ -192,7 +192,7 @@ impl MlConfig { let consent_endpoint = self.url.join("fhir/Consent").unwrap(); - let response: reqwest::Response = self.client + let response: reqwest::Response = CLIENT .post(consent_endpoint) .header("Authorization", format!("MainzellisteToken {}", token.id)) .header("Content-Type", "application/fhir+json")