draxl-patch 0.1.1

Structured semantic patch operations over the Draxl model
Documentation
use super::error::{map_fragment_parse_error, patch_text_error, PatchTextError};
use super::surface::{
    SurfaceDest, SurfaceFragment, SurfaceNodeRef, SurfacePatchOp, SurfacePath, SurfaceRankedDest,
    SurfaceSlotOwner, SurfaceSlotRef, SurfaceValue, SurfaceValueKind,
};
use crate::model::{PatchDest, PatchNode, PatchOp, PatchPath, PatchValue, RankedDest, SlotOwner};
use crate::schema::{
    clearable_path_spec, find_node_kind, invalid_clear_path_message, invalid_ranked_slot_message,
    invalid_set_path_message, invalid_single_slot_message, node_kind_label, path_spec,
    ranked_slot_spec, replace_fragment_kind, single_slot_spec, value_kind_label, FragmentKind,
    NodeKind, ValueKind,
};
use draxl_ast::{File, LowerLanguage, Span};

#[derive(Debug, Clone)]
pub(super) struct ResolvedPatchOp {
    pub op: PatchOp,
    pub span: Span,
}

pub(super) fn resolve_patch_ops(
    language: LowerLanguage,
    file: &File,
    source: &str,
    surface_ops: &[SurfacePatchOp],
) -> Result<Vec<ResolvedPatchOp>, PatchTextError> {
    let mut working = file.clone();
    let mut resolved = Vec::with_capacity(surface_ops.len());
    for surface_op in surface_ops {
        let op = resolve_op(language, &working, source, surface_op)?;
        crate::apply_op_for_language(language, &mut working, op.op.clone())
            .map_err(|err| patch_text_error(source, surface_op.span(), &err.message))?;
        resolved.push(op);
    }
    Ok(resolved)
}

pub(super) fn resolve_op(
    language: LowerLanguage,
    file: &File,
    source: &str,
    surface_op: &SurfacePatchOp,
) -> Result<ResolvedPatchOp, PatchTextError> {
    let span = surface_op.span();
    let op = match surface_op {
        SurfacePatchOp::Insert { dest, fragment, .. } => {
            let resolved_dest = resolve_ranked_dest(language, file, source, dest)?;
            let owner_kind = dest_owner_kind(language, file, source, &dest.slot.owner)?;
            let fragment_kind = ranked_slot_spec(language, owner_kind, &dest.slot.slot)
                .map(|spec| spec.fragment_kind)
                .expect("ranked destination fragment kind must already be validated before use");
            PatchOp::Insert {
                dest: resolved_dest,
                node: parse_fragment(language, source, fragment, fragment_kind)?,
            }
        }
        SurfacePatchOp::Put { slot, fragment, .. } => {
            let owner_kind = dest_owner_kind(language, file, source, &slot.owner)?;
            let fragment_kind = single_slot_spec(language, owner_kind, &slot.slot)
                .map(|spec| spec.fragment_kind)
                .ok_or_else(|| {
                    patch_text_error(
                        source,
                        slot.slot_span,
                        &invalid_single_slot_message(
                            language,
                            &owner_label(&slot.owner),
                            &slot.slot,
                        ),
                    )
                })?;
            PatchOp::Put {
                slot: resolve_slot_ref(language, file, source, slot)?,
                node: parse_fragment(language, source, fragment, fragment_kind)?,
            }
        }
        SurfacePatchOp::Replace {
            target, fragment, ..
        } => {
            let target_kind = resolve_node_kind(language, file, source, target)?;
            PatchOp::Replace {
                target_id: target.id.clone(),
                replacement: parse_fragment(
                    language,
                    source,
                    fragment,
                    replace_fragment_kind(language, target_kind),
                )?,
            }
        }
        SurfacePatchOp::Delete { target, .. } => PatchOp::Delete {
            target_id: ensure_node_exists(language, file, source, target)?.id,
        },
        SurfacePatchOp::Move { target, dest, .. } => PatchOp::Move {
            target_id: ensure_node_exists(language, file, source, target)?.id,
            dest: resolve_move_dest(language, file, source, dest)?,
        },
        SurfacePatchOp::Set { path, value, .. } => PatchOp::Set {
            path: resolve_path(language, file, source, path)?,
            value: resolve_value(language, file, source, path, value)?,
        },
        SurfacePatchOp::Clear { path, .. } => {
            let node_kind = resolve_node_kind(language, file, source, &path.node)?;
            ensure_single_segment_path(source, path)?;
            let segment = &path.segments[0];
            if clearable_path_spec(language, node_kind, &segment.name).is_none() {
                return Err(patch_text_error(
                    source,
                    segment.span,
                    &invalid_clear_path_message(language, &path.node.id, &segment.name, node_kind),
                ));
            }
            PatchOp::Clear {
                path: resolve_path(language, file, source, path)?,
            }
        }
        SurfacePatchOp::Attach { node, target, .. } => PatchOp::Attach {
            node_id: ensure_node_exists(language, file, source, node)?.id,
            target_id: ensure_node_exists(language, file, source, target)?.id,
        },
        SurfacePatchOp::Detach { node, .. } => PatchOp::Detach {
            node_id: ensure_node_exists(language, file, source, node)?.id,
        },
    };

    Ok(ResolvedPatchOp { op, span })
}

