Skip to main content

arc_lang/
ast.rs

1use serde::{Deserialize, Serialize};
2
3// ── Source location ──────────────────────────────────────────────
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6pub struct Span {
7    pub line: usize,
8    pub col: usize,
9    pub len: usize,
10}
11
12// ── Top-level document ───────────────────────────────────────────
13
14#[derive(Debug, Clone, Default)]
15pub struct Document {
16    pub directives: Vec<Directive>,
17    pub nodes: Vec<Node>,
18    pub connections: Vec<Connection>,
19    pub groups: Vec<Group>,
20    pub includes: Vec<Include>,
21}
22
23impl Document {
24    /// Get the direction directive, defaulting to Down.
25    pub fn direction(&self) -> Direction {
26        self.directives.iter().find_map(|d| {
27            if let Directive::Direction(dir) = d { Some(*dir) } else { None }
28        }).unwrap_or(Direction::Down)
29    }
30
31    /// Get the theme name, defaulting to "light".
32    pub fn theme_name(&self) -> &str {
33        self.directives.iter().find_map(|d| {
34            if let Directive::Theme(ref name) = d { Some(name.as_str()) } else { None }
35        }).unwrap_or("light")
36    }
37
38    /// Get the spacing directive, defaulting to Normal.
39    pub fn spacing(&self) -> Spacing {
40        self.directives.iter().find_map(|d| {
41            if let Directive::Spacing(s) = d { Some(*s) } else { None }
42        }).unwrap_or(Spacing::Normal)
43    }
44
45    /// Find a node by ID.
46    pub fn find_node(&self, id: &str) -> Option<&Node> {
47        self.nodes.iter().find(|n| n.id == id)
48    }
49
50    /// Collect all node IDs referenced in connections but not declared.
51    pub fn undeclared_node_ids(&self) -> Vec<String> {
52        let declared: std::collections::HashSet<&str> =
53            self.nodes.iter().map(|n| n.id.as_str()).collect();
54        let mut referenced: std::collections::HashSet<String> = std::collections::HashSet::new();
55        for conn in &self.connections {
56            if !declared.contains(conn.from.as_str()) {
57                referenced.insert(conn.from.clone());
58            }
59            if !declared.contains(conn.to.as_str()) {
60                referenced.insert(conn.to.clone());
61            }
62        }
63        // Also check group member refs
64        fn collect_group_refs(group: &Group, declared: &std::collections::HashSet<&str>, refs: &mut std::collections::HashSet<String>) {
65            for m in &group.members {
66                match m {
67                    GroupMember::NodeRef(id) => {
68                        if !declared.contains(id.as_str()) {
69                            refs.insert(id.clone());
70                        }
71                    }
72                    GroupMember::Group(g) => collect_group_refs(g, declared, refs),
73                    _ => {}
74                }
75            }
76        }
77        for g in &self.groups {
78            collect_group_refs(g, &declared, &mut referenced);
79        }
80        let mut ids: Vec<String> = referenced.into_iter().collect();
81        ids.sort();
82        ids
83    }
84}
85
86// ── Directives ───────────────────────────────────────────────────
87
88#[derive(Debug, Clone)]
89pub enum Directive {
90    Direction(Direction),
91    Theme(String),
92    Spacing(Spacing),
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub enum Direction {
97    Down,
98    Right,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum Spacing {
103    Compact,
104    Normal,
105    Wide,
106}
107
108impl Spacing {
109    pub fn layer_gap(&self) -> f64 {
110        match self {
111            Spacing::Compact => 100.0,
112            Spacing::Normal => 160.0,
113            Spacing::Wide => 220.0,
114        }
115    }
116
117    pub fn node_gap(&self) -> f64 {
118        match self {
119            Spacing::Compact => 20.0,
120            Spacing::Normal => 35.0,
121            Spacing::Wide => 50.0,
122        }
123    }
124}
125
126// ── Nodes ────────────────────────────────────────────────────────
127
128#[derive(Debug, Clone)]
129pub struct Node {
130    pub node_type: NodeType,
131    pub id: String,
132    pub label: Option<String>,
133    pub tags: Vec<String>,
134    pub span: Span,
135}
136
137impl Node {
138    pub fn display_label(&self) -> &str {
139        self.label.as_deref().unwrap_or(&self.id)
140    }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum NodeType {
146    Service,
147    Db,
148    Cache,
149    Queue,
150    Gateway,
151    User,
152    Store,
153    Fn,
154    Worker,
155    External,
156}
157
158impl NodeType {
159    pub fn from_str_fuzzy(s: &str) -> Option<Self> {
160        match s.to_lowercase().as_str() {
161            "service" | "svc" => Some(Self::Service),
162            "db" | "database" | "datastore" => Some(Self::Db),
163            "cache" => Some(Self::Cache),
164            "queue" | "mq" | "broker" => Some(Self::Queue),
165            "gateway" | "gw" | "proxy" | "lb" => Some(Self::Gateway),
166            "user" | "client" | "actor" => Some(Self::User),
167            "store" | "storage" | "bucket" | "blob" => Some(Self::Store),
168            "fn" | "func" | "function" | "lambda" => Some(Self::Fn),
169            "worker" | "job" | "cron" => Some(Self::Worker),
170            "external" | "ext" | "cloud" | "third-party" => Some(Self::External),
171            _ => None,
172        }
173    }
174
175    /// Suggest the canonical type name for close misspellings.
176    pub fn suggest(s: &str) -> Option<&'static str> {
177        let s_lower = s.to_lowercase();
178        let candidates = [
179            "service", "db", "cache", "queue", "gateway",
180            "user", "store", "fn", "worker", "external",
181        ];
182        candidates.iter().find(|c| {
183            edit_distance(&s_lower, c) <= 2
184        }).copied()
185    }
186
187    pub fn as_str(&self) -> &'static str {
188        match self {
189            Self::Service => "service",
190            Self::Db => "db",
191            Self::Cache => "cache",
192            Self::Queue => "queue",
193            Self::Gateway => "gateway",
194            Self::User => "user",
195            Self::Store => "store",
196            Self::Fn => "fn",
197            Self::Worker => "worker",
198            Self::External => "external",
199        }
200    }
201}
202
203// ── Connections ──────────────────────────────────────────────────
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206pub enum ArrowKind {
207    Solid,         // ->
208    Dashed,        // -->
209    Bidirectional, // <->
210    Blocked,       // -x
211}
212
213#[derive(Debug, Clone)]
214pub struct Connection {
215    pub from: String,
216    pub arrow: ArrowKind,
217    pub to: String,
218    pub label: Option<String>,
219    pub tags: Vec<String>,
220    pub span: Span,
221}
222
223// ── Groups ───────────────────────────────────────────────────────
224
225#[derive(Debug, Clone)]
226pub struct Group {
227    pub label: String,
228    pub tags: Vec<String>,
229    pub members: Vec<GroupMember>,
230    pub span: Span,
231}
232
233#[derive(Debug, Clone)]
234pub enum GroupMember {
235    NodeRef(String),
236    NodeRefList(Vec<String>),
237    Node(Node),
238    Connection(Connection),
239    Group(Group),
240}
241
242// ── Includes ─────────────────────────────────────────────────────
243
244#[derive(Debug, Clone)]
245pub struct Include {
246    pub path: String,
247    pub span: Span,
248}
249
250// ── Diagnostics ──────────────────────────────────────────────────
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct Diagnostic {
254    pub line: usize,
255    pub col: usize,
256    pub code: String,
257    pub message: String,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub suggestion: Option<String>,
260    pub severity: Severity,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "lowercase")]
265pub enum Severity {
266    Error,
267    Warning,
268    Info,
269}
270
271// ── Utility: edit distance ───────────────────────────────────────
272
273fn edit_distance(a: &str, b: &str) -> usize {
274    let a_bytes = a.as_bytes();
275    let b_bytes = b.as_bytes();
276    let m = a_bytes.len();
277    let n = b_bytes.len();
278    let mut dp = vec![vec![0usize; n + 1]; m + 1];
279    for i in 0..=m { dp[i][0] = i; }
280    for j in 0..=n { dp[0][j] = j; }
281    for i in 1..=m {
282        for j in 1..=n {
283            let cost = if a_bytes[i - 1] == b_bytes[j - 1] { 0 } else { 1 };
284            dp[i][j] = (dp[i - 1][j] + 1)
285                .min(dp[i][j - 1] + 1)
286                .min(dp[i - 1][j - 1] + cost);
287        }
288    }
289    dp[m][n]
290}