Skip to main content

nika_core/ast/
schema.rs

1//! Schema version definitions for Nika workflows.
2//!
3//! This module owns the `SchemaVersion` enum which is shared across
4//! the AST pipeline — used by both the analyzer (Phase 2) and the
5//! analyzed AST types.
6//!
7//! Extracted from `analyzed/workflow.rs` so that it can be imported
8//! without pulling in the full analyzed AST.
9
10/// Validated schema version.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SchemaVersion {
13    /// nika/workflow@0.1
14    V01,
15    /// nika/workflow@0.2
16    V02,
17    /// nika/workflow@0.3
18    V03,
19    /// nika/workflow@0.4
20    V04,
21    /// nika/workflow@0.5
22    V05,
23    /// nika/workflow@0.6
24    V06,
25    /// nika/workflow@0.7
26    V07,
27    /// nika/workflow@0.8
28    V08,
29    /// nika/workflow@0.9
30    V09,
31    /// nika/workflow@0.10
32    V10,
33    /// nika/workflow@0.11
34    V11,
35    /// nika/workflow@0.12
36    V12,
37}
38
39impl SchemaVersion {
40    /// Parse a schema version string.
41    pub fn parse(s: &str) -> Option<Self> {
42        match s {
43            "nika/workflow@0.1" => Some(Self::V01),
44            "nika/workflow@0.2" => Some(Self::V02),
45            "nika/workflow@0.3" => Some(Self::V03),
46            "nika/workflow@0.4" => Some(Self::V04),
47            "nika/workflow@0.5" => Some(Self::V05),
48            "nika/workflow@0.6" => Some(Self::V06),
49            "nika/workflow@0.7" => Some(Self::V07),
50            "nika/workflow@0.8" => Some(Self::V08),
51            "nika/workflow@0.9" => Some(Self::V09),
52            "nika/workflow@0.10" => Some(Self::V10),
53            "nika/workflow@0.11" => Some(Self::V11),
54            "nika/workflow@0.12" => Some(Self::V12),
55            _ => None,
56        }
57    }
58
59    /// Get the string representation.
60    pub fn as_str(&self) -> &'static str {
61        match self {
62            Self::V01 => "nika/workflow@0.1",
63            Self::V02 => "nika/workflow@0.2",
64            Self::V03 => "nika/workflow@0.3",
65            Self::V04 => "nika/workflow@0.4",
66            Self::V05 => "nika/workflow@0.5",
67            Self::V06 => "nika/workflow@0.6",
68            Self::V07 => "nika/workflow@0.7",
69            Self::V08 => "nika/workflow@0.8",
70            Self::V09 => "nika/workflow@0.9",
71            Self::V10 => "nika/workflow@0.10",
72            Self::V11 => "nika/workflow@0.11",
73            Self::V12 => "nika/workflow@0.12",
74        }
75    }
76
77    /// Get all valid schema versions.
78    pub fn all() -> &'static [Self] {
79        &[
80            Self::V01,
81            Self::V02,
82            Self::V03,
83            Self::V04,
84            Self::V05,
85            Self::V06,
86            Self::V07,
87            Self::V08,
88            Self::V09,
89            Self::V10,
90            Self::V11,
91            Self::V12,
92        ]
93    }
94
95    /// Get the latest schema version.
96    pub fn latest() -> Self {
97        Self::V12
98    }
99
100    /// Get the numeric version for comparison (e.g., V03 returns 3).
101    pub fn version_number(&self) -> u32 {
102        match self {
103            Self::V01 => 1,
104            Self::V02 => 2,
105            Self::V03 => 3,
106            Self::V04 => 4,
107            Self::V05 => 5,
108            Self::V06 => 6,
109            Self::V07 => 7,
110            Self::V08 => 8,
111            Self::V09 => 9,
112            Self::V10 => 10,
113            Self::V11 => 11,
114            Self::V12 => 12,
115        }
116    }
117
118    /// Check if a version supports a minimum required version.
119    pub fn supports(&self, min_version: Self) -> bool {
120        self.version_number() >= min_version.version_number()
121    }
122
123    /// Get a migration hint for upgrading from this schema version.
124    ///
125    /// Returns None for the latest version.
126    pub fn migration_hint(&self) -> Option<&'static str> {
127        match self {
128            Self::V01 => Some("@0.2 adds MCP servers, invoke: and agent: verbs"),
129            Self::V02 => Some("@0.3 adds for_each iteration and retry config"),
130            Self::V03 => Some("@0.4 adds decompose and structured output"),
131            Self::V04 => Some("@0.5 adds extended thinking and output format"),
132            Self::V05 => Some("@0.6 adds agents: definitions and skills:"),
133            Self::V06 => Some("@0.7 adds log: config and catch: error handling"),
134            Self::V07 => Some("@0.8 adds fetch: verb improvements"),
135            Self::V08 => Some("@0.9 adds context: files and imports:"),
136            Self::V09 => Some("@0.10 adds inputs: with defaults and artifacts:"),
137            Self::V10 => Some("@0.11 adds with: bindings replacing include:"),
138            Self::V11 => Some("@0.12 adds depends_on:, imports:, and ?? fallback operator"),
139            Self::V12 => None,
140        }
141    }
142
143    /// Check if MCP servers are supported.
144    pub fn supports_mcp(&self) -> bool {
145        self.supports(Self::V02)
146    }
147
148    /// Check if invoke/agent verbs are supported.
149    pub fn supports_invoke_agent(&self) -> bool {
150        self.supports(Self::V02)
151    }
152
153    /// Check if for_each is supported.
154    pub fn supports_for_each(&self) -> bool {
155        self.supports(Self::V03)
156    }
157
158    /// Check if skills are supported.
159    pub fn supports_skills(&self) -> bool {
160        self.supports(Self::V06)
161    }
162
163    /// Check if agent definitions are supported.
164    pub fn supports_agent_defs(&self) -> bool {
165        self.supports(Self::V06)
166    }
167
168    /// Check if context files are supported.
169    pub fn supports_context(&self) -> bool {
170        self.supports(Self::V09)
171    }
172
173    /// Check if include/DAG fusion is supported.
174    pub fn supports_include(&self) -> bool {
175        self.supports(Self::V09)
176    }
177
178    /// Check if inputs are supported.
179    pub fn supports_inputs(&self) -> bool {
180        self.supports(Self::V10)
181    }
182
183    /// Check if artifacts are supported.
184    pub fn supports_artifacts(&self) -> bool {
185        self.supports(Self::V10)
186    }
187
188    /// Check if retry configuration is supported.
189    pub fn supports_retry(&self) -> bool {
190        self.supports(Self::V03)
191    }
192
193    /// Check if with: binding syntax is supported.
194    pub fn supports_with(&self) -> bool {
195        self.supports(Self::V12)
196    }
197
198    /// Check if imports: syntax is supported.
199    pub fn supports_imports(&self) -> bool {
200        self.supports(Self::V12)
201    }
202
203    /// Check if depends_on: syntax is supported.
204    pub fn supports_depends_on(&self) -> bool {
205        self.supports(Self::V12)
206    }
207}
208
209impl std::fmt::Display for SchemaVersion {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(f, "{}", self.as_str())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_schema_version_parse() {
221        assert_eq!(
222            SchemaVersion::parse("nika/workflow@0.1"),
223            Some(SchemaVersion::V01)
224        );
225        assert_eq!(
226            SchemaVersion::parse("nika/workflow@0.12"),
227            Some(SchemaVersion::V12)
228        );
229        assert_eq!(SchemaVersion::parse("invalid"), None);
230        assert_eq!(SchemaVersion::parse("nika/workflow@0.99"), None);
231    }
232
233    #[test]
234    fn test_schema_version_latest() {
235        assert_eq!(SchemaVersion::latest(), SchemaVersion::V12);
236        assert_eq!(SchemaVersion::latest().as_str(), "nika/workflow@0.12");
237    }
238
239    #[test]
240    fn test_schema_version_number() {
241        assert_eq!(SchemaVersion::V01.version_number(), 1);
242        assert_eq!(SchemaVersion::V05.version_number(), 5);
243        assert_eq!(SchemaVersion::V12.version_number(), 12);
244    }
245
246    #[test]
247    fn test_schema_version_supports() {
248        // V01 only supports V01
249        assert!(SchemaVersion::V01.supports(SchemaVersion::V01));
250        assert!(!SchemaVersion::V01.supports(SchemaVersion::V02));
251
252        // V12 supports all versions
253        assert!(SchemaVersion::V12.supports(SchemaVersion::V01));
254        assert!(SchemaVersion::V12.supports(SchemaVersion::V12));
255    }
256
257    #[test]
258    fn test_schema_version_feature_gates() {
259        // MCP requires v0.2+
260        assert!(!SchemaVersion::V01.supports_mcp());
261        assert!(SchemaVersion::V02.supports_mcp());
262        assert!(SchemaVersion::V12.supports_mcp());
263
264        // for_each requires v0.3+
265        assert!(!SchemaVersion::V01.supports_for_each());
266        assert!(!SchemaVersion::V02.supports_for_each());
267        assert!(SchemaVersion::V03.supports_for_each());
268        assert!(SchemaVersion::V12.supports_for_each());
269
270        // skills requires v0.6+
271        assert!(!SchemaVersion::V05.supports_skills());
272        assert!(SchemaVersion::V06.supports_skills());
273        assert!(SchemaVersion::V12.supports_skills());
274
275        // context requires v0.9+
276        assert!(!SchemaVersion::V08.supports_context());
277        assert!(SchemaVersion::V09.supports_context());
278        assert!(SchemaVersion::V12.supports_context());
279
280        // inputs requires v0.10+
281        assert!(!SchemaVersion::V09.supports_inputs());
282        assert!(SchemaVersion::V10.supports_inputs());
283
284        // with:/imports:/depends_on: require v0.12+
285        assert!(!SchemaVersion::V11.supports_with());
286        assert!(SchemaVersion::V12.supports_with());
287        assert!(!SchemaVersion::V11.supports_imports());
288        assert!(SchemaVersion::V12.supports_imports());
289        assert!(!SchemaVersion::V11.supports_depends_on());
290        assert!(SchemaVersion::V12.supports_depends_on());
291    }
292}