use super::graph::{Condition, ObjectKind, WorkflowGraph};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionResult {
DrilledIn {
target_uid: String,
},
OpenedUrl {
url_template: String,
},
RanScript {
target_uid: String,
},
TypedAutocomplete {
text: String,
},
DeadEnd,
}
impl ActionResult {
pub fn assert_drills_in(&self) -> &str {
match self {
Self::DrilledIn { target_uid } => target_uid,
other => panic!("expected DrilledIn, got {other:?}"),
}
}
pub fn assert_opens_url(&self) -> &str {
match self {
Self::OpenedUrl { url_template } => url_template,
other => panic!("expected OpenedUrl, got {other:?}"),
}
}
pub fn assert_runs_script(&self) -> &str {
match self {
Self::RanScript { target_uid } => target_uid,
other => panic!("expected RanScript, got {other:?}"),
}
}
pub fn assert_autocompletes(&self) -> &str {
match self {
Self::TypedAutocomplete { text } => text,
other => panic!("expected TypedAutocomplete, got {other:?}"),
}
}
pub fn is_dead_end(&self) -> bool {
matches!(self, Self::DeadEnd)
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ItemContext<'a> {
pub arg: Option<&'a str>,
pub variables: Option<&'a serde_json::Value>,
}
pub(crate) fn resolve_action(
graph: &WorkflowGraph,
source_uid: &str,
item_valid: bool,
item_autocomplete: Option<&str>,
item_context: &ItemContext<'_>,
modifiers: u64,
) -> ActionResult {
if !item_valid {
if let Some(text) = item_autocomplete {
return ActionResult::TypedAutocomplete {
text: text.to_string(),
};
}
}
resolve_from_node(graph, source_uid, item_context, modifiers, &mut Vec::new())
}
fn resolve_from_node(
graph: &WorkflowGraph,
uid: &str,
item_context: &ItemContext<'_>,
modifiers: u64,
visited: &mut Vec<String>,
) -> ActionResult {
if visited.contains(&uid.to_string()) {
return ActionResult::DeadEnd;
}
visited.push(uid.to_string());
let connections = graph.outgoing_connections(uid, Some(modifiers));
let connections = if connections.is_empty() && modifiers != 0 {
graph.outgoing_connections(uid, Some(0))
} else {
connections
};
if connections.is_empty() {
return ActionResult::DeadEnd;
}
let dest_uid = &connections[0].destination_uid;
let Some(dest_node) = graph.objects().get(dest_uid) else {
return ActionResult::DeadEnd;
};
match &dest_node.kind {
ObjectKind::ScriptFilter => ActionResult::DrilledIn {
target_uid: dest_uid.clone(),
},
ObjectKind::OpenUrl => {
let url_template = dest_node
.config_value("url")
.unwrap_or_default()
.to_string();
ActionResult::OpenedUrl { url_template }
}
ObjectKind::RunScript => ActionResult::RanScript {
target_uid: dest_uid.clone(),
},
ObjectKind::Conditional => {
resolve_conditional(graph, dest_node, dest_uid, item_context, modifiers, visited)
}
ObjectKind::CallExternalTrigger => {
resolve_external_trigger(graph, dest_node, item_context, modifiers, visited)
}
ObjectKind::Clipboard
| ObjectKind::Other(_)
| ObjectKind::Keyword
| ObjectKind::ExternalTrigger => ActionResult::RanScript {
target_uid: dest_uid.clone(),
},
}
}
fn resolve_conditional(
graph: &WorkflowGraph,
cond_node: &super::graph::ObjectNode,
cond_uid: &str,
item_context: &ItemContext<'_>,
modifiers: u64,
visited: &mut Vec<String>,
) -> ActionResult {
let conditions = &cond_node.conditions;
let connections = graph.outgoing_connections(cond_uid, Some(modifiers));
let connections = if connections.is_empty() && modifiers != 0 {
graph.outgoing_connections(cond_uid, Some(0))
} else {
connections
};
if connections.is_empty() {
return ActionResult::DeadEnd;
}
let has_output_routing = connections.iter().any(|c| c.source_output_uid.is_some());
if conditions.is_empty() || !has_output_routing {
let dest_uid = &connections[0].destination_uid;
let Some(dest_node) = graph.objects().get(dest_uid) else {
return ActionResult::DeadEnd;
};
return classify_or_continue(graph, dest_node, dest_uid, item_context, modifiers, visited);
}
if let Some(matched_uid) = evaluate_conditions(conditions, item_context) {
if let Some(conn) = connections
.iter()
.find(|c| c.source_output_uid.as_deref() == Some(matched_uid))
{
let dest_uid = &conn.destination_uid;
let Some(dest_node) = graph.objects().get(dest_uid) else {
return ActionResult::DeadEnd;
};
return classify_or_continue(
graph,
dest_node,
dest_uid,
item_context,
modifiers,
visited,
);
}
return ActionResult::DeadEnd;
}
let condition_uids: Vec<Option<&str>> = conditions.iter().map(|c| c.uid.as_deref()).collect();
let else_conn = connections.iter().find(|c| {
let output = c.source_output_uid.as_deref();
output.is_none() || !condition_uids.contains(&output)
});
if let Some(conn) = else_conn {
let dest_uid = &conn.destination_uid;
let Some(dest_node) = graph.objects().get(dest_uid) else {
return ActionResult::DeadEnd;
};
classify_or_continue(graph, dest_node, dest_uid, item_context, modifiers, visited)
} else {
ActionResult::DeadEnd
}
}
fn classify_or_continue(
graph: &WorkflowGraph,
dest_node: &super::graph::ObjectNode,
dest_uid: &str,
item_context: &ItemContext<'_>,
modifiers: u64,
visited: &mut Vec<String>,
) -> ActionResult {
match &dest_node.kind {
ObjectKind::ScriptFilter => ActionResult::DrilledIn {
target_uid: dest_uid.to_string(),
},
ObjectKind::OpenUrl => {
let url_template = dest_node
.config_value("url")
.unwrap_or_default()
.to_string();
ActionResult::OpenedUrl { url_template }
}
ObjectKind::RunScript => ActionResult::RanScript {
target_uid: dest_uid.to_string(),
},
ObjectKind::Conditional => {
resolve_conditional(graph, dest_node, dest_uid, item_context, modifiers, visited)
}
ObjectKind::CallExternalTrigger => {
resolve_external_trigger(graph, dest_node, item_context, modifiers, visited)
}
ObjectKind::Clipboard
| ObjectKind::Other(_)
| ObjectKind::Keyword
| ObjectKind::ExternalTrigger => ActionResult::RanScript {
target_uid: dest_uid.to_string(),
},
}
}
fn resolve_external_trigger(
graph: &WorkflowGraph,
call_node: &super::graph::ObjectNode,
item_context: &ItemContext<'_>,
modifiers: u64,
visited: &mut Vec<String>,
) -> ActionResult {
let Some(trigger_id) = call_node.config_value("externaltriggerid") else {
return ActionResult::DeadEnd;
};
let Some(trigger_uid) = graph.external_trigger_uid(trigger_id) else {
return ActionResult::DeadEnd;
};
resolve_from_node(graph, trigger_uid, item_context, modifiers, visited)
}
fn evaluate_conditions<'a>(
conditions: &'a [Condition],
item_context: &ItemContext<'_>,
) -> Option<&'a str> {
for condition in conditions {
let input_value = resolve_input_string(&condition.input_string, item_context);
if condition.match_mode.evaluate(
&input_value,
&condition.match_string,
condition.match_case_sensitive,
) {
return condition.uid.as_deref();
}
}
None
}
fn resolve_input_string(input_string: &str, item_context: &ItemContext<'_>) -> String {
if input_string == "{query}" {
return item_context.arg.unwrap_or("").to_string();
}
if let Some(var_name) = input_string
.strip_prefix("{var:")
.and_then(|s| s.strip_suffix('}'))
{
if let Some(vars) = item_context.variables {
if let Some(val) = vars.get(var_name).and_then(|v| v.as_str()) {
return val.to_string();
}
}
return String::new();
}
input_string.to_string()
}