Skip to content

Commit

Permalink
refactor dosage parsing into traits
Browse files Browse the repository at this point in the history
Get rid of `format_dosage` and `parse_dosage` functions in favor of `fmt::Display` and `std::str::FromStr` traits implementations which serve same functionality. There are also mixed changes related to output formatting.
  • Loading branch information
keinsell committed Jan 4, 2025
1 parent 918eb8f commit 870e84b
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 61 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ clap_complete = "4.5.40"
atty = "0.2.14"
ryu = "1.0.18"
float-pretty-print = "0.1.1"
delegate = "0.13.1"
derivative = "2.2.0"
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
Expand Down
4 changes: 2 additions & 2 deletions src/command/ingestion/list_ingestions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::lib::formatter::FormatterVector;
use crate::lib::orm::ingestion;
use crate::lib::orm::prelude::Ingestion;
use crate::lib::CommandHandler;
use crate::lib::{CommandHandler, Context};
use crate::view_model::ingestion::ViewModel;
use async_std::task::block_on;
use clap::Parser;
Expand Down Expand Up @@ -35,7 +35,7 @@ impl CommandHandler for ListIngestion
.await
.map_err(|e| e.to_string())
})
.unwrap();
.unwrap();

if ingestions.is_empty()
{
Expand Down
19 changes: 12 additions & 7 deletions src/command/ingestion/log_ingestion.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use crate::lib::CommandHandler;
use crate::lib::Context;
use crate::lib::dosage::Dosage;
use crate::lib::dosage::parse_dosage;
use crate::lib::orm::ingestion;
use crate::lib::orm::prelude::Ingestion;
use crate::lib::parse_date_string;
use crate::lib::route_of_administration::RouteOfAdministrationClassification;
use crate::lib::CommandHandler;
use crate::lib::Context;
use crate::view_model::ingestion::ViewModel;
use chrono::DateTime;
use chrono::Local;
Expand All @@ -18,6 +17,7 @@ use sea_orm::ActiveValue;
use sea_orm::EntityTrait;
use sea_orm_migration::async_trait::async_trait;
use std::fmt::Debug;
use std::str::FromStr;

/**
# Log Ingestion
Expand Down Expand Up @@ -51,7 +51,12 @@ pub struct LogIngestion
#[arg(index = 1, value_name = "SUBSTANCE", required = true)]
pub substance_name: String,
/// Dosage of given substance provided as string with unit (e.g., 10 mg)
#[arg(index = 2, value_name = "DOSAGE", required = true, value_parser=parse_dosage)]
#[arg(
index = 2,
value_name = "DOSAGE",
required = true,
value_parser = Dosage::from_str
)]
pub dosage: Dosage,
/// Date of ingestion, by default current date is used if not provided.
///
Expand Down Expand Up @@ -90,9 +95,9 @@ impl CommandHandler for LogIngestion
updated_at: ActiveValue::Set(Local::now().to_utc().naive_local()),
created_at: ActiveValue::Set(Local::now().to_utc().naive_local()),
})
.exec_with_returning(context.database_connection)
.await
.into_diagnostic();
.exec_with_returning(context.database_connection)
.await
.into_diagnostic();

if let Err(e) = created_ingestion
{
Expand Down
12 changes: 6 additions & 6 deletions src/command/ingestion/update_ingestion.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
use crate::lib::CommandHandler;
use crate::lib::Context;
use crate::lib::dosage::Dosage;
use crate::lib::dosage::parse_dosage;
use crate::lib::orm::ingestion;
use crate::lib::orm::prelude::Ingestion;
use crate::lib::parse_date_string;
use crate::lib::route_of_administration::RouteOfAdministrationClassification;
use crate::lib::CommandHandler;
use crate::lib::Context;
use crate::view_model::ingestion::ViewModel;
use chrono::DateTime;
use chrono::Local;
use clap::Parser;
use log::info;
use measurements::Measurement;
use miette::IntoDiagnostic;
use miette::miette;
use miette::IntoDiagnostic;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue;
use sea_orm::EntityTrait;
use sea_orm::prelude::async_trait::async_trait;
use std::str::FromStr;


#[derive(Parser, Debug)]
Expand All @@ -33,7 +33,7 @@ pub struct UpdateIngestion
pub substance_name: Option<String>,

/// New dosage (optional, e.g., 20 mg)
#[arg(short = 'd', long = "dosage", value_name = "DOSAGE", value_parser=parse_dosage)]
#[arg(short = 'd', long = "dosage", value_name = "DOSAGE", value_parser=Dosage::from_str)]
pub dosage: Option<Dosage>,

/// New ingestion date (optional, e.g., "today 10:00")
Expand Down
68 changes: 43 additions & 25 deletions src/lib/dosage.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,63 @@
use clap::builder::TypedValueParser;
use delegate::delegate;
use derivative::Derivative;
use float_pretty_print::PrettyPrintFloat;
use measurements::{Mass, Measurement};
use miette::IntoDiagnostic;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use std::fmt;

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

/// Function will take human-readable input as representation of mass of
/// substance that was ingested (also referred as Dosage)
pub fn parse_dosage(input: &str) -> miette::Result<Dosage>
{
Mass::from_str(input).into_diagnostic()
impl std::str::FromStr for Dosage {
type Err = String;

/// Parse a &str into a valid `Dosage`.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mass = Mass::from_str(s).unwrap();
Ok(Dosage(mass))
}
}


impl fmt::Display for Dosage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let suggested_unit = self.0.get_appropriate_units();
let value_of_unit = format!("{:4.4}", PrettyPrintFloat(suggested_unit.1));
let unit = suggested_unit.0;
let formatted = format!("{} {}", value_of_unit.trim_start(), unit);
write!(f, "{}", formatted)
}
}

pub fn format_dosage(input: &Dosage) -> miette::Result<String> {
let suggested_unit = input.get_appropriate_units();
let float = format!("{:4.4}", PrettyPrintFloat(suggested_unit.1));
let unit = suggested_unit.0;
Ok(format!("{} {}", float.trim_start(), unit))
impl Dosage {
pub fn from_base_units(units: f64) -> Dosage {
Dosage(Mass::from_base_units(units))
}

delegate! {
to self.0 {
pub fn as_base_units(&self) -> f64;
}
}
}


#[cfg(test)]
mod tests
{
use super::*;
use std::str::FromStr;

#[test]
fn test_parse_dosage()
{
assert_eq!(parse_dosage("100g").unwrap(), Mass::from_grams(100f64));
assert_eq!(parse_dosage("100kg").unwrap(), Mass::from_kilograms(100f64));
assert_eq!(
parse_dosage("100ug").unwrap(),
Mass::from_micrograms(100f64)
)
fn test_parse_dosage() {
assert_eq!(Dosage::from_str("100g").unwrap(), Dosage(Mass::from_grams(100f64)));
assert_eq!(Dosage::from_str("100kg").unwrap(), Dosage(Mass::from_kilograms(100f64)));
assert_eq!(Dosage::from_str("100kg").unwrap(), Dosage(Mass::from_kilograms(100f64)));
}

#[test]
fn test_print_dosage() {
let dosage = Mass::from_grams(0.1);
assert_eq!(format_dosage(&dosage).unwrap(), "100 mg");
fn test_format_dosage() {
let dosage = Dosage(Mass::from_grams(0.1));
assert_eq!(dosage.to_string(), "100 mg");
}
}
41 changes: 41 additions & 0 deletions src/lib/route_of_administration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::str::FromStr;

#[derive(
clap::ValueEnum, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Eq, Hash,
Expand All @@ -25,3 +27,42 @@ pub enum RouteOfAdministrationClassification
Sublingual,
Transdermal,
}

impl fmt::Display for RouteOfAdministrationClassification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
RouteOfAdministrationClassification::Buccal => "Buccal",
RouteOfAdministrationClassification::Inhaled => "Inhaled",
RouteOfAdministrationClassification::Insufflated => "Insufflated",
RouteOfAdministrationClassification::Intramuscular => "Intramuscular",
RouteOfAdministrationClassification::Intravenous => "Intravenous",
RouteOfAdministrationClassification::Oral => "Oral",
RouteOfAdministrationClassification::Rectal => "Rectal",
RouteOfAdministrationClassification::Smoked => "Smoked",
RouteOfAdministrationClassification::Sublingual => "Sublingual",
RouteOfAdministrationClassification::Transdermal => "Transdermal",
};

write!(f, "{}", name)
}
}

impl FromStr for RouteOfAdministrationClassification {
type Err = ();

fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"buccal" => Ok(Self::Buccal),
"inhaled" => Ok(Self::Inhaled),
"insufflated" => Ok(Self::Insufflated),
"intramuscular" => Ok(Self::Intramuscular),
"intravenous" => Ok(Self::Intravenous),
"oral" => Ok(Self::Oral),
"rectal" => Ok(Self::Rectal),
"smoked" => Ok(Self::Smoked),
"sublingual" => Ok(Self::Sublingual),
"transdermal" => Ok(Self::Transdermal),
_ => Err(()),
}
}
}
38 changes: 20 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ extern crate chrono_english;
extern crate date_time_parser;

use atty::Stream;
use clap::command;
use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
use clap::command;
use lazy_static::lazy_static;
use lib::CommandHandler;
use lib::Context;
use lib::DATABASE_CONNECTION;
use lib::migrate_database;
use lib::setup_diagnostics;
use lib::setup_logger;
use lib::CommandHandler;
use lib::Context;
use lib::DATABASE_CONNECTION;
use rust_embed::Embed;
use sea_orm::prelude::async_trait::async_trait;
use std::string::ToString;
Expand Down Expand Up @@ -48,6 +48,21 @@ fn default_output_format() -> OutputFormat
}
}

#[derive(clap::ValueEnum, Clone, Debug)]
/// The output format specifies how application data is presented:
///
/// - `Pretty`: Used in interactive shells to display data in a visually appealing table format.
/// - `Json`: Used in non-interactive shells (e.g., scripts or when data is piped) to provide raw JSON for automated parsing.
pub enum OutputFormat
{
/// Pretty printed tables
Pretty,
/// JSON formatted output
Json,
// TODO: Application may support custom templates like liquidless or smth
}


#[derive(Parser)]
#[command(
version = env!("CARGO_PKG_VERSION"),
Expand All @@ -59,27 +74,14 @@ pub struct CLI
#[command(subcommand)]
pub command: ApplicationCommands,

/// Specifies the output format for the application's data.
///
/// - `Pretty`: When the shell is interactive, data will be presented in a human-readable table format.
/// - `Json`: When the shell is non-interactive (e.g., when piping data or running in a script),
/// data will be returned in raw JSON format for easier parsing.
/// Pretty-print or return raw version of data in JSON
#[arg(short, long = "format", value_enum, default_value_t = default_output_format())]
pub format: OutputFormat,

#[command(flatten)]
verbose: clap_verbosity_flag::Verbosity,
}

#[derive(clap::ValueEnum, Clone, Debug)]
pub enum OutputFormat
{
/// Pretty printed tables
Pretty,
/// JSON formatted output
Json,
// TODO: Application may support custom templates like liquidless or smth
}

fn default_complete_shell() -> clap_complete::Shell
{
Expand Down
4 changes: 1 addition & 3 deletions src/view_model/ingestion.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use crate::lib::dosage::format_dosage;
use crate::lib::dosage::Dosage;
use crate::lib::formatter::Formatter;
use crate::lib::orm::ingestion;
use crate::lib::route_of_administration::RouteOfAdministrationClassification;
use core::convert::From;
use measurements::Measurement;
use serde::Deserialize;
use serde::Serialize;
use std::fmt::Debug;
Expand Down Expand Up @@ -37,7 +35,7 @@ impl From<ingestion::Model> for ViewModel
.id(model.id)
.substance_name(model.substance_name)
.route(route_enum.to_string())
.dosage(format_dosage(&dosage).unwrap())
.dosage(dosage.to_string())
.ingested_at(model.ingested_at.to_string())
.build()
}
Expand Down

0 comments on commit 870e84b

Please sign in to comment.