use std::fmt;
use std::str::FromStr;
use crate::commands::create::planner_for;
use crate::config::Config;
use crate::relationships::RelationKind;
use super::registry::{CREATE_FIELDS, ReferenceKind, Template};
use super::resolver::MergedFields;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticKind {
UnknownTemplate,
InvalidScope,
ScopeMismatch,
MissingRequired,
UnknownReference,
InvalidValue,
}
impl DiagnosticKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
DiagnosticKind::UnknownTemplate => "unknown-template",
DiagnosticKind::InvalidScope => "invalid-scope",
DiagnosticKind::ScopeMismatch => "scope-mismatch",
DiagnosticKind::MissingRequired => "missing-required",
DiagnosticKind::UnknownReference => "unknown-reference",
DiagnosticKind::InvalidValue => "invalid-value",
}
}
}
impl fmt::Display for DiagnosticKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateDiagnostic {
kind: DiagnosticKind,
field: String,
message: String,
}
impl TemplateDiagnostic {
pub(crate) fn new(
kind: DiagnosticKind,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
kind,
field: field.into(),
message: message.into(),
}
}
#[must_use]
pub fn kind(&self) -> DiagnosticKind {
self.kind
}
#[must_use]
pub fn field(&self) -> &str {
&self.field
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for TemplateDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.field, self.message)
}
}
pub(crate) fn missing_required(
template: &Template,
merged: &MergedFields,
) -> Vec<TemplateDiagnostic> {
let mut out = Vec::new();
for field in CREATE_FIELDS {
let required = *field == "title"
|| template
.prompts
.iter()
.any(|prompt| prompt.key == *field && prompt.required);
if required && merged.is_blank(field) {
let message = if *field == "title" {
"a title is required; set it directly or with a required prompt/default".to_string()
} else {
format!("required prompt `{field}` has no value")
};
out.push(TemplateDiagnostic::new(
DiagnosticKind::MissingRequired,
*field,
message,
));
}
}
out
}
pub(crate) fn invalid_values(merged: &MergedFields) -> Vec<TemplateDiagnostic> {
let mut out = Vec::new();
if let Some(raw) = merged.get("priority") {
let trimmed = raw.trim();
if !trimmed.is_empty()
&& let Err(error) = crate::commands::parse_priority(trimmed)
{
out.push(TemplateDiagnostic::new(
DiagnosticKind::InvalidValue,
"priority",
error.to_string(),
));
}
}
out
}
pub(crate) fn validate_references(
config: &Config,
template: &Template,
merged: &MergedFields,
) -> Vec<TemplateDiagnostic> {
let planner = planner_for(config);
let mut out = Vec::new();
for (index, reference) in template.references.iter().enumerate() {
let field = format!("references[{index}].value");
let resolved = match reference.kind {
ReferenceKind::Label | ReferenceKind::Assignee | ReferenceKind::File => true,
ReferenceKind::Cycle => {
if planner.cycle(&reference.value).is_some() {
true
} else {
let known = planner.cycle_names();
out.push(TemplateDiagnostic::new(
DiagnosticKind::UnknownReference,
&field,
unknown_message("cycle", &reference.value, &known),
));
false
}
}
ReferenceKind::Milestone => {
if planner.milestone(&reference.value).is_ok() {
true
} else {
let known = planner.milestone_names().unwrap_or_default();
out.push(TemplateDiagnostic::new(
DiagnosticKind::UnknownReference,
&field,
unknown_message("milestone", &reference.value, &known),
));
false
}
}
ReferenceKind::Relationship => {
if let Err(error) = RelationKind::from_str(&reference.value) {
out.push(TemplateDiagnostic::new(
DiagnosticKind::UnknownReference,
&field,
error.to_string(),
));
false
} else {
true
}
}
};
if !resolved || !reference.required {
continue;
}
if let Some(target) = reference_field(reference.kind)
&& !contains_value(merged, target, &reference.value)
{
out.push(TemplateDiagnostic::new(
DiagnosticKind::MissingRequired,
&field,
format!(
"required {} `{}` is missing from `{target}`",
kind_label(reference.kind),
reference.value
),
));
}
}
out
}
fn unknown_message(kind: &str, value: &str, known: &[String]) -> String {
let list = if known.is_empty() {
"none configured".to_string()
} else {
known.join(", ")
};
format!("unknown {kind} `{value}` — valid {kind}s: {list}")
}
fn reference_field(kind: ReferenceKind) -> Option<&'static str> {
match kind {
ReferenceKind::Label => Some("labels"),
ReferenceKind::Assignee => Some("assignee"),
ReferenceKind::File => Some("files"),
ReferenceKind::Cycle => Some("cycles"),
ReferenceKind::Milestone => Some("milestones"),
ReferenceKind::Relationship => None,
}
}
fn kind_label(kind: ReferenceKind) -> &'static str {
match kind {
ReferenceKind::Label => "label",
ReferenceKind::Assignee => "assignee",
ReferenceKind::File => "file",
ReferenceKind::Cycle => "cycle",
ReferenceKind::Milestone => "milestone",
ReferenceKind::Relationship => "relationship",
}
}
fn contains_value(merged: &MergedFields, field: &str, needle: &str) -> bool {
let needle = needle.trim();
match merged.get(field) {
None => false,
Some(raw) if field == "assignee" => raw.trim() == needle,
Some(raw) => crate::commands::parse_csv_list(raw)
.iter()
.any(|value| value == needle),
}
}