#![allow(dead_code, unused)]
use typed_use_cases::{Actor, Entity, UseCase};
#[derive(Debug, Clone, Actor)]
struct Anonymous;
#[derive(Debug, Clone, Actor)]
struct Registered {
user_id: u64,
}
#[derive(Debug, Clone, Actor)]
struct Authenticated {
user_id: u64,
}
#[derive(Debug, Clone)]
struct Product {
id: u64,
name: String,
stock: u32,
price: f64,
}
#[derive(Debug, Clone, Entity)]
struct Catalog {
products: Vec<Product>,
}
impl Entity for Product {}
#[derive(Debug, Clone, Entity)]
struct Cart {
owner: Authenticated,
items: Vec<u64>,
}
#[derive(Debug, Clone, Entity)]
struct Order {
owner: Authenticated,
items: Vec<u64>,
shipping_address: String,
status: String,
}
trait ProductRepository {
fn find_by_id(&self, id: u64) -> Option<Product>;
fn find_all(&self) -> Vec<Product>;
}
trait CartRepository {
fn save(&self, cart: &Cart) -> Result<(), String>;
fn find_by_user(&self, user_id: u64) -> Option<Cart>;
}
trait InventoryService {
fn check_availability(&self, product_id: u64, quantity: u32) -> bool;
fn reserve(&self, product_id: u64, quantity: u32) -> Result<(), String>;
}
trait PaymentService {
fn process_payment(&self, amount: f64, user_id: u64) -> Result<String, String>;
}
struct InMemoryProductRepo;
struct InMemoryCartRepo;
struct MockInventoryService;
struct MockPaymentService;
impl ProductRepository for InMemoryProductRepo {
fn find_by_id(&self, id: u64) -> Option<Product> {
Some(Product {
id,
name: format!("Product {}", id),
stock: 10,
price: 99.99,
})
}
fn find_all(&self) -> Vec<Product> {
vec![
Product { id: 1, name: "Laptop".to_string(), stock: 10, price: 999.99 },
Product { id: 2, name: "Mouse".to_string(), stock: 50, price: 29.99 },
]
}
}
impl CartRepository for InMemoryCartRepo {
fn save(&self, _cart: &Cart) -> Result<(), String> {
Ok(())
}
fn find_by_user(&self, user_id: u64) -> Option<Cart> {
Some(Cart {
owner: Authenticated { user_id },
items: vec![],
})
}
}
impl InventoryService for MockInventoryService {
fn check_availability(&self, _product_id: u64, _quantity: u32) -> bool {
true
}
fn reserve(&self, _product_id: u64, _quantity: u32) -> Result<(), String> {
Ok(())
}
}
impl PaymentService for MockPaymentService {
fn process_payment(&self, _amount: f64, _user_id: u64) -> Result<String, String> {
Ok("payment_id_123".to_string())
}
}
trait BrowseCatalog: UseCase<
Anonymous,
Catalog,
Input = (),
Output = Catalog,
Dependencies = (),
> {}
trait AddItemToCart: UseCase<
Authenticated,
Cart,
Input = Product,
Output = Result<Cart, String>,
Dependencies = (Box<dyn InventoryService>, Box<dyn CartRepository>),
> {}
trait Checkout: UseCase<
Authenticated,
Order,
Input = String, Output = Result<Order, String>,
Dependencies = (Box<dyn PaymentService>, Box<dyn CartRepository>),
> {}
struct System;
impl UseCase<Anonymous, Catalog> for System {
const NAME: &'static str = "Browse catalog";
const DESCRIPTION: &'static str = "An anonymous user can browse the product catalog";
type Input = ();
type Output = Catalog;
type Dependencies = ();
fn satisfy(
_actor: Anonymous,
entity: Catalog,
_input: Self::Input,
_deps: Self::Dependencies,
) -> Self::Output {
entity
}
}
impl BrowseCatalog for System {}
impl UseCase<Authenticated, Cart> for System {
const NAME: &'static str = "Add item to cart";
const DESCRIPTION: &'static str = "An authenticated user can add a product to their cart";
type Input = Product;
type Output = Result<Cart, String>;
type Dependencies = (Box<dyn InventoryService>, Box<dyn CartRepository>);
fn satisfy(
actor: Authenticated,
mut entity: Cart,
input: Self::Input,
deps: Self::Dependencies,
) -> Self::Output {
let (inventory_service, cart_repo) = deps;
if entity.owner.user_id != actor.user_id {
return Err("Cart does not belong to user".to_string());
}
if !inventory_service.check_availability(input.id, 1) {
return Err("Product not available".to_string());
}
inventory_service.reserve(input.id, 1)
.map_err(|e| format!("Failed to reserve item: {}", e))?;
entity.items.push(input.id);
cart_repo.save(&entity)
.map_err(|e| format!("Failed to save cart: {}", e))?;
Ok(entity)
}
}
impl AddItemToCart for System {}
impl UseCase<Authenticated, Order> for System {
const NAME: &'static str = "Checkout";
const DESCRIPTION: &'static str = "An authenticated user can checkout with at least one item";
type Input = String; type Output = Result<Order, String>;
type Dependencies = (Box<dyn PaymentService>, Box<dyn CartRepository>);
fn satisfy(
actor: Authenticated,
mut entity: Order,
input: Self::Input,
deps: Self::Dependencies,
) -> Self::Output {
let (payment_service, _cart_repo) = deps;
if entity.owner.user_id != actor.user_id {
return Err("Order does not belong to user".to_string());
}
if entity.items.is_empty() {
return Err("Cannot checkout with empty cart".to_string());
}
let total = entity.items.len() as f64 * 100.0;
let payment_id = payment_service.process_payment(total, actor.user_id)
.map_err(|e| format!("Payment failed: {}", e))?;
entity.shipping_address = input;
entity.status = format!("paid:{}", payment_id);
Ok(entity)
}
}
impl Checkout for System {}
typed_use_cases::implement_all_use_cases!(System: [
BrowseCatalog,
AddItemToCart,
Checkout,
]);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_is_zero_sized() {
assert_eq!(std::mem::size_of::<System>(), 0);
}
#[test]
fn print_use_case_metadata() {
println!("\n=== Use Cases ===");
println!("1. {} - {}",
<System as UseCase<Anonymous, Catalog>>::NAME,
<System as UseCase<Anonymous, Catalog>>::DESCRIPTION
);
println!("2. {} - {}",
<System as UseCase<Authenticated, Cart>>::NAME,
<System as UseCase<Authenticated, Cart>>::DESCRIPTION
);
println!("3. {} - {}",
<System as UseCase<Authenticated, Order>>::NAME,
<System as UseCase<Authenticated, Order>>::DESCRIPTION
);
}
#[test]
fn browse_catalog_works() {
let actor = Anonymous;
let catalog = Catalog {
products: vec![
Product {
id: 1,
name: "Laptop".to_string(),
stock: 10,
price: 999.99,
},
Product {
id: 2,
name: "Mouse".to_string(),
stock: 50,
price: 29.99,
},
],
};
let result = <System as UseCase<Anonymous, Catalog>>::satisfy(
actor,
catalog.clone(),
(),
(),
);
assert_eq!(result.products.len(), 2);
}
#[test]
fn add_item_to_cart_works() {
let actor = Authenticated { user_id: 1 };
let cart = Cart {
owner: actor.clone(),
items: vec![],
};
let product = Product {
id: 1,
name: "Laptop".to_string(),
stock: 10,
price: 999.99,
};
let inventory = Box::new(MockInventoryService) as Box<dyn InventoryService>;
let cart_repo = Box::new(InMemoryCartRepo) as Box<dyn CartRepository>;
let result = <System as UseCase<Authenticated, Cart>>::satisfy(
actor,
cart,
product,
(inventory, cart_repo),
);
assert!(result.is_ok());
let cart = result.unwrap();
assert_eq!(cart.items.len(), 1);
assert_eq!(cart.items[0], 1);
}
#[test]
fn checkout_works() {
let actor = Authenticated { user_id: 1 };
let order = Order {
owner: actor.clone(),
items: vec![1, 2],
shipping_address: String::new(),
status: String::new(),
};
let payment = Box::new(MockPaymentService) as Box<dyn PaymentService>;
let cart_repo = Box::new(InMemoryCartRepo) as Box<dyn CartRepository>;
let result = <System as UseCase<Authenticated, Order>>::satisfy(
actor,
order,
"123 Main St".to_string(),
(payment, cart_repo),
);
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.items.len(), 2);
assert_eq!(order.shipping_address, "123 Main St");
assert!(order.status.starts_with("paid:"));
}
#[test]
fn checkout_fails_with_empty_cart() {
let actor = Authenticated { user_id: 1 };
let order = Order {
owner: actor.clone(),
items: vec![],
shipping_address: String::new(),
status: String::new(),
};
let payment = Box::new(MockPaymentService) as Box<dyn PaymentService>;
let cart_repo = Box::new(InMemoryCartRepo) as Box<dyn CartRepository>;
let result = <System as UseCase<Authenticated, Order>>::satisfy(
actor,
order,
"123 Main St".to_string(),
(payment, cart_repo),
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Cannot checkout with empty cart");
}
#[test]
fn add_item_fails_for_wrong_user() {
let actor = Authenticated { user_id: 1 };
let wrong_owner = Authenticated { user_id: 2 };
let cart = Cart {
owner: wrong_owner,
items: vec![],
};
let product = Product {
id: 1,
name: "Laptop".to_string(),
stock: 10,
price: 999.99,
};
let inventory = Box::new(MockInventoryService) as Box<dyn InventoryService>;
let cart_repo = Box::new(InMemoryCartRepo) as Box<dyn CartRepository>;
let result = <System as UseCase<Authenticated, Cart>>::satisfy(
actor,
cart,
product,
(inventory, cart_repo),
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Cart does not belong to user");
}
}
fn main() {
println!("E-commerce example - see tests for usage");
println!("\nUse Cases:");
println!("1. {} - {}",
<System as UseCase<Anonymous, Catalog>>::NAME,
<System as UseCase<Anonymous, Catalog>>::DESCRIPTION
);
println!("2. {} - {}",
<System as UseCase<Authenticated, Cart>>::NAME,
<System as UseCase<Authenticated, Cart>>::DESCRIPTION
);
println!("3. {} - {}",
<System as UseCase<Authenticated, Order>>::NAME,
<System as UseCase<Authenticated, Order>>::DESCRIPTION
);
}