govctl 0.19.0

Project governance CLI for RFC, ADR, and Work Item management
use super::TomlEditableEntry;
use super::work_dependencies::{is_work_dependency_target, validate_work_dependency_edit};
use crate::cmd::edit::adapter::{TomlAdapter, WorkTomlAdapter};
use crate::cmd::edit::engine as edit_engine;
use crate::cmd::edit::refs::{is_refs_target, validate_ref_edit};
use crate::cmd::edit::rules::NestedObjectSetMode;
use crate::cmd::edit::runtime as edit_runtime;
use crate::cmd::edit::{
    ArtifactType, deserialize_edit_doc, serialize_edit_doc, unexpected_edit_state,
};
use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use crate::model::WorkItemEntry;
use crate::write::WriteOp;

pub(in crate::cmd::edit) fn set_toml_field<A>(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
    value: &str,
    op: WriteOp,
    artifact: ArtifactType,
    allow_forced_simple_set: bool,
) -> DiagnosticResult<()>
where
    A: TomlAdapter,
    A::Entry: TomlEditableEntry,
{
    let mut entry = A::load(config, id)?;
    apply_toml_target_to_entry(
        &mut entry,
        target,
        value,
        artifact,
        allow_forced_simple_set,
        id,
    )?;
    if is_refs_target(target) {
        validate_ref_edit(config, artifact, id, value)?;
    }
    A::write(config, &entry, op)?;
    Ok(())
}

pub(in crate::cmd::edit) fn set_work_toml_field(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
    value: &str,
    op: WriteOp,
    allow_forced_simple_set: bool,
) -> DiagnosticResult<()> {
    let mut entry = WorkTomlAdapter::load(config, id)?;
    if let Some(index) = acceptance_criterion_index(target) {
        set_acceptance_criterion(&mut entry, index, value, id)?;
    } else {
        apply_toml_target_to_entry(
            &mut entry,
            target,
            value,
            ArtifactType::WorkItem,
            allow_forced_simple_set,
            id,
        )?;
    }
    if is_refs_target(target) {
        validate_ref_edit(config, ArtifactType::WorkItem, id, value)?;
    }
    if is_work_dependency_target(target) {
        validate_work_dependency_edit(config, &entry)?;
    }
    WorkTomlAdapter::write(config, &entry, op)?;
    Ok(())
}

fn acceptance_criterion_index(target: &edit_engine::ResolvedTarget) -> Option<i32> {
    match target {
        edit_engine::ResolvedTarget::IndexedItem {
            index,
            item_kind: edit_engine::TargetKind::Object,
            object_set_mode: Some(NestedObjectSetMode::AcceptanceCriterion),
            ..
        } => Some(*index),
        _ => None,
    }
}

fn set_acceptance_criterion(
    entry: &mut WorkItemEntry,
    index: i32,
    value: &str,
    id: &str,
) -> DiagnosticResult<()> {
    let parsed = crate::write::parse_changelog_change_with_fallback(value)?;
    if parsed.message.is_empty() {
        return Err(empty_acceptance_criterion(id));
    }
    let criteria = &mut entry.spec.content.acceptance_criteria;
    let resolved = crate::cmd::edit::path::resolve_index(index, criteria.len())?;
    let criterion = &mut criteria[resolved];
    criterion.text = parsed.message;
    if parsed.explicit {
        criterion.category = parsed.category;
    }
    Ok(())
}

fn empty_acceptance_criterion(id: &str) -> Diagnostic {
    Diagnostic::new(
        DiagnosticCode::E0805EmptyValue,
        "Acceptance criterion text cannot be empty",
        id,
    )
}

fn apply_toml_target_to_entry<E>(
    entry: &mut E,
    target: &edit_engine::ResolvedTarget,
    value: &str,
    artifact: ArtifactType,
    allow_forced_simple_set: bool,
    id: &str,
) -> DiagnosticResult<()>
where
    E: TomlEditableEntry,
{
    let mut doc = serialize_edit_doc(entry.spec(), id)?;
    match target {
        edit_engine::ResolvedTarget::Node {
            path,
            kind: edit_engine::TargetKind::Scalar,
            origin,
            ..
        } => match origin {
            edit_engine::TargetOrigin::Simple => {
                let simple = path
                    .as_simple()
                    .ok_or_else(|| unexpected_edit_state(id, "simple target path expected"))?;
                if allow_forced_simple_set {
                    edit_runtime::set_simple_field_forced(artifact, &mut doc, simple, value, id)?;
                } else {
                    edit_runtime::set_simple_field(artifact, &mut doc, simple, value, id)?;
                }
            }
            edit_engine::TargetOrigin::Nested => {
                edit_runtime::set_nested_field(artifact, &mut doc, path, value, id)?;
            }
        },
        edit_engine::ResolvedTarget::IndexedItem {
            origin,
            path,
            container_path,
            index,
            item_kind: edit_engine::TargetKind::Scalar,
            ..
        } => match origin {
            edit_engine::TargetOrigin::Simple => {
                let simple = container_path.as_simple().ok_or_else(|| {
                    unexpected_edit_state(id, "simple indexed container expected")
                })?;
                edit_runtime::set_simple_list_item(artifact, &mut doc, simple, *index, value, id)?;
            }
            edit_engine::TargetOrigin::Nested => {
                edit_runtime::set_nested_field(artifact, &mut doc, path, value, id)?;
            }
        },
        _ => {
            return Err(Diagnostic::new(
                DiagnosticCode::E0817PathTypeMismatch,
                "set requires a scalar field or indexed scalar list item",
                id,
            ));
        }
    }
    *entry.spec_mut() = deserialize_edit_doc(doc, id)?;
    Ok(())
}