use crate::diagnostics::Diagnostic;
use crate::time_tracking::entry::TimeEntryError;
use crate::time_tracking::filters::TimeFilterError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeDiagnosticKind {
InvalidDuration,
InvalidDate,
MissingAuthor,
MalformedEntry,
InvalidPeriod,
InvalidAssignee,
UnknownIssue,
UnsupportedRemoteField,
WriteFailed,
}
impl TimeDiagnosticKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidDuration => "invalid_duration",
Self::InvalidDate => "invalid_date",
Self::MissingAuthor => "missing_author",
Self::MalformedEntry => "malformed_entry",
Self::InvalidPeriod => "invalid_period",
Self::InvalidAssignee => "invalid_assignee",
Self::UnknownIssue => "unknown_issue",
Self::UnsupportedRemoteField => "unsupported_remote_field",
Self::WriteFailed => "write_failed",
}
}
pub const fn default_remedy(self) -> &'static str {
match self {
Self::InvalidDuration => {
"write `duration_minutes` as a non-negative whole number of minutes, or remove \
the entry"
}
Self::InvalidDate => {
"write the date as `YYYY-MM-DD` (year >= 1970), or remove it: an optional date \
that cannot be read is not the same as no date"
}
Self::MissingAuthor => {
"name who did the work: `author:` takes a handle (`ada`, `@ada`) or a full \
`Name <email>` identity"
}
Self::MalformedEntry => {
"give the entry a stable `id:` that is never reused, keep `note:` to one line, and \
rename any preserved key that would shadow a typed field"
}
Self::InvalidPeriod => {
"swap the bounds: the period is inclusive and its start must not be later than its \
end"
}
Self::InvalidAssignee => {
"name an identity (`ada`, `@ada`, or `Ada Lovelace <ada@example.com>`), or drop \
the criterion"
}
Self::UnknownIssue => {
"read that issue into the scope, or drop the selector: an issue the snapshot does \
not hold is not an issue that recorded nothing"
}
Self::UnsupportedRemoteField => {
"keep the time data local: the remote target is left untouched and the local \
entries are unchanged"
}
Self::WriteFailed => {
"fix the reported store error and retry; nothing was written, so the current \
entries and History are unchanged"
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeDiagnostic {
pub kind: TimeDiagnosticKind,
pub issue: Option<u32>,
pub entry_id: Option<String>,
pub field: Option<&'static str>,
pub reason: String,
pub remedy: String,
}
impl TimeDiagnostic {
pub fn new(kind: TimeDiagnosticKind, reason: impl Into<String>) -> Self {
Self {
kind,
issue: None,
entry_id: None,
field: None,
reason: reason.into(),
remedy: kind.default_remedy().to_string(),
}
}
pub fn from_entry_problem(issue: u32, entry_id: &str, problem: &TimeEntryError) -> Self {
let kind = match problem {
TimeEntryError::InvalidDuration { .. } => TimeDiagnosticKind::InvalidDuration,
TimeEntryError::InvalidDate { .. } => TimeDiagnosticKind::InvalidDate,
TimeEntryError::MissingAuthor => TimeDiagnosticKind::MissingAuthor,
TimeEntryError::MissingId { .. }
| TimeEntryError::NoteNotSingleLine
| TimeEntryError::ExtensionShadowsField { .. } => TimeDiagnosticKind::MalformedEntry,
};
Self::new(kind, problem.to_string())
.for_entry(issue, entry_id)
.in_field(problem.field())
}
pub fn from_filter_problem(problem: &TimeFilterError) -> Self {
let kind = match problem {
TimeFilterError::InvalidDate { .. } => TimeDiagnosticKind::InvalidDate,
TimeFilterError::InvertedPeriod { .. } => TimeDiagnosticKind::InvalidPeriod,
TimeFilterError::InvalidAssignee { .. } => TimeDiagnosticKind::InvalidAssignee,
};
Self::new(kind, problem.message())
.in_field(problem.field())
.with_remedy(problem.remedy())
}
pub fn unknown_issue(issue: u32) -> Self {
Self::new(
TimeDiagnosticKind::UnknownIssue,
format!(
"issue #{issue} is not among the issues this snapshot holds, so its time was not \
totalled"
),
)
.for_issue(issue)
}
pub fn for_issue(mut self, issue: u32) -> Self {
self.issue = Some(issue);
self
}
pub fn for_entry(mut self, issue: u32, entry_id: &str) -> Self {
self.issue = Some(issue);
self.entry_id = Some(entry_id.to_string());
self
}
pub fn in_field(mut self, field: &'static str) -> Self {
self.field = Some(field);
self
}
pub fn with_remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = remedy.into();
self
}
pub const fn is_scope_finding(&self) -> bool {
self.entry_id.is_none()
}
pub fn render(&self) -> String {
let mut diagnostic = Diagnostic::warning("E-tix-time", self.reason.clone())
.with_note(format!("kind: {}", self.kind.as_str()));
if let Some(field) = self.field {
diagnostic = diagnostic.with_note(format!("field: {field}"));
}
if let Some(issue) = self.issue {
diagnostic = diagnostic.with_note(format!("issue: #{issue}"));
}
if let Some(entry_id) = self.entry_id.as_deref() {
let named = if entry_id.trim().is_empty() {
"(no id)"
} else {
entry_id
};
diagnostic = diagnostic.with_note(format!("entry: {named}"));
}
diagnostic.with_help(&self.remedy).render()
}
}