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;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Written {
pub id: Option<String>,
#[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(),
}
}
pub fn created(mut self, created: bool) -> Self {
self.created = created;
self
}
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(", "))
}
}
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,
})
}
pub fn scope_names(root: &Path) -> Result<Vec<String>> {
Ok(config::read(root)?.scopes.into_keys().collect())
}
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()
}
const CLOSED_BY: [(&str, ClosedBy); 2] =
[("fact", ClosedBy::Fact), ("external", ClosedBy::External)];
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)
))
})
}
pub fn closed_by_help() -> String {
format!("One of {}.", words(&CLOSED_BY))
}
pub fn closure_help() -> String {
format!("One of {}.", words(&CLOSURE))
}
pub fn relation_help() -> String {
format!("One of {}.", Relation::names())
}
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(", ")
}
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()
))
})
}
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()
}
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()
}