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}