1
use std::ops::RangeInclusive;
2
use std::sync::Arc;
3

            
4
use rand::distributions::Standard;
5
use rand::prelude::Distribution;
6
use rand::Rng;
7

            
8
use crate::model::{InitialDataSet, Product};
9
use crate::utils::gen_range;
10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
159
1148
                products_in_cart.push(plan.push(Operation::AddProductToCart(AddProductToCart {
160
1148
                    product,
161
1148
                    cart,
162
1148
                })));
163
3354
            }
164
        }
165

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

            
189
556
        plan
190
556
    }
191
}
192

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

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