1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6pub struct Span {
7 pub line: usize,
8 pub col: usize,
9 pub len: usize,
10}
11
12#[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 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 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 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 pub fn find_node(&self, id: &str) -> Option<&Node> {
47 self.nodes.iter().find(|n| n.id == id)
48 }
49
50 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 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#[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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206pub enum ArrowKind {
207 Solid, Dashed, Bidirectional, Blocked, }
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#[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#[derive(Debug, Clone)]
245pub struct Include {
246 pub path: String,
247 pub span: Span,
248}
249
250#[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
271fn 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}