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")]
14pub enum ResponseSelectionMode {
15 First,
17 Scenario,
19 Sequential,
21 Random,
23 WeightedRandom,
25}
26
27impl Default for ResponseSelectionMode {
28 fn default() -> Self {
29 Self::First
30 }
31}
32
33impl ResponseSelectionMode {
34 pub fn from_str(s: &str) -> Option<Self> {
36 match s.to_lowercase().as_str() {
37 "first" => Some(Self::First),
38 "scenario" => Some(Self::Scenario),
39 "sequential" | "round-robin" | "round_robin" => Some(Self::Sequential),
40 "random" => Some(Self::Random),
41 "weighted_random" | "weighted-random" | "weighted" => Some(Self::WeightedRandom),
42 _ => None,
43 }
44 }
45}
46
47#[derive(Debug)]
49pub struct ResponseSelector {
50 mode: ResponseSelectionMode,
52 sequential_counter: Arc<AtomicUsize>,
54 weights: Option<HashMap<String, f64>>,
56}
57
58impl ResponseSelector {
59 pub fn new(mode: ResponseSelectionMode) -> Self {
61 Self {
62 mode,
63 sequential_counter: Arc::new(AtomicUsize::new(0)),
64 weights: None,
65 }
66 }
67
68 pub fn with_weights(mut self, weights: HashMap<String, f64>) -> Self {
70 self.weights = Some(weights);
71 self
72 }
73
74 pub fn select(&self, options: &[String]) -> usize {
82 if options.is_empty() {
83 return 0;
84 }
85
86 match self.mode {
87 ResponseSelectionMode::First => 0,
88 ResponseSelectionMode::Scenario => {
89 0
92 }
93 ResponseSelectionMode::Sequential => {
94 let current = self.sequential_counter.fetch_add(1, Ordering::Relaxed);
96 current % options.len()
97 }
98 ResponseSelectionMode::Random => {
99 use rand::Rng;
100 let mut rng = rand::thread_rng();
101 rng.gen_range(0..options.len())
102 }
103 ResponseSelectionMode::WeightedRandom => self.select_weighted_random(options),
104 }
105 }
106
107 fn select_weighted_random(&self, options: &[String]) -> usize {
109 use rand::Rng;
110 let mut rng = rand::thread_rng();
111
112 if let Some(ref weights) = self.weights {
114 let total_weight: f64 =
115 options.iter().map(|opt| weights.get(opt).copied().unwrap_or(1.0)).sum();
116
117 if total_weight > 0.0 {
118 let random = rng.gen::<f64>() * total_weight;
119 let mut cumulative = 0.0;
120
121 for (idx, opt) in options.iter().enumerate() {
122 cumulative += weights.get(opt).copied().unwrap_or(1.0);
123 if random <= cumulative {
124 return idx;
125 }
126 }
127 }
128 }
129
130 rng.gen_range(0..options.len())
132 }
133
134 pub fn reset_sequential(&self) {
136 self.sequential_counter.store(0, Ordering::Relaxed);
137 }
138
139 pub fn get_sequential_index(&self) -> usize {
141 self.sequential_counter.load(Ordering::Relaxed)
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_first_mode() {
151 let selector = ResponseSelector::new(ResponseSelectionMode::First);
152 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
153
154 assert_eq!(selector.select(&options), 0);
155 assert_eq!(selector.select(&options), 0); }
157
158 #[test]
159 fn test_sequential_mode() {
160 let selector = ResponseSelector::new(ResponseSelectionMode::Sequential);
161 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
162
163 assert_eq!(selector.select(&options), 0);
164 assert_eq!(selector.select(&options), 1);
165 assert_eq!(selector.select(&options), 2);
166 assert_eq!(selector.select(&options), 0); assert_eq!(selector.select(&options), 1);
168 }
169
170 #[test]
171 fn test_random_mode() {
172 let selector = ResponseSelector::new(ResponseSelectionMode::Random);
173 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
174
175 for _ in 0..100 {
177 let idx = selector.select(&options);
178 assert!(idx < options.len());
179 }
180 }
181
182 #[test]
183 fn test_weighted_random_mode() {
184 let mut weights = HashMap::new();
185 weights.insert("a".to_string(), 0.5);
186 weights.insert("b".to_string(), 0.3);
187 weights.insert("c".to_string(), 0.2);
188
189 let selector =
190 ResponseSelector::new(ResponseSelectionMode::WeightedRandom).with_weights(weights);
191 let options = vec!["a".to_string(), "b".to_string(), "c".to_string()];
192
193 for _ in 0..100 {
195 let idx = selector.select(&options);
196 assert!(idx < options.len());
197 }
198 }
199
200 #[test]
201 fn test_mode_from_str() {
202 assert_eq!(ResponseSelectionMode::from_str("first"), Some(ResponseSelectionMode::First));
203 assert_eq!(
204 ResponseSelectionMode::from_str("sequential"),
205 Some(ResponseSelectionMode::Sequential)
206 );
207 assert_eq!(
208 ResponseSelectionMode::from_str("round-robin"),
209 Some(ResponseSelectionMode::Sequential)
210 );
211 assert_eq!(ResponseSelectionMode::from_str("random"), Some(ResponseSelectionMode::Random));
212 assert_eq!(ResponseSelectionMode::from_str("invalid"), None);
213 }
214
215 #[test]
216 fn test_reset_sequential() {
217 let selector = ResponseSelector::new(ResponseSelectionMode::Sequential);
218 let options = vec!["a".to_string(), "b".to_string()];
219
220 assert_eq!(selector.select(&options), 0);
221 assert_eq!(selector.select(&options), 1);
222
223 selector.reset_sequential();
224 assert_eq!(selector.select(&options), 0);
225 }
226}