runtara_dsl/
lib.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//! This crate defines the scenario DSL types used throughout the codebase:
6//! - Runtime deserialization of scenario JSON
7//! - Compiler type-safe access to scenario structure
8//! - Auto-generation of JSON Schema via schemars (in build.rs)
9//!
10//! Changes to these types automatically update `specs/dsl/v{VERSION}/schema.json` on rebuild.
11
12// Provide imports needed by schema_types.rs
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17// Include the schema types
18include!("schema_types.rs");
19
20// Path utilities
21pub mod paths;
22
23// Agent capability metadata types for runtime introspection
24pub mod agent_meta;
25
26// Specification generation (DSL schema, OpenAPI, compatibility)
27pub mod spec;
28
29// Step type metadata registration (auto-registers step types with inventory)
30mod step_registration;
31
32// ============================================================================
33// Parsing Functions
34// ============================================================================
35
36/// Parse an execution graph from JSON Value
37pub fn parse_execution_graph(json: &serde_json::Value) -> Result<ExecutionGraph, String> {
38    serde_json::from_value(json.clone())
39        .map_err(|e| format!("Failed to parse execution graph: {}", e))
40}
41
42/// Parse a complete scenario from JSON Value
43pub fn parse_scenario(json: &serde_json::Value) -> Result<Scenario, String> {
44    serde_json::from_value(json.clone()).map_err(|e| format!("Failed to parse scenario: {}", e))
45}
46
47/// Metadata about a step type for documentation
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct StepTypeInfo {
50    #[serde(rename = "type")]
51    pub step_type: String,
52    pub category: String,
53    pub description: String,
54}
55
56/// Get metadata for all step types (collected via inventory from step_registration.rs)
57///
58/// This function returns step type metadata that is automatically derived from
59/// the actual step struct definitions, ensuring the DSL schema is always in sync
60/// with the implementation.
61pub fn get_step_types() -> Vec<StepTypeInfo> {
62    // Start step is a virtual step (not a struct), add it manually
63    let mut steps = vec![StepTypeInfo {
64        step_type: "Start".to_string(),
65        category: "control".to_string(),
66        description: "Entry point - receives scenario inputs".to_string(),
67    }];
68
69    // Collect step types registered via inventory
70    for meta in agent_meta::get_all_step_types() {
71        steps.push(StepTypeInfo {
72            step_type: meta.id.to_string(),
73            category: meta.category.to_string(),
74            description: meta.description.to_string(),
75        });
76    }
77
78    // Sort by step type for consistent ordering
79    steps.sort_by(|a, b| a.step_type.cmp(&b.step_type));
80
81    steps
82}
83
84// ============================================================================
85// MemoryTier Methods
86// ============================================================================
87
88impl MemoryTier {
89    /// Total memory allocation in bytes
90    pub fn total_memory_bytes(&self) -> usize {
91        match self {
92            MemoryTier::S => 8 * 1024 * 1024,    // 8MB
93            MemoryTier::M => 64 * 1024 * 1024,   // 64MB
94            MemoryTier::L => 128 * 1024 * 1024,  // 128MB
95            MemoryTier::XL => 256 * 1024 * 1024, // 256MB
96        }
97    }
98
99    /// Stack size in bytes
100    pub fn stack_size_bytes(&self) -> usize {
101        match self {
102            MemoryTier::S => 1 * 1024 * 1024,  // 1MB
103            MemoryTier::M => 4 * 1024 * 1024,  // 4MB
104            MemoryTier::L => 8 * 1024 * 1024,  // 8MB
105            MemoryTier::XL => 8 * 1024 * 1024, // 8MB
106        }
107    }
108
109    /// Get as string
110    pub fn as_str(&self) -> &'static str {
111        match self {
112            MemoryTier::S => "S",
113            MemoryTier::M => "M",
114            MemoryTier::L => "L",
115            MemoryTier::XL => "XL",
116        }
117    }
118
119    /// Parse from string (case-insensitive)
120    pub fn from_str(s: &str) -> Option<Self> {
121        match s.to_uppercase().as_str() {
122            "S" => Some(MemoryTier::S),
123            "M" => Some(MemoryTier::M),
124            "L" => Some(MemoryTier::L),
125            "XL" => Some(MemoryTier::XL),
126            _ => None,
127        }
128    }
129}
130
131impl std::fmt::Display for MemoryTier {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}", self.as_str())
134    }
135}
136
137// ============================================================================
138// SchemaFieldType Helper Methods
139// ============================================================================
140
141impl SchemaFieldType {
142    /// Get as string representation
143    pub fn as_str(&self) -> &'static str {
144        match self {
145            SchemaFieldType::String => "string",
146            SchemaFieldType::Integer => "integer",
147            SchemaFieldType::Number => "number",
148            SchemaFieldType::Boolean => "boolean",
149            SchemaFieldType::Array => "array",
150            SchemaFieldType::Object => "object",
151        }
152    }
153}
154
155impl std::fmt::Display for SchemaFieldType {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{}", self.as_str())
158    }
159}
160
161impl From<&SchemaFieldType> for String {
162    fn from(t: &SchemaFieldType) -> Self {
163        t.as_str().to_string()
164    }
165}
166
167// ============================================================================
168// MappingValue Helper Methods
169// ============================================================================
170
171impl MappingValue {
172    /// Check if this is a reference (dynamic data lookup)
173    pub fn is_reference(&self) -> bool {
174        matches!(self, MappingValue::Reference(_))
175    }
176
177    /// Check if this is an immediate (static/literal) value
178    pub fn is_immediate(&self) -> bool {
179        matches!(self, MappingValue::Immediate(_))
180    }
181
182    /// Get the string value if this is a reference
183    pub fn as_reference_str(&self) -> Option<&str> {
184        match self {
185            MappingValue::Reference(r) => Some(&r.value),
186            _ => None,
187        }
188    }
189
190    /// Get the value if this is an immediate
191    pub fn as_immediate_value(&self) -> Option<&serde_json::Value> {
192        match self {
193            MappingValue::Immediate(i) => Some(&i.value),
194            _ => None,
195        }
196    }
197}
198
199// ============================================================================
200// Tests
201// ============================================================================
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_get_step_types_from_inventory() {
209        let step_types = get_step_types();
210
211        // Should have at least 7 step types (Start + 6 registered)
212        assert!(
213            step_types.len() >= 7,
214            "Expected at least 7 step types, got {}",
215            step_types.len()
216        );
217
218        // Verify expected step types are present
219        let step_ids: Vec<&str> = step_types.iter().map(|s| s.step_type.as_str()).collect();
220
221        assert!(step_ids.contains(&"Start"), "Missing Start step type");
222        assert!(step_ids.contains(&"Finish"), "Missing Finish step type");
223        assert!(step_ids.contains(&"Agent"), "Missing Agent step type");
224        assert!(
225            step_ids.contains(&"Conditional"),
226            "Missing Conditional step type"
227        );
228        assert!(step_ids.contains(&"Split"), "Missing Split step type");
229        assert!(step_ids.contains(&"Switch"), "Missing Switch step type");
230        assert!(
231            step_ids.contains(&"StartScenario"),
232            "Missing StartScenario step type"
233        );
234    }
235
236    #[test]
237    fn test_step_type_categories() {
238        let step_types = get_step_types();
239
240        for step in &step_types {
241            match step.step_type.as_str() {
242                "Agent" | "StartScenario" => {
243                    assert_eq!(
244                        step.category, "execution",
245                        "{} should be execution category",
246                        step.step_type
247                    );
248                }
249                "Start" | "Finish" | "Conditional" | "Split" | "Switch" => {
250                    assert_eq!(
251                        step.category, "control",
252                        "{} should be control category",
253                        step.step_type
254                    );
255                }
256                _ => {}
257            }
258        }
259    }
260
261    #[test]
262    fn test_step_type_schema_generation() {
263        // Verify that schema generation functions work
264        for meta in agent_meta::get_all_step_types() {
265            let schema = (meta.schema_fn)();
266            // Just verify it doesn't panic and returns something
267            assert!(
268                schema.schema.metadata.is_some() || schema.definitions.len() > 0,
269                "Schema for {} should have metadata or definitions",
270                meta.id
271            );
272        }
273    }
274
275    // ========================================================================
276    // New Type Tests (v3.0.0)
277    // ========================================================================
278
279    #[test]
280    fn test_value_type_serialization() {
281        // Test new type names serialize correctly
282        assert_eq!(
283            serde_json::to_string(&ValueType::Integer).unwrap(),
284            "\"integer\""
285        );
286        assert_eq!(
287            serde_json::to_string(&ValueType::Number).unwrap(),
288            "\"number\""
289        );
290        assert_eq!(
291            serde_json::to_string(&ValueType::Boolean).unwrap(),
292            "\"boolean\""
293        );
294        assert_eq!(
295            serde_json::to_string(&ValueType::String).unwrap(),
296            "\"string\""
297        );
298        assert_eq!(serde_json::to_string(&ValueType::Json).unwrap(), "\"json\"");
299        assert_eq!(serde_json::to_string(&ValueType::File).unwrap(), "\"file\"");
300    }
301
302    #[test]
303    fn test_value_type_deserialization() {
304        // Test new type names deserialize correctly
305        assert_eq!(
306            serde_json::from_str::<ValueType>("\"integer\"").unwrap(),
307            ValueType::Integer
308        );
309        assert_eq!(
310            serde_json::from_str::<ValueType>("\"number\"").unwrap(),
311            ValueType::Number
312        );
313        assert_eq!(
314            serde_json::from_str::<ValueType>("\"boolean\"").unwrap(),
315            ValueType::Boolean
316        );
317    }
318
319    #[test]
320    fn test_schema_field_type_serialization() {
321        assert_eq!(
322            serde_json::to_string(&SchemaFieldType::String).unwrap(),
323            "\"string\""
324        );
325        assert_eq!(
326            serde_json::to_string(&SchemaFieldType::Integer).unwrap(),
327            "\"integer\""
328        );
329        assert_eq!(
330            serde_json::to_string(&SchemaFieldType::Number).unwrap(),
331            "\"number\""
332        );
333        assert_eq!(
334            serde_json::to_string(&SchemaFieldType::Boolean).unwrap(),
335            "\"boolean\""
336        );
337        assert_eq!(
338            serde_json::to_string(&SchemaFieldType::Array).unwrap(),
339            "\"array\""
340        );
341        assert_eq!(
342            serde_json::to_string(&SchemaFieldType::Object).unwrap(),
343            "\"object\""
344        );
345    }
346
347    #[test]
348    fn test_schema_field_type_as_str() {
349        assert_eq!(SchemaFieldType::String.as_str(), "string");
350        assert_eq!(SchemaFieldType::Integer.as_str(), "integer");
351        assert_eq!(SchemaFieldType::Number.as_str(), "number");
352        assert_eq!(SchemaFieldType::Boolean.as_str(), "boolean");
353        assert_eq!(SchemaFieldType::Array.as_str(), "array");
354        assert_eq!(SchemaFieldType::Object.as_str(), "object");
355    }
356
357    #[test]
358    fn test_schema_field_type_display() {
359        assert_eq!(format!("{}", SchemaFieldType::String), "string");
360        assert_eq!(format!("{}", SchemaFieldType::Integer), "integer");
361    }
362
363    #[test]
364    fn test_switch_match_type_serialization() {
365        assert_eq!(
366            serde_json::to_string(&SwitchMatchType::Eq).unwrap(),
367            "\"EQ\""
368        );
369        assert_eq!(
370            serde_json::to_string(&SwitchMatchType::Gt).unwrap(),
371            "\"GT\""
372        );
373        assert_eq!(
374            serde_json::to_string(&SwitchMatchType::Between).unwrap(),
375            "\"BETWEEN\""
376        );
377    }
378
379    #[test]
380    fn test_switch_config_serialization() {
381        let config = SwitchConfig {
382            value: MappingValue::Reference(ReferenceValue {
383                value: "data.status".to_string(),
384                type_hint: None,
385                default: None,
386            }),
387            cases: vec![SwitchCase {
388                match_type: SwitchMatchType::Eq,
389                match_value: serde_json::json!("active"),
390                output: serde_json::json!({"result": true}),
391            }],
392            default: Some(serde_json::json!({"result": false})),
393        };
394
395        let json = serde_json::to_value(&config).unwrap();
396        assert!(json.get("value").is_some());
397        assert!(json.get("cases").is_some());
398        assert!(json.get("default").is_some());
399    }
400
401    #[test]
402    fn test_split_config_serialization() {
403        let config = SplitConfig {
404            value: MappingValue::Reference(ReferenceValue {
405                value: "data.items".to_string(),
406                type_hint: None,
407                default: None,
408            }),
409            parallelism: Some(5),
410            sequential: Some(false),
411            dont_stop_on_failed: Some(true),
412            variables: None,
413            max_retries: None,
414            retry_delay: None,
415            timeout: None,
416        };
417
418        let json = serde_json::to_value(&config).unwrap();
419        assert!(json.get("value").is_some());
420        assert_eq!(json.get("parallelism").unwrap(), 5);
421        assert_eq!(json.get("sequential").unwrap(), false);
422        assert_eq!(json.get("dontStopOnFailed").unwrap(), true);
423    }
424
425    #[test]
426    fn test_switch_step_with_config() {
427        let step = SwitchStep {
428            id: "switch1".to_string(),
429            name: Some("My Switch".to_string()),
430            config: Some(SwitchConfig {
431                value: MappingValue::Immediate(ImmediateValue {
432                    value: serde_json::json!("test"),
433                }),
434                cases: vec![],
435                default: None,
436            }),
437        };
438
439        let json = serde_json::to_value(&step).unwrap();
440        assert_eq!(json.get("id").unwrap(), "switch1");
441        assert!(json.get("config").is_some());
442    }
443
444    #[test]
445    fn test_split_step_with_config() {
446        let step = SplitStep {
447            id: "split1".to_string(),
448            name: None,
449            subgraph: Box::new(ExecutionGraph {
450                name: None,
451                description: None,
452                steps: HashMap::new(),
453                entry_point: "start".to_string(),
454                execution_plan: vec![],
455                variables: HashMap::new(),
456                input_schema: HashMap::new(),
457                output_schema: HashMap::new(),
458                notes: None,
459                nodes: None,
460                edges: None,
461            }),
462            config: Some(SplitConfig {
463                value: MappingValue::Reference(ReferenceValue {
464                    value: "data.items".to_string(),
465                    type_hint: None,
466                    default: None,
467                }),
468                parallelism: None,
469                sequential: None,
470                dont_stop_on_failed: None,
471                variables: None,
472                max_retries: None,
473                retry_delay: None,
474                timeout: None,
475            }),
476            input_schema: HashMap::new(),
477            output_schema: HashMap::new(),
478        };
479
480        let json = serde_json::to_value(&step).unwrap();
481        assert_eq!(json.get("id").unwrap(), "split1");
482        assert!(json.get("config").is_some());
483        assert!(json.get("subgraph").is_some());
484    }
485
486    #[test]
487    fn test_dsl_version() {
488        assert_eq!(DSL_VERSION, "3.0.0");
489    }
490}