From 1100939badc830ef5ed121c4c4ba9ff9e8309b4d Mon Sep 17 00:00:00 2001 From: hectorLop Date: Sat, 13 Jan 2024 11:06:15 +0100 Subject: [PATCH] Added server capabilities --- Cargo.toml | 2 ++ src/cli.rs | 39 ++++++++++++++++++++++ src/investment.rs | 38 ++++++++++++--------- src/investment_config.rs | 9 +++++ src/main.rs | 71 ++++++++++++++-------------------------- src/server.rs | 52 +++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 62 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/investment_config.rs create mode 100644 src/server.rs diff --git a/Cargo.toml b/Cargo.toml index faf40e5..e8e99ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ serde_json = "1.0.107" rand = "0.8.5" rstest = "0.18.2" thiserror = "1.0.56" +axum = "0.7.3" +tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..8886193 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,39 @@ +use crate::investment; +use crate::investment_config; +use crate::types; + +pub fn run_cli_simulation(config_file: String) { + let config: investment_config::Configuration = config::Config::builder() + .add_source(config::File::new(&config_file, config::FileFormat::Json)) + .build() + .expect("Error loading configuration file") + .try_deserialize() + .expect("Error deserializing the configuration"); + + let investment = investment::Investment::new( + types::PositiveFloat::try_from(config.deposit as f64).unwrap(), + config.years, + config + .annual_contributions + .to_annual_contributions(config.years), + config.interest_rates.to_interest_rates(config.years), + ); + + let investment_snapshots = investment.simulate().unwrap(); + let investment_results: Vec = investment_snapshots + .iter() + .map(|snapshot| snapshot.result()) + .collect(); + for (year, result) in investment_results.iter().enumerate() { + println!( + "Investment result year {}\n {}", + year + 1, + serde_json::to_string(result).unwrap() + ); + } + let investment_result = investment::get_investment_result(investment_results).unwrap(); + println!( + "Investment result\n {}", + serde_json::to_string(&investment_result).unwrap() + ); +} diff --git a/src/investment.rs b/src/investment.rs index 58c6053..ee38895 100644 --- a/src/investment.rs +++ b/src/investment.rs @@ -1,7 +1,6 @@ use crate::error; use crate::types::PositiveFloat; use fake::Dummy; -use serde; #[derive(Debug, Clone, Dummy)] pub struct Investment { @@ -145,7 +144,7 @@ pub struct InvestmentSnapshotResult { #[cfg(test)] mod investment_status_tests { use super::InvestmentSnapshot; - use crate::types::PositiveFloat; + use crate::types; use fake::{Fake, Faker}; use rand::Rng; @@ -158,7 +157,7 @@ mod investment_status_tests { Self(rng.gen_range(-2.0..2.0)) } } - impl quickcheck::Arbitrary for PositiveFloat { + impl quickcheck::Arbitrary for types::PositiveFloat { fn arbitrary(_g: &mut quickcheck::Gen) -> Self { Faker.fake() } @@ -175,7 +174,10 @@ mod investment_status_tests { } #[quickcheck_macros::quickcheck] - fn final_balance_properties(balance: PositiveFloat, return_rate: ReturnRateFixture) -> bool { + fn final_balance_properties( + balance: types::PositiveFloat, + return_rate: ReturnRateFixture, + ) -> bool { let status = InvestmentSnapshot::new(0, balance, balance.0, return_rate.0).unwrap(); let result = status.result(); @@ -191,7 +193,7 @@ mod investment_status_tests { #[quickcheck_macros::quickcheck] fn investment_snapshot_result_consistency( year: usize, - net_contribution: PositiveFloat, + net_contribution: types::PositiveFloat, initial_balance: FloatFixture, return_rate: FloatFixture, ) -> bool { @@ -208,26 +210,28 @@ mod investment_status_tests { #[test] fn test_investment_snapshot_with_nan() { - let status = InvestmentSnapshot::new(2022, PositiveFloat(1000.0), std::f64::NAN, 0.12); + let status = + InvestmentSnapshot::new(2022, types::PositiveFloat(1000.0), std::f64::NAN, 0.12); assert!(status.is_err()); - let status = InvestmentSnapshot::new(2022, PositiveFloat(1000.0), 10000.0, std::f64::NAN); + let status = + InvestmentSnapshot::new(2022, types::PositiveFloat(1000.0), 10000.0, std::f64::NAN); assert!(status.is_err()); } } #[cfg(test)] mod test_investment { - use super::{Investment, PositiveFloat}; - use crate::{AnnualContribution, Interest}; + use super::Investment; + use crate::types; use assert_float_eq::{afe_is_f64_near, afe_near_error_msg, assert_f64_near}; #[test] fn test_investment_simulation() { let investment = Investment::new( - PositiveFloat::try_from(10000.0).unwrap(), + types::PositiveFloat::try_from(10000.0).unwrap(), 3, - AnnualContribution::Single(PositiveFloat(0.0)).to_annual_contributions(3), - Interest::Single(0.05).to_interest_rates(3), + types::AnnualContribution::Single(types::PositiveFloat(0.0)).to_annual_contributions(3), + types::Interest::Single(0.05).to_interest_rates(3), ); let investment_results = investment.simulate().unwrap(); let expected: [f64; 3] = [10500.0, 11025.0, 11576.25]; @@ -240,9 +244,10 @@ mod test_investment { #[test] fn test_investment_simulation_with_annual_contribution() { let investment = Investment::new( - PositiveFloat::try_from(10000.0).unwrap(), + types::PositiveFloat::try_from(10000.0).unwrap(), 3, - AnnualContribution::Single(PositiveFloat(3600.0)).to_annual_contributions(3), + types::AnnualContribution::Single(types::PositiveFloat(3600.0)) + .to_annual_contributions(3), vec![0.05, 0.05, 0.05], ); let investment_results = investment.simulate().unwrap(); @@ -256,9 +261,10 @@ mod test_investment { #[test] fn test_investment_simulation_with_annual_contribution_and_negative_rates() { let investment = Investment::new( - PositiveFloat::try_from(10000.0).unwrap(), + types::PositiveFloat::try_from(10000.0).unwrap(), 3, - AnnualContribution::Single(PositiveFloat(3600.0)).to_annual_contributions(3), + types::AnnualContribution::Single(types::PositiveFloat(3600.0)) + .to_annual_contributions(3), vec![0.05, -0.05, -0.05], ); let investment_results = investment.simulate().unwrap(); diff --git a/src/investment_config.rs b/src/investment_config.rs new file mode 100644 index 0000000..e4c76b7 --- /dev/null +++ b/src/investment_config.rs @@ -0,0 +1,9 @@ +use crate::types; + +#[derive(serde::Deserialize)] +pub struct Configuration { + pub deposit: usize, + pub interest_rates: types::Interest, + pub years: usize, + pub annual_contributions: types::AnnualContribution, +} diff --git a/src/main.rs b/src/main.rs index 294be49..5650674 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,61 +1,40 @@ -use clap::{arg, command, Parser}; -use config::{Config, File, FileFormat}; +use clap::{arg, command, Parser, ValueEnum}; +mod cli; mod distributions; mod error; mod investment; +mod investment_config; +mod server; mod types; -use investment::{Investment, InvestmentSnapshotResult}; -use types::{AnnualContribution, Interest, PositiveFloat}; +#[derive(Clone, ValueEnum, Debug, PartialEq)] +enum AppMode { + Server, + Cli, +} -#[derive(Parser)] +#[derive(Parser, Debug)] #[command(about = "Simulate index funds behaviour!")] struct Args { - #[arg(short, long, help = "Configuration file")] - config_file: String, -} - -#[derive(serde::Deserialize)] -struct Configuration { - deposit: usize, - interest_rates: Interest, - years: usize, - annual_contributions: AnnualContribution, + #[arg(short, long, help = "Application mode")] + mode: AppMode, + #[arg(short, long, help = "Configuration file", required = false)] + config_file: Option, } -fn main() { +#[tokio::main] +async fn main() { let args = Args::parse(); - let config: Configuration = Config::builder() - .add_source(File::new(&args.config_file, FileFormat::Json)) - .build() - .expect("Error loading configuration file") - .try_deserialize() - .expect("Error deserializing the configuration"); + if args.mode == AppMode::Cli && args.config_file.is_none() { + eprintln!("Error: `config_file` is required when `mode` is set to `Cli`"); + } - let investment = Investment::new( - PositiveFloat::try_from(config.deposit as f64).unwrap(), - config.years, - config - .annual_contributions - .to_annual_contributions(config.years), - config.interest_rates.to_interest_rates(config.years), - ); - let investment_snapshots = investment.simulate().unwrap(); - let investment_results: Vec = investment_snapshots - .iter() - .map(|snapshot| snapshot.result()) - .collect(); - for (year, result) in investment_results.iter().enumerate() { - println!( - "Investment result year {}\n {}", - year + 1, - serde_json::to_string(result).unwrap() - ); + match args.mode { + AppMode::Cli => cli::run_cli_simulation(args.config_file.unwrap()), + AppMode::Server => { + let server = server::Server::new("0.0.0.0".to_string(), "3000".to_string()); + server.serve().await; + } } - let investment_result = investment::get_investment_result(investment_results).unwrap(); - println!( - "Investment result\n {}", - serde_json::to_string(&investment_result).unwrap() - ); } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..91e6ae0 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,52 @@ +use axum::extract; +use axum::response; +use axum::routing::post; +use axum::Router; + +use crate::investment; +use crate::investment_config; +use crate::types; + +pub struct Server { + host: String, + port: String, +} + +impl Server { + pub fn new(host: String, port: String) -> Self { + Self { host, port } + } + + pub async fn serve(&self) { + let app = Router::new().route("/simulate", post(get_investment_result)); + let listener = tokio::net::TcpListener::bind(format!("{}:{}", self.host, self.port)) + .await + .unwrap(); + println!("Listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app).await.unwrap(); + println!("Exiting"); + } +} + +async fn get_investment_result( + extract::Json(config): extract::Json, +) -> response::Json { + let investment = investment::Investment::new( + types::PositiveFloat::try_from(config.deposit as f64).unwrap(), + config.years, + config + .annual_contributions + .to_annual_contributions(config.years), + config.interest_rates.to_interest_rates(config.years), + ); + + let investment_snapshots = investment.simulate().unwrap(); + let investment_results: Vec = investment_snapshots + .iter() + .map(|snapshot| snapshot.result()) + .collect(); + + let investment_result = investment::get_investment_result(investment_results).unwrap(); + + response::Json(investment_result) +}