use crate::SuggestId;
use ryo_executor::executor::MutationSpec;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChoiceId(String);
impl ChoiceId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ChoiceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for ChoiceId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[repr(u8)]
#[derive(Default)]
pub enum Rating {
Low = 1,
#[default]
Medium = 2,
High = 3,
}
impl Rating {
pub fn stars(&self) -> &'static str {
match self {
Rating::Low => "★☆☆",
Rating::Medium => "★★☆",
Rating::High => "★★★",
}
}
pub fn value(&self) -> u8 {
*self as u8
}
}
impl fmt::Display for Rating {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.stars())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeOffs {
pub extensibility: Rating,
pub performance: Rating,
pub complexity: Rating,
pub breaking_change: bool,
pub affected_files: Vec<PathBuf>,
}
impl TradeOffs {
pub fn default_medium() -> Self {
Self {
extensibility: Rating::Medium,
performance: Rating::Medium,
complexity: Rating::Medium,
breaking_change: false,
affected_files: Vec::new(),
}
}
pub fn with_extensibility(mut self, rating: Rating) -> Self {
self.extensibility = rating;
self
}
pub fn with_performance(mut self, rating: Rating) -> Self {
self.performance = rating;
self
}
pub fn with_complexity(mut self, rating: Rating) -> Self {
self.complexity = rating;
self
}
pub fn with_breaking_change(mut self, breaking: bool) -> Self {
self.breaking_change = breaking;
self
}
pub fn with_affected_files(mut self, files: Vec<PathBuf>) -> Self {
self.affected_files = files;
self
}
pub fn score(&self) -> f32 {
let ext = self.extensibility.value() as f32;
let perf = self.performance.value() as f32;
let comp = self.complexity.value() as f32;
let base_score = ext + perf - comp / 2.0;
if self.breaking_change {
base_score - 1.0
} else {
base_score
}
}
}
impl Default for TradeOffs {
fn default() -> Self {
Self::default_medium()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesignChoice {
pub id: ChoiceId,
pub label: String,
pub title: String,
pub description: String,
pub trade_offs: TradeOffs,
pub specs: Vec<MutationSpec>,
}
impl DesignChoice {
pub fn new(
id: impl Into<ChoiceId>,
label: impl Into<String>,
title: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
id: id.into(),
label: label.into(),
title: title.into(),
description: description.into(),
trade_offs: TradeOffs::default(),
specs: Vec::new(),
}
}
pub fn with_trade_offs(mut self, trade_offs: TradeOffs) -> Self {
self.trade_offs = trade_offs;
self
}
pub fn with_specs(mut self, specs: Vec<MutationSpec>) -> Self {
self.specs = specs;
self
}
pub fn spec_count(&self) -> usize {
self.specs.len()
}
pub fn is_breaking(&self) -> bool {
self.trade_offs.breaking_change
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesignChoiceSet {
pub suggestion_id: SuggestId,
pub pattern_name: String,
pub choices: Vec<DesignChoice>,
pub recommended: Option<ChoiceId>,
}
impl DesignChoiceSet {
pub fn new(suggestion_id: SuggestId, pattern_name: impl Into<String>) -> Self {
Self {
suggestion_id,
pattern_name: pattern_name.into(),
choices: Vec::new(),
recommended: None,
}
}
pub fn add_choice(mut self, choice: DesignChoice) -> Self {
self.choices.push(choice);
self
}
pub fn with_recommended(mut self, id: impl Into<ChoiceId>) -> Self {
self.recommended = Some(id.into());
self
}
pub fn get_choice(&self, id: &ChoiceId) -> Option<&DesignChoice> {
self.choices.iter().find(|c| &c.id == id)
}
pub fn get_recommended(&self) -> Option<&DesignChoice> {
self.recommended.as_ref().and_then(|id| self.get_choice(id))
}
pub fn has_alternatives(&self) -> bool {
self.choices.len() > 1
}
pub fn choice_count(&self) -> usize {
self.choices.len()
}
pub fn choices_by_score(&self) -> Vec<&DesignChoice> {
let mut sorted: Vec<_> = self.choices.iter().collect();
sorted.sort_by(|a, b| {
b.trade_offs
.score()
.partial_cmp(&a.trade_offs.score())
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SuggestIdGenerator;
#[test]
fn test_choice_id() {
let id = ChoiceId::new("A");
assert_eq!(id.as_str(), "A");
assert_eq!(id.to_string(), "A");
let id2: ChoiceId = "B".into();
assert_eq!(id2.as_str(), "B");
}
#[test]
fn test_rating_stars() {
assert_eq!(Rating::Low.stars(), "★☆☆");
assert_eq!(Rating::Medium.stars(), "★★☆");
assert_eq!(Rating::High.stars(), "★★★");
}
#[test]
fn test_rating_ordering() {
assert!(Rating::Low < Rating::Medium);
assert!(Rating::Medium < Rating::High);
}
#[test]
fn test_trade_offs_score() {
let good = TradeOffs::default_medium()
.with_extensibility(Rating::High)
.with_performance(Rating::High)
.with_complexity(Rating::Low);
assert!(good.score() > 4.0);
let bad = TradeOffs::default_medium()
.with_extensibility(Rating::Low)
.with_performance(Rating::Low)
.with_complexity(Rating::High);
assert!(bad.score() < 2.0);
let breaking = TradeOffs::default_medium().with_breaking_change(true);
let non_breaking = TradeOffs::default_medium().with_breaking_change(false);
assert!(breaking.score() < non_breaking.score());
}
#[test]
fn test_design_choice_builder() {
let choice = DesignChoice::new(
ChoiceId::new("A"),
"A",
"Full Dynamic",
"Use Box<dyn Trait>",
)
.with_trade_offs(
TradeOffs::default_medium()
.with_extensibility(Rating::High)
.with_performance(Rating::Low),
);
assert_eq!(choice.id.as_str(), "A");
assert_eq!(choice.title, "Full Dynamic");
assert_eq!(choice.trade_offs.extensibility, Rating::High);
assert_eq!(choice.trade_offs.performance, Rating::Low);
}
#[test]
fn test_design_choice_set() {
let mut gen = SuggestIdGenerator::new();
let suggestion_id = gen.next_id();
let set = DesignChoiceSet::new(suggestion_id, "EnumToTrait")
.add_choice(DesignChoice::new("A", "A", "Dynamic", "Box<dyn>"))
.add_choice(DesignChoice::new("B", "B", "Static", "Generics"))
.with_recommended("A");
assert_eq!(set.choice_count(), 2);
assert!(set.has_alternatives());
assert!(set.get_choice(&ChoiceId::new("A")).is_some());
assert!(set.get_choice(&ChoiceId::new("B")).is_some());
assert!(set.get_choice(&ChoiceId::new("C")).is_none());
let recommended = set.get_recommended();
assert!(recommended.is_some());
assert_eq!(recommended.unwrap().title, "Dynamic");
}
#[test]
fn test_choices_by_score() {
let mut gen = SuggestIdGenerator::new();
let suggestion_id = gen.next_id();
let set = DesignChoiceSet::new(suggestion_id, "Test")
.add_choice(
DesignChoice::new("A", "A", "Low Score", "desc").with_trade_offs(
TradeOffs::default_medium()
.with_extensibility(Rating::Low)
.with_performance(Rating::Low),
),
)
.add_choice(
DesignChoice::new("B", "B", "High Score", "desc").with_trade_offs(
TradeOffs::default_medium()
.with_extensibility(Rating::High)
.with_performance(Rating::High),
),
);
let sorted = set.choices_by_score();
assert_eq!(sorted[0].id.as_str(), "B"); assert_eq!(sorted[1].id.as_str(), "A");
}
#[test]
fn test_trade_offs_serde() {
let trade_offs = TradeOffs::default_medium()
.with_extensibility(Rating::High)
.with_breaking_change(true)
.with_affected_files(vec![PathBuf::from("src/lib.rs")]);
let json = serde_json::to_string(&trade_offs).unwrap();
let parsed: TradeOffs = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.extensibility, Rating::High);
assert!(parsed.breaking_change);
assert_eq!(parsed.affected_files.len(), 1);
}
}