1use crate::SuggestId;
25use ryo_executor::executor::MutationSpec;
26use serde::{Deserialize, Serialize};
27use std::fmt;
28use std::path::PathBuf;
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct ChoiceId(String);
33
34impl ChoiceId {
35 pub fn new(id: impl Into<String>) -> Self {
37 Self(id.into())
38 }
39
40 pub fn as_str(&self) -> &str {
42 &self.0
43 }
44}
45
46impl fmt::Display for ChoiceId {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}", self.0)
49 }
50}
51
52impl From<&str> for ChoiceId {
53 fn from(s: &str) -> Self {
54 Self::new(s)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
60#[repr(u8)]
61#[derive(Default)]
62pub enum Rating {
63 Low = 1,
65 #[default]
67 Medium = 2,
68 High = 3,
70}
71
72impl Rating {
73 pub fn stars(&self) -> &'static str {
75 match self {
76 Rating::Low => "★☆☆",
77 Rating::Medium => "★★☆",
78 Rating::High => "★★★",
79 }
80 }
81
82 pub fn value(&self) -> u8 {
84 *self as u8
85 }
86}
87
88impl fmt::Display for Rating {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 write!(f, "{}", self.stars())
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct TradeOffs {
100 pub extensibility: Rating,
102
103 pub performance: Rating,
105
106 pub complexity: Rating,
108
109 pub breaking_change: bool,
111
112 pub affected_files: Vec<PathBuf>,
114}
115
116impl TradeOffs {
117 pub fn default_medium() -> Self {
119 Self {
120 extensibility: Rating::Medium,
121 performance: Rating::Medium,
122 complexity: Rating::Medium,
123 breaking_change: false,
124 affected_files: Vec::new(),
125 }
126 }
127
128 pub fn with_extensibility(mut self, rating: Rating) -> Self {
130 self.extensibility = rating;
131 self
132 }
133
134 pub fn with_performance(mut self, rating: Rating) -> Self {
136 self.performance = rating;
137 self
138 }
139
140 pub fn with_complexity(mut self, rating: Rating) -> Self {
142 self.complexity = rating;
143 self
144 }
145
146 pub fn with_breaking_change(mut self, breaking: bool) -> Self {
148 self.breaking_change = breaking;
149 self
150 }
151
152 pub fn with_affected_files(mut self, files: Vec<PathBuf>) -> Self {
154 self.affected_files = files;
155 self
156 }
157
158 pub fn score(&self) -> f32 {
161 let ext = self.extensibility.value() as f32;
162 let perf = self.performance.value() as f32;
163 let comp = self.complexity.value() as f32;
164
165 let base_score = ext + perf - comp / 2.0;
168 if self.breaking_change {
169 base_score - 1.0
170 } else {
171 base_score
172 }
173 }
174}
175
176impl Default for TradeOffs {
177 fn default() -> Self {
178 Self::default_medium()
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct DesignChoice {
185 pub id: ChoiceId,
187
188 pub label: String,
190
191 pub title: String,
193
194 pub description: String,
196
197 pub trade_offs: TradeOffs,
199
200 pub specs: Vec<MutationSpec>,
203}
204
205impl DesignChoice {
206 pub fn new(
208 id: impl Into<ChoiceId>,
209 label: impl Into<String>,
210 title: impl Into<String>,
211 description: impl Into<String>,
212 ) -> Self {
213 Self {
214 id: id.into(),
215 label: label.into(),
216 title: title.into(),
217 description: description.into(),
218 trade_offs: TradeOffs::default(),
219 specs: Vec::new(),
220 }
221 }
222
223 pub fn with_trade_offs(mut self, trade_offs: TradeOffs) -> Self {
225 self.trade_offs = trade_offs;
226 self
227 }
228
229 pub fn with_specs(mut self, specs: Vec<MutationSpec>) -> Self {
231 self.specs = specs;
232 self
233 }
234
235 pub fn spec_count(&self) -> usize {
237 self.specs.len()
238 }
239
240 pub fn is_breaking(&self) -> bool {
242 self.trade_offs.breaking_change
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct DesignChoiceSet {
252 pub suggestion_id: SuggestId,
254
255 pub pattern_name: String,
257
258 pub choices: Vec<DesignChoice>,
260
261 pub recommended: Option<ChoiceId>,
263}
264
265impl DesignChoiceSet {
266 pub fn new(suggestion_id: SuggestId, pattern_name: impl Into<String>) -> Self {
268 Self {
269 suggestion_id,
270 pattern_name: pattern_name.into(),
271 choices: Vec::new(),
272 recommended: None,
273 }
274 }
275
276 pub fn add_choice(mut self, choice: DesignChoice) -> Self {
278 self.choices.push(choice);
279 self
280 }
281
282 pub fn with_recommended(mut self, id: impl Into<ChoiceId>) -> Self {
284 self.recommended = Some(id.into());
285 self
286 }
287
288 pub fn get_choice(&self, id: &ChoiceId) -> Option<&DesignChoice> {
290 self.choices.iter().find(|c| &c.id == id)
291 }
292
293 pub fn get_recommended(&self) -> Option<&DesignChoice> {
295 self.recommended.as_ref().and_then(|id| self.get_choice(id))
296 }
297
298 pub fn has_alternatives(&self) -> bool {
300 self.choices.len() > 1
301 }
302
303 pub fn choice_count(&self) -> usize {
305 self.choices.len()
306 }
307
308 pub fn choices_by_score(&self) -> Vec<&DesignChoice> {
310 let mut sorted: Vec<_> = self.choices.iter().collect();
311 sorted.sort_by(|a, b| {
312 b.trade_offs
313 .score()
314 .partial_cmp(&a.trade_offs.score())
315 .unwrap_or(std::cmp::Ordering::Equal)
316 });
317 sorted
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::SuggestIdGenerator;
325
326 #[test]
327 fn test_choice_id() {
328 let id = ChoiceId::new("A");
329 assert_eq!(id.as_str(), "A");
330 assert_eq!(id.to_string(), "A");
331
332 let id2: ChoiceId = "B".into();
333 assert_eq!(id2.as_str(), "B");
334 }
335
336 #[test]
337 fn test_rating_stars() {
338 assert_eq!(Rating::Low.stars(), "★☆☆");
339 assert_eq!(Rating::Medium.stars(), "★★☆");
340 assert_eq!(Rating::High.stars(), "★★★");
341 }
342
343 #[test]
344 fn test_rating_ordering() {
345 assert!(Rating::Low < Rating::Medium);
346 assert!(Rating::Medium < Rating::High);
347 }
348
349 #[test]
350 fn test_trade_offs_score() {
351 let good = TradeOffs::default_medium()
353 .with_extensibility(Rating::High)
354 .with_performance(Rating::High)
355 .with_complexity(Rating::Low);
356 assert!(good.score() > 4.0);
357
358 let bad = TradeOffs::default_medium()
360 .with_extensibility(Rating::Low)
361 .with_performance(Rating::Low)
362 .with_complexity(Rating::High);
363 assert!(bad.score() < 2.0);
364
365 let breaking = TradeOffs::default_medium().with_breaking_change(true);
367 let non_breaking = TradeOffs::default_medium().with_breaking_change(false);
368 assert!(breaking.score() < non_breaking.score());
369 }
370
371 #[test]
372 fn test_design_choice_builder() {
373 let choice = DesignChoice::new(
374 ChoiceId::new("A"),
375 "A",
376 "Full Dynamic",
377 "Use Box<dyn Trait>",
378 )
379 .with_trade_offs(
380 TradeOffs::default_medium()
381 .with_extensibility(Rating::High)
382 .with_performance(Rating::Low),
383 );
384
385 assert_eq!(choice.id.as_str(), "A");
386 assert_eq!(choice.title, "Full Dynamic");
387 assert_eq!(choice.trade_offs.extensibility, Rating::High);
388 assert_eq!(choice.trade_offs.performance, Rating::Low);
389 }
390
391 #[test]
392 fn test_design_choice_set() {
393 let mut gen = SuggestIdGenerator::new();
394 let suggestion_id = gen.next_id();
395
396 let set = DesignChoiceSet::new(suggestion_id, "EnumToTrait")
397 .add_choice(DesignChoice::new("A", "A", "Dynamic", "Box<dyn>"))
398 .add_choice(DesignChoice::new("B", "B", "Static", "Generics"))
399 .with_recommended("A");
400
401 assert_eq!(set.choice_count(), 2);
402 assert!(set.has_alternatives());
403 assert!(set.get_choice(&ChoiceId::new("A")).is_some());
404 assert!(set.get_choice(&ChoiceId::new("B")).is_some());
405 assert!(set.get_choice(&ChoiceId::new("C")).is_none());
406
407 let recommended = set.get_recommended();
408 assert!(recommended.is_some());
409 assert_eq!(recommended.unwrap().title, "Dynamic");
410 }
411
412 #[test]
413 fn test_choices_by_score() {
414 let mut gen = SuggestIdGenerator::new();
415 let suggestion_id = gen.next_id();
416
417 let set = DesignChoiceSet::new(suggestion_id, "Test")
418 .add_choice(
419 DesignChoice::new("A", "A", "Low Score", "desc").with_trade_offs(
420 TradeOffs::default_medium()
421 .with_extensibility(Rating::Low)
422 .with_performance(Rating::Low),
423 ),
424 )
425 .add_choice(
426 DesignChoice::new("B", "B", "High Score", "desc").with_trade_offs(
427 TradeOffs::default_medium()
428 .with_extensibility(Rating::High)
429 .with_performance(Rating::High),
430 ),
431 );
432
433 let sorted = set.choices_by_score();
434 assert_eq!(sorted[0].id.as_str(), "B"); assert_eq!(sorted[1].id.as_str(), "A");
436 }
437
438 #[test]
439 fn test_trade_offs_serde() {
440 let trade_offs = TradeOffs::default_medium()
441 .with_extensibility(Rating::High)
442 .with_breaking_change(true)
443 .with_affected_files(vec![PathBuf::from("src/lib.rs")]);
444
445 let json = serde_json::to_string(&trade_offs).unwrap();
446 let parsed: TradeOffs = serde_json::from_str(&json).unwrap();
447
448 assert_eq!(parsed.extensibility, Rating::High);
449 assert!(parsed.breaking_change);
450 assert_eq!(parsed.affected_files.len(), 1);
451 }
452}