This commit is contained in:
kristof 2025-04-04 12:15:30 +02:00
parent 04233e6f1a
commit be3ad84c7d
12 changed files with 406 additions and 21 deletions

7
herodb/Cargo.lock generated
View File

@ -658,6 +658,7 @@ dependencies = [
"bincode",
"brotli",
"chrono",
"lazy_static",
"paste",
"poem",
"poem-openapi",
@ -831,6 +832,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"

View File

@ -21,6 +21,7 @@ poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
tokio = { version = "1", features = ["full"] }
rhai = "1.15.1"
paste = "1.0"
lazy_static = "1.4.0"
[[example]]
name = "rhai_demo"

View File

@ -1,10 +1,11 @@
use chrono::{DateTime, Utc, Duration};
use herodb::db::{DB, DBBuilder};
use chrono::{Utc, Duration};
use herodb::db::DBBuilder;
use herodb::models::biz::{
Currency, CurrencyBuilder,
Product, ProductBuilder, ProductComponent, ProductComponentBuilder,
Product, ProductBuilder, ProductComponentBuilder,
ProductType, ProductStatus,
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus
Sale, SaleBuilder, SaleItemBuilder, SaleStatus,
ExchangeRate, ExchangeRateBuilder, EXCHANGE_RATE_SERVICE
};
use std::path::PathBuf;
use std::fs;
@ -26,6 +27,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.register_model::<Product>()
.register_model::<Currency>()
.register_model::<Sale>()
.register_model::<ExchangeRate>()
.build()?;
println!("\n1. Creating Products with Builder Pattern");
@ -39,14 +41,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Insert the currency
db.insert_currency(&usd)?;
println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code);
println!("Currency created: ${} {}", usd.amount, usd.currency_code);
// Create product components using the builder
// Create product components using the builder with energy usage and cost
let component1 = ProductComponentBuilder::new()
.id(101)
.name("Basic Support")
.description("24/7 email support")
.quantity(1)
.energy_usage(5.0) // 5 watts
.cost(CurrencyBuilder::new()
.amount(5.0)
.currency_code("USD")
.build()?)
.build()?;
let component2 = ProductComponentBuilder::new()
@ -54,6 +61,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.name("Premium Support")
.description("24/7 phone and email support")
.quantity(1)
.energy_usage(10.0) // 10 watts
.cost(CurrencyBuilder::new()
.amount(15.0)
.currency_code("USD")
.build()?)
.build()?;
// Create products using the builder
@ -67,7 +79,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()?)
.type_(ProductType::Service)
.category("Subscription")
.status(ProductStatus::Available)
.status(ProductStatus::Active)
.max_amount(1000)
.validity_days(30)
.add_component(component1)
@ -83,7 +95,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.build()?)
.type_(ProductType::Service)
.category("Subscription")
.status(ProductStatus::Available)
.status(ProductStatus::Active)
.max_amount(500)
.validity_days(30)
.add_component(component2)
@ -93,18 +105,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
db.insert_product(&product1)?;
db.insert_product(&product2)?;
println!("Product created: {} (${:.2})", product1.name, product1.price.amount);
println!("Product created: {} (${:.2})", product2.name, product2.price.amount);
println!("Product created: {} (${}) USD", product1.name, product1.price.amount);
println!("Product created: {} (${}) USD", product2.name, product2.price.amount);
println!("\n2. Retrieving Products");
println!("--------------------");
// Retrieve products using model-specific methods
let retrieved_product1 = db.get_product(1)?;
println!("Retrieved: {} (${:.2})", retrieved_product1.name, retrieved_product1.price.amount);
println!("Retrieved: {} (${}) USD", retrieved_product1.name, retrieved_product1.price.amount);
println!("Components:");
for component in &retrieved_product1.components {
println!(" - {} ({})", component.name, component.description);
println!(" - {} ({}, Energy: {}W, Cost: ${} USD)",
component.name,
component.description,
component.energy_usage,
component.cost.amount
);
}
// Calculate total energy usage
let total_energy = retrieved_product1.total_energy_usage();
println!("Total energy usage: {}W", total_energy);
// Calculate components cost
if let Some(components_cost) = retrieved_product1.components_cost_in_usd() {
println!("Total components cost: ${} USD", components_cost.amount);
}
println!("\n3. Listing All Products");
@ -114,7 +140,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let all_products = db.list_products()?;
println!("Found {} products:", all_products.len());
for product in all_products {
println!(" - {} (${:.2}, {})",
println!(" - {} (${} USD, {})",
product.name,
product.price.amount,
if product.is_purchasable() { "Available" } else { "Unavailable" }
@ -152,7 +178,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Insert the sale using model-specific methods
db.insert_sale(&sale)?;
println!("Sale created: #{} for {} (${:.2})",
println!("Sale created: #{} for {} (${} USD)",
sale.id,
sale.buyer_name,
sale.total_amount.amount
@ -171,7 +197,77 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Updated sale status to {:?}", retrieved_sale.status);
println!("\n6. Deleting Objects");
println!("\n6. Working with Exchange Rates");
println!("----------------------------");
// Create and set exchange rates using the builder
let eur_rate = ExchangeRateBuilder::new()
.base_currency("EUR")
.target_currency("USD")
.rate(1.18)
.build()?;
let gbp_rate = ExchangeRateBuilder::new()
.base_currency("GBP")
.target_currency("USD")
.rate(1.38)
.build()?;
// Insert exchange rates into the database
db.insert_exchange_rate(&eur_rate)?;
db.insert_exchange_rate(&gbp_rate)?;
// Set the exchange rates in the service
EXCHANGE_RATE_SERVICE.set_rate(eur_rate.clone());
EXCHANGE_RATE_SERVICE.set_rate(gbp_rate.clone());
println!("Exchange rates set:");
println!(" - 1 EUR = {} USD", eur_rate.rate);
println!(" - 1 GBP = {} USD", gbp_rate.rate);
// Create currencies in different denominations
let eur_price = CurrencyBuilder::new()
.amount(100.0)
.currency_code("EUR")
.build()?;
let gbp_price = CurrencyBuilder::new()
.amount(85.0)
.currency_code("GBP")
.build()?;
// Convert to USD
if let Some(eur_in_usd) = eur_price.to_usd() {
println!("{} EUR = {} USD", eur_price.amount, eur_in_usd.amount);
} else {
println!("Could not convert EUR to USD");
}
if let Some(gbp_in_usd) = gbp_price.to_usd() {
println!("{} GBP = {} USD", gbp_price.amount, gbp_in_usd.amount);
} else {
println!("Could not convert GBP to USD");
}
// Convert between currencies
if let Some(eur_in_gbp) = eur_price.to_currency("GBP") {
println!("{} EUR = {} GBP", eur_price.amount, eur_in_gbp.amount);
} else {
println!("Could not convert EUR to GBP");
}
// Test product price conversion
let retrieved_product2 = db.get_product(2)?;
if let Some(price_in_eur) = retrieved_product2.cost_in_currency("EUR") {
println!("Product '{}' price: ${} USD = {} EUR",
retrieved_product2.name,
retrieved_product2.price.amount,
price_in_eur.amount
);
}
println!("\n7. Deleting Objects");
println!("------------------");
// Delete a product

View File

@ -1,7 +1,7 @@
use crate::db::db::DB;
use crate::db::base::{SledDBResult, SledModel};
use crate::impl_model_methods;
use crate::models::biz::{Product, Sale, Currency};
use crate::models::biz::{Product, Sale, Currency, ExchangeRate};
// Implement model-specific methods for Product
impl_model_methods!(Product, product, products);
@ -10,4 +10,7 @@ impl_model_methods!(Product, product, products);
impl_model_methods!(Sale, sale, sales);
// Implement model-specific methods for Currency
impl_model_methods!(Currency, currency, currencies);
impl_model_methods!(Currency, currency, currencies);
// Implement model-specific methods for ExchangeRate
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);

View File

@ -1,6 +1,7 @@
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
/// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -17,6 +18,26 @@ impl Currency {
currency_code,
}
}
/// Convert the currency to USD
pub fn to_usd(&self) -> Option<Currency> {
if self.currency_code == "USD" {
return Some(self.clone());
}
EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, "USD")
.map(|amount| Currency::new(amount, "USD".to_string()))
}
/// Convert the currency to another currency
pub fn to_currency(&self, target_currency: &str) -> Option<Currency> {
if self.currency_code == target_currency {
return Some(self.clone());
}
EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, target_currency)
.map(|amount| Currency::new(amount, target_currency.to_string()))
}
}
/// Builder for Currency

