Skip to main content

cloacina_workflow/
trigger.rs

1/*
2 *  Copyright 2026 Colliery Software
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *  Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17//! Trigger types for workflow authoring.
18//!
19//! T-0552 (I-0102 follow-up) relocated the `Trigger` trait from `cloacina`
20//! (engine-only) into this leaf crate so packaged cdylibs can collect
21//! `TriggerEntry` inventory entries (which hold `Arc<dyn Trigger>`) at
22//! link time, and the unified `cloacina::package!()` shell macro can walk
23//! them at FFI call time. Engine paths re-export `cloacina_workflow::Trigger`.
24
25use crate::Context;
26use async_trait::async_trait;
27use std::collections::hash_map::DefaultHasher;
28use std::fmt;
29use std::hash::{Hash, Hasher};
30use std::time::Duration;
31
32/// Result of a trigger poll operation.
33#[derive(Debug)]
34pub enum TriggerResult {
35    /// Do not fire the workflow, continue polling on the next interval.
36    Skip,
37    /// Fire the workflow with an optional context.
38    Fire(Option<Context<serde_json::Value>>),
39}
40
41impl TriggerResult {
42    /// Returns true if this result indicates the workflow should fire.
43    pub fn should_fire(&self) -> bool {
44        matches!(self, TriggerResult::Fire(_))
45    }
46
47    /// Extracts the context if this is a Fire result.
48    pub fn into_context(self) -> Option<Context<serde_json::Value>> {
49        match self {
50            TriggerResult::Fire(ctx) => ctx,
51            TriggerResult::Skip => None,
52        }
53    }
54
55    /// Computes a hash of the context for deduplication purposes.
56    ///
57    /// If no context is provided, returns a constant hash. This allows
58    /// deduplication based on the specific trigger conditions.
59    pub fn context_hash(&self) -> String {
60        match self {
61            TriggerResult::Skip => "skip".to_string(),
62            TriggerResult::Fire(None) => "fire_no_context".to_string(),
63            TriggerResult::Fire(Some(ctx)) => {
64                let mut hasher = DefaultHasher::new();
65                if let Ok(serialized) = serde_json::to_string(ctx.data()) {
66                    serialized.hash(&mut hasher);
67                }
68                format!("{:016x}", hasher.finish())
69            }
70        }
71    }
72}
73
74/// Errors that can occur during trigger polling.
75#[derive(Debug, thiserror::Error)]
76pub enum TriggerError {
77    /// Error during trigger polling
78    #[error("Trigger poll error: {message}")]
79    PollError { message: String },
80    /// Context creation error
81    #[error("Context error: {0}")]
82    ContextError(#[from] crate::error::ContextError),
83}
84
85/// Core trait for user-defined triggers.
86///
87/// Triggers are polling functions that determine when a workflow should
88/// execute. Each trigger has a name, poll interval, and a `poll()` method
89/// that returns whether the workflow should fire.
90#[async_trait]
91pub trait Trigger: Send + Sync + fmt::Debug {
92    /// Returns the unique name of this trigger.
93    fn name(&self) -> &str;
94
95    /// Returns how often this trigger should be polled.
96    fn poll_interval(&self) -> Duration;
97
98    /// Returns whether concurrent executions with the same context are
99    /// allowed. When `false`, if a workflow execution with the same context
100    /// hash is already running, the trigger will not fire again until it
101    /// completes.
102    fn allow_concurrent(&self) -> bool;
103
104    /// Polls the trigger condition and returns whether to fire the workflow.
105    ///
106    /// Called at the configured `poll_interval`. Returns `TriggerResult::Skip`
107    /// to continue polling, `TriggerResult::Fire(ctx)` to fire the workflow.
108    /// Errors are logged and polling continues on the next interval.
109    async fn poll(&self) -> Result<TriggerResult, TriggerError>;
110
111    /// Returns this trigger's cron expression, if any. Cron-shaped triggers
112    /// override this to return `Some(expr)`; their `poll_interval` is ignored
113    /// and the reconciler routes them to the cron scheduler instead of the
114    /// runtime trigger registry. Default `None` covers all custom-poll
115    /// triggers.
116    fn cron_expression(&self) -> Option<String> {
117        None
118    }
119}