use super::TASK_NOTIFICATION_CLOSE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutomationKind {
BackgroundCommand,
Workflow,
Agent,
Monitor,
Task,
}
impl AutomationKind {
#[must_use]
pub fn from_summary(summary: Option<&str>) -> Self {
let s = summary.unwrap_or("").trim_start();
let lower = s.to_ascii_lowercase();
if lower.starts_with("background command") {
AutomationKind::BackgroundCommand
} else if lower.starts_with("dynamic workflow") || lower.starts_with("workflow") {
AutomationKind::Workflow
} else if lower.starts_with("monitor")
|| lower.starts_with("scheduled")
|| lower.starts_with("cron")
{
AutomationKind::Monitor
} else if lower.starts_with("agent") {
AutomationKind::Agent
} else {
AutomationKind::Task
}
}
#[must_use]
pub fn slug(self) -> &'static str {
match self {
AutomationKind::BackgroundCommand => "background-command",
AutomationKind::Workflow => "workflow",
AutomationKind::Agent => "agent",
AutomationKind::Monitor => "monitor",
AutomationKind::Task => "task",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AutomationTrigger {
pub kind: AutomationKind,
pub task_id: Option<String>,
pub status: Option<String>,
pub summary: Option<String>,
pub event: Option<String>,
}
pub(crate) const ORPHAN_SENTINEL_PREFIX: &str = "__orphan_summary";
pub(crate) const ORPHAN_KIND_SENTINEL: &str = "__orphan_summary__:";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TaskIds {
pub ids: Vec<String>,
pub orphan_kind: Option<String>,
}
pub(crate) fn section_task_ids(section: &str) -> TaskIds {
let scope = match section.find(TASK_NOTIFICATION_CLOSE) {
Some(end) => §ion[..end],
None => section,
};
let mut out = TaskIds::default();
for tag in all_xml_tags(scope, "task-id") {
if let Some(kind) = tag.strip_prefix(ORPHAN_KIND_SENTINEL) {
if out.orphan_kind.is_none() && !kind.is_empty() {
out.orphan_kind = Some(kind.to_string());
}
} else if !tag.starts_with(ORPHAN_SENTINEL_PREFIX) {
out.ids.push(tag);
}
}
out
}
pub(crate) fn all_xml_tags(s: &str, tag: &str) -> Vec<String> {
let open = format!("<{tag}>");
let close = format!("</{tag}>");
let mut out = Vec::new();
let mut at = 0usize;
while let Some(i) = s[at..].find(&open) {
let start = at + i + open.len();
let Some(j) = s[start..].find(&close) else {
break;
};
let inner = s[start..start + j].trim();
if !inner.is_empty() {
out.push(inner.to_string());
}
at = start + j + close.len();
}
out
}
pub(crate) fn extract_xml_tag(s: &str, tag: &str) -> Option<String> {
let open = format!("<{tag}>");
let close = format!("</{tag}>");
let start = s.find(&open)? + open.len();
let end_rel = s[start..].find(&close)?;
let inner = s[start..start + end_rel].trim();
if inner.is_empty() {
None
} else {
Some(inner.to_string())
}
}
pub(crate) fn normalize_line(s: &str) -> String {
normalize_collapse(s, |_| {})
}
pub(crate) fn normalize_line_with_newlines(s: &str) -> (String, Vec<u32>) {
let mut positions: Vec<u32> = Vec::new();
let out = normalize_collapse(s, |at| positions.push(at));
(out, positions)
}
fn normalize_collapse(s: &str, mut on_newline: impl FnMut(u32)) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = 0usize;
let mut run_at = 0usize;
let mut prev_ws = false;
for ch in s.chars() {
if ch.is_whitespace() {
if !prev_ws {
run_at = chars;
if !out.is_empty() {
out.push(' ');
chars += 1;
}
}
prev_ws = true;
if ch == '\n' {
on_newline(run_at.min(u32::MAX as usize) as u32);
}
} else {
out.push(ch);
chars += 1;
prev_ws = false;
}
}
while out.ends_with(' ') {
out.pop();
}
out
}