gy 2.1.0

good,yes — the ledger of what work rests on before a requirement is confirmed
//! The write side: projecting an `Outcome` to output, resolving the scope from
//! `gy.toml`, and parsing the small closed sets the write commands take.
use gy_ledger::{
    ClosedBy, Closure, Error, NodeData, NodeId, Outcome, Relation, Repository, Result, Store,
    config,
};
use serde::Serialize;
use std::fmt::{self, Display};
use std::path::Path;

/// What a write prints: the id, and what it changed, is missing, can follow,
/// and left unresolved.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Written {
    pub id: Option<String>,
    /// A node was created: the id line wears its name (n-16c8, r-045a).
    #[serde(skip)]
    pub created: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    pub changed: Vec<String>,
    pub missing: Vec<String>,
    pub next: Vec<String>,
    pub unresolved: Vec<String>,
}
impl Written {
    pub fn of<T>(outcome: &Outcome<T>) -> Self {
        Self {
            id: outcome.id.as_ref().map(ToString::to_string),
            created: false,
            reference: None,
            changed: outcome.changed.clone(),
            missing: outcome.missing.clone(),
            next: outcome.next.clone(),
            unresolved: outcome.unresolved.clone(),
        }
    }
    /// A creating write names its id line; the others return an id the caller
    /// already knows, so it stays bare.
    pub fn created(mut self, created: bool) -> Self {
        self.created = created;
        self
    }
    /// Put a requirement's outward reference beside its id (proposal-v3 12).
    pub fn reference(mut self, reference: Option<String>) -> Self {
        self.reference = reference;
        self
    }
}
impl Display for Written {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(id) = &self.id {
            let name = if self.created { "id: " } else { "" };
            match &self.reference {
                Some(reference) => writeln!(f, "{name}{id} ({reference})")?,
                None => writeln!(f, "{name}{id}")?,
            }
        }
        writeln!(f, "changed: {}", self.changed.join(", "))?;
        writeln!(f, "missing: {}", self.missing.join(", "))?;
        writeln!(f, "next: {}", self.next.join(", "))?;
        writeln!(f, "unresolved: {}", self.unresolved.join(", "))
    }
}

/// A requirement's outward reference, for the id line.
pub fn requirement_reference<S: Store>(
    repository: &Repository<S>,
    id: &NodeId,
) -> Result<Option<String>> {
    let node = repository
        .get(id)?
        .ok_or_else(|| Error::invalid(format!("{id} does not exist")))?;
    Ok(match node.data() {
        NodeData::Requirement(data) => data.reference.as_ref().map(|ref_| ref_.0.clone()),
        _ => None,
    })
}

/// The scope names gy.toml declares, for a move that must name one of them.
pub fn scope_names(root: &Path) -> Result<Vec<String>> {
    Ok(config::read(root)?.scopes.into_keys().collect())
}

/// The scope a write uses: `--scope`, or the only one gy.toml lists.
pub fn scope(root: &Path, given: Option<&str>) -> Result<String> {
    let settings = config::read(root)?;
    if let Some(name) = given {
        if !settings.scopes.contains_key(name) {
            let names: Vec<&str> = settings.scopes.keys().map(String::as_str).collect();
            return Err(Error::invalid(format!(
                "unknown scope {name}; expected one of {}",
                names.join(", ")
            )));
        }
        return Ok(name.to_string());
    }
    match settings.scopes.len() {
        1 => Ok(settings.scopes.keys().next().expect("one scope").clone()),
        0 => Err(Error::invalid(
            "gy.toml has no scope; add one or pass --scope",
        )),
        _ => Err(Error::invalid("gy.toml has several scopes; pass --scope")),
    }
}

pub fn resolve_all<S: Store>(repository: &Repository<S>, texts: &[String]) -> Result<Vec<NodeId>> {
    texts.iter().map(|text| repository.resolve(text)).collect()
}

pub fn resolve_opt<S: Store>(
    repository: &Repository<S>,
    text: Option<&str>,
) -> Result<Option<NodeId>> {
    text.map(|text| repository.resolve(text)).transpose()
}

/// The `--by` words a need close takes: one table for the parse and the help.
const CLOSED_BY: [(&str, ClosedBy); 2] =
    [("fact", ClosedBy::Fact), ("external", ClosedBy::External)];

/// The `--by` words a question close takes: one table for the parse and the help.
const CLOSURE: [(&str, Closure); 3] = [
    ("fact", Closure::Fact),
    ("decision", Closure::Decision),
    ("non-decision", Closure::NonDecision),
];

pub fn closed_by(text: &str) -> Result<ClosedBy> {
    find(&CLOSED_BY, text).ok_or_else(|| {
        Error::invalid(format!(
            "unknown --by {text}; expected one of {}",
            words(&CLOSED_BY)
        ))
    })
}

pub fn closure(text: &str) -> Result<Closure> {
    find(&CLOSURE, text).ok_or_else(|| {
        Error::invalid(format!(
            "unknown --by {text}; expected one of {}",
            words(&CLOSURE)
        ))
    })
}

/// The help for `need close --by`, from the table the parse reads (n-c82e).
pub fn closed_by_help() -> String {
    format!("One of {}.", words(&CLOSED_BY))
}

/// The help for `question close --by`, from the table the parse reads (n-c82e).
pub fn closure_help() -> String {
    format!("One of {}.", words(&CLOSURE))
}

/// The help for `link <relation>`, from the names the parse reads (n-c82e).
pub fn relation_help() -> String {
    format!("One of {}.", Relation::names())
}

/// The help for `decide --relate`, from the names the parse reads (n-c82e).
pub fn relate_help() -> String {
    format!(
        "One lineage relation and its decision: <relation> <D>, at most once. One of {}.",
        Relation::names()
    )
}

fn find<T: Copy>(table: &[(&str, T)], text: &str) -> Option<T> {
    table
        .iter()
        .find(|(name, _)| *name == text)
        .map(|(_, value)| *value)
}

fn words<T>(table: &[(&str, T)]) -> String {
    table
        .iter()
        .map(|(name, _)| *name)
        .collect::<Vec<_>>()
        .join(", ")
}

/// The canonical name of any relation a `link` may name.
pub fn relation(text: &str) -> Result<Relation> {
    Relation::ALL
        .into_iter()
        .find(|relation| relation.name() == text)
        .ok_or_else(|| {
            Error::invalid(format!(
                "unknown relation {text}; expected one of {}",
                Relation::names()
            ))
        })
}

/// `key=value` pairs, as `--set` and `--append` take them.
pub fn pairs(values: &[String]) -> Result<Vec<(String, String)>> {
    values
        .iter()
        .map(|value| {
            value
                .split_once('=')
                .map(|(key, value)| (key.to_string(), value.to_string()))
                .ok_or_else(|| Error::invalid(format!("expected key=value, got {value}")))
        })
        .collect()
}

/// A body read from a file, when one is named.
pub fn body_file(path: Option<&str>) -> Result<Option<String>> {
    path.map(|path| {
        std::fs::read_to_string(path).map_err(|error| Error::invalid(format!("{path}: {error}")))
    })
    .transpose()
}