mockforge_core/openapi/
response_selection.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "lowercase")]
14#[derive(Default)]
15pub enum ResponseSelectionMode {
16 #[default]
18 First,
19 Scenario,
21 Sequential,
23 Random,
25 WeightedRandom,
27}
28
29impl ResponseSelectionMode {
30 #[allow(clippy::should_implement_trait)]
32 pub fn from_str(s: &str) -> Option<Self> {
33 match s.to_lowercase().as_str() {
34 "first" => Some(Self::First),
35 "scenario" => Some(Self::Scenario),
36 "sequential" | "round-robin" | "round_robin" => Some(Self::Sequential),
37 "random" => Some(Self::Random),
38 "weighted_random" | "weighted-random" | "weighted" => Some(Self::WeightedRandom),
39 _ => None,
40 }
41 }
42}
43
44#[derive(Debug)]
46pub struct ResponseSelector {
47 mode: ResponseSelectionMode,
49 sequential_counter: Arc<AtomicUsize>,
51 weights: Option<HashMap<String, f64>>,
53}
54
55impl ResponseSelector {
56 pub fn new(mode: ResponseSelectionMode) -> Self {
58 Self {
59 mode,
60 sequential_counter: Arc::new(AtomicUsize::new(0)),
61 weights: None,
62 }
63 }
64
65 pub fn with_weights(mut self, weights: HashMap<String, f64>) -> Self {
67 self.weights = Some(weights);
68 self
69 }
70
71 pub fn select(&self, options: &[String]) -> usize {
79 if options.is_empty() {
80 return 0;
81 }
82
83 match self.mode {
84 ResponseSelectionMode::First => 0,
85 ResponseSelectionMode::Scenario => {
86 0
89 }
90 ResponseSelectionMode::Sequential => {
91 let current = self.sequential_counter.fetch_add(1, Ordering::Relaxed);
93 current % options.len()
94 }
95 ResponseSelectionMode::Random => {
96 use rand::Rng;
97 let mut rng = rand::thread_rng();
98 rng.gen_range(0..options.len())
99 }
100 ResponseSelectionMode::WeightedRandom => self.select_weighted_random(options),
101 }
102 }
103
104 fn select_weighted_random(&self, options: &[String]) -> usize {
106 use rand::Rng;
107 let mut rng = rand::thread_rng();
108
109 if let Some(ref weights) = self.weights {
111 let total_weight: f64 =
112 options.iter().map(|opt| weights.get(opt).copied().unwrap_or(1.0)).sum();
113
114 if total_weight > 0.0 {
115 let random = rng.gen::<f64>() * total_weight;
116 let mut cumulative = 0.0;
117
118 for (idx, opt) in options.iter().enumerate() {
119 cumulative += weights.get(opt).copied().unwrap_or(1.0);
120 if random <= cumulative {
121 return idx;
122 }
123 }
124 }
125 }
126
127 rng.gen_range(0..options.len())
129 }
130
131 pub fn reset_sequential(&self) {
133 self.sequential_counter.store(0, Ordering::Relaxed);
134 }
135
136 pub fn get_sequential_index(&self) -> usize {
138 self.sequential_counter.load(Ordering::Relaxed)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_first_mode() {
148 let selector = ResponseSelector::new(ResponseSelectionMode::First);
149 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
150
151 assert_eq!(selector.select(&options), 0);
152 assert_eq!(selector.select(&options), 0); }
154
155 #[test]
156 fn test_sequential_mode() {
157 let selector = ResponseSelector::new(ResponseSelectionMode::Sequential);
158 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
159
160 assert_eq!(selector.select(&options), 0);
161 assert_eq!(selector.select(&options), 1);
162 assert_eq!(selector.select(&options), 2);
163 assert_eq!(selector.select(&options), 0); assert_eq!(selector.select(&options), 1);
165 }
166
167 #[test]
168 fn test_random_mode() {
169 let selector = ResponseSelector::new(ResponseSelectionMode::Random);
170 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
171
172 for _ in 0..100 {
174 let idx = selector.select(&options);
175 assert!(idx < options.len());
176 }
177 }
178
179 #[test]
180 fn test_weighted_random_mode() {
181 let mut weights = HashMap::new();
182 weights.insert("a".to_string(), 0.5);
183 weights.insert("b".to_string(), 0.3);
184 weights.insert("c".to_string(), 0.2);
185
186 let selector =
187 ResponseSelector::new(ResponseSelectionMode::WeightedRandom).with_weights(weights);
188 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
189
190 for _ in 0..100 {
192 let idx = selector.select(&options);
193 assert!(idx < options.len());
194 }
195 }
196
197 #[test]
198 fn test_mode_from_str() {
199 assert_eq!(ResponseSelectionMode::from_str("first"), Some(ResponseSelectionMode::First));
200 assert_eq!(
201 ResponseSelectionMode::from_str("sequential"),
202 Some(ResponseSelectionMode::Sequential)
203 );
204 assert_eq!(
205 ResponseSelectionMode::from_str("round-robin"),
206 Some(ResponseSelectionMode::Sequential)
207 );
208 assert_eq!(ResponseSelectionMode::from_str("random"), Some(ResponseSelectionMode::Random));
209 assert_eq!(ResponseSelectionMode::from_str("invalid"), None);
210 }
211
212 #[test]
213 fn test_reset_sequential() {
214 let selector = ResponseSelector::new(ResponseSelectionMode::Sequential);
215 let options = vec!["a".to_string(), "b".to_string()];
216
217 assert_eq!(selector.select(&options), 0);
218 assert_eq!(selector.select(&options), 1);
219
220 selector.reset_sequential();
221 assert_eq!(selector.select(&options), 0);
222 }
223}