Skip to content

Commit

Permalink
add substance domain model
Browse files Browse the repository at this point in the history
  • Loading branch information
keinsell committed Jan 4, 2025
1 parent 8bc078b commit 35e34ee
Show file tree
Hide file tree
Showing 13 changed files with 440 additions and 87 deletions.
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ serde = { version = "1.0.216", features = ["derive", "std", "unstable"] }
lazy_static = "1.5.0"
figlet-rs = "0.1.5"
serde_json = "1.0.133"
chrono = { version = "0.4.39", features = ["std", "serde", "iana-time-zone"] }
log = { version = "0.4.22", features = ["serde", "kv"] }
chrono = { version = "0.4.39", features = ["std", "serde", "iana-time-zone", "rkyv"] }
log = { version = "0.4.22", features = ["serde", "kv", "max_level_trace", "release_max_level_off"] }
measurements = { version = "0.11.0", features = ["std", "serde", "from_str", "regex"] }
pubchem = "0.1.1"
tabled = "0.17.0"
tabled = { version = "0.17.0", features = ["std", "macros", "ansi", "derive", "tabled_derive"] }
rust-embed = "8.5.0"
assert_cmd = "2.0.16"
logforth = "0.19.0"
Expand All @@ -46,6 +46,12 @@ ryu = "1.0.18"
float-pretty-print = "0.1.1"
delegate = "0.13.1"
derivative = "2.2.0"
hashbrown = "0.15.2"
iso8601-duration = "0.2.0"
ptree = "0.5.2"
indicatif = { version = "0.17.9", features = ["rayon", "futures"] }
futures = "0.3.31"
colored = "2.2.0"
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
Expand Down
191 changes: 127 additions & 64 deletions src/command/substance/get_substance.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
use crate::command;
use crate::lib::CommandHandler;
use crate::lib::Context;
use crate::lib::dosage::Dosage;
use crate::lib::orm;
use crate::lib::orm::substance;
use crate::view_model::substance::ViewModel;
use crate::lib::route_of_administration::RouteOfAdministrationClassification;
use crate::lib::substance::DosageClassification;
use crate::lib::substance::PhaseClassification;
use clap::Parser;
use log::info;
use log::warn;
use iso8601_duration::Duration;
use miette::IntoDiagnostic;
use miette::miette;
use sea_orm::ColumnTrait;
use sea_orm::EntityTrait;
use sea_orm::ModelTrait;
use sea_orm::QueryFilter;
use sea_orm::QueryOrder;
use sea_orm::prelude::async_trait::async_trait;
use tabled::Table;
use tabled::settings::Style;
use std::str::FromStr;

#[derive(Parser, Debug)]
#[command(
Expand All @@ -28,38 +31,134 @@ pub struct GetSubstance
pub substance_name: String,
}

use futures::stream::FuturesUnordered;
use futures::stream::StreamExt;
use miette::Result;

#[async_trait]
impl CommandHandler for GetSubstance
impl CommandHandler<crate::lib::substance::Substance> for GetSubstance
{
async fn handle<'a>(&self, context: Context<'a>) -> miette::Result<()>
async fn handle<'a>(&self, context: Context<'a>) -> Result<crate::lib::substance::Substance>
{
let substances = substance::Entity::find()
.filter(substance::Column::CommonNames.contains(&self.substance_name.to_lowercase()))
.order_by_asc(substance::Column::Name)
.all(context.database_connection)
// Step 1: Fetch the substance from the database
let db_substance = substance::Entity::find()
.filter(substance::Column::Name.contains(self.substance_name.to_lowercase()))
.one(context.database_connection)
.await
.into_diagnostic()?;

if substances.is_empty()
// Return an error if substance is not found
let db_substance = match db_substance
{
warn!("No substances found for query '{}'.", self.substance_name);
println!("No substances found for '{}'.", self.substance_name);
}
else
| Some(substance) => substance,
| None => return Err(miette!("Error: Substance not found.")),
};

// Step 2: Fetch routes of administration
let routes_of_administration = db_substance
.find_related(orm::substance_route_of_administration::Entity)
.all(context.database_connection)
.await
.into_diagnostic()?;

// Create an empty Substance struct
let mut substance = crate::lib::substance::Substance {
name: db_substance.name,
routes_of_administration: crate::lib::substance::RoutesOfAdministration::new(),
};

