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
9/// The Static Analysis View of a Circuit.
10///
11/// `Schematic` is the graph representation extracted from the Axon Builder.
12/// It is used for visualization, documentation, and verification.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Schematic {
15    /// 스키마 버전 (호환성 체크용)
16    pub schema_version: String,
17    /// Circuit 고유 식별자
18    pub id: String,
19    /// Circuit 이름
20    pub name: String,
21    /// 설명
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub description: Option<String>,
24    /// 생성 시각
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub generated_at: Option<DateTime<Utc>>,
27    /// 노드 목록
28    pub nodes: Vec<Node>,
29    /// 엣지 목록
30    pub edges: Vec<Edge>,
31}
32
33impl Default for Schematic {
34    fn default() -> Self {
35        Self {
36            schema_version: SCHEMA_VERSION.to_string(),
37            id: Uuid::new_v4().to_string(),
38            name: String::new(),
39            description: None,
40            generated_at: Some(Utc::now()),
41            nodes: Vec::new(),
42            edges: Vec::new(),
43        }
44    }
45}
46
47impl Schematic {
48    pub fn new(name: impl Into<String>) -> Self {
49        Self {
50            name: name.into(),
51            ..Default::default()
52        }
53    }
54
55    /// 기존 ID를 유지하면서 새 Schematic 생성
56    pub fn with_id(name: impl Into<String>, id: impl Into<String>) -> Self {
57        Self {
58            id: id.into(),
59            name: name.into(),
60            ..Default::default()
61        }
62    }
63}
64
65/// 소스 코드 위치 정보 (Studio Code↔Node 매핑용)
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SourceLocation {
68    /// 파일 경로 (프로젝트 루트 기준 상대 경로)
69    pub file: String,
70    /// 라인 번호 (1-indexed)
71    pub line: u32,
72    /// 컬럼 번호 (optional)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub column: Option<u32>,
75}
76
77impl SourceLocation {
78    pub fn new(file: impl Into<String>, line: u32) -> Self {
79        Self {
80            file: file.into(),
81            line,
82            column: None,
83        }
84    }
85
86    pub fn with_column(file: impl Into<String>, line: u32, column: u32) -> Self {
87        Self {
88            file: file.into(),
89            line,
90            column: Some(column),
91        }
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Node {
97    pub id: String, // Uuid typically
98    pub kind: NodeKind,
99    pub label: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub description: Option<String>,
102    pub input_type: String,
103    pub output_type: String, // Primary output type for Next
104    pub resource_type: String,
105    pub metadata: StepMetadata,
106    /// 소스 코드 위치 (Studio Code↔Node 매핑용)
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub source_location: Option<SourceLocation>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum NodeKind {
113    Ingress,                  // Handler / Start
114    Atom,                     // Single action
115    Synapse,                  // Connection point / Branch
116    Egress,                   // Response / End
117    Subgraph(Box<Schematic>), // Nested graph
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub enum EdgeType {
122    Linear,         // Outcome::Next
123    Branch(String), // Outcome::Branch(id)
124    Jump,           // Outcome::Jump
125    Fault,          // Outcome::Fault
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Edge {
130    pub from: String,
131    pub to: String,
132    pub kind: EdgeType,
133    pub label: Option<String>,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_schematic_default_has_version_and_id() {
142        let schematic = Schematic::new("Test Circuit");
143        assert_eq!(schematic.schema_version, SCHEMA_VERSION);
144        assert!(!schematic.id.is_empty());
145        assert!(schematic.generated_at.is_some());
146    }
147
148    #[test]
149    fn test_schematic_serialization_with_new_fields() {
150        let schematic = Schematic::new("Test");
151        let json = serde_json::to_string_pretty(&schematic).unwrap();
152
153        assert!(json.contains("schema_version"));
154        assert!(json.contains("\"1.0\""));
155        assert!(json.contains("generated_at"));
156    }
157
158    #[test]
159    fn test_source_location_optional_in_json() {
160        let schematic = Schematic::new("Test");
161        let json = serde_json::to_string(&schematic).unwrap();
162
163        // description과 source_location은 None이면 JSON에서 생략됨
164        assert!(!json.contains("description"));
165    }
166
167    #[test]
168    fn test_source_location_creation() {
169        let loc = SourceLocation::new("src/main.rs", 42);
170        assert_eq!(loc.file, "src/main.rs");
171        assert_eq!(loc.line, 42);
172        assert!(loc.column.is_none());
173
174        let loc_with_col = SourceLocation::with_column("src/lib.rs", 10, 5);
175        assert_eq!(loc_with_col.column, Some(5));
176    }
177}