use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use anyhow::{Context, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixGuidanceEntry {
#[serde(rename = "ruleID")]
pub rule_id: String,
pub strategy: FixStrategyKind,
pub confidence: FixConfidence,
pub source: FixSource,
pub symbol: String,
pub file: String,
pub fix_description: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub after: Option<String>,
pub search_pattern: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub replacement: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FixStrategyKind {
Rename,
UpdateSignature,
UpdateType,
FindAlternative,
UpdateImport,
EnsureDependency,
ManualReview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FixConfidence {
Exact,
High,
Medium,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FixSource {
Pattern,
Llm,
Manual,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FixGuidanceDoc {
pub migration: MigrationInfo,
pub summary: FixSummary,
pub fixes: Vec<FixGuidanceEntry>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MigrationInfo {
pub from_ref: String,
pub to_ref: String,
pub generated_by: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FixSummary {
pub total_fixes: usize,
pub auto_fixable: usize,
pub needs_review: usize,
pub manual_only: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingEntry {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub component: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub prop: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberMappingEntry {
pub old_name: String,
pub new_name: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PropMigrationEntry {
pub old_name: String,
pub new_name: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub old_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub new_type: Option<String>,
#[serde(default)]
pub type_changed: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeprecatedMigrationContext {
pub old_package: String,
pub new_package: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub matching_props: Vec<PropMigrationEntry>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub new_props: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub removed_props: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FixStrategyEntry {
pub strategy: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub component: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub prop: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub mappings: Vec<MappingEntry>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub member_mappings: Vec<MemberMappingEntry>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub removed_members: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub replacement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub overlap_ratio: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub new_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub target_structure: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub retained_props: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub prop_to_child: BTreeMap<String, String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub unmapped_removed_props: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub removed_children: Vec<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub child_props_to_parent: BTreeMap<String, String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub prop_value_changes: BTreeMap<String, Vec<MappingEntry>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub new_imports: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub removed_imports: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub import_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub deprecated_migration: Option<DeprecatedMigrationContext>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub exclude_patterns: Vec<String>,
}
impl FixStrategyEntry {
pub fn new(strategy: &str) -> Self {
Self {
strategy: strategy.into(),
..Default::default()
}
}
pub fn rename(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
strategy: "Rename".into(),
from: Some(from.into()),
to: Some(to.into()),
..Default::default()
}
}
pub fn with_from_to(strategy: &str, from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
strategy: strategy.into(),
from: Some(from.into()),
to: Some(to.into()),
..Default::default()
}
}
pub fn remove_prop(component: impl Into<String>, prop: impl Into<String>) -> Self {
Self {
strategy: "RemoveProp".into(),
component: Some(component.into()),
prop: Some(prop.into()),
..Default::default()
}
}
pub fn structural_migration(
removed_symbol: &str,
replacement_symbol: &str,
member_mappings: Vec<MemberMappingEntry>,
removed_members: Vec<String>,
overlap_ratio: f64,
) -> Self {
Self {
strategy: "LlmAssisted".into(),
from: Some(removed_symbol.into()),
to: Some(replacement_symbol.into()),
member_mappings,
removed_members,
replacement: Some(replacement_symbol.into()),
overlap_ratio: Some(overlap_ratio),
..Default::default()
}
}
pub fn ensure_dependency(package: impl Into<String>, new_version: impl Into<String>) -> Self {
Self {
strategy: "EnsureDependency".into(),
package: Some(package.into()),
new_version: Some(new_version.into()),
..Default::default()
}
}
pub fn to_mapping(&self) -> MappingEntry {
MappingEntry {
from: self.from.clone(),
to: self.to.clone(),
component: self.component.clone(),
prop: self.prop.clone(),
}
}
}
pub fn extract_fix_strategies(
rules: &[crate::rule::KonveyorRule],
) -> HashMap<String, FixStrategyEntry> {
rules
.iter()
.filter_map(|r| {
r.fix_strategy
.as_ref()
.map(|s| (r.rule_id.clone(), s.clone()))
})
.collect()
}
pub fn write_fix_strategies(
fix_dir: &Path,
strategies: &HashMap<String, FixStrategyEntry>,
) -> Result<()> {
let path = fix_dir.join("fix-strategies.json");
let json =
serde_json::to_string_pretty(strategies).context("Failed to serialize fix strategies")?;
std::fs::write(&path, &json).with_context(|| format!("Failed to write {}", path.display()))?;
Ok(())
}
pub fn strategy_priority(strategy: &str) -> u8 {
match strategy {
"Rename" => 5,
"RemoveProp" => 4,
"CssVariablePrefix" => 4,
"ImportPathChange" => 3,
"PropValueChange" => 2,
"PropTypeChange" => 2,
"LlmAssisted" => 1,
_ => 0,
}
}