// Step 3: Resolve detailed data for each route concurrently
let db_connection = context.database_connection.clone();
let route_futures = routes_of_administration.into_iter().map(|route| {
let db = db_connection.clone();
async move {
// Resolve classifications
let classification = RouteOfAdministrationClassification::from_str(&route.name)
.map_err(|e| miette!(format!("{:?}", e)))?;
let mut roa = crate::lib::substance::RouteOfAdministration {
classification: classification.clone(),
dosages: Default::default(),
phases: Default::default(),
};

// Fetch dosages for the route
let dosages = route
.find_related(orm::substance_route_of_administration_dosage::Entity)
.all(&db)
.await
.into_diagnostic()?;

for dosage in dosages
{
let dosage_classification = DosageClassification::from_str(&*dosage.intensity)
.map_err(|_| miette!("Failed to parse dosage classification"))?;

let lower_bound = dosage.lower_bound_amount.map_or(0.0, |amount| {
Dosage::from_str(&format!("{:?} {}", amount, dosage.unit))
.unwrap()
.as_base_units()
});
let upper_bound = dosage.upper_bound_amount.map_or(f64::INFINITY, |amount| {
Dosage::from_str(&format!("{:?} {}", amount, dosage.unit))
.unwrap()
.as_base_units()
});

roa.dosages.insert(
dosage_classification,
crate::lib::substance::DosageRange::from(lower_bound..upper_bound),
);
}

// Fetch phases for the route
let phases = route
.find_related(orm::substance_route_of_administration_phase::Entity)
.all(&db)
.await
.into_diagnostic()?;

for phase in phases
{
let classification = PhaseClassification::from_str(&*phase.classification)
.map_err(|_| miette!("Failed to parse phase classification"))?;

let lower_duration =
Duration::from_str(&phase.lower_duration.unwrap_or_default())
.map_err(|_| miette!("Failed to parse duration"))?;
let upper_duration =
Duration::from_str(&phase.upper_duration.unwrap_or_default())
.map_err(|_| miette!("Failed to parse duration"))?;

roa.phases.insert(
classification,
crate::lib::substance::DurationRange::from(lower_duration..upper_duration),
);
}

Ok::<_, miette::Report>((classification, roa))
}
});

// Use `FuturesUnordered` to manage multiple concurrent tasks efficiently
let mut route_stream = FuturesUnordered::from_iter(route_futures);

while let Some(result) = route_stream.next().await
{
let table = Table::new(substances.into_iter().map(ViewModel::from))
.with(Style::modern())
.to_string();

info!(
"Found {} substances for query '{}'.",
table.lines().count(),
self.substance_name
);
println!("{}", table);
match result
{
| Ok((classification, roa)) =>
{
substance
.routes_of_administration
.insert(classification, roa);
}
| Err(e) => return Err(e), // Return early on error
}
}

Ok(())
// Print or return the final substance
print!("{}", &substance.to_string());
Ok(substance)
}
}

Expand Down Expand Up @@ -105,40 +204,4 @@ mod tests
"No substance with ID starting with 'e30c6c' found"
);
}

// #[async_std::test]
// async fn should_find_substance_by_alias_name()
// {
// migrate_database(&DATABASE_CONNECTION).await.unwrap();
// let ctx = Context::from(Context { database_connection:
// &DATABASE_CONNECTION });
//
// let command = GetSubstance {
// substance_name: "vitamin d3".to_string(),
// };
//
// // TODO: Define series of tests for substances that are known like
// "D3 Vitamin" // We currently do not have them in database but
// pubchem seems to provide information // about all of those.
//
// todo!();
//
// let result = command.handle(Context {
// database_connection: &DATABASE_CONNECTION,
// }).await;
// assert!(result.is_ok(), "Query failed: {:?}", result);
//
// let matched_substances = substance::Entity::find()
// .filter(substance::Column::CommonNames.contains("vitamin d3"))
// .all(ctx.database_connection)
// .await
// .unwrap();
//
// assert!(
// matched_substances
// .iter()
// .any(|s| s.id.starts_with("e30c6c")),
// "No substance with ID starting with 'e30c6c' found"
// );
// }
}
2 changes: 1 addition & 1 deletion src/command/substance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl CommandHandler for SubstanceCommand
{
match &self.commands
{
| SubstanceCommands::Get(command) => command.handle(context).await,
| SubstanceCommands::Get(command) => command.handle(context).await.map(|_| ()),
}
}
}
3 changes: 2 additions & 1 deletion src/lib/dosage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use measurements::Measurement;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::ops::RangeBounds;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Derivative)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Derivative, Eq, PartialOrd)]
pub struct Dosage(Mass);