fn resolve_move_dest(
    language: LowerLanguage,
    file: &File,
    source: &str,
    dest: &SurfaceDest,
) -> Result<PatchDest, PatchTextError> {
    match dest {
        SurfaceDest::Ranked(dest) => Ok(PatchDest::Ranked(resolve_ranked_dest(
            language, file, source, dest,
        )?)),
        SurfaceDest::Slot(slot) => {
            let owner_kind = dest_owner_kind(language, file, source, &slot.owner)?;
            if single_slot_spec(language, owner_kind, &slot.slot).is_none() {
                return Err(patch_text_error(
                    source,
                    slot.slot_span,
                    &format!(
                        "slot `{}.{}` is not a single-child move destination on {}",
                        owner_label(&slot.owner),
                        slot.slot,
                        node_kind_label(language, owner_kind)
                    ),
                ));
            }
            Ok(PatchDest::Slot(resolve_slot_ref(
                language, file, source, slot,
            )?))
        }
    }
}

fn resolve_ranked_dest(
    language: LowerLanguage,
    file: &File,
    source: &str,
    dest: &SurfaceRankedDest,
) -> Result<RankedDest, PatchTextError> {
    let owner_kind = dest_owner_kind(language, file, source, &dest.slot.owner)?;
    if ranked_slot_spec(language, owner_kind, &dest.slot.slot).is_none() {
        return Err(patch_text_error(
            source,
            dest.slot.slot_span,
            &invalid_ranked_slot_message(language, &owner_label(&dest.slot.owner), &dest.slot.slot),
        ));
    }
    Ok(RankedDest {
        slot: resolve_slot_ref(language, file, source, &dest.slot)?,
        rank: dest.rank.clone(),
    })
}

fn resolve_slot_ref(
    language: LowerLanguage,
    file: &File,
    source: &str,
    slot: &SurfaceSlotRef,
) -> Result<crate::model::SlotRef, PatchTextError> {
    Ok(crate::model::SlotRef {
        owner: match &slot.owner {
            SurfaceSlotOwner::File { .. } => SlotOwner::File,
            SurfaceSlotOwner::Node(node) => {
                ensure_node_exists(language, file, source, node)?;
                SlotOwner::Node(node.id.clone())
            }
        },
        slot: slot.slot.clone(),
    })
}

fn resolve_path(
    language: LowerLanguage,
    file: &File,
    source: &str,
    path: &SurfacePath,
) -> Result<PatchPath, PatchTextError> {
    ensure_single_segment_path(source, path)?;
    ensure_node_exists(language, file, source, &path.node)?;
    Ok(PatchPath {
        node_id: path.node.id.clone(),
        segments: path
            .segments
            .iter()
            .map(|segment| segment.name.clone())
            .collect(),
    })
}

