use crate::metadata::{JsonBudget, JsonLimits};
use crate::{PlanError, PlanErrorCode, RefactorOperation, RefactorPlan, RefactorPlanLimits};
use blazingly_json::Value;
use std::collections::BTreeMap;
const PLAN_FIELDS: [&str; 12] = [
"schemaVersion",
"operation",
"operations",
"completeness",
"createdAt",
"graphRevision",
"completenessProof",
"uncertainReferences",
"notModified",
"warnings",
"followUp",
"syntaxCheck",
];
pub(crate) fn validate_extensions(
plan: &RefactorPlan,
limits: RefactorPlanLimits,
) -> Result<(), PlanError> {
let mut budget = JsonBudget::new(JsonLimits {
bytes: limits.max_extension_bytes,
nodes: limits.max_extension_nodes,
depth: limits.max_extension_depth,
key_bytes: limits.max_path_bytes,
});
visit(&mut budget, &plan.evidence.extensions, &PLAN_FIELDS, || {
"plan".to_owned()
})?;
visit_evidence(&mut budget, plan)?;
for (index, operation) in plan.operations.iter().enumerate() {
visit_operation(&mut budget, operation, index)?;
}
Ok(())
}
fn visit_evidence(budget: &mut JsonBudget, plan: &RefactorPlan) -> Result<(), PlanError> {
if let Some(proof) = &plan.evidence.completeness_proof {
visit(budget, &proof.extensions, &["scope", "planner"], || {
"completenessProof".to_owned()
})?;
visit(
budget,
&proof.scope.extensions,
&["kind", "value", "roots", "languages"],
|| "completenessProof.scope".to_owned(),
)?;
visit(
budget,
&proof.planner.extensions,
&["name", "version", "backend", "backendVersion"],
|| "completenessProof.planner".to_owned(),
)?;
}
for (index, reference) in plan
.evidence
.uncertain_references
.iter()
.flatten()
.enumerate()
{
visit(
budget,
&reference.extensions,
&[
"path", "file", "line", "subject", "kind", "reason", "excerpt",
],
|| format!("uncertainReferences[{index}]"),
)?;
if let Some(subject) = &reference.subject {
visit_subject(budget, &subject.extensions, || {
format!("uncertainReferences[{index}]")
})?;
}
}
visit_not_modified(budget, plan)
}
fn visit_not_modified(budget: &mut JsonBudget, plan: &RefactorPlan) -> Result<(), PlanError> {
for (index, entry) in plan.evidence.not_modified.iter().flatten().enumerate() {
visit(
budget,
&entry.extensions,
&["path", "file", "subject", "operationIndex", "reason"],
|| format!("notModified[{index}]"),
)?;
if let Some(subject) = &entry.subject {
visit_subject(budget, &subject.extensions, || {
format!("notModified[{index}]")
})?;
}
}
Ok(())
}
fn visit_subject<F: Fn() -> String>(
budget: &mut JsonBudget,
extensions: &BTreeMap<String, Value>,
parent: F,
) -> Result<(), PlanError> {
visit(budget, extensions, &["kind", "value"], || {
format!("{}.subject", parent())
})
}
fn visit_operation(
budget: &mut JsonBudget,
operation: &RefactorOperation,
index: usize,
) -> Result<(), PlanError> {
match operation {
RefactorOperation::Modify(file) => visit(
budget,
&file.extensions,
&["path", "sha256", "edits"],
|| "modify".to_owned(),
)
.and_then(|()| visit_edits(budget, &file.edits, index)),
RefactorOperation::Create(file) => visit(
budget,
&file.extensions,
&["path", "contents", "permissions"],
|| "create".to_owned(),
),
RefactorOperation::Delete(file) => visit(
budget,
&file.extensions,
&["path", "expectedSha256"],
|| "delete".to_owned(),
),
RefactorOperation::Rename(file) => visit(
budget,
&file.extensions,
&["from", "to", "expectedSourceSha256", "edits"],
|| "rename".to_owned(),
)
.and_then(|()| visit_edits(budget, &file.edits, index)),
}
.map_err(|error| error.at_operation(index))
}
fn visit_edits(
budget: &mut JsonBudget,
edits: &[crate::TextEdit],
operation_index: usize,
) -> Result<(), PlanError> {
const FIELDS: [&str; 7] = [
"startLine",
"startChar",
"endLine",
"endChar",
"before",
"after",
"provenance",
];
for (index, edit) in edits.iter().enumerate() {
visit(budget, &edit.extensions, &FIELDS, || {
format!("operations[{operation_index}].edits[{index}]")
})?;
}
Ok(())
}
fn visit<F: FnOnce() -> String>(
budget: &mut JsonBudget,
extensions: &BTreeMap<String, Value>,
reserved: &[&str],
field: F,
) -> Result<(), PlanError> {
if extensions.is_empty() {
return Ok(());
}
if let Some(key) = extensions
.keys()
.find(|key| reserved.contains(&key.as_str()))
{
return Err(PlanError::new(
PlanErrorCode::ExtensionConflict,
format!("extension field {key:?} is reserved"),
)
.at_field(format!("{}.{key}", field())));
}
budget.visit_map(extensions)
}