impl std::str::FromStr for Dosage
Expand Down
14 changes: 4 additions & 10 deletions src/lib/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub trait Formatter: Serialize + Tabled + Sized
{
| OutputFormat::Pretty => Table::new(std::iter::once(self))
.with(tabled::settings::Style::modern())
.with(tabled::settings::Alignment::center())
.to_string(),
| OutputFormat::Json => serde_json::to_string_pretty(self)
.unwrap_or_else(|_| "Error serializing to JSON".to_string()),
Expand All @@ -28,16 +29,9 @@ impl<T: Formatter> FormatterVector<T>
{
match format
{
| OutputFormat::Pretty =>
{
if self.0.is_empty()
{
return "No items found.".to_string();
}
Table::new(&self.0)
.with(tabled::settings::Style::modern())
.to_string()
}
| OutputFormat::Pretty => Table::new(&self.0)
.with(tabled::settings::Style::modern())
.to_string(),
| OutputFormat::Json => serde_json::to_string_pretty(&self.0)
.unwrap_or_else(|_| "Error serializing to JSON".to_string()),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- Step 1: Create a new table with NULLABLE lower_bound_amount and upper_bound_amount
CREATE TABLE `substance_route_of_administration_dosage_new`
(
`id` TEXT NOT NULL PRIMARY KEY,
`intensity` TEXT NOT NULL,
`lower_bound_amount` REAL NULL,
`upper_bound_amount` REAL NULL,
`unit` TEXT NOT NULL,
`routeOfAdministrationId` TEXT NULL,
CONSTRAINT `route_of_administration_dosage_routeOfAdministrationId_fkey`
FOREIGN KEY (`routeOfAdministrationId`) REFERENCES `substance_route_of_administration` (`id`)
ON UPDATE CASCADE
ON DELETE SET NULL
);

-- Step 2: Copy data from the old table to the new table, skipping rows with `0.0` bounds
INSERT INTO `substance_route_of_administration_dosage_new` (`id`, `intensity`, `lower_bound_amount`,
`upper_bound_amount`, `unit`, `routeOfAdministrationId`)
SELECT `id`,
`intensity`,
`lower_bound_amount`,
`upper_bound_amount`,
`unit`,
`routeOfAdministrationId`
FROM `substance_route_of_administration_dosage`;

-- Step 3: Drop the old table
DROP TABLE `substance_route_of_administration_dosage`;

-- Step 4: Rename the new table to the original name
ALTER TABLE `substance_route_of_administration_dosage_new`
RENAME TO `substance_route_of_administration_dosage`;

UPDATE substance_route_of_administration_dosage
SET lower_bound_amount = NULL
WHERE intensity = 'threshold';

UPDATE substance_route_of_administration_dosage
SET upper_bound_amount = NULL
WHERE intensity = 'heavy';
3 changes: 2 additions & 1 deletion src/lib/migration/migrations/atlas.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
h1:puu37dd6GHWGaEV8fpiar5Uldvijkr3D8lMxIDXewZ4=
h1:4iQajbNu+KzPnAT79k0i5bqMCd5XO4N2ZW6B5ATmpbg=
20250101000001_add_ingestion_table.sql h1:9u7Hg7oMQAqDIHfGFYwZC3xmcHixHOrmiwL3VZbufWQ=
20250101000002_import_substance.sql h1:XJuYaltpeWe1LJOMArORZIhVo9U0ulXk7ObAPlpZNYs=
20250101235153_drop_unrelated_data.sql h1:PhD/D6b9tbrjusjmDG8x+Z2oFqLwELhnYSDBWOgqcJw=
20250104060831_update_dosage_bounds.sql h1:sd10cfw6uuXSgtkCyI28s4GrVerQpjMFbbHS0JAIhkU=
7 changes: 6 additions & 1 deletion src/lib/migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ macro_rules! sql_migration {
Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr>
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr>
{
panic!("Down migrations aren't supported before official release")
}
Expand Down Expand Up @@ -72,6 +72,11 @@ impl MigratorTrait for Migrator
"20250101235153_drop_unrelated_data",
"20250101235153_drop_unrelated_data"
),
import_migration!(
M20250104060831UpdateDosageBounds,
"20250104060831_update_dosage_bounds",
"20250104060831_update_dosage_bounds"
),
]
}
}
Loading

0 comments on commit 35e34ee

Please sign in to comment.