1use std::borrow::Cow;
25use std::collections::HashMap;
26use std::fmt;
27
28use crate::common::{doc_anchor_for_rule, ErrorInfo, Phase};
29
30pub mod implementations;
31pub mod registry;
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum TriggerKind {
35 Trigger,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub enum TriggerValueType {
40 Number,
41 Series,
42 Bool,
43 Event,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub enum TriggerEvent {
48 Emitted,
49 NotEmitted,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub enum TriggerValue {
54 Number(f64),
55 Series(Vec<f64>),
56 Bool(bool),
57 Event(TriggerEvent),
58}
59
60impl TriggerValue {
61 pub fn value_type(&self) -> TriggerValueType {
62 match self {
63 TriggerValue::Number(_) => TriggerValueType::Number,
64 TriggerValue::Series(_) => TriggerValueType::Series,
65 TriggerValue::Bool(_) => TriggerValueType::Bool,
66 TriggerValue::Event(_) => TriggerValueType::Event,
67 }
68 }
69
70 pub fn as_number(&self) -> Option<f64> {
71 match self {
72 TriggerValue::Number(n) => Some(*n),
73 _ => None,
74 }
75 }
76
77 pub fn as_bool(&self) -> Option<bool> {
78 match self {
79 TriggerValue::Bool(b) => Some(*b),
80 _ => None,
81 }
82 }
83
84 pub fn as_event(&self) -> Option<&TriggerEvent> {
85 match self {
86 TriggerValue::Event(e) => Some(e),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub enum ParameterType {
94 Int,
95 Number,
96 Bool,
97 String,
98 Enum,
99}
100
101#[derive(Debug, Clone, PartialEq)]
102pub enum ParameterValue {
103 Int(i64),
104 Number(f64),
105 Bool(bool),
106 String(String),
107 Enum(String),
108}
109
110impl ParameterValue {
111 pub fn value_type(&self) -> ParameterType {
112 match self {
113 ParameterValue::Int(_) => ParameterType::Int,
114 ParameterValue::Number(_) => ParameterType::Number,
115 ParameterValue::Bool(_) => ParameterType::Bool,
116 ParameterValue::String(_) => ParameterType::String,
117 ParameterValue::Enum(_) => ParameterType::Enum,
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
123pub enum Cardinality {
124 Single,
125 Multiple,
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct InputSpec {
130 pub name: String,
131 pub value_type: TriggerValueType,
132 pub required: bool,
133 pub cardinality: Cardinality,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct OutputSpec {
138 pub name: String,
139 pub value_type: TriggerValueType,
140}
141
142#[derive(Debug, Clone, PartialEq)]
143pub struct ParameterSpec {
144 pub name: String,
145 pub value_type: ParameterType,
146 pub default: Option<ParameterValue>,
147 pub required: bool,
148 pub bounds: Option<String>,
149}
150
151#[derive(Debug, Clone, PartialEq)]
152pub enum Cadence {
153 Continuous,
154 Event,
155}
156
157#[derive(Debug, Clone, PartialEq)]
158pub struct ExecutionSpec {
159 pub deterministic: bool,
160 pub cadence: Cadence,
161}
162
163#[derive(Debug, Clone, PartialEq)]
164pub struct StateSpec {
165 pub allowed: bool,
166 pub description: Option<String>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
170pub struct TriggerPrimitiveManifest {
171 pub id: String,
172 pub version: String,
173 pub kind: TriggerKind,
174 pub inputs: Vec<InputSpec>,
175 pub outputs: Vec<OutputSpec>,
176 pub parameters: Vec<ParameterSpec>,
177 pub execution: ExecutionSpec,
178 pub state: StateSpec,
179 pub side_effects: bool,
180}
181
182#[derive(Debug, Clone, PartialEq)]
183#[non_exhaustive]
184pub enum TriggerValidationError {
185 InvalidId {
186 id: String,
187 },
188 InvalidVersion {
189 version: String,
190 },
191 WrongKind {
192 expected: TriggerKind,
193 got: TriggerKind,
194 },
195 NoInputsDeclared {
196 trigger: String,
197 },
198 DuplicateInput {
199 name: String,
200 first_index: usize,
201 second_index: usize,
202 },
203 SideEffectsNotAllowed,
204 NonDeterministicExecution,
205 DuplicateId(String),
206 TriggerWrongOutputCount {
207 got: usize,
208 },
209 InvalidInputCardinality {
210 input: String,
211 got: String,
212 },
213 InvalidInputType {
214 input: String,
215 expected: TriggerValueType,
216 got: TriggerValueType,
217 },
218 InvalidOutputType {
219 output: String,
220 expected: TriggerValueType,
221 got: TriggerValueType,
222 },
223 InvalidParameterType {
224 parameter: String,
225 expected: ParameterType,
226 got: ParameterType,
227 },
228 StatefulTriggerNotAllowed {
230 trigger_id: String,
231 },
232}
233
234impl ErrorInfo for TriggerValidationError {
235 fn rule_id(&self) -> &'static str {
236 match self {
237 Self::InvalidId { .. } => "TRG-1",
238 Self::InvalidVersion { .. } => "TRG-2",
239 Self::WrongKind { .. } => "TRG-3",
240 Self::NoInputsDeclared { .. } => "TRG-4",
241 Self::DuplicateInput { .. } => "TRG-5",
242 Self::InvalidInputType { .. } => "TRG-6",
243 Self::TriggerWrongOutputCount { .. } => "TRG-7",
244 Self::InvalidOutputType { .. } => "TRG-8",
245 Self::StatefulTriggerNotAllowed { .. } => "TRG-9",
246 Self::SideEffectsNotAllowed => "TRG-10",
247 Self::NonDeterministicExecution => "TRG-11",
248 Self::InvalidInputCardinality { .. } => "TRG-12",
249 Self::DuplicateId(_) => "TRG-13",
250 Self::InvalidParameterType { .. } => "TRG-14",
251 }
252 }
253
254 fn phase(&self) -> Phase {
255 Phase::Registration
256 }
257
258 fn doc_anchor(&self) -> &'static str {
259 doc_anchor_for_rule(self.rule_id())
260 }
261
262 fn summary(&self) -> Cow<'static, str> {
263 match self {
264 Self::InvalidId { id } => Cow::Owned(format!("Invalid trigger ID: '{}'", id)),
265 Self::InvalidVersion { version } => {
266 Cow::Owned(format!("Invalid version: '{}'", version))
267 }
268 Self::WrongKind { expected, got } => Cow::Owned(format!(
269 "Wrong kind: expected {:?}, got {:?}",
270 expected, got
271 )),
272 Self::NoInputsDeclared { .. } => Cow::Borrowed("Trigger has no inputs"),
273 Self::DuplicateInput { name, .. } => {
274 Cow::Owned(format!("Duplicate input name: '{}'", name))
275 }
276 Self::InvalidInputType {
277 input,
278 expected,
279 got,
280 } => Cow::Owned(format!(
281 "Input '{}' has invalid type: expected {:?}, got {:?}",
282 input, expected, got
283 )),
284 Self::TriggerWrongOutputCount { got } => Cow::Owned(format!(
285 "Trigger must declare exactly one output (got {})",
286 got
287 )),
288 Self::InvalidOutputType {
289 output,
290 expected,
291 got,
292 } => Cow::Owned(format!(
293 "Output '{}' has invalid type: expected {:?}, got {:?}",
294 output, expected, got
295 )),
296 Self::StatefulTriggerNotAllowed { .. } => Cow::Borrowed("Trigger state is not allowed"),
297 Self::SideEffectsNotAllowed => Cow::Borrowed("Trigger side effects are not allowed"),
298 Self::NonDeterministicExecution => {
299 Cow::Borrowed("Trigger execution must be deterministic")
300 }
301 Self::InvalidInputCardinality { input, got } => Cow::Owned(format!(
302 "Input '{}' has invalid cardinality '{}'",
303 input, got
304 )),
305 Self::DuplicateId(_) => Cow::Borrowed("Duplicate trigger ID: already registered"),
306 Self::InvalidParameterType {
307 parameter,
308 expected,
309 got,
310 } => Cow::Owned(format!(
311 "Parameter '{}' has invalid type: expected {:?}, got {:?}",
312 parameter, expected, got
313 )),
314 }
315 }
316
317 fn path(&self) -> Option<Cow<'static, str>> {
318 match self {
319 Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
320 Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
321 Self::DuplicateId(_) => Some(Cow::Borrowed("$.id")),
322 Self::WrongKind { .. } => Some(Cow::Borrowed("$.kind")),
323 Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("$.inputs")),
324 Self::DuplicateInput { second_index, .. } => {
325 Some(Cow::Owned(format!("$.inputs[{}].name", second_index)))
326 }
327 Self::InvalidInputType { .. } => Some(Cow::Borrowed("$.inputs[].type")),
328 Self::InvalidInputCardinality { .. } => Some(Cow::Borrowed("$.inputs[].cardinality")),
329 Self::TriggerWrongOutputCount { .. } => Some(Cow::Borrowed("$.outputs")),
330 Self::InvalidOutputType { .. } => Some(Cow::Borrowed("$.outputs[0].type")),
331 Self::StatefulTriggerNotAllowed { .. } => Some(Cow::Borrowed("$.state.allowed")),
332 Self::SideEffectsNotAllowed => Some(Cow::Borrowed("$.side_effects")),
333 Self::NonDeterministicExecution => Some(Cow::Borrowed("$.execution.deterministic")),
334 Self::InvalidParameterType { .. } => Some(Cow::Borrowed("$.parameters[].default")),
335 }
336 }
337
338 fn fix(&self) -> Option<Cow<'static, str>> {
339 match self {
340 Self::InvalidId { .. } => Some(Cow::Borrowed(
341 "ID must start with lowercase letter and contain only lowercase letters, digits, and underscores",
342 )),
343 Self::DuplicateId(_) => Some(Cow::Borrowed("Choose a unique ID not already registered")),
344 Self::InvalidVersion { .. } => Some(Cow::Borrowed(
345 "Version must be valid semver (e.g., '1.0.0')",
346 )),
347 Self::WrongKind { .. } => Some(Cow::Borrowed("Set kind: trigger")),
348 Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("Add at least one input")),
349 Self::DuplicateInput { name, .. } => Some(Cow::Owned(format!(
350 "Rename input '{}' to a unique value",
351 name
352 ))),
353 Self::InvalidInputType { .. } => Some(Cow::Borrowed(
354 "Use a valid input type: number, bool, series, or event",
355 )),
356 Self::TriggerWrongOutputCount { .. } => {
357 Some(Cow::Borrowed("Declare exactly one output"))
358 }
359 Self::InvalidOutputType { .. } => {
360 Some(Cow::Borrowed("Output type must be event"))
361 }
362 Self::StatefulTriggerNotAllowed { .. } => {
363 Some(Cow::Borrowed("Set state.allowed: false"))
364 }
365 Self::SideEffectsNotAllowed => Some(Cow::Borrowed("Set side_effects: false")),
366 Self::NonDeterministicExecution => {
367 Some(Cow::Borrowed("Set execution.deterministic: true"))
368 }
369 Self::InvalidInputCardinality { .. } => {
370 Some(Cow::Borrowed("Set input cardinality to single"))
371 }
372 Self::InvalidParameterType { .. } => Some(Cow::Borrowed(
373 "Change parameter default value to match the declared parameter type",
374 )),
375 }
376 }
377}
378
379impl fmt::Display for TriggerValidationError {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 write!(f, "{} ({})", self.summary(), self.rule_id())
382 }
383}
384
385impl std::error::Error for TriggerValidationError {}
386
387pub trait TriggerPrimitive: Send + Sync {
407 fn manifest(&self) -> &TriggerPrimitiveManifest;
408
409 fn evaluate(
410 &self,
411 inputs: &HashMap<String, TriggerValue>,
412 parameters: &HashMap<String, ParameterValue>,
413 ) -> HashMap<String, TriggerValue>;
414}
415
416pub use implementations::emit_if_event_and_true::EmitIfEventAndTrue;
417pub use implementations::emit_if_true::EmitIfTrue;
418pub use registry::TriggerRegistry;