Skip to main content

domain_model_optics/
domain_model_optics.rs

1//! Domain Model with Optics Example
2//!
3//! Demonstrates Lens (product type field access), ComposedLens (deep access via .then()),
4//! Prism (sum type variant focus), and transform (reusable update functions).
5//!
6//! Run with: cargo run -p karpal-std --example domain_model_optics
7
8use karpal_std::prelude::*;
9
10// --- Domain model ---
11
12#[derive(Debug, Clone, PartialEq)]
13struct Order {
14    id: u32,
15    customer: Customer,
16    items: Vec<Item>,
17    payment: Payment,
18}
19
20#[derive(Debug, Clone, PartialEq)]
21struct Customer {
22    name: String,
23    address: Address,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27struct Address {
28    street: String,
29    city: String,
30    zip: String,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34struct Item {
35    name: String,
36    price_cents: i64,
37    quantity: u32,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41enum Payment {
42    CreditCard {
43        last4: String,
44        exp: String,
45    },
46    BankTransfer {
47        iban: String,
48    },
49    Wallet {
50        provider: String,
51        balance_cents: i64,
52    },
53}
54
55// --- Lenses ---
56
57fn customer_lens() -> SimpleLens<Order, Customer> {
58    Lens::new(
59        |o: &Order| o.customer.clone(),
60        |o, customer| Order { customer, ..o },
61    )
62}
63
64fn address_lens() -> SimpleLens<Customer, Address> {
65    Lens::new(
66        |c: &Customer| c.address.clone(),
67        |c, address| Customer { address, ..c },
68    )
69}
70
71fn city_lens() -> SimpleLens<Address, String> {
72    Lens::new(
73        |a: &Address| a.city.clone(),
74        |a, city| Address { city, ..a },
75    )
76}
77
78fn zip_lens() -> SimpleLens<Address, String> {
79    Lens::new(|a: &Address| a.zip.clone(), |a, zip| Address { zip, ..a })
80}
81
82fn name_lens() -> SimpleLens<Customer, String> {
83    Lens::new(
84        |c: &Customer| c.name.clone(),
85        |c, name| Customer { name, ..c },
86    )
87}
88
89// --- Prisms ---
90
91fn credit_card_prism() -> SimplePrism<Payment, (String, String)> {
92    Prism::new(
93        |p| match p {
94            Payment::CreditCard { last4, exp } => Ok((last4, exp)),
95            other => Err(other),
96        },
97        |(last4, exp)| Payment::CreditCard { last4, exp },
98    )
99}
100
101fn wallet_prism() -> SimplePrism<Payment, (String, i64)> {
102    Prism::new(
103        |p| match p {
104            Payment::Wallet {
105                provider,
106                balance_cents,
107            } => Ok((provider, balance_cents)),
108            other => Err(other),
109        },
110        |(provider, balance_cents)| Payment::Wallet {
111            provider,
112            balance_cents,
113        },
114    )
115}
116
117fn bank_transfer_prism() -> SimplePrism<Payment, String> {
118    Prism::new(
119        |p| match p {
120            Payment::BankTransfer { iban } => Ok(iban),
121            other => Err(other),
122        },
123        |iban| Payment::BankTransfer { iban },
124    )
125}
126
127// --- Sample data ---
128
129fn sample_order() -> Order {
130    Order {
131        id: 1001,
132        customer: Customer {
133            name: "Alice Smith".into(),
134            address: Address {
135                street: "123 Main St".into(),
136                city: "Springfield".into(),
137                zip: "62701".into(),
138            },
139        },
140        items: vec![
141            Item {
142                name: "Keyboard".into(),
143                price_cents: 7999,
144                quantity: 1,
145            },
146            Item {
147                name: "USB Cable".into(),
148                price_cents: 899,
149                quantity: 3,
150            },
151        ],
152        payment: Payment::CreditCard {
153            last4: "4242".into(),
154            exp: "12/25".into(),
155        },
156    }
157}
158
159fn main() {
160    println!("=== Domain Model with Optics Example ===\n");
161
162    let order = sample_order();
163
164    // 1. Lens: get/set/over
165    println!("--- Lens: basic get/set/over ---");
166    let customer = customer_lens();
167    let address = address_lens();
168    let city = city_lens();
169
170    println!("  Customer name: {}", name_lens().get(&order.customer));
171    println!("  City: {}", city.get(&address.get(&customer.get(&order))));
172
173    // 2. Composed Lens: deep access with .then()
174    println!("\n--- ComposedLens: deep access with .then() ---");
175    let order_city = customer_lens().then(address_lens()).then(city_lens());
176    let order_zip = customer_lens().then(address_lens()).then(zip_lens());
177
178    println!("  Order city: {}", order_city.get(&order));
179    println!("  Order zip:  {}", order_zip.get(&order));
180
181    // Deep set
182    let updated = order_city.set(order.clone(), "Shelbyville".into());
183    println!("  After city update: {}", order_city.get(&updated));
184    println!("  Original unchanged: {}", order_city.get(&order));
185
186    // Deep over (modify)
187    let uppercased = order_city.over(order.clone(), |c| c.to_uppercase());
188    println!("  Uppercased city: {}", order_city.get(&uppercased));
189
190    // 3. Lens::transform — reusable update function
191    println!("\n--- Lens::transform — reusable update function ---");
192    let normalize_city: Box<dyn Fn(String) -> String> = Box::new(|c| c.trim().to_uppercase());
193    let normalize_order_city = city_lens().transform::<FnP>(normalize_city);
194    let addr = Address {
195        street: "456 Oak Ave".into(),
196        city: "  new york  ".into(),
197        zip: "10001".into(),
198    };
199    let normalized = normalize_order_city(addr);
200    println!("  Normalized city: {:?}", normalized.city);
201
202    // 4. Prism: sum type focus
203    println!("\n--- Prism: sum type focus ---");
204    let cc = credit_card_prism();
205    let wallet = wallet_prism();
206    let bank = bank_transfer_prism();
207
208    println!("  Credit card last4: {:?}", cc.preview(&order.payment));
209    println!("  Wallet preview:    {:?}", wallet.preview(&order.payment));
210
211    // Prism::review — construct a variant
212    let new_payment = wallet.review(("PayPal".into(), 5000));
213    println!("  New wallet payment: {:?}", new_payment);
214
215    // Prism::over — modify only if matched
216    let updated_payment = cc.over(order.payment.clone(), |(last4, _exp)| {
217        (last4, "01/28".into())
218    });
219    println!("  Updated CC expiry: {:?}", updated_payment);
220
221    // Prism on non-matching variant: passes through unchanged
222    let unchanged = wallet.over(order.payment.clone(), |(prov, bal)| (prov, bal + 1000));
223    println!("  Wallet over on CC (unchanged): {:?}", unchanged);
224
225    // 5. Prism::transform — reusable pattern-matching function
226    println!("\n--- Prism::transform — reusable variant modifier ---");
227    let add_balance: Box<dyn Fn((String, i64)) -> (String, i64)> =
228        Box::new(|(prov, bal)| (prov, bal + 2500));
229    let add_wallet_balance = wallet.transform::<FnP>(add_balance);
230
231    let wallet_payment = Payment::Wallet {
232        provider: "PayPal".into(),
233        balance_cents: 10000,
234    };
235    let topped_up = add_wallet_balance(wallet_payment);
236    println!("  Topped up: {:?}", topped_up);
237
238    // Apply same transform to non-wallet — passes through
239    let still_cc = add_wallet_balance(order.payment.clone());
240    println!("  CC unchanged: {:?}", still_cc);
241
242    // 6. Summary: combine multiple optics
243    println!("\n--- Combining lenses and prisms ---");
244    let orders = vec![
245        sample_order(),
246        Order {
247            id: 1002,
248            customer: Customer {
249                name: "Bob Jones".into(),
250                address: Address {
251                    street: "789 Elm St".into(),
252                    city: "Capital City".into(),
253                    zip: "12345".into(),
254                },
255            },
256            items: vec![Item {
257                name: "Mouse".into(),
258                price_cents: 2999,
259                quantity: 1,
260            }],
261            payment: Payment::Wallet {
262                provider: "Stripe".into(),
263                balance_cents: 50000,
264            },
265        },
266        Order {
267            id: 1003,
268            customer: Customer {
269                name: "Carol White".into(),
270                address: Address {
271                    street: "321 Pine Rd".into(),
272                    city: "Springfield".into(),
273                    zip: "62702".into(),
274                },
275            },
276            items: vec![],
277            payment: Payment::BankTransfer {
278                iban: "DE89370400440532013000".into(),
279            },
280        },
281    ];
282
283    let order_city_lens = customer_lens().then(address_lens()).then(city_lens());
284    println!("  All cities:");
285    for o in &orders {
286        let payment_type = match &o.payment {
287            Payment::CreditCard { .. } => "CC",
288            Payment::BankTransfer { .. } => "Bank",
289            Payment::Wallet { .. } => "Wallet",
290        };
291        println!(
292            "    Order #{}: {} ({}, pays via {})",
293            o.id,
294            order_city_lens.get(o),
295            name_lens().get(&o.customer),
296            payment_type
297        );
298    }
299
300    // Find all bank IBANs using Prism
301    println!("\n  Bank IBANs:");
302    for o in &orders {
303        if let Some(iban) = bank.preview(&o.payment) {
304            println!("    Order #{}: {}", o.id, iban);
305        }
306    }
307}