govctl 0.12.0

Project governance CLI for RFC, ADR, and Work Item management
use super::super::adapter::{DocAdapter, RfcTomlAdapter};
use super::super::engine as edit_engine;
use super::super::matching::MatchOptions;
use super::super::runtime as edit_runtime;
use super::super::target_doc::{NestedGetMode, add_to_target_doc, render_target_from_doc};
use super::super::target_doc_remove::{notify_removed, remove_target_from_doc};
use super::{ArtifactType, deserialize_edit_doc, serialize_edit_doc};
use crate::cmd::output::print_json;
use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use crate::model::ChangelogEntry;
use crate::write::{WriteOp, current_changelog_entry, current_changelog_entry_mut};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct CurrentChangelogDoc {
    changelog: CurrentChangelogView,
}

#[derive(Debug, Serialize, Deserialize)]
struct CurrentChangelogView {
    version: String,
    date: String,
    summary: Option<String>,
    added: Vec<String>,
    changed: Vec<String>,
    deprecated: Vec<String>,
    removed: Vec<String>,
    fixed: Vec<String>,
    security: Vec<String>,
}

impl From<&ChangelogEntry> for CurrentChangelogView {
    fn from(entry: &ChangelogEntry) -> Self {
        Self {
            version: entry.version.clone(),
            date: entry.date.clone(),
            summary: entry.notes.clone(),
            added: entry.added.clone(),
            changed: entry.changed.clone(),
            deprecated: entry.deprecated.clone(),
            removed: entry.removed.clone(),
            fixed: entry.fixed.clone(),
            security: entry.security.clone(),
        }
    }
}

pub(in crate::cmd::edit) fn is_target(target: &edit_engine::ResolvedTarget) -> bool {
    target
        .path()
        .segments
        .first()
        .is_some_and(|segment| segment.name == "changelog")
}

pub(in crate::cmd::edit) fn get(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
) -> DiagnosticResult<()> {
    let loaded = RfcTomlAdapter::load(config, id)?;
    let doc = current_changelog_doc(&loaded.data, id)?;

    let root_only = target.path().segments.len() == 1 && target.path().segments[0].index.is_none();
    if !root_only {
        render_target_from_doc(ArtifactType::Rfc, &doc, target, id, NestedGetMode::Allow)?;
        return Err(Diagnostic::new(
            DiagnosticCode::E0804FieldNotEditable,
            "RFC changelog get supports only the current-entry root path",
            id,
        ));
    }

    print_json(
        doc.get("changelog").unwrap_or(&serde_json::Value::Null),
        DiagnosticCode::E0903UnexpectedError,
        "Failed to serialize current RFC changelog entry",
        id,
    )
}

pub(in crate::cmd::edit) fn set(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
    value: &str,
    op: WriteOp,
) -> DiagnosticResult<()> {
    let mut loaded = RfcTomlAdapter::load(config, id)?;
    crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?;
    let mut doc = current_changelog_doc(&loaded.data, id)?;
    edit_runtime::set_nested_field(ArtifactType::Rfc, &mut doc, target.path(), value, id)?;
    apply_current_changelog_doc(&mut loaded.data, doc, id)?;
    RfcTomlAdapter::write(config, &loaded, op)
}

pub(in crate::cmd::edit) fn add(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
    value: &str,
    op: WriteOp,
) -> DiagnosticResult<()> {
    let mut loaded = RfcTomlAdapter::load(config, id)?;
    crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?;
    let mut doc = current_changelog_doc(&loaded.data, id)?;
    add_to_target_doc(ArtifactType::Rfc, &mut doc, target, value, id)?;
    apply_current_changelog_doc(&mut loaded.data, doc, id)?;
    RfcTomlAdapter::write(config, &loaded, op)
}

pub(in crate::cmd::edit) fn remove(
    config: &Config,
    id: &str,
    target: &edit_engine::ResolvedTarget,
    opts: &MatchOptions,
    op: WriteOp,
) -> DiagnosticResult<()> {
    if !matches!(
        target,
        edit_engine::ResolvedTarget::IndexedItem {
            origin: edit_engine::TargetOrigin::Nested,
            ..
        }
    ) {
        return Err(Diagnostic::new(
            DiagnosticCode::E0817PathTypeMismatch,
            "RFC changelog removal requires an indexed current-category path",
            id,
        ));
    }

    let mut loaded = RfcTomlAdapter::load(config, id)?;
    crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?;
    let mut doc = current_changelog_doc(&loaded.data, id)?;
    let (display_field, removed) =
        remove_target_from_doc(ArtifactType::Rfc, &mut doc, id, target, opts)?;
    apply_current_changelog_doc(&mut loaded.data, doc, id)?;
    RfcTomlAdapter::write(config, &loaded, op)?;
    notify_removed(id, &display_field, &removed, op);
    Ok(())
}

fn current_changelog_doc(
    rfc: &crate::model::RfcSpec,
    id: &str,
) -> DiagnosticResult<serde_json::Value> {
    let entry = current_changelog_entry(rfc)?;
    serialize_edit_doc(
        &CurrentChangelogDoc {
            changelog: CurrentChangelogView::from(entry),
        },
        id,
    )
}

fn apply_current_changelog_doc(
    rfc: &mut crate::model::RfcSpec,
    doc: serde_json::Value,
    id: &str,
) -> DiagnosticResult<()> {
    let view: CurrentChangelogDoc = deserialize_edit_doc(doc, id)?;
    let entry = current_changelog_entry_mut(rfc)?;
    entry.notes = view.changelog.summary;
    entry.added = view.changelog.added;
    entry.changed = view.changelog.changed;
    entry.deprecated = view.changelog.deprecated;
    entry.removed = view.changelog.removed;
    entry.fixed = view.changelog.fixed;
    entry.security = view.changelog.security;
    Ok(())
}