use crate::error::Result;
use crate::ingest::rust::RustSymbolKind;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub steps: Vec<PatchStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchStep {
pub file: String,
pub symbol: String,
#[serde(rename = "kind")]
pub symbol_kind: Option<String>,
#[serde(rename = "with")]
pub with_file: String,
}
pub fn parse_plan(plan_path: &Path) -> Result<Plan> {
let content = fs::read_to_string(plan_path)?;
let plan: Plan =
serde_json::from_str(&content).map_err(|e| crate::SpliceError::InvalidPlanSchema {
message: format!("JSON parse error: {}", e),
})?;
if plan.steps.is_empty() {
return Err(crate::SpliceError::InvalidPlanSchema {
message: "Plan must contain at least one step".to_string(),
});
}
for (i, step) in plan.steps.iter().enumerate() {
if step.file.is_empty() {
return Err(crate::SpliceError::InvalidPlanSchema {
message: format!("Step {} has empty 'file' field", i + 1),
});
}
if step.symbol.is_empty() {
return Err(crate::SpliceError::InvalidPlanSchema {
message: format!("Step {} has empty 'symbol' field", i + 1),
});
}
if step.with_file.is_empty() {
return Err(crate::SpliceError::InvalidPlanSchema {
message: format!("Step {} has empty 'with' field", i + 1),
});
}
if let Some(ref kind) = step.symbol_kind {
match kind.as_str() {
"function" | "struct" | "enum" | "trait" | "impl" => {
}
_ => {
return Err(crate::SpliceError::InvalidPlanSchema {
message: format!(
"Step {} has invalid 'kind': '{}'. Must be one of: function, struct, enum, trait, impl",
i + 1,
kind
),
});
}
}
}
}
Ok(plan)
}
pub fn execute_plan(plan_path: &Path, workspace_dir: &Path) -> Result<Vec<String>> {
use crate::ingest::rust::RustSymbolKind;
let plan = parse_plan(plan_path)?;
let mut success_messages = Vec::new();
for (step_num, step) in plan.steps.iter().enumerate() {
let step_index = step_num + 1;
let file_path = workspace_dir.join(&step.file);
let with_file_path = workspace_dir.join(&step.with_file);
let rust_kind = match &step.symbol_kind {
None => None,
Some(kind) => Some(match kind.as_str() {
"function" => RustSymbolKind::Function,
"struct" => RustSymbolKind::Struct,
"enum" => RustSymbolKind::Enum,
"trait" => RustSymbolKind::Trait,
"impl" => RustSymbolKind::Impl,
_ => {
return Err(crate::SpliceError::Other(format!(
"Invalid symbol kind: {}",
kind
)));
}
}),
};
match execute_single_step(
&file_path,
&step.symbol,
rust_kind,
&with_file_path,
workspace_dir,
) {
Ok(msg) => {
println!("Step {}: {}", step_index, msg);
success_messages.push(msg);
}
Err(e) => {
return Err(crate::SpliceError::PlanExecutionFailed {
step: step_index,
error: e.to_string(),
});
}
}
}
Ok(success_messages)
}
fn execute_single_step(
file_path: &Path,
symbol_name: &str,
kind: Option<RustSymbolKind>,
replacement_file: &Path,
workspace_dir: &Path,
) -> Result<String> {
use crate::graph::CodeGraph;
use crate::ingest::rust::extract_rust_symbols;
use crate::patch::apply_patch_with_validation;
use crate::resolve::resolve_symbol;
use crate::symbol::Language;
use crate::validate::AnalyzerMode;
let source = std::fs::read(file_path)?;
let symbols = extract_rust_symbols(file_path, &source)?;
let graph_db_path = file_path
.parent()
.ok_or_else(|| {
crate::SpliceError::Other(format!("File path has no parent: {}", file_path.display()))
})?
.join(".splice_graph.db");
let mut code_graph = CodeGraph::open(&graph_db_path)?;
for symbol in &symbols {
code_graph.store_symbol_with_file_and_language(
file_path,
&symbol.name,
symbol.kind.as_str(),
Language::Rust,
symbol.byte_start,
symbol.byte_end,
symbol.line_start,
symbol.line_end,
symbol.col_start,
symbol.col_end,
)?;
}
let kind_str = kind.map(|k| k.as_str());
let resolved = resolve_symbol(&code_graph, Some(file_path), kind_str, symbol_name)?;
let replacement_content = std::fs::read_to_string(replacement_file)?;
let (before_hash, after_hash) = apply_patch_with_validation(
file_path,
resolved.byte_start,
resolved.byte_end,
&replacement_content,
workspace_dir,
Language::Rust,
AnalyzerMode::Off,
false, false, )?;
Ok(format!(
"Patched '{}' at bytes {}..{} (hash: {} -> {})",
symbol_name, resolved.byte_start, resolved.byte_end, before_hash, after_hash
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_plan() {
let json = r#"{"steps": [{"file": "src/lib.rs", "symbol": "foo", "kind": "function", "with": "patch.rs"}]}"#;
let plan: Plan = serde_json::from_str(json).unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].file, "src/lib.rs");
assert_eq!(plan.steps[0].symbol, "foo");
}
#[test]
fn test_parse_plan_empty_steps_fails() {
let _plan = Plan { steps: vec![] };
let result = serde_json::from_str::<Plan>(r#"{"steps": []}"#).unwrap();
assert!(result.steps.is_empty());
}
}