1use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19
20use bynk_ide::architecture::{self, ArchModel, CapabilityOrigin, NodeKind};
21
22#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ArchitectureModelParams {
33 pub text_document: tower_lsp::lsp_types::TextDocumentIdentifier,
34}
35
36pub fn architecture_model_for(
41 unit_sources: &HashMap<String, Vec<PathBuf>>,
42 snapshots: &HashMap<PathBuf, String>,
43 sequence_info: &HashMap<String, bynk_ide::ContextSequenceInfo>,
44) -> ArchModel {
45 architecture::architecture_model(unit_sources, snapshots, sequence_info)
46}
47
48#[derive(Debug, Clone, serde::Serialize)]
55pub struct WireLoc {
56 pub uri: tower_lsp::lsp_types::Url,
57 pub range: tower_lsp::lsp_types::Range,
58}
59
60#[derive(Debug, Clone, serde::Serialize)]
61pub struct WireArchModel {
62 pub nodes: Vec<WireArchNode>,
63 pub edges: Vec<WireArchEdge>,
64}
65
66#[derive(Debug, Clone, serde::Serialize)]
67pub struct WireArchNode {
68 pub name: String,
69 pub kind: &'static str,
70 pub loc: WireLoc,
71 pub capabilities: Vec<WireArchCapability>,
72 pub providers: Vec<WireArchProvider>,
73 pub services: Vec<WireArchService>,
74 pub agents: Vec<WireArchAgent>,
75}
76
77#[derive(Debug, Clone, serde::Serialize)]
78pub struct WireArchCapability {
79 pub name: String,
80 pub local: bool,
81 pub from: Option<String>,
85 pub loc: WireLoc,
86}
87
88#[derive(Debug, Clone, serde::Serialize)]
89pub struct WireArchProvider {
90 pub capability: String,
91 #[serde(rename = "providerName")]
92 pub provider_name: String,
93 pub external: bool,
94 pub loc: WireLoc,
95}
96
97#[derive(Debug, Clone, serde::Serialize)]
98pub struct WireArchService {
99 pub name: String,
100 #[serde(rename = "handlerCount")]
101 pub handler_count: usize,
102 pub loc: WireLoc,
103}
104
105#[derive(Debug, Clone, serde::Serialize)]
106pub struct WireArchAgent {
107 pub name: String,
108 #[serde(rename = "handlerCount")]
109 pub handler_count: usize,
110 pub loc: WireLoc,
111}
112
113#[derive(Debug, Clone, serde::Serialize)]
114pub struct WireArchEdge {
115 pub from: String,
116 pub to: String,
117 pub capabilities: Vec<String>,
120 pub loc: WireLoc,
121}
122
123fn kind_str(k: NodeKind) -> &'static str {
124 match k {
125 NodeKind::Context => "Context",
126 NodeKind::Adapter => "Adapter",
127 }
128}
129
130fn wire_loc(
135 project_root: &Path,
136 snapshots: &HashMap<PathBuf, String>,
137 loc: &architecture::Located,
138) -> Option<WireLoc> {
139 let text = snapshots.get(&loc.file)?;
140 let uri = tower_lsp::lsp_types::Url::from_file_path(project_root.join(&loc.file)).ok()?;
141 Some(WireLoc {
142 uri,
143 range: crate::position::span_to_range(text, loc.span),
144 })
145}
146
147pub fn to_wire(
151 model: &ArchModel,
152 project_root: &Path,
153 snapshots: &HashMap<PathBuf, String>,
154) -> WireArchModel {
155 WireArchModel {
156 nodes: model
157 .nodes
158 .iter()
159 .filter_map(|n| {
160 Some(WireArchNode {
161 name: n.name.clone(),
162 kind: kind_str(n.kind),
163 loc: wire_loc(project_root, snapshots, &n.loc)?,
164 capabilities: n
165 .capabilities
166 .iter()
167 .filter_map(|c| {
168 let (local, from) = match &c.origin {
169 CapabilityOrigin::Local => (true, None),
170 CapabilityOrigin::Consumed { from } => (false, Some(from.clone())),
171 };
172 Some(WireArchCapability {
173 name: c.name.clone(),
174 local,
175 from,
176 loc: wire_loc(project_root, snapshots, &c.loc)?,
177 })
178 })
179 .collect(),
180 providers: n
181 .providers
182 .iter()
183 .filter_map(|p| {
184 Some(WireArchProvider {
185 capability: p.capability.clone(),
186 provider_name: p.provider_name.clone(),
187 external: p.external,
188 loc: wire_loc(project_root, snapshots, &p.loc)?,
189 })
190 })
191 .collect(),
192 services: n
193 .services
194 .iter()
195 .filter_map(|s| {
196 Some(WireArchService {
197 name: s.name.clone(),
198 handler_count: s.handler_count,
199 loc: wire_loc(project_root, snapshots, &s.loc)?,
200 })
201 })
202 .collect(),
203 agents: n
204 .agents
205 .iter()
206 .filter_map(|a| {
207 Some(WireArchAgent {
208 name: a.name.clone(),
209 handler_count: a.handler_count,
210 loc: wire_loc(project_root, snapshots, &a.loc)?,
211 })
212 })
213 .collect(),
214 })
215 })
216 .collect(),
217 edges: model
218 .edges
219 .iter()
220 .filter_map(|e| {
221 Some(WireArchEdge {
222 from: e.from.clone(),
223 to: e.to.clone(),
224 capabilities: e.capabilities.clone(),
225 loc: wire_loc(project_root, snapshots, &e.loc)?,
226 })
227 })
228 .collect(),
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use std::fs;
236
237 fn setup_project(test_name: &str, files: &[(&str, &str)]) -> PathBuf {
238 let root = std::env::temp_dir().join(format!(
239 "bynk-lsp-architecture-request-test-{test_name}-{}",
240 std::process::id()
241 ));
242 let _ = fs::remove_dir_all(&root);
243 fs::create_dir_all(&root).expect("create test root");
244 for (rel, contents) in files {
245 let p = root.join(rel);
246 if let Some(parent) = p.parent() {
247 fs::create_dir_all(parent).expect("create parent");
248 }
249 fs::write(&p, contents).expect("write file");
250 }
251 root
252 }
253
254 const PROVIDER_SRC: &str = r#"context platformtime
255
256exports capability { Clock }
257
258capability Clock {
259 fn now() -> Effect[Int]
260}
261
262provides Clock = SystemClock {
263 fn now() -> Effect[Int] {
264 0
265 }
266}
267"#;
268 const CONSUMER_SRC: &str = r#"context ops.jobs
269
270consumes platformtime { Clock }
271
272service run {
273 on call() -> Effect[Int] given Clock {
274 let now <- Clock.now()
275 now
276 }
277}
278"#;
279
280 #[test]
281 fn params_deserialize_from_camel_case_wire_json() {
282 let json = serde_json::json!({
283 "textDocument": { "uri": "file:///a/b.bynk" }
284 });
285 let params: ArchitectureModelParams =
286 serde_json::from_value(json).expect("camelCase textDocument must deserialize");
287 assert_eq!(params.text_document.uri.as_str(), "file:///a/b.bynk");
288 }
289
290 #[test]
295 fn each_node_uri_points_at_its_own_declaring_file_not_the_others() {
296 let root = setup_project(
297 "twofile",
298 &[
299 ("platformtime.bynk", PROVIDER_SRC),
300 ("jobs.bynk", CONSUMER_SRC),
301 ],
302 );
303 let diag = bynk_ide::diagnose_project(
304 &root,
305 &bynk_testkit::read_project_sources(&bynk_ide::AnalysisRoots::SingleTree(root.clone())),
306 );
307 let snapshots: HashMap<PathBuf, String> = diag
308 .files
309 .iter()
310 .map(|f| (f.source_path.clone(), f.text.clone()))
311 .collect();
312 let model = architecture_model_for(&diag.unit_sources, &snapshots, &diag.sequence_info);
313 let wire = to_wire(&model, &root, &snapshots);
314
315 assert_eq!(wire.nodes.len(), 2);
316 let jobs = wire.nodes.iter().find(|n| n.name == "ops.jobs").unwrap();
317 let platformtime = wire
318 .nodes
319 .iter()
320 .find(|n| n.name == "platformtime")
321 .unwrap();
322
323 assert!(
324 jobs.loc.uri.as_str().ends_with("jobs.bynk"),
325 "got {}",
326 jobs.loc.uri
327 );
328 assert!(
329 platformtime.loc.uri.as_str().ends_with("platformtime.bynk"),
330 "got {}",
331 platformtime.loc.uri
332 );
333 assert_ne!(jobs.loc.uri, platformtime.loc.uri);
334
335 assert_eq!(jobs.capabilities.len(), 1);
339 assert!(jobs.capabilities[0].loc.uri.as_str().ends_with("jobs.bynk"));
340 assert_eq!(jobs.capabilities[0].from.as_deref(), Some("platformtime"));
341
342 assert_eq!(wire.edges.len(), 1);
343 assert_eq!(wire.edges[0].from, "ops.jobs");
344 assert_eq!(wire.edges[0].to, "platformtime");
345 assert!(wire.edges[0].loc.uri.as_str().ends_with("jobs.bynk"));
346 }
347}