From f91abca7d52f457022a4e95247c0028a37fe9cfa Mon Sep 17 00:00:00 2001 From: keinsell Date: Sat, 18 Jan 2025 10:50:35 +0100 Subject: [PATCH] enhance json formatter for ingestion analysis --- Cargo.toml | 59 +++- docs/route-of-administration.md | 0 src/analyzer/mod.rs | 330 +++++------------- src/cli/analyze.rs | 131 ++++++- src/cli/ingestion.rs | 41 +-- src/cli/mod.rs | 5 + src/formatter.rs | 22 +- src/journal/mod.rs | 12 + src/main.rs | 15 +- src/prelude.rs | 8 + src/substance/dosage.rs | 1 + src/substance/mod.rs | 51 +-- src/substance/repository.rs | 2 +- .../route_of_administration/phase.rs | 57 ++- src/tui/mod.rs | 8 + src/utils.rs | 22 +- 16 files changed, 412 insertions(+), 352 deletions(-) create mode 100644 docs/route-of-administration.md create mode 100644 src/journal/mod.rs create mode 100644 src/prelude.rs diff --git a/Cargo.toml b/Cargo.toml index 98be6e7a..60648c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,10 @@ async-std = { version = "1", features = ["attributes", "async-global-executor"] atty = "0.2.14" chrono = { version = "0.4.39", features = ["std", "serde", "iana-time-zone", "rkyv"] } chrono-english = "0.1.7" -clap = { version = "4.5.23", features = ["derive", "wrap_help", "suggestions", "std", "usage", "error-context", "help", "unstable-styles", "unicode", "env", "unstable-v5"] } +clap = { version = "4.5.23", features = ["derive", "wrap_help", "suggestions", "std", "cargo"] } clap-verbosity-flag = "3.0.1" clap_complete = "4.5.40" +clap_mangen = "0.2.24" date_time_parser = "0.2.0" delegate = "0.13.1" derivative = "2.2.0" @@ -59,6 +60,12 @@ ratatui = { version = "0.26.0", features = ["all-widgets", "macros", "serde"] } crossterm = "0.27.0" async-trait = "0.1.85" indicatif = "0.17.9" +tracing-subscriber = "0.3.19" +tracing-indicatif = "0.3.8" +tracing = "0.1.41" +itertools = "0.13.0" +rayon = "1.10.0" +predicates = "3.1.3" [expand] color = "always" @@ -67,3 +74,53 @@ pager = true [profile.dist] inherits = "release" lto = "fat" + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/neuronek", dest = "/usr/bin/neuronek", mode = "755" }, + { source = "LICENSE-MIT", dest = "/usr/share/doc/neuronek/LICENSE", mode = "644" }, + { source = "README.md", dest = "/usr/share/doc/neuronek/README.md", mode = "644" }, + { source = "man/neuronek.1", dest = "/usr/share/man/man1/neuronek.1", mode = "644", doc = true }, + { source = "completions/neuronek.bash", dest = "/usr/share/bash-completion/completions/neuronek", mode = "644" }, + { source = "completions/neuronek.fish", dest = "/usr/share/fish/vendor_completions.d/neuronek.fish", mode = "644" }, + { source = "completions/_neuronek", dest = "/usr/share/zsh/vendor-completions/", mode = "644" }, +] + +[package.metadata.deb] +assets = [ + [ + "target/release/neuronek", + "usr/bin/", + "755", + ], + [ + "../LICENSE-MIT", + "/usr/share/doc/neuronek/LICENSE", + "644", + ], + [ + "../README.md", + "usr/share/doc/neuronek/README", + "644", + ], + [ + "../man/neuronek.1", + "/usr/share/man/man1/neuronek.1", + "644", + ], + [ + "../completions/neuronek.bash", + "/usr/share/bash-completion/completions/neuronek", + "644", + ], + [ + "../completions/neuronek.fish", + "/usr/share/fish/vendor_completions.d/neuronek.fish", + "644", + ], + [ + "../completions/_neuronek", + "/usr/share/zsh/vendor-completions/", + "644", + ], +] diff --git a/docs/route-of-administration.md b/docs/route-of-administration.md new file mode 100644 index 00000000..e69de29b diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 6f32863c..f8ffd98a 100755 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -1,9 +1,12 @@ +use crate::ingestion::Ingestion; +use crate::ingestion::IngestionDate; +use crate::substance::DosageClassification; +use crate::substance::Dosages; use crate::substance::Substance; +use crate::substance::route_of_administration::phase::PHASE_ORDER; use chrono::TimeDelta; -use chrono_humanize::HumanTime; use miette::miette; use serde::Serialize; -use std::fmt; use std::ops::Add; use std::ops::Range; @@ -20,7 +23,7 @@ pub struct IngestionAnalysis /// This field is wrapped in an `Option` and a `Box` to allow for optional /// ownership. #[serde(skip_serializing)] - ingestion: Option>, + pub(crate) ingestion: Option>, /// The substance.rs ingested in this event. /// This field is wrapped in an `Option` and a `Box` to allow for optional @@ -31,12 +34,14 @@ pub struct IngestionAnalysis /// The classification of the dosage for this ingestion. /// This field is an `Option` to allow for cases where the dosage /// classification cannot be determined. - dosage: Option, + pub(crate) dosage_classification: Option, /// The current phase of the ingestion, if it can be determined. /// This field is an `Option` to allow for cases where the current phase /// cannot be determined. - pub(crate) current_phase: Option, + #[serde(skip_serializing)] + pub(crate) current_phase: + Option, /// The start time of the ingestion event (copy of ingestion.ingestion_date) ingestion_start: IngestionDate, @@ -49,38 +54,21 @@ pub struct IngestionAnalysis /// A vector of `IngestionPhase` structs representing the different phases /// of the ingestion event. pub(crate) phases: Vec, - - /// The progress of the ingestion event, represented as a float between 0.0 - /// and 1.0. This value represents the fraction of the total duration - /// (excluding the afterglow phase) that has elapsed. - pub(crate) progress: f64, } #[derive(Debug, Clone, Serialize)] pub struct IngestionPhase { - pub(crate) class: crate::substance::PhaseClassification, + pub(crate) class: crate::substance::route_of_administration::phase::PhaseClassification, pub(crate) duration_range: Range, pub(crate) prev: Option>, pub(crate) next: Option>, } -pub fn progress_bar(progress: f64, width: usize) -> String -{ - let filled_length = (progress * width as f64).round() as usize; - let empty_length = width - filled_length; - let filled_bar = "█".repeat(filled_length); - let empty_bar = "░".repeat(empty_length); - format!("[{}{}]", filled_bar, empty_bar) -} - impl IngestionAnalysis { - pub async fn analyze( - ingestion: crate::ingestion::Ingestion, - substance: crate::substance::Substance, - ) -> miette::Result + pub async fn analyze(ingestion: Ingestion, substance: Substance) -> miette::Result { let roa = substance .routes_of_administration @@ -94,7 +82,6 @@ impl IngestionAnalysis let mut total_end_time_excl_afterglow: Option = None; let mut current_start_range = ingestion.ingestion_date; - let mut prev_phase: Option = None; for &phase_type in PHASE_ORDER.iter() @@ -106,26 +93,26 @@ impl IngestionAnalysis .map(|(_, v)| v) { let start_time_range = current_start_range; - let duration_range = &phase; let end_time_range = start_time_range - .add(TimeDelta::from_std(duration_range.start.to_std().unwrap()).unwrap()); + .add(TimeDelta::from_std(phase.start.to_std().unwrap()).unwrap()); - total_start_time = total_start_time - .map_or(Some(start_time_range), |s| Some(s.min(start_time_range))); + total_start_time = + Some(total_start_time.map_or(start_time_range, |s| s.min(start_time_range))); total_end_time = - total_end_time.map_or(Some(end_time_range), |e| Some(e.max(end_time_range))); + Some(total_end_time.map_or(end_time_range, |e| e.max(end_time_range))); - // Update total_end_time_excl_afterglow - if phase_type != crate::substance::PhaseClassification::Afterglow + if phase_type != crate::substance::route_of_administration::phase::PhaseClassification::Afterglow { - total_end_time_excl_afterglow = total_end_time_excl_afterglow - .map_or(Some(end_time_range), |e| Some(e.max(end_time_range))); + total_end_time_excl_afterglow = Some( + total_end_time_excl_afterglow + .map_or(end_time_range, |e| e.max(end_time_range)), + ); } let new_phase = IngestionPhase { class: phase_type, duration_range: start_time_range..end_time_range, - prev: prev_phase.as_ref().map(|p| Box::new(p.clone())), + prev: prev_phase.clone().map(Box::new), next: None, }; @@ -136,7 +123,6 @@ impl IngestionAnalysis phases.push(new_phase.clone()); prev_phase = Some(new_phase); - current_start_range = end_time_range; } } @@ -146,11 +132,6 @@ impl IngestionAnalysis .map(|(start, end)| start..end) .ok_or_else(|| miette!("Could not compute total duration"))?; - let total_range_excl_afterglow = total_start_time - .zip(total_end_time_excl_afterglow) - .map(|(start, end)| start..end) - .ok_or_else(|| miette!("Could not compute total duration excluding afterglow"))?; - let current_date = chrono::Local::now(); let current_phase = phases .iter() @@ -160,207 +141,62 @@ impl IngestionAnalysis }) .map(|phase| phase.class); - let roa_dosages = roa.dosages; - let dosage_classification = classify_dosage(ingestion.dosage.clone(), &roa_dosages)?; - - - let now = chrono::Local::now(); - let total_duration = total_range_excl_afterglow.end - total_range_excl_afterglow.start; - let elapsed_time = if now < total_range_excl_afterglow.start - { - chrono::Duration::zero() - } - else - { - (now - total_range_excl_afterglow.start).min(total_duration) - }; - - let progress = elapsed_time.num_seconds() as f64 / total_duration.num_seconds() as f64; - - let progress = progress.clamp(0.0, 1.0); + let dosage_classification = classify_dosage(ingestion.dosage.clone(), &roa.dosages); Ok(Self { ingestion: Some(Box::new(ingestion)), substance: Some(Box::new(substance)), - dosage: Some(dosage_classification), + dosage_classification, current_phase, ingestion_start: total_range.start, ingestion_end: total_range.end, phases, - progress, }) } -} - - -use crate::ingestion::IngestionDate; -use crate::substance::DosageClassification; -use crate::substance::Dosages; -use crate::substance::route_of_administration::phase::PHASE_ORDER; -use chrono::Utc; -use owo_colors::OwoColorize; -impl fmt::Display for IngestionAnalysis -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + /// The progress of the ingestion event, represented as a float between 0.0 + /// and 1.0. This value represents the fraction of the total duration + /// (excluding the afterglow phase) that has elapsed. + pub fn progress(&self) -> f64 { - let now = Utc::now(); - let substance_name = self - .substance - .as_ref() - .map(|s| s.name.as_str()) - .unwrap_or(""); - let dosage = self - .ingestion - .as_ref() - .map(|i| i.dosage.to_string()) - .unwrap_or_default(); - - writeln!( - f, - "{} {} {}", - "Ingestion Analysis:".bold(), - substance_name.cyan(), - dosage.yellow() - )?; - writeln!(f)?; - - let progress_bar = progress_bar(self.progress, 30); - let progress_percentage = format!("{:.2}%", self.progress * 100.0); - writeln!( - f, - "Progress: {} {}", - progress_bar, - progress_percentage.bold() - )?; - writeln!(f)?; - - let ingestion_symbol = if self.ingestion_start <= now && now < self.ingestion_end + let now = chrono::Local::now(); + let total_duration = self.ingestion_end - self.ingestion_start; + let elapsed_time = if now < self.ingestion_start { - "●".blue().to_string() + chrono::Duration::zero() } else { - "○".dimmed().to_string() + (now - self.ingestion_start).min(total_duration) }; - let ingestion_time_info = format!("Ingested: {}", HumanTime::from(self.ingestion_start)); - writeln!(f, "{} {}", ingestion_symbol, ingestion_time_info)?; - - for phase in &self.phases - { - let is_passed = phase.duration_range.end < now; - let is_current = phase.duration_range.start <= now && now < phase.duration_range.end; - - let symbol = if is_passed - { - "✓".green().to_string() - } - else if is_current - { - "●".yellow().to_string() - } - else - { - "○".dimmed().to_string() - }; - - let phase_name = if is_passed - { - phase.class.to_string().green().to_string() - } - else if is_current - { - phase.class.to_string().yellow().bold().to_string() - } - else - { - phase.class.to_string().dimmed().to_string() - }; - - let time_info = if is_passed - { - format!("completed {}", HumanTime::from(phase.duration_range.end)) - .green() - .to_string() - } - else if is_current - { - format!("started {}", HumanTime::from(phase.duration_range.start)) - .yellow() - .to_string() - } - else - { - format!("starts {}", HumanTime::from(phase.duration_range.start)) - .dimmed() - .to_string() - }; - - writeln!(f, "{} {} {}", symbol, phase_name, time_info)?; - } - - if let Some(phase_classification) = self.current_phase - { - writeln!(f)?; - writeln!( - f, - "Current Phase: {}", - phase_classification.to_string().yellow().bold() - )?; - } - - if let Some(dosage_class) = &self.dosage - { - writeln!( - f, - "Dosage Classification: {}", - dosage_class.to_string().cyan() - )?; - } - - Ok(()) + (elapsed_time.num_seconds() as f64 / total_duration.num_seconds() as f64).clamp(0.0, 1.0) } } -pub fn classify_dosage( + +pub(super) fn classify_dosage( dosage: crate::substance::dosage::Dosage, - roa_dosages: &Dosages, -) -> Result + dosages: &Dosages, +) -> Option { - for (_classification, range) in roa_dosages - { - if range.contains(&dosage) - { - return Ok(*_classification); - } - } - - // If no range contains the dosage, find the closest classification - let mut closest_classification = None; - - for (classification, range) in roa_dosages - { - if let Some(end) = &range.end - { - if &dosage <= end - { - closest_classification = Some(DosageClassification::Threshold); - break; - } - } - - if let Some(start) = &range.start - { - if &dosage >= start - { - closest_classification = Some(DosageClassification::Heavy); - } - } - } - - closest_classification - .ok_or_else(|| miette!("No dosage classification found for value: {}", dosage)) + dosages + .iter() + .find(|(_, range)| range.contains(&dosage)) + .map(|(classification, _)| *classification) + .or_else(|| { + dosages + .iter() + .filter_map(|(_classification, range)| { + match (range.start.as_ref(), range.end.as_ref()) + { + | (Some(start), _) if &dosage >= start => Some(DosageClassification::Heavy), + | (_, Some(end)) if &dosage <= end => Some(DosageClassification::Threshold), + | _ => None, + } + }) + .next() + }) } #[cfg(test)] @@ -375,31 +211,33 @@ mod tests use rstest::rstest; use std::str::FromStr; - - #[rstest] - #[case("10mg", DosageClassification::Threshold)] - #[case("100mg", DosageClassification::Medium)] - #[case("1000mg", DosageClassification::Heavy)] - async fn should_classify_dosage( - #[case] dosage_str: &str, - #[case] expected: DosageClassification, - ) - { - let db = &DATABASE_CONNECTION; - migrate_database(db).await.unwrap(); - let caffeine = crate::substance::repository::get_substance("caffeine", db) - .await - .unwrap(); - let oral_caffeine_roa = caffeine - .unwrap().routes_of_administration - .get(&crate::substance::route_of_administration::RouteOfAdministrationClassification::Oral) - .unwrap() - .clone() - .dosages; - - let dosage_instance = Dosage::from_str(dosage_str).unwrap(); - let classification = classify_dosage(dosage_instance, &oral_caffeine_roa).unwrap(); - - assert_eq!(classification, expected); - } + // #[rstest] + // #[case("10mg", DosageClassification::Threshold)] + // #[case("100mg", DosageClassification::Medium)] + // #[case("1000mg", DosageClassification::Heavy)] + // async fn should_classify_dosage( + // #[case] dosage_str: &str, + // #[case] expected: DosageClassification, + // ) + // { + // let db = &DATABASE_CONNECTION; + // migrate_database(db).await.unwrap(); + // let caffeine = + // crate::substance::repository::get_substance("caffeine", db) + // .await + // .unwrap(); + // let oral_caffeine_roa = caffeine + // .unwrap().routes_of_administration + // .get(& + // crate::substance::route_of_administration::RouteOfAdministrationClassification::Oral) + // .unwrap() + // .clone() + // .dosages; + // + // let dosage_instance = Dosage::from_str(dosage_str).unwrap(); + // let classification = classify_dosage(dosage_instance, + // &oral_caffeine_roa).unwrap(); + // + // assert_eq!(classification, expected); + // } } diff --git a/src/cli/analyze.rs b/src/cli/analyze.rs index de1fc0bf..34ef7369 100644 --- a/src/cli/analyze.rs +++ b/src/cli/analyze.rs @@ -1,15 +1,125 @@ +use crate::analyzer::IngestionAnalysis; +use crate::analyzer::IngestionPhase; +use crate::cli::OutputFormat; +use crate::formatter::Formatter; use crate::ingestion::Ingestion; +use crate::substance::DosageClassification; use crate::substance::dosage::Dosage; use crate::substance::route_of_administration::RouteOfAdministrationClassification; +use crate::substance::route_of_administration::phase::PhaseClassification; use crate::utils::AppContext; use crate::utils::CommandHandler; use async_trait::async_trait; use chrono::DateTime; use chrono::Local; +use chrono_humanize::HumanTime; use clap::Parser; use miette::IntoDiagnostic; +use owo_colors::OwoColorize; use sea_orm::EntityTrait; +use std::borrow::Cow; use std::str::FromStr; +use tabled::Table; +use tabled::Tabled; +use tabled::settings::Style; + +fn display_date(date: &DateTime) -> String { HumanTime::from(*date).to_string() } + +#[derive(Debug, serde::Serialize, Tabled)] +struct AnalyzerReportPhaseViewModel +{ + #[tabled(rename = "Phase")] + phase: PhaseClassification, + + #[tabled(rename = "Start Time")] + #[tabled(display_with = "display_date")] + from: DateTime, + + #[tabled(rename = "End Time")] + #[tabled(display_with = "display_date")] + to: DateTime, +} + +#[derive(Debug, serde::Serialize)] +struct AnalyzerReportViewModel +{ + ingestion_id: Option, + substance_name: String, + dosage: String, + #[serde(rename = "dosage_classification")] + classification: Option, + phases: Vec, +} + +impl Tabled for AnalyzerReportViewModel +{ + const LENGTH: usize = 5; + + fn fields(&self) -> Vec> + { + let nested_phases_table = Table::new(&self.phases) + .with(Style::modern_rounded()) + .to_string(); + vec![ + Cow::Owned( + self.ingestion_id + .map_or("N/A".to_string(), |id| id.to_string()), + ), + Cow::Borrowed(self.substance_name.as_str()), + Cow::Borrowed(self.dosage.as_str()), + Cow::Owned( + self.classification + .as_ref() + .map_or("N/A".to_string(), |c| c.to_string()), + ), + Cow::Owned(nested_phases_table.to_string()), + ] + } + + fn headers() -> Vec> + { + vec![ + Cow::Borrowed("Ingestion ID"), + Cow::Borrowed("Substance"), + Cow::Borrowed("Dosage"), + Cow::Borrowed("Dosage Classification"), + Cow::Borrowed("Phase Details"), + ] + } +} + +impl Formatter for AnalyzerReportViewModel {} + +impl From<&IngestionPhase> for AnalyzerReportPhaseViewModel +{ + fn from(value: &IngestionPhase) -> Self + { + AnalyzerReportPhaseViewModel { + phase: value.class.clone(), + from: value.duration_range.start, + to: value.duration_range.end, + } + } +} + +impl From for AnalyzerReportViewModel +{ + fn from(value: IngestionAnalysis) -> Self + { + AnalyzerReportViewModel { + ingestion_id: None, + substance_name: value.ingestion.as_ref().unwrap().substance.clone(), + dosage: value.ingestion.as_ref().unwrap().dosage.to_string(), + classification: value.dosage_classification, + phases: value + .phases + .iter() + .map(AnalyzerReportPhaseViewModel::from) + .collect(), + } + } +} + /// Analyze a previously logged ingestion activity. /// @@ -103,7 +213,7 @@ impl CommandHandler for AnalyzeIngestion }; let substance = crate::substance::repository::get_substance( - self.substance.as_ref().unwrap(), + &ingestion.substance.clone(), ctx.database_connection, ) .await?; @@ -111,20 +221,11 @@ impl CommandHandler for AnalyzeIngestion if substance.is_some() { let substance = substance.unwrap(); - let analysis = - crate::analyzer::IngestionAnalysis::analyze(ingestion, substance).await?; - - match ctx.stdout_format - { - | crate::cli::OutputFormat::Pretty => println!("{}", analysis), - | crate::cli::OutputFormat::Json => - { - println!( - "{}", - serde_json::to_string_pretty(&analysis).into_diagnostic()? - ); - } - } + let analysis: AnalyzerReportViewModel = + crate::analyzer::IngestionAnalysis::analyze(ingestion, substance) + .await? + .into(); + println!("{}", analysis.format(ctx.stdout_format)); } Ok(()) diff --git a/src/cli/ingestion.rs b/src/cli/ingestion.rs index e3ca5069..87f4efbd 100644 --- a/src/cli/ingestion.rs +++ b/src/cli/ingestion.rs @@ -28,6 +28,8 @@ use serde::Deserialize; use serde::Serialize; use std::str::FromStr; use tabled::Tabled; +use tracing::Level; +use tracing::event; use typed_builder::TypedBuilder; /** @@ -91,15 +93,17 @@ impl CommandHandler for LogIngestion { async fn handle<'a>(&self, context: AppContext<'a>) -> miette::Result<()> { - let pubchem = pubchem::Compound::with_name(&self.substance_name) + // Substance name powered by PubChem API, fallback to user input + let substance_name = pubchem::Compound::with_name(&self.substance_name) .title() - .into_diagnostic()?; + .into_diagnostic() + .unwrap_or(self.substance_name.clone()); - let ingestion: crate::ingestion::Ingestion = Ingestion::insert(ingestion::ActiveModel { + let ingestion = Ingestion::insert(ingestion::ActiveModel { id: ActiveValue::default(), - substance_name: ActiveValue::Set(pubchem.to_lowercase()), + substance_name: ActiveValue::Set(substance_name.to_lowercase()), route_of_administration: ActiveValue::Set( - serde_json::to_value(&self.route_of_administration) + serde_json::to_value(self.route_of_administration) .unwrap() .as_str() .unwrap() @@ -112,10 +116,14 @@ impl CommandHandler for LogIngestion }) .exec_with_returning(context.database_connection) .await - .into_diagnostic()? - .into(); + .into_diagnostic()?; + + event!(Level::INFO, "Ingestion logged | {:#?}", ingestion.clone()); - info!("Ingestion logged. {:#?}", ingestion); + println!( + "{}", + IngestionViewModel::from(ingestion).format(context.stdout_format) + ); Ok(()) } @@ -333,22 +341,7 @@ pub struct IngestionViewModel pub ingested_at: DateTime, } -impl Formatter for IngestionViewModel -{ - fn format(&self, format: OutputFormat) -> String - { - match format - { - | OutputFormat::Json => serde_json::to_string_pretty(self).unwrap(), - | OutputFormat::Pretty => - { - let table = tabled::Table::new([self]).to_string(); - table - } - } - } -} - +impl Formatter for IngestionViewModel {} impl From for IngestionViewModel { fn from(model: Model) -> Self diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 00858752..6b496f0e 100755 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,8 +3,12 @@ use clap::ColorChoice; use clap::CommandFactory; use clap::Parser; use clap::Subcommand; +use clap_complete::Shell; use ingestion::IngestionCommand; +use miette::IntoDiagnostic; use sea_orm::prelude::async_trait::async_trait; +use std::fs; +use std::fs::create_dir_all; use substance::SubstanceCommand; use crate::utils::AppContext; @@ -61,6 +65,7 @@ struct GenerateCompletion shell: clap_complete::Shell, } + #[async_trait] impl CommandHandler for GenerateCompletion { diff --git a/src/formatter.rs b/src/formatter.rs index 61fa41ed..9f2b8c92 100755 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -9,14 +9,24 @@ pub trait Formatter: Serialize + Tabled + Sized { match format { - | OutputFormat::Pretty => Table::new(std::iter::once(self)) - .with(tabled::settings::Style::modern_rounded()) - .with(tabled::settings::Alignment::center()) - .to_string(), - | OutputFormat::Json => serde_json::to_string_pretty(self) - .unwrap_or_else(|_| "Error serializing to JSON".to_string()), + | OutputFormat::Pretty => self.pretty(), + | OutputFormat::Json => self.json(), } } + + fn json(&self) -> String + { + serde_json::to_string_pretty(self) + .unwrap_or_else(|_| "Error serializing to JSON".to_string()) + } + + fn pretty(&self) -> String + { + Table::new(std::iter::once(self)) + .with(tabled::settings::Style::modern_rounded()) + .with(tabled::settings::Alignment::center()) + .to_string() + } } pub struct FormatterVector(Vec); diff --git a/src/journal/mod.rs b/src/journal/mod.rs new file mode 100644 index 00000000..f235f0e6 --- /dev/null +++ b/src/journal/mod.rs @@ -0,0 +1,12 @@ +use crate::substance::route_of_administration::phase::PhaseClassification; +use chrono::DateTime; +use chrono::Local; + +struct JournalEvent +{ + id: Option, + from: DateTime, + to: DateTime, + phase_classification: PhaseClassification, + ingestion_id: i32, +} diff --git a/src/main.rs b/src/main.rs index a4e342f2..e01ec4e4 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ +#![allow(unused_imports)] extern crate chrono; extern crate chrono_english; extern crate date_time_parser; - +#[macro_use] extern crate log; +use prelude::*; use crate::cli::Cli; use crate::utils::AppContext; @@ -12,15 +14,16 @@ use crate::utils::setup_diagnostics; use crate::utils::setup_logger; use atty::Stream; use clap::Parser; -use futures::executor::block_on; use std::env; mod analyzer; mod cli; pub mod formatter; mod ingestion; +mod journal; mod migration; pub mod orm; +mod prelude; mod substance; mod tui; mod utils; @@ -31,11 +34,9 @@ async fn main() -> miette::Result<()> setup_diagnostics(); setup_logger(); - block_on(async { - migrate_database(&DATABASE_CONNECTION) - .await - .expect("Database migration failed"); - }); + migrate_database(&DATABASE_CONNECTION) + .await + .expect("Database migration failed"); // TODO: Perform a check of completion scripts existence and update them or // install them https://askubuntu.com/a/1188315 diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 00000000..b429e28a --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,8 @@ +#![allow(unused_imports)] +extern crate chrono; +extern crate chrono_english; +extern crate date_time_parser; +extern crate predicates; + +use predicates::prelude::*; +use rayon::prelude::*; diff --git a/src/substance/dosage.rs b/src/substance/dosage.rs index 473fea2a..a502a281 100755 --- a/src/substance/dosage.rs +++ b/src/substance/dosage.rs @@ -43,6 +43,7 @@ impl Dosage delegate! { to self.0 { pub fn as_base_units(&self) -> f64; + } } } diff --git a/src/substance/mod.rs b/src/substance/mod.rs index 325055dd..bf11f6f7 100755 --- a/src/substance/mod.rs +++ b/src/substance/mod.rs @@ -10,7 +10,6 @@ use crate::substance::route_of_administration::RouteOfAdministrationClassificati use dosage::Dosage; use hashbrown::HashMap; use iso8601_duration::Duration; -use miette::miette; use serde::Deserialize; use serde::Serialize; use std::fmt; @@ -24,53 +23,6 @@ use tabled::settings::Width; use tabled::settings::object::Columns; use tabled::settings::object::Rows; -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Hash)] -pub enum PhaseClassification -{ - Onset, - Comeup, - Peak, - Comedown, - Afterglow, -} - -impl FromStr for PhaseClassification -{ - type Err = miette::Report; - - fn from_str(s: &str) -> Result - { - match s.to_lowercase().as_str() - { - | "onset" => Ok(Self::Onset), - | "comeup" => Ok(Self::Comeup), - | "peak" => Ok(Self::Peak), - | "comedown" => Ok(Self::Comedown), - | "offset" => Ok(Self::Comedown), - | "afterglow" => Ok(Self::Afterglow), - | _ => Err(miette!( - "Could not parse phase classification {} from string", - &s - )), - } - } -} - -impl fmt::Display for PhaseClassification -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result - { - match self - { - | PhaseClassification::Onset => write!(f, "Onset"), - | PhaseClassification::Comeup => write!(f, "Comeup"), - | PhaseClassification::Peak => write!(f, "Peak"), - | PhaseClassification::Comedown => write!(f, "Comedown"), - | PhaseClassification::Afterglow => write!(f, "Afterglow"), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Hash)] pub enum DosageClassification { @@ -223,7 +175,6 @@ impl DosageRange pub type DurationRange = Range; pub type Dosages = HashMap; -pub type Phases = HashMap; #[derive(Debug, Clone)] pub struct RouteOfAdministration @@ -234,5 +185,7 @@ pub struct RouteOfAdministration } use crate::formatter::Formatter; +use route_of_administration::phase::Phases; use tabled::Tabled; + pub(crate) type SubstanceTable = crate::orm::substance::Model; diff --git a/src/substance/repository.rs b/src/substance/repository.rs index 12431dbb..db48f32c 100755 --- a/src/substance/repository.rs +++ b/src/substance/repository.rs @@ -3,12 +3,12 @@ use crate::orm::substance; use crate::substance::DosageClassification; use crate::substance::DosageRange; use crate::substance::DurationRange; -use crate::substance::PhaseClassification; use crate::substance::RouteOfAdministration; use crate::substance::RoutesOfAdministration; use crate::substance::Substance; use crate::substance::dosage::Dosage; use crate::substance::route_of_administration::RouteOfAdministrationClassification; +use crate::substance::route_of_administration::phase::PhaseClassification; use futures::StreamExt; use futures::stream::FuturesUnordered; use iso8601_duration::Duration; diff --git a/src/substance/route_of_administration/phase.rs b/src/substance/route_of_administration/phase.rs index bd323576..d172af81 100755 --- a/src/substance/route_of_administration/phase.rs +++ b/src/substance/route_of_administration/phase.rs @@ -1,4 +1,10 @@ -use crate::substance::PhaseClassification; +use crate::substance::DurationRange; +use hashbrown::HashMap; +use miette::miette; +use serde::Deserialize; +use serde::Serialize; +use std::fmt; +use std::str::FromStr; pub const PHASE_ORDER: [PhaseClassification; 5] = [ PhaseClassification::Onset, @@ -7,3 +13,52 @@ pub const PHASE_ORDER: [PhaseClassification; 5] = [ PhaseClassification::Comedown, PhaseClassification::Afterglow, ]; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq, Hash)] +pub enum PhaseClassification +{ + Onset, + Comeup, + Peak, + Comedown, + Afterglow, +} + +impl FromStr for PhaseClassification +{ + type Err = miette::Report; + + fn from_str(s: &str) -> Result + { + match s.to_lowercase().as_str() + { + | "onset" => Ok(Self::Onset), + | "comeup" => Ok(Self::Comeup), + | "peak" => Ok(Self::Peak), + | "comedown" => Ok(Self::Comedown), + | "offset" => Ok(Self::Comedown), + | "afterglow" => Ok(Self::Afterglow), + | _ => Err(miette!( + "Could not parse phase classification {} from string", + &s + )), + } + } +} + +impl fmt::Display for PhaseClassification +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + { + match self + { + | PhaseClassification::Onset => write!(f, "Onset"), + | PhaseClassification::Comeup => write!(f, "Comeup"), + | PhaseClassification::Peak => write!(f, "Peak"), + | PhaseClassification::Comedown => write!(f, "Comedown"), + | PhaseClassification::Afterglow => write!(f, "Afterglow"), + } + } +} + +pub type Phases = HashMap; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 84cbd527..a7bca086 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,3 +1,6 @@ +use crate::orm::ingestion; +use crate::orm::prelude::Ingestion; +use crate::utils::DATABASE_CONNECTION; use crossterm::event::Event; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -7,6 +10,7 @@ use crossterm::terminal::EnterAlternateScreen; use crossterm::terminal::LeaveAlternateScreen; use crossterm::terminal::disable_raw_mode; use crossterm::terminal::enable_raw_mode; +use futures::executor::block_on; use miette::IntoDiagnostic; use miette::Result; use ratatui::Frame; @@ -21,7 +25,11 @@ use ratatui::style::Color; use ratatui::style::Style; use ratatui::widgets::Block; use ratatui::widgets::Borders; +use ratatui::widgets::List; +use ratatui::widgets::ListItem; use ratatui::widgets::block::Title; +use sea_orm::EntityTrait; +use sea_orm::QueryOrder; use std::io; use std::io::Stdout; diff --git a/src/utils.rs b/src/utils.rs index d3250fca..d340486b 100755 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,6 +16,12 @@ use sea_orm_migration::IntoSchemaManagerConnection; use sea_orm_migration::MigratorTrait; use std::env::temp_dir; use std::path::PathBuf; +use std::thread::sleep; +use tracing::instrument; +use tracing_indicatif::IndicatifLayer; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; #[derive(Debug, Clone)] pub struct AppContext<'a> @@ -116,13 +122,14 @@ fn initialize_database(config: &Config) -> std::result::Result<(), String> Ok(()) } +#[instrument] pub async fn migrate_database(database_connection: &DatabaseConnection) -> miette::Result<()> { let is_interactive_terminal = atty::is(Stream::Stdout); let spinner = if is_interactive_terminal { let s = indicatif::ProgressBar::new_spinner(); - s.enable_steady_tick(std::time::Duration::from_millis(100)); + s.enable_steady_tick(std::time::Duration::from_millis(10)); Some(s) } else @@ -161,7 +168,18 @@ pub async fn migrate_database(database_connection: &DatabaseConnection) -> miett pub fn setup_diagnostics() { miette::set_panic_hook(); } // TODO: Implement logging -pub fn setup_logger() {} +pub fn setup_logger() +{ + let indicatif_layer = IndicatifLayer::new(); + + if atty::is(Stream::Stdout) && cfg!(debug_assertions) + { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) + .with(indicatif_layer) + .init(); + } +} pub fn parse_date_string(humanized_input: &str) -> miette::Result>