runtara_dsl/schema_types.rs
1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3// DSL Type Definitions - Single Source of Truth
4//
5// These types define the scenario DSL structure and are used by:
6// 1. Runtime - for deserializing scenario JSON
7// 2. Compiler - for type-safe access to scenario structure
8// 3. build.rs - for auto-generating JSON Schema via schemars
9//
10// IMPORTANT: Changes to these types automatically update the JSON Schema.
11// The schema is generated at build time to `specs/dsl/v{VERSION}/schema.json`.
12//
13// NOTE: This file is included by build.rs via include!() macro, so it cannot
14// have `use` statements or `//!` doc comments. Imports are provided by the
15// including module.
16
17/// DSL version - bump when making breaking changes
18pub const DSL_VERSION: &str = "3.0.0";
19
20// ============================================================================
21// Root Types
22// ============================================================================
23
24/// Complete scenario definition
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
27#[serde(rename_all = "camelCase")]
28pub struct Scenario {
29 /// The execution graph containing all steps
30 pub execution_graph: ExecutionGraph,
31
32 /// Memory allocation tier for scenario execution
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub memory_tier: Option<MemoryTier>,
35
36 /// Enable step-level debug instrumentation
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub debug_mode: Option<bool>,
39}
40
41/// Memory allocation tier for scenario execution
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
43#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
44pub enum MemoryTier {
45 S,
46 M,
47 L,
48 #[default]
49 XL,
50}
51
52// ============================================================================
53// Execution Graph
54// ============================================================================
55
56/// The execution graph containing all steps and control flow
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
59#[serde(rename_all = "camelCase")]
60pub struct ExecutionGraph {
61 /// Human-readable name for the scenario
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub name: Option<String>,
64
65 /// Detailed description of what the scenario does
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub description: Option<String>,
68
69 /// Map of step IDs to step definitions
70 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
71 pub steps: HashMap<String, Step>,
72
73 /// ID of the entry point step (step with no incoming edges)
74 pub entry_point: String,
75
76 /// Ordered list of step transitions defining control flow
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub execution_plan: Vec<ExecutionPlanEdge>,
79
80 /// Constant variables available as `variables.<name>` during execution.
81 /// These are static values defined at design time, not overridable at runtime.
82 /// Keys are variable names, values contain type and value.
83 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
84 pub variables: HashMap<String, Variable>,
85
86 /// Schema defining expected input data structure for this scenario.
87 /// Keys are field names, values define the field type and constraints.
88 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
89 pub input_schema: HashMap<String, SchemaField>,
90
91 /// Schema defining output data structure for this scenario.
92 /// Keys are field names, values define the field type and constraints.
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
94 pub output_schema: HashMap<String, SchemaField>,
95
96 /// Visual annotations for UI (not used in compilation)
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub notes: Option<Vec<Note>>,
99
100 /// UI node positions for the visual scenario editor.
101 /// This is opaque data managed by the UI - the runtime does not interpret this field.
102 /// Typically contains an array of node objects with position coordinates.
103 /// Not used in compilation or execution.
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub nodes: Option<serde_json::Value>,
106
107 /// UI edge positions for the visual scenario editor.
108 /// This is opaque data managed by the UI - the runtime does not interpret this field.
109 /// Typically contains an array of edge objects connecting nodes.
110 /// Not used in compilation or execution.
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub edges: Option<serde_json::Value>,
113}
114
115/// An edge in the execution plan defining control flow
116#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
117#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
118#[serde(rename_all = "camelCase")]
119pub struct ExecutionPlanEdge {
120 /// Source step ID
121 pub from_step: String,
122
123 /// Target step ID
124 pub to_step: String,
125
126 /// Edge label for control flow:
127 /// - `"true"`/`"false"` for Conditional step branches
128 /// - `"onError"` for error handling transition (step failed after retries)
129 /// - `None` or empty for normal sequential flow
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub label: Option<String>,
132}
133
134/// Visual annotation for scenario editor UI
135#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
136#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
137pub struct Note {
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub id: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub text: Option<String>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub position: Option<Position>,
146}
147
148/// Position coordinates for UI elements
149#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
150#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
151pub struct Position {
152 pub x: f64,
153 pub y: f64,
154}
155
156// ============================================================================
157// Step Types
158// ============================================================================
159
160/// Union of all step types, discriminated by stepType field
161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
162#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
163#[serde(tag = "stepType")]
164pub enum Step {
165 /// Exit point - defines scenario outputs
166 Finish(FinishStep),
167
168 /// Executes an agent capability
169 Agent(AgentStep),
170
171 /// Evaluates conditions and branches
172 Conditional(ConditionalStep),
173
174 /// Iterates over an array, executing subgraph for each item
175 Split(SplitStep),
176
177 /// Multi-way branch based on value matching
178 Switch(SwitchStep),
179
180 /// Executes a nested child scenario
181 StartScenario(StartScenarioStep),
182
183 /// Conditional loop - repeat until condition is false
184 While(WhileStep),
185
186 /// Emit custom log/debug events
187 Log(LogStep),
188
189 /// Acquire a connection for use with secure agents
190 Connection(ConnectionStep),
191}
192
193/// Common fields shared by all step types
194#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
195#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
196#[serde(rename_all = "camelCase")]
197pub struct StepCommon {
198 /// Unique step identifier
199 pub id: String,
200
201 /// Human-readable step name
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub name: Option<String>,
204}
205
206/// Exit point step - defines scenario outputs
207#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
209#[serde(rename_all = "camelCase")]
210pub struct FinishStep {
211 /// Unique step identifier
212 pub id: String,
213
214 /// Human-readable step name
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub name: Option<String>,
217
218 /// Maps scenario data to output values
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub input_mapping: Option<InputMapping>,
221}
222
223/// Executes an agent capability
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
225#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
226#[serde(rename_all = "camelCase")]
227pub struct AgentStep {
228 /// Unique step identifier
229 pub id: String,
230
231 /// Human-readable step name
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub name: Option<String>,
234
235 /// Agent name (e.g., "utils", "transform", "http", "sftp")
236 pub agent_id: String,
237
238 /// Capability name (e.g., "random-double", "group-by", "http-request")
239 pub capability_id: String,
240
241 /// Connection ID for agents requiring authentication
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub connection_id: Option<String>,
244
245 /// Maps data to agent capability inputs
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub input_mapping: Option<InputMapping>,
248
249 /// Maximum retry attempts (default: 3)
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub max_retries: Option<u32>,
252
253 /// Base delay between retries in milliseconds (default: 1000)
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub retry_delay: Option<u64>,
256
257 /// Step timeout in milliseconds. If exceeded, step fails.
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub timeout: Option<u64>,
260}
261
262/// Evaluates conditions and branches execution
263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
264#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
265#[serde(rename_all = "camelCase")]
266pub struct ConditionalStep {
267 /// Unique step identifier
268 pub id: String,
269
270 /// Human-readable step name
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub name: Option<String>,
273
274 /// The condition expression to evaluate
275 pub condition: ConditionExpression,
276}
277
278/// Iterates over an array, executing subgraph for each item
279#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
280#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
281#[schemars(title = "SplitStep")]
282#[serde(rename_all = "camelCase")]
283pub struct SplitStep {
284 /// Unique step identifier
285 pub id: String,
286
287 /// Human-readable step name
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub name: Option<String>,
290
291 /// Nested execution graph for each iteration
292 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
293 pub subgraph: Box<ExecutionGraph>,
294
295 /// Split configuration: array to iterate, parallelism settings, error handling
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub config: Option<SplitConfig>,
298
299 /// Schema defining the expected shape of each item in the array.
300 /// Keys are field names, values define the field type and constraints.
301 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
302 pub input_schema: HashMap<String, SchemaField>,
303
304 /// Schema defining the expected output from each iteration.
305 /// Keys are field names, values define the field type and constraints.
306 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
307 pub output_schema: HashMap<String, SchemaField>,
308}
309
310/// Multi-way branch based on value matching
311#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
312#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
313#[schemars(title = "SwitchStep")]
314#[serde(rename_all = "camelCase")]
315pub struct SwitchStep {
316 /// Unique step identifier
317 pub id: String,
318
319 /// Human-readable step name
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub name: Option<String>,
322
323 /// Switch configuration: value to switch on, cases, and default
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub config: Option<SwitchConfig>,
326}
327
328/// Executes a nested child scenario
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
330#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
331#[serde(rename_all = "camelCase")]
332pub struct StartScenarioStep {
333 /// Unique step identifier
334 pub id: String,
335
336 /// Human-readable step name
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub name: Option<String>,
339
340 /// ID of the child scenario to execute
341 pub child_scenario_id: String,
342
343 /// Version of child scenario ("latest" or specific version number)
344 pub child_version: ChildVersion,
345
346 /// Maps parent data to child scenario inputs
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub input_mapping: Option<InputMapping>,
349
350 /// Maximum retry attempts (default: 3)
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub max_retries: Option<u32>,
353
354 /// Base delay between retries in milliseconds (default: 1000)
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub retry_delay: Option<u64>,
357
358 /// Step timeout in milliseconds. If exceeded, step fails.
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub timeout: Option<u64>,
361}
362
363/// Child scenario version specification
364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
365#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
366#[serde(untagged)]
367pub enum ChildVersion {
368 /// Use latest version
369 Latest(String),
370 /// Use specific version number
371 Specific(i32),
372}
373
374/// Conditional loop - repeat subgraph until condition is false
375#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
376#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
377#[schemars(title = "WhileStep")]
378#[serde(rename_all = "camelCase")]
379pub struct WhileStep {
380 /// Unique step identifier
381 pub id: String,
382
383 /// Human-readable step name
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub name: Option<String>,
386
387 /// The condition expression to evaluate before each iteration.
388 /// Loop continues while condition is true.
389 pub condition: ConditionExpression,
390
391 /// Nested execution graph to execute on each iteration
392 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
393 pub subgraph: Box<ExecutionGraph>,
394
395 /// While loop configuration
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub config: Option<WhileConfig>,
398}
399
400/// Configuration for a While step.
401#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
402#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
403#[schemars(title = "WhileConfig")]
404#[serde(rename_all = "camelCase")]
405pub struct WhileConfig {
406 /// Maximum number of iterations (default: 10).
407 /// Prevents infinite loops.
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub max_iterations: Option<u32>,
410
411 /// Step timeout in milliseconds. If exceeded, step fails.
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub timeout: Option<u64>,
414}
415
416impl Default for WhileConfig {
417 fn default() -> Self {
418 Self {
419 max_iterations: Some(10),
420 timeout: None,
421 }
422 }
423}
424
425/// Emit custom log/debug events during workflow execution
426#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
427#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
428#[schemars(title = "LogStep")]
429#[serde(rename_all = "camelCase")]
430pub struct LogStep {
431 /// Unique step identifier
432 pub id: String,
433
434 /// Human-readable step name
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub name: Option<String>,
437
438 /// Log level
439 #[serde(default)]
440 pub level: LogLevel,
441
442 /// Log message
443 pub message: String,
444
445 /// Additional context data to include in the log event.
446 /// Keys are field names, values specify how to obtain the data.
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub context: Option<InputMapping>,
449}
450
451/// Log level for Log steps
452#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
453#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
454#[serde(rename_all = "lowercase")]
455pub enum LogLevel {
456 /// Debug level - verbose diagnostic information
457 Debug,
458 /// Info level - general informational messages
459 #[default]
460 Info,
461 /// Warn level - warning conditions
462 Warn,
463 /// Error level - error conditions
464 Error,
465}
466
467/// Acquire a connection dynamically for use with secure agents.
468///
469/// Connection data is sensitive and protected:
470/// - Never logged or stored in checkpoints
471/// - Can only be passed to agents marked as `secure: true` (http, sftp)
472/// - Compile-time validation prevents leakage to non-secure steps
473///
474/// Example:
475/// ```json
476/// {
477/// "stepType": "Connection",
478/// "id": "api_conn",
479/// "connectionId": "my-api-connection",
480/// "integrationId": "bearer"
481/// }
482/// ```
483#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
484#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
485#[schemars(title = "ConnectionStep")]
486#[serde(rename_all = "camelCase")]
487pub struct ConnectionStep {
488 /// Unique step identifier
489 pub id: String,
490
491 /// Human-readable step name
492 #[serde(skip_serializing_if = "Option::is_none")]
493 pub name: Option<String>,
494
495 /// Reference to connection in the connection registry
496 pub connection_id: String,
497
498 /// Type of connection (bearer, api_key, basic_auth, sftp, etc.)
499 pub integration_id: String,
500}
501
502// ============================================================================
503// Input Mapping Types
504// ============================================================================
505
506/// Maps data from various sources to step inputs.
507/// Keys are destination field names, values specify how to obtain the data.
508///
509/// Example:
510/// ```json
511/// {
512/// "name": { "valueType": "reference", "value": "data.user.name" },
513/// "count": { "valueType": "immediate", "value": 5 },
514/// "items": { "valueType": "reference", "value": "steps.fetch.outputs.items" }
515/// }
516/// ```
517pub type InputMapping = HashMap<String, MappingValue>;
518
519/// A mapping value specifies how to obtain data for a field.
520///
521/// Uses explicit `valueType` discriminator:
522/// - `reference`: Value is a path to data (e.g., "data.name", "steps.step1.outputs.result")
523/// - `immediate`: Value is a literal (string, number, boolean, object, array)
524/// - `composite`: Value is a structured object or array with nested MappingValues
525///
526/// Example reference: `{ "valueType": "reference", "value": "data.user.name" }`
527/// Example immediate: `{ "valueType": "immediate", "value": "Hello World" }`
528/// Example composite: `{ "valueType": "composite", "value": { "name": {...}, "id": {...} } }`
529#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
530#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
531#[serde(tag = "valueType", rename_all = "lowercase")]
532pub enum MappingValue {
533 /// Reference to data at a path (e.g., "data.user.name", "variables.count")
534 Reference(ReferenceValue),
535
536 /// Immediate/literal value (string, number, boolean, object, array)
537 Immediate(ImmediateValue),
538
539 /// Composite value - structured object or array with nested MappingValues
540 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
541 Composite(CompositeValue),
542}
543
544/// A reference to data at a specific path.
545///
546/// Paths use dot notation: "data.user.name", "steps.step1.outputs.items", "variables.counter"
547///
548/// Available root contexts:
549/// - `data` - Current iteration data (in Split) or scenario input data
550/// - `variables` - Scenario variables
551/// - `steps.<stepId>.outputs` - Outputs from a previous step
552/// - `scenario.inputs` - Original scenario inputs
553///
554/// Example: `{ "valueType": "reference", "value": "data.user.name" }`
555/// With type hint: `{ "valueType": "reference", "value": "steps.http.outputs.body.count", "type": "int" }`
556#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
557#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
558#[serde(rename_all = "camelCase")]
559pub struct ReferenceValue {
560 /// Path to the data using dot notation (e.g., "data.user.name")
561 pub value: String,
562
563 /// Expected type hint for the referenced value.
564 /// Used when the source type is unknown (e.g., HTTP response body).
565 /// If omitted, the value is passed through as-is (typically as JSON).
566 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
567 pub type_hint: Option<ValueType>,
568
569 /// Default value to use when the reference path returns null or doesn't exist.
570 /// This allows graceful handling of optional fields while providing fallback values.
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub default: Option<serde_json::Value>,
573}
574
575/// An immediate (literal) value.
576///
577/// For non-string types (number, boolean, object, array), the type is unambiguous.
578/// For strings, this is always treated as a literal string, never as a reference.
579///
580/// Example: `{ "valueType": "immediate", "value": "Hello World" }`
581#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
582#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
583#[serde(rename_all = "camelCase")]
584pub struct ImmediateValue {
585 /// The literal value (string, number, boolean, object, or array)
586 pub value: serde_json::Value,
587}
588
589/// A composite value that builds structured objects or arrays from nested MappingValues.
590///
591/// Two forms are supported:
592/// - Object: `{ "valueType": "composite", "value": { "field": {...} } }`
593/// - Array: `{ "valueType": "composite", "value": [{...}, {...}] }`
594///
595/// Example object composite:
596/// ```json
597/// {
598/// "valueType": "composite",
599/// "value": {
600/// "name": {"valueType": "immediate", "value": "John"},
601/// "userId": {"valueType": "reference", "value": "data.user.id"}
602/// }
603/// }
604/// ```
605///
606/// Example array composite:
607/// ```json
608/// {
609/// "valueType": "composite",
610/// "value": [
611/// {"valueType": "reference", "value": "data.firstItem"},
612/// {"valueType": "immediate", "value": "static-value"}
613/// ]
614/// }
615/// ```
616#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
617#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
618#[serde(rename_all = "camelCase")]
619pub struct CompositeValue {
620 /// Either an object (HashMap) or array (Vec) of nested MappingValues.
621 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
622 pub value: CompositeInner,
623}
624
625/// Inner value for CompositeValue - either an object or array of MappingValues.
626#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
627#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
628#[serde(untagged)]
629pub enum CompositeInner {
630 /// Object composite: each field maps to a MappingValue
631 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
632 Object(HashMap<String, MappingValue>),
633 /// Array composite: each element is a MappingValue
634 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
635 Array(Vec<MappingValue>),
636}
637
638/// Type hints for reference values.
639/// Used to interpret data from unknown sources (e.g., HTTP responses).
640///
641/// Note: Type names are aligned with VariableType for consistency:
642/// - `integer` for whole numbers
643/// - `number` for floating point
644/// - `boolean` for true/false
645/// - `json` for pass-through JSON (distinct from `object`/`array` in VariableType)
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
647#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
648#[schemars(title = "ValueType")]
649#[serde(rename_all = "lowercase")]
650pub enum ValueType {
651 /// String value
652 String,
653 /// Integer number
654 Integer,
655 /// Floating point number
656 Number,
657 /// Boolean value
658 Boolean,
659 /// JSON object or array (pass through as-is)
660 Json,
661 /// Base64-encoded file data (FileData structure with content, filename, mimeType)
662 File,
663}
664
665/// Base64-encoded file data structure.
666/// Used for file inputs/outputs in scenarios and operators.
667#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
668#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
669#[serde(rename_all = "camelCase")]
670pub struct FileData {
671 /// Base64-encoded file content
672 pub content: String,
673
674 /// Original filename (optional)
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub filename: Option<String>,
677
678 /// MIME type, e.g., "text/csv", "application/pdf" (optional)
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub mime_type: Option<String>,
681}
682
683// ============================================================================
684// Variable Types
685// ============================================================================
686
687/// Data types for variables.
688/// Matches the operator field types for consistency.
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
690#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
691#[schemars(title = "VariableType")]
692#[serde(rename_all = "lowercase")]
693pub enum VariableType {
694 /// String value
695 String,
696 /// Numeric value (floating point)
697 Number,
698 /// Integer value
699 Integer,
700 /// Boolean value
701 Boolean,
702 /// Array of values
703 Array,
704 /// JSON object
705 Object,
706 /// Base64-encoded file data (FileData structure)
707 File,
708}
709
710/// Data types for schema fields.
711/// Used in input/output schema definitions.
712#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
713#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
714#[schemars(title = "SchemaFieldType")]
715#[serde(rename_all = "lowercase")]
716pub enum SchemaFieldType {
717 /// String value
718 String,
719 /// Integer number
720 Integer,
721 /// Floating point number
722 Number,
723 /// Boolean value
724 Boolean,
725 /// Array of values (use `items` to specify element type)
726 Array,
727 /// JSON object
728 Object,
729 /// Base64-encoded file data (FileData structure with content, filename, mimeType)
730 File,
731}
732
733/// A typed variable definition with its value.
734///
735/// Variables are static values available during scenario execution
736/// via the `variables.*` path in mappings.
737#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
738#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
739#[serde(rename_all = "camelCase")]
740pub struct Variable {
741 /// Variable type
742 #[serde(rename = "type")]
743 pub var_type: VariableType,
744
745 /// The actual value (must match the declared type)
746 pub value: serde_json::Value,
747
748 /// Human-readable description
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub description: Option<String>,
751}
752
753/// A field definition for input/output schemas.
754///
755/// Used to define the structure of scenario inputs and outputs.
756/// The field name is the key in the HashMap.
757#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
758#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
759#[schemars(title = "SchemaField")]
760#[serde(rename_all = "camelCase")]
761pub struct SchemaField {
762 /// Field type (string, integer, number, boolean, array, object)
763 #[serde(rename = "type")]
764 pub field_type: SchemaFieldType,
765
766 /// Human-readable description
767 #[serde(skip_serializing_if = "Option::is_none")]
768 pub description: Option<String>,
769
770 /// Whether this field is required
771 #[serde(default)]
772 pub required: bool,
773
774 /// Default value if not provided
775 #[serde(skip_serializing_if = "Option::is_none")]
776 pub default: Option<serde_json::Value>,
777
778 /// Example value for documentation
779 #[serde(skip_serializing_if = "Option::is_none")]
780 pub example: Option<serde_json::Value>,
781
782 /// For array types, the type of items in the array
783 #[serde(skip_serializing_if = "Option::is_none")]
784 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
785 pub items: Option<Box<SchemaField>>,
786
787 /// Allowed values (enum)
788 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
789 pub enum_values: Option<Vec<serde_json::Value>>,
790}
791
792// ============================================================================
793// Condition Types (for Conditional steps)
794// ============================================================================
795
796/// Condition expression operators
797#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
798#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
799#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
800pub enum ConditionOperator {
801 // Logical operators
802 And,
803 Or,
804 Not,
805
806 // Comparison operators
807 Gt,
808 Gte,
809 Lt,
810 Lte,
811 Eq,
812 Ne,
813
814 // String operators
815 StartsWith,
816 EndsWith,
817
818 // Array operators
819 Contains,
820 In,
821 NotIn,
822
823 // Utility operators
824 Length,
825 IsDefined,
826 IsEmpty,
827 IsNotEmpty,
828}
829
830/// A condition expression for conditional branching.
831/// Can be either an operation (with operator and arguments) or a simple value check.
832#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
833#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
834#[serde(tag = "type", rename_all = "lowercase")]
835pub enum ConditionExpression {
836 /// A comparison or logical operation
837 Operation(ConditionOperation),
838
839 /// A direct value (reference or immediate) - evaluated as truthy/falsy
840 Value(MappingValue),
841}
842
843/// An operation in a condition expression
844#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
845#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
846#[serde(rename_all = "camelCase")]
847pub struct ConditionOperation {
848 /// The operator (AND, OR, GT, EQ, STARTS_WITH, etc.)
849 pub op: ConditionOperator,
850
851 /// The arguments to the operator (1+ depending on operator).
852 /// Each argument can be a nested expression or a value (reference/immediate).
853 pub arguments: Vec<ConditionArgument>,
854}
855
856/// An argument to a condition operation.
857/// Can be a nested expression or a mapping value (reference or immediate).
858///
859/// Uses untagged serialization to avoid duplicate "type" fields when nesting
860/// expressions (since both ConditionExpression and MappingValue use internally-tagged enums).
861/// The deserializer distinguishes variants by structure:
862/// - Expression: has "op" and "arguments" fields (from ConditionExpression::Operation)
863/// or has "valueType" field (from ConditionExpression::Value -> MappingValue)
864/// - Value: has "valueType" field (from MappingValue)
865#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
866#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
867#[serde(untagged)]
868pub enum ConditionArgument {
869 /// Nested expression (for AND, OR, NOT, or any operator that takes expressions)
870 #[cfg_attr(feature = "utoipa", schema(no_recursion))]
871 Expression(Box<ConditionExpression>),
872
873 /// A mapping value - either reference (data path) or immediate (literal)
874 Value(MappingValue),
875}
876
877// ============================================================================
878// Switch Case Types
879// ============================================================================
880
881/// Match type for switch cases.
882/// Supports all ConditionOperator values plus compound match types.
883#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
884#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
885#[schemars(title = "SwitchMatchType")]
886#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
887pub enum SwitchMatchType {
888 // Comparison operators (same as ConditionOperator)
889 /// Greater than
890 Gt,
891 /// Greater than or equal
892 Gte,
893 /// Less than
894 Lt,
895 /// Less than or equal
896 Lte,
897 /// Equality check
898 Eq,
899 /// Not equal
900 Ne,
901
902 // String operators (same as ConditionOperator)
903 /// String starts with prefix
904 StartsWith,
905 /// String ends with suffix
906 EndsWith,
907
908 // Array operators (same as ConditionOperator)
909 /// Array contains value
910 Contains,
911 /// Value in array
912 In,
913 /// Value not in array
914 NotIn,
915
916 // Utility operators (same as ConditionOperator)
917 /// Check if value is defined (not null)
918 IsDefined,
919 /// Check if value is empty
920 IsEmpty,
921 /// Check if value is not empty
922 IsNotEmpty,
923
924 // Compound match types (Switch-specific)
925 /// Range check [min, max] - shorthand for GTE min AND LTE max
926 Between,
927 /// Object with optional {gte, gt, lte, lt} bounds
928 Range,
929}
930
931/// Configuration for a Switch step.
932/// Defines the value to switch on, the cases to match, and the default output.
933#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
934#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
935#[schemars(title = "SwitchConfig")]
936#[serde(rename_all = "camelCase")]
937pub struct SwitchConfig {
938 /// The value to switch on (evaluated at runtime)
939 pub value: MappingValue,
940
941 /// Array of cases to match against the value
942 #[serde(default, skip_serializing_if = "Vec::is_empty")]
943 pub cases: Vec<SwitchCase>,
944
945 /// Default output if no case matches
946 #[serde(skip_serializing_if = "Option::is_none")]
947 pub default: Option<serde_json::Value>,
948}
949
950/// A single case in a Switch step.
951/// Defines a match condition and the output to produce if matched.
952#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
953#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
954#[schemars(title = "SwitchCase")]
955#[serde(rename_all = "camelCase")]
956pub struct SwitchCase {
957 /// The type of match to perform
958 pub match_type: SwitchMatchType,
959
960 /// The value to match against (interpretation depends on match_type)
961 #[serde(rename = "match")]
962 pub match_value: serde_json::Value,
963
964 /// The output to produce if this case matches
965 pub output: serde_json::Value,
966}
967
968// ============================================================================
969// Split Config Types
970// ============================================================================
971
972/// Configuration for a Split step.
973/// Defines the array to iterate over and execution options.
974#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
975#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
976#[schemars(title = "SplitConfig")]
977#[serde(rename_all = "camelCase")]
978pub struct SplitConfig {
979 /// The array to iterate over
980 pub value: MappingValue,
981
982 /// Maximum concurrent iterations (0 = unlimited)
983 #[serde(default, skip_serializing_if = "Option::is_none")]
984 pub parallelism: Option<u32>,
985
986 /// Execute iterations sequentially instead of in parallel
987 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub sequential: Option<bool>,
989
990 /// Continue execution even if some iterations fail
991 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub dont_stop_on_failed: Option<bool>,
993
994 /// Additional variables to pass to each iteration's subgraph
995 #[serde(default, skip_serializing_if = "Option::is_none")]
996 pub variables: Option<InputMapping>,
997
998 /// Maximum retry attempts for the split operation (default: 0 - no retries)
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1000 pub max_retries: Option<u32>,
1001
1002 /// Base delay between retries in milliseconds (default: 1000)
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub retry_delay: Option<u64>,
1005
1006 /// Step timeout in milliseconds. If exceeded, step fails.
1007 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub timeout: Option<u64>,
1009}