Skip to content

Commit

Permalink
Added server capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorLop committed Jan 13, 2024
1 parent 90eb07c commit 1100939
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 62 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 39 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -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::InvestmentSnapshotResult> = 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()
);
}
38 changes: 22 additions & 16 deletions src/investment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::error;
use crate::types::PositiveFloat;
use fake::Dummy;
use serde;

#[derive(Debug, Clone, Dummy)]
pub struct Investment {
Expand Down Expand Up @@ -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;

Expand All @@ -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()
}
Expand All @@ -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();

Expand All @@ -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 {
Expand All @@ -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];
Expand All @@ -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();
Expand All @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions src/investment_config.rs
Original file line number Diff line number Diff line change
@@ -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,
}
71 changes: 25 additions & 46 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

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<InvestmentSnapshotResult> = 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()
);
}
52 changes: 52 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -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<investment_config::Configuration>,
) -> response::Json<investment::InvestmentResult> {
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::InvestmentSnapshotResult> = investment_snapshots
.iter()
.map(|snapshot| snapshot.result())
.collect();

let investment_result = investment::get_investment_result(investment_results).unwrap();

response::Json(investment_result)
}

0 comments on commit 1100939

Please sign in to comment.