View File

@ -0,0 +1,167 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::base::{SledModel, Storable};
/// ExchangeRate represents an exchange rate between two currencies
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeRate {
pub base_currency: String,
pub target_currency: String,
pub rate: f64,
pub timestamp: DateTime<Utc>,
}
impl ExchangeRate {
/// Create a new exchange rate
pub fn new(base_currency: String, target_currency: String, rate: f64) -> Self {
Self {
base_currency,
target_currency,
rate,
timestamp: Utc::now(),
}
}
}
/// Builder for ExchangeRate
pub struct ExchangeRateBuilder {
base_currency: Option<String>,
target_currency: Option<String>,
rate: Option<f64>,
timestamp: Option<DateTime<Utc>>,
}
impl ExchangeRateBuilder {
/// Create a new ExchangeRateBuilder with all fields set to None
pub fn new() -> Self {
Self {
base_currency: None,
target_currency: None,
rate: None,
timestamp: None,
}
}
/// Set the base currency
pub fn base_currency<S: Into<String>>(mut self, base_currency: S) -> Self {
self.base_currency = Some(base_currency.into());
self
}
/// Set the target currency
pub fn target_currency<S: Into<String>>(mut self, target_currency: S) -> Self {
self.target_currency = Some(target_currency.into());
self
}
/// Set the rate
pub fn rate(mut self, rate: f64) -> Self {
self.rate = Some(rate);
self
}
/// Set the timestamp
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
/// Build the ExchangeRate object
pub fn build(self) -> Result<ExchangeRate, &'static str> {
let now = Utc::now();
Ok(ExchangeRate {
base_currency: self.base_currency.ok_or("base_currency is required")?,
target_currency: self.target_currency.ok_or("target_currency is required")?,
rate: self.rate.ok_or("rate is required")?,
timestamp: self.timestamp.unwrap_or(now),
})
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for ExchangeRate {}
// Implement SledModel trait
impl SledModel for ExchangeRate {
fn get_id(&self) -> String {
format!("{}_{}", self.base_currency, self.target_currency)
}
fn db_prefix() -> &'static str {
"exchange_rate"
}
}
/// ExchangeRateService provides methods to get and set exchange rates
#[derive(Clone)]
pub struct ExchangeRateService {
rates: Arc<Mutex<HashMap<String, ExchangeRate>>>,
}
impl ExchangeRateService {
/// Create a new exchange rate service
pub fn new() -> Self {
Self {
rates: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Set an exchange rate
pub fn set_rate(&self, exchange_rate: ExchangeRate) {
let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency);
let mut rates = self.rates.lock().unwrap();
rates.insert(key, exchange_rate);
}
/// Get an exchange rate
pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option<ExchangeRate> {
let key = format!("{}_{}", base_currency, target_currency);
let rates = self.rates.lock().unwrap();
rates.get(&key).cloned()
}
/// Convert an amount from one currency to another
pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option<f64> {
// If the currencies are the same, return the amount
if from_currency == to_currency {
return Some(amount);
}
// Try to get the direct exchange rate
if let Some(rate) = self.get_rate(from_currency, to_currency) {
return Some(amount * rate.rate);
}
// Try to get the inverse exchange rate
if let Some(rate) = self.get_rate(to_currency, from_currency) {
return Some(amount / rate.rate);
}
// Try to convert via USD
if from_currency != "USD" && to_currency != "USD" {
if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") {
return self.convert(from_to_usd, "USD", to_currency);
}
}
None
}
}
// Create a global instance of the exchange rate service
lazy_static::lazy_static! {
pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = {
let service = ExchangeRateService::new();
// Set some default exchange rates
service.set_rate(ExchangeRate::new("USD".to_string(), "EUR".to_string(), 0.85));
service.set_rate(ExchangeRate::new("USD".to_string(), "GBP".to_string(), 0.75));
service.set_rate(ExchangeRate::new("USD".to_string(), "JPY".to_string(), 110.0));
service.set_rate(ExchangeRate::new("USD".to_string(), "CAD".to_string(), 1.25));
service.set_rate(ExchangeRate::new("USD".to_string(), "AUD".to_string(), 1.35));
service
};
}