fn resolve_value(
    language: LowerLanguage,
    file: &File,
    source: &str,
    path: &SurfacePath,
    value: &SurfaceValue,
) -> Result<PatchValue, PatchTextError> {
    let node_kind = resolve_node_kind(language, file, source, &path.node)?;
    ensure_single_segment_path(source, path)?;
    let segment = &path.segments[0];
    let value_kind = path_spec(language, node_kind, &segment.name)
        .map(|spec| spec.value_kind)
        .ok_or_else(|| {
            patch_text_error(
                source,
                segment.span,
                &invalid_set_path_message(language, &path.node.id, &segment.name, node_kind),
            )
        })?;
    match (&value.kind, value_kind) {
        (SurfaceValueKind::Ident(inner), ValueKind::Ident) => Ok(PatchValue::Ident(inner.clone())),
        (SurfaceValueKind::Str(inner), ValueKind::Str) => Ok(PatchValue::Str(inner.clone())),
        (SurfaceValueKind::Bool(inner), ValueKind::Bool) => Ok(PatchValue::Bool(*inner)),
        (_, expected) => Err(patch_text_error(
            source,
            value.span,
            &format!(
                "path `@{}.{}` expects {}",
                path.node.id,
                segment.name,
                value_kind_label(language, expected)
            ),
        )),
    }
}

fn parse_fragment(
    language: LowerLanguage,
    source: &str,
    fragment: &SurfaceFragment,
    fragment_kind: FragmentKind,
) -> Result<PatchNode, PatchTextError> {
    match fragment_kind {
        FragmentKind::Item => {
            draxl_parser::parse_item_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Item)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Field => {
            draxl_parser::parse_field_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Field)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Variant => {
            draxl_parser::parse_variant_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Variant)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Param => {
            draxl_parser::parse_param_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Param)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Stmt => {
            draxl_parser::parse_stmt_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Stmt)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::MatchArm => {
            draxl_parser::parse_match_arm_fragment_for_language(language, &fragment.source)
                .map(PatchNode::MatchArm)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Expr => {
            draxl_parser::parse_expr_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Expr)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Type => {
            draxl_parser::parse_type_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Type)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Pattern => {
            draxl_parser::parse_pattern_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Pattern)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Doc => {
            draxl_parser::parse_doc_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Doc)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
        FragmentKind::Comment => {
            draxl_parser::parse_comment_fragment_for_language(language, &fragment.source)
                .map(PatchNode::Comment)
                .map_err(|error| map_fragment_parse_error(source, fragment.span.start, error))
        }
    }
}

fn dest_owner_kind(
    language: LowerLanguage,
    file: &File,
    source: &str,
    owner: &SurfaceSlotOwner,
) -> Result<NodeKind, PatchTextError> {
    match owner {
        SurfaceSlotOwner::File { .. } => Ok(NodeKind::File),
        SurfaceSlotOwner::Node(node) => resolve_node_kind(language, file, source, node),
    }
}

fn ensure_single_segment_path(source: &str, path: &SurfacePath) -> Result<(), PatchTextError> {
    if path.segments.len() == 1 {
        Ok(())
    } else {
        Err(patch_text_error(
            source,
            path.span,
            "only single-segment scalar patch paths are supported in the current Rust profile",
        ))
    }
}

fn ensure_node_exists(
    language: LowerLanguage,
    file: &File,
    source: &str,
    node: &SurfaceNodeRef,
) -> Result<SurfaceNodeRef, PatchTextError> {
    if find_node_kind(language, file, &node.id).is_some() {
        Ok(node.clone())
    } else {
        Err(patch_text_error(
            source,
            node.span,
            &format!("node `@{}` was not found", node.id),
        ))
    }
}

fn resolve_node_kind(
    language: LowerLanguage,
    file: &File,
    source: &str,
    node: &SurfaceNodeRef,
) -> Result<NodeKind, PatchTextError> {
    find_node_kind(language, file, &node.id).ok_or_else(|| {
        patch_text_error(
            source,
            node.span,
            &format!("node `@{}` was not found", node.id),
        )
    })
}

fn owner_label(owner: &SurfaceSlotOwner) -> String {
    match owner {
        SurfaceSlotOwner::File { .. } => "file".to_owned(),
        SurfaceSlotOwner::Node(node) => format!("@{}", node.id),
    }
}