Skip to main content

ranvier_core/
schematic.rs

1use crate::metadata::StepMetadata;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// 스키마 버전 상수
7pub const SCHEMA_VERSION: &str = "1.0";
8
9fn default_schema_version() -> String {
10    SCHEMA_VERSION.to_string()
11}
12
13fn parse_schema_version(version: &str) -> Option<(u64, u64)> {
14    let trimmed = version.trim();
15    if trimmed.is_empty() {
16        return None;
17    }
18
19    let mut parts = trimmed.split('.');
20    let major = parts.next()?.parse().ok()?;
21    let minor = parts.next().unwrap_or("0").parse().ok()?;
22    Some((major, minor))
23}
24
25/// Returns true when the provided schematic schema version is supported by this crate.
26///
27/// Compatibility is evaluated at major-version level.
28pub fn is_supported_schema_version(version: &str) -> bool {
29    let Some((major, _)) = parse_schema_version(version) else {
30        return false;
31    };
32    let Some((supported_major, _)) = parse_schema_version(SCHEMA_VERSION) else {
33        return false;
34    };
35    major == supported_major
36}
37
38/// The Static Analysis View of a Circuit.
39///
40/// `Schematic` is the graph representation extracted from the Axon Builder.
41/// It is used for visualization, documentation, and verification.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Schematic {
44    /// 스키마 버전 (호환성 체크용)
45    #[serde(default = "default_schema_version")]
46    pub schema_version: String,
47    /// Circuit 고유 식별자
48    pub id: String,
49    /// Circuit 이름
50    pub name: String,
51    /// 설명
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub description: Option<String>,
54    /// 생성 시각
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub generated_at: Option<DateTime<Utc>>,
57    /// 노드 목록
58    pub nodes: Vec<Node>,
59    /// 엣지 목록
60    pub edges: Vec<Edge>,
61}
62
63impl Default for Schematic {
64    fn default() -> Self {
65        Self {
66            schema_version: SCHEMA_VERSION.to_string(),
67            id: Uuid::new_v4().to_string(),
68            name: String::new(),
69            description: None,
70            generated_at: Some(Utc::now()),
71            nodes: Vec::new(),
72            edges: Vec::new(),
73        }
74    }
75}
76
77impl Schematic {
78    pub fn new(name: impl Into<String>) -> Self {
79        Self {
80            name: name.into(),
81            ..Default::default()
82        }
83    }
84
85    pub fn is_supported_schema_version(&self) -> bool {
86        is_supported_schema_version(&self.schema_version)
87    }
88
89    /// 기존 ID를 유지하면서 새 Schematic 생성
90    pub fn with_id(name: impl Into<String>, id: impl Into<String>) -> Self {
91        Self {
92            id: id.into(),
93            name: name.into(),
94            ..Default::default()
95        }
96    }
97}
98
99/// 소스 코드 위치 정보 (Studio Code↔Node 매핑용)
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SourceLocation {
102    /// 파일 경로 (프로젝트 루트 기준 상대 경로)
103    pub file: String,
104    /// 라인 번호 (1-indexed)
105    pub line: u32,
106    /// 컬럼 번호 (optional)
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub column: Option<u32>,
109}
110
111impl SourceLocation {
112    pub fn new(file: impl Into<String>, line: u32) -> Self {
113        Self {
114            file: file.into(),
115            line,
116            column: None,
117        }
118    }
119
120    pub fn with_column(file: impl Into<String>, line: u32, column: u32) -> Self {
121        Self {
122            file: file.into(),
123            line,
124            column: Some(column),
125        }
126    }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct Node {
131    pub id: String, // Uuid typically
132    pub kind: NodeKind,
133    pub label: String,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub description: Option<String>,
136    pub input_type: String,
137    pub output_type: String, // Primary output type for Next
138    pub resource_type: String,
139    pub metadata: StepMetadata,
140    /// Optional transition-level Bus capability policy metadata.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub bus_capability: Option<BusCapabilitySchema>,
143    /// 소스 코드 위치 (Studio Code↔Node 매핑용)
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub source_location: Option<SourceLocation>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct BusCapabilitySchema {
150    #[serde(skip_serializing_if = "Vec::is_empty", default)]
151    pub allow: Vec<String>,
152    #[serde(skip_serializing_if = "Vec::is_empty", default)]
153    pub deny: Vec<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum NodeKind {
158    Ingress,                  // Handler / Start
159    Atom,                     // Single action
160    Synapse,                  // Connection point / Branch
161    Egress,                   // Response / End
162    Subgraph(Box<Schematic>), // Nested graph
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum EdgeType {
167    Linear,         // Outcome::Next
168    Branch(String), // Outcome::Branch(id)
169    Jump,           // Outcome::Jump
170    Fault,          // Outcome::Fault
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct Edge {
175    pub from: String,
176    pub to: String,
177    pub kind: EdgeType,
178    pub label: Option<String>,
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_schematic_default_has_version_and_id() {
187        let schematic = Schematic::new("Test Circuit");
188        assert_eq!(schematic.schema_version, SCHEMA_VERSION);
189        assert!(schematic.is_supported_schema_version());
190        assert!(!schematic.id.is_empty());
191        assert!(schematic.generated_at.is_some());
192    }
193
194    #[test]
195    fn test_schematic_serialization_with_new_fields() {
196        let schematic = Schematic::new("Test");
197        let json = serde_json::to_string_pretty(&schematic).unwrap();
198
199        assert!(json.contains("schema_version"));
200        assert!(json.contains("\"1.0\""));
201        assert!(json.contains("generated_at"));
202    }
203
204    #[test]
205    fn test_source_location_optional_in_json() {
206        let schematic = Schematic::new("Test");
207        let json = serde_json::to_string(&schematic).unwrap();
208
209        // description과 source_location은 None이면 JSON에서 생략됨
210        assert!(!json.contains("description"));
211    }
212
213    #[test]
214    fn test_source_location_creation() {
215        let loc = SourceLocation::new("src/main.rs", 42);
216        assert_eq!(loc.file, "src/main.rs");
217        assert_eq!(loc.line, 42);
218        assert!(loc.column.is_none());
219
220        let loc_with_col = SourceLocation::with_column("src/lib.rs", 10, 5);
221        assert_eq!(loc_with_col.column, Some(5));
222    }
223
224    #[test]
225    fn test_schema_version_defaults_when_missing_in_json() {
226        let json = r#"{
227            "id": "test-id",
228            "name": "Legacy Schematic",
229            "nodes": [],
230            "edges": []
231        }"#;
232        let schematic: Schematic = serde_json::from_str(json).unwrap();
233        assert_eq!(schematic.schema_version, SCHEMA_VERSION);
234        assert!(schematic.is_supported_schema_version());
235    }
236
237    #[test]
238    fn test_supported_schema_version_major_compatibility() {
239        assert!(is_supported_schema_version("1"));
240        assert!(is_supported_schema_version("1.0"));
241        assert!(is_supported_schema_version("1.1"));
242        assert!(is_supported_schema_version("1.0.9"));
243        assert!(!is_supported_schema_version("2.0"));
244        assert!(!is_supported_schema_version(""));
245        assert!(!is_supported_schema_version("invalid"));
246    }
247}