View File

@ -1,13 +1,16 @@
pub mod currency;
pub mod product;
pub mod sale;
pub mod exchange_rate;
// Re-export all model types for convenience
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::{Sale, SaleItem, SaleStatus};
pub use currency::Currency;
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
pub use currency::CurrencyBuilder;
pub use currency::CurrencyBuilder;
pub use exchange_rate::ExchangeRateBuilder;

View File

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc, Duration};
use serde::{Deserialize, Serialize};
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
/// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -13,6 +13,10 @@ pub enum ProductType {
/// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductStatus {
Active,
Error,
EndOfLife,
Paused,
Available,
Unavailable,
}
@ -26,6 +30,8 @@ pub struct ProductComponent {
pub quantity: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub energy_usage: f64, // Energy usage in watts
pub cost: Currency, // Cost of the component
}
impl ProductComponent {
@ -39,8 +45,20 @@ impl ProductComponent {
quantity,
created_at: now,
updated_at: now,
energy_usage: 0.0,
cost: Currency::new(0.0, "USD".to_string()),
}
}
/// Get the total energy usage for this component (energy_usage * quantity)
pub fn total_energy_usage(&self) -> f64 {
self.energy_usage * self.quantity as f64
}
/// Get the total cost for this component (cost * quantity)
pub fn total_cost(&self) -> Currency {
Currency::new(self.cost.amount * self.quantity as f64, self.cost.currency_code.clone())
}
}
/// Builder for ProductComponent
@ -51,6 +69,8 @@ pub struct ProductComponentBuilder {
quantity: Option<i32>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
energy_usage: Option<f64>,
cost: Option<Currency>,
}
impl ProductComponentBuilder {
@ -63,6 +83,8 @@ impl ProductComponentBuilder {
quantity: None,
created_at: None,
updated_at: None,
energy_usage: None,
cost: None,
}
}
@ -102,6 +124,18 @@ impl ProductComponentBuilder {
self
}
/// Set the energy usage in watts
pub fn energy_usage(mut self, energy_usage: f64) -> Self {
self.energy_usage = Some(energy_usage);
self
}
/// Set the cost
pub fn cost(mut self, cost: Currency) -> Self {
self.cost = Some(cost);
self
}
/// Build the ProductComponent object
pub fn build(self) -> Result<ProductComponent, &'static str> {
let now = Utc::now();
@ -112,6 +146,8 @@ impl ProductComponentBuilder {
quantity: self.quantity.ok_or("quantity is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
energy_usage: self.energy_usage.unwrap_or(0.0),
cost: self.cost.unwrap_or_else(|| Currency::new(0.0, "USD".to_string())),
})
}
}
@ -188,13 +224,60 @@ impl Product {
/// Check if the product is available for purchase
pub fn is_purchasable(&self) -> bool {
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
(self.status == ProductStatus::Available || self.status == ProductStatus::Active)
&& Utc::now() <= self.purchase_till
}
/// Check if the product is still active (for services)
pub fn is_active(&self) -> bool {
Utc::now() <= self.active_till
}
/// Calculate the total cost in the specified currency
pub fn cost_in_currency(&self, currency_code: &str) -> Option<Currency> {
// If the price is already in the requested currency, return it
if self.price.currency_code == currency_code {
return Some(self.price.clone());
}
// Convert the price to the requested currency
self.price.to_currency(currency_code)
}
/// Calculate the total cost in USD
pub fn cost_in_usd(&self) -> Option<Currency> {
self.cost_in_currency("USD")
}
/// Calculate the total energy usage of the product (sum of all components)
pub fn total_energy_usage(&self) -> f64 {
self.components.iter().map(|c| c.total_energy_usage()).sum()
}
/// Calculate the total cost of all components
pub fn components_cost(&self, currency_code: &str) -> Option<Currency> {
if self.components.is_empty() {
return Some(Currency::new(0.0, currency_code.to_string()));
}
// Sum up the costs of all components, converting to the requested currency
let mut total = 0.0;
for component in &self.components {
let component_cost = component.total_cost();
if let Some(converted_cost) = component_cost.to_currency(currency_code) {
total += converted_cost.amount;
} else {
return None; // Conversion failed
}
}
Some(Currency::new(total, currency_code.to_string()))
}
/// Calculate the total cost of all components in USD
pub fn components_cost_in_usd(&self) -> Option<Currency> {
self.components_cost("USD")
}
}
/// Builder for Product
@ -355,4 +438,4 @@ impl SledModel for Product {
}
// Import Currency from the currency module
use crate::models::biz::Currency;
use crate::models::biz::Currency;

View File

@ -0,0 +1,4 @@
segment_size: 524288
use_compression: false
version: 0.34
v

Binary file not shown.

Binary file not shown.

Binary file not shown.