use crate::{EditPlan, PlanAnnotations, PlanError, PlanErrorCode, RefactorOperation, RefactorPlan};
use blazingly_json::Value;
const LEGACY_TOP_LEVEL_FIELDS: [&str; 12] = [
"schemaVersion",
"operation",
"files",
"completeness",
"createdAt",
"graphRevision",
"completenessProof",
"uncertainReferences",
"notModified",
"warnings",
"followUp",
"syntaxCheck",
];
pub(crate) fn from_text_edit_plan(plan: EditPlan) -> Result<RefactorPlan, PlanError> {
let evidence = extract_annotations(&plan)?;
Ok(RefactorPlan {
schema_version: crate::REFACTOR_PLAN_SCHEMA.to_owned(),
operation: plan.operation,
operations: plan
.files
.into_iter()
.map(RefactorOperation::Modify)
.collect(),
completeness: plan.completeness,
evidence,
})
}
pub(crate) fn into_text_edit_plan(plan: RefactorPlan) -> Result<EditPlan, PlanError> {
let mut files = Vec::with_capacity(plan.operations.len());
for (index, operation) in plan.operations.into_iter().enumerate() {
let RefactorOperation::Modify(file) = operation else {
return Err(PlanError::new(
PlanErrorCode::NotTextOnly,
"legacy edit-plan conversion requires only modify operations",
)
.at_operation(index));
};
files.push(file);
}
let mut edit = EditPlan::new(plan.operation, files);
edit.completeness = plan.completeness;
attach_annotations(edit, &plan.evidence)
}
pub fn extract_annotations(plan: &EditPlan) -> Result<PlanAnnotations, PlanError> {
let object = plan.extensions.clone().into();
blazingly_json::from_value(Value::Object(object)).map_err(|error| {
PlanError::new(
PlanErrorCode::EvidenceMalformed,
format!("plan evidence is malformed: {error}"),
)
})
}
pub fn attach_annotations(
mut plan: EditPlan,
annotations: &PlanAnnotations,
) -> Result<EditPlan, PlanError> {
validate_evidence_extension_keys(annotations)?;
let Value::Object(object) = encode_evidence(annotations)? else {
return Err(json_shape_error());
};
for (key, value) in object {
if plan.extensions.insert(key.clone(), value).is_some() {
return Err(PlanError::new(
PlanErrorCode::ExtensionConflict,
format!("plan extension {key:?} already exists"),
)
.at_field(key));
}
}
Ok(plan)
}
pub fn detach_annotations(mut plan: EditPlan) -> Result<(EditPlan, PlanAnnotations), PlanError> {
let annotations = extract_annotations(&plan)?;
plan.extensions.clear();
Ok((plan, annotations))
}
pub fn replace_annotations(
plan: EditPlan,
annotations: &PlanAnnotations,
) -> Result<EditPlan, PlanError> {
let (base, _) = detach_annotations(plan)?;
attach_annotations(base, annotations)
}
fn encode_evidence(annotations: &PlanAnnotations) -> Result<Value, PlanError> {
blazingly_json::to_value(annotations).map_err(|error| {
PlanError::new(
PlanErrorCode::JsonEncoding,
format!("could not encode plan evidence: {error}"),
)
})
}
fn validate_evidence_extension_keys(annotations: &PlanAnnotations) -> Result<(), PlanError> {
check_keys(
&annotations.extensions,
&LEGACY_TOP_LEVEL_FIELDS,
"evidence",
)?;
if let Some(proof) = &annotations.completeness_proof {
check_keys(
&proof.extensions,
&["scope", "planner"],
"completenessProof",
)?;
check_keys(
&proof.scope.extensions,
&["kind", "value", "roots", "languages"],
"completenessProof.scope",
)?;
check_keys(
&proof.planner.extensions,
&["name", "version", "backend", "backendVersion"],
"completenessProof.planner",
)?;
}
for (index, reference) in annotations
.uncertain_references
.iter()
.flatten()
.enumerate()
{
check_keys(
&reference.extensions,
&[
"path", "file", "line", "subject", "kind", "reason", "excerpt",
],
&format!("uncertainReferences[{index}]"),
)?;
if let Some(subject) = &reference.subject {
check_keys(
&subject.extensions,
&["kind", "value"],
&format!("uncertainReferences[{index}].subject"),
)?;
}
}
for (index, entry) in annotations.not_modified.iter().flatten().enumerate() {
check_keys(
&entry.extensions,
&["path", "file", "subject", "operationIndex", "reason"],
&format!("notModified[{index}]"),
)?;
if let Some(subject) = &entry.subject {
check_keys(
&subject.extensions,
&["kind", "value"],
&format!("notModified[{index}].subject"),
)?;
}
}
Ok(())
}
fn check_keys(
extensions: &std::collections::BTreeMap<String, Value>,
reserved: &[&str],
field: &str,
) -> Result<(), PlanError> {
if let Some(key) = extensions
.keys()
.find(|key| reserved.contains(&key.as_str()))
{
return Err(PlanError::new(
PlanErrorCode::ExtensionConflict,
format!("extension {key:?} collides with a reserved field"),
)
.at_field(format!("{field}.{key}")));
}
Ok(())
}
fn json_shape_error() -> PlanError {
PlanError::new(
PlanErrorCode::JsonEncoding,
"plan evidence did not encode as an object",
)
}