1
use std::{ops::RangeInclusive, sync::Arc};
2

            
3
use rand::{distributions::Standard, prelude::Distribution, Rng};
4

            
5
use crate::{
6
    model::{InitialDataSet, Product},
7
    utils::gen_range,
8
};
9

            
10
/// A single database operation.
11
#[derive(Clone, Debug)]
12
pub enum Operation {
13
    LookupProduct(LookupProduct),
14
    FindProduct(FindProduct),
15
    CreateCart(CreateCart),
16
    AddProductToCart(AddProductToCart),
17
    RateProduct(ReviewProduct),
18
    Checkout(Checkout),
19
}
20

            
21
/// Bulk-load data.
22
#[derive(Clone, Debug)]
23
pub struct Load {
24
    pub initial_data: Arc<InitialDataSet>,
25
}
26

            
27
/// Lookup a product by id.
28
#[derive(Clone, Debug)]
29
pub struct LookupProduct {
30
    pub id: u32,
31
}
32

            
33
/// Find a product by name (indexed).
34
#[derive(Clone, Debug)]
35
pub struct FindProduct {
36
    pub name: String,
37
}
38

            
39
/// Create a shopping cart.
40
#[derive(Clone, Debug)]
41
pub struct CreateCart;
42

            
43
/// Add a previously-searched-for product to a shopping cart.
44
#[derive(Clone, Debug)]
45
pub struct AddProductToCart {
46
    pub product: ResultId,
47
    pub cart: ResultId,
48
}
49

            
50
/// Convert a cart to an order.
51
#[derive(Clone, Debug)]
52
pub struct Checkout {
53
    pub customer_id: u32,
54
    pub cart: ResultId,
55
}
56

            
57
/// Review a product
58
#[derive(Clone, Debug)]
59
pub struct ReviewProduct {
60
    pub customer_id: u32,
61
    pub product_id: ResultId,
62
    pub review: Option<String>,
63
    pub rating: u8,
64
}
65

            
66
/// A sequence of [`Operation`]s.
67
556
#[derive(Clone, Debug, Default)]
68
pub struct Plan {
69
    pub operations: Vec<Operation>,
70
}
71

            
72
impl Plan {
73
6072
    pub fn push(&mut self, operation: Operation) -> ResultId {
74
6072
        let id = self.operations.len();
75
6072
        self.operations.push(operation);
76
6072
        ResultId(id)
77
6072
    }
78
}
79

            
80
/// An identifier for a result from a previous [`Operation`].
81
#[derive(Debug, Clone, Copy)]
82
pub struct ResultId(pub usize);
83
#[derive(Debug)]
84
#[must_use]
85
pub enum OperationResult {
86
    Ok,
87
    Product {
88
        id: u32,
89
        product: Product,
90
        rating: Option<f32>,
91
    },
92
    Cart {
93
        id: u32,
94
    },
95
    CartProduct {
96
        id: u32,
97
    },
98
}
99

            
100
pub struct ShopperPlanConfig {
101
    pub chance_of_adding_product_to_cart: f64,
102
    pub chance_of_purchasing: f64,
103
    pub chance_of_rating: f64,
104

            
105
    pub product_search_attempts: RangeInclusive<u32>,
106
}
107

            
108
impl ShopperPlanConfig {
109
556
    pub fn random_plan<R: Rng>(&self, rng: &mut R, dataset: &InitialDataSet) -> Plan {
110
556
        let mut plan = Plan::default();
111
556
        let mut shopping_cart = None;
112

            
113
556
        let attempts = if self.product_search_attempts.start() == self.product_search_attempts.end()
114
        {
115
            *self.product_search_attempts.start()
116
        } else {
117
556
            rng.gen_range(self.product_search_attempts.clone())
118
        };
119
556
        let mut products_in_cart = Vec::new();
120
556
        for _ in 0..attempts {
121
            // Find a product
122
4302
            let product = match rng.gen::<ProductSearchStrategy>() {
123
                ProductSearchStrategy::ByName => {
124
2189
                    let product_to_find = dataset
125
2189
                        .products
126
2189
                        .values()
127
2189
                        .nth(rng.gen_range(0..dataset.products.len()))
128
2189
                        .unwrap();
129
2189

            
130
2189
                    plan.push(Operation::FindProduct(FindProduct {
131
2189
                        name: product_to_find.name.clone(),
132
2189
                    }))
133
                }
134
                ProductSearchStrategy::Direct => {
135
2113
                    let product_to_find = dataset
136
2113
                        .products
137
2113
                        .keys()
138
2113
                        .nth(rng.gen_range(0..dataset.products.len()))
139
2113
                        .unwrap();
140
2113

            
141
2113
                    plan.push(Operation::LookupProduct(LookupProduct {
142
2113
                        id: *product_to_find,
143
2113
                    }))
144
                }
145
            };
146

            
147
            // Add it to the cart?
148
4302
            if rng.gen_bool(self.chance_of_adding_product_to_cart) {
149
1111
                let cart = if let Some(shopping_cart) = shopping_cart {
150
654
                    shopping_cart
151
                } else {
152
                    // Create a shopping cart
153
457
                    let new_cart = plan.push(Operation::CreateCart(CreateCart));
154
457
                    shopping_cart = Some(new_cart);
155
457
                    new_cart
156
                };
157

            
158
1111
                products_in_cart.push(plan.push(Operation::AddProductToCart(AddProductToCart {
159
1111
                    product,
160
1111
                    cart,
161
1111
                })));
162
3191
            }
163
        }
164

            
165
556
        if !dataset.customers.is_empty() {
166
556
            if let Some(cart) = shopping_cart {
167
457
                if rng.gen_bool(self.chance_of_purchasing) {
168
                    // TODO checkout instead of using an existing customer.
169
115
                    let customer_index = gen_range(rng, 0..=(dataset.customers.len() - 1));
170
115
                    let customer_id = *dataset.customers.keys().nth(customer_index).unwrap();
171
115
                    plan.push(Operation::Checkout(Checkout { cart, customer_id }));
172
413
                    for product_id in products_in_cart {
173
298
                        if rng.gen_bool(self.chance_of_rating) {
174
87
                            plan.push(Operation::RateProduct(ReviewProduct {
175
87
                                customer_id,
176
87
                                product_id,
177
87
                                rating: rng.gen_range(1..=5),
178
87
                                review: None,
179
87
                            }));
180
211
                        }
181
                    }
182
342
                }
183
99
            } /*else if rng.gen_bool(self.chance_of_rating) {
184
                  todo!("allow ratings to happen for existing orders")
185
              }*/
186
        }
187

            
188
556
        plan
189
556
    }
190
}
191

            
192
pub enum ProductSearchStrategy {
193
    Direct,
194
    ByName,
195
}
196

            
197
impl Distribution<ProductSearchStrategy> for Standard {
198
4302
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> ProductSearchStrategy {
199
4302
        match rng.gen_range(0..2) {
200
2113
            0 => ProductSearchStrategy::Direct,
201
2189
            _ => ProductSearchStrategy::ByName,
202
        }
203
4302
    }
204
}