use std::collections::HashMap;
use std::path::{Path, PathBuf};
use bynk_ide::architecture::{self, ArchModel, CapabilityOrigin, NodeKind};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchitectureModelParams {
pub text_document: tower_lsp::lsp_types::TextDocumentIdentifier,
}
pub fn architecture_model_for(
unit_sources: &HashMap<String, Vec<PathBuf>>,
snapshots: &HashMap<PathBuf, String>,
sequence_info: &HashMap<String, bynk_ide::ContextSequenceInfo>,
) -> ArchModel {
architecture::architecture_model(unit_sources, snapshots, sequence_info)
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireLoc {
pub uri: tower_lsp::lsp_types::Url,
pub range: tower_lsp::lsp_types::Range,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchModel {
pub nodes: Vec<WireArchNode>,
pub edges: Vec<WireArchEdge>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchNode {
pub name: String,
pub kind: &'static str,
pub loc: WireLoc,
pub capabilities: Vec<WireArchCapability>,
pub providers: Vec<WireArchProvider>,
pub services: Vec<WireArchService>,
pub agents: Vec<WireArchAgent>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchCapability {
pub name: String,
pub local: bool,
pub from: Option<String>,
pub loc: WireLoc,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchProvider {
pub capability: String,
#[serde(rename = "providerName")]
pub provider_name: String,
pub external: bool,
pub loc: WireLoc,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchService {
pub name: String,
#[serde(rename = "handlerCount")]
pub handler_count: usize,
pub loc: WireLoc,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchAgent {
pub name: String,
#[serde(rename = "handlerCount")]
pub handler_count: usize,
pub loc: WireLoc,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct WireArchEdge {
pub from: String,
pub to: String,
pub capabilities: Vec<String>,
pub loc: WireLoc,
}
fn kind_str(k: NodeKind) -> &'static str {
match k {
NodeKind::Context => "Context",
NodeKind::Adapter => "Adapter",
}
}
fn wire_loc(
project_root: &Path,
snapshots: &HashMap<PathBuf, String>,
loc: &architecture::Located,
) -> Option<WireLoc> {
let text = snapshots.get(&loc.file)?;
let uri = tower_lsp::lsp_types::Url::from_file_path(project_root.join(&loc.file)).ok()?;
Some(WireLoc {
uri,
range: crate::position::span_to_range(text, loc.span),
})
}
pub fn to_wire(
model: &ArchModel,
project_root: &Path,
snapshots: &HashMap<PathBuf, String>,
) -> WireArchModel {
WireArchModel {
nodes: model
.nodes
.iter()
.filter_map(|n| {
Some(WireArchNode {
name: n.name.clone(),
kind: kind_str(n.kind),
loc: wire_loc(project_root, snapshots, &n.loc)?,
capabilities: n
.capabilities
.iter()
.filter_map(|c| {
let (local, from) = match &c.origin {
CapabilityOrigin::Local => (true, None),
CapabilityOrigin::Consumed { from } => (false, Some(from.clone())),
};
Some(WireArchCapability {
name: c.name.clone(),
local,
from,
loc: wire_loc(project_root, snapshots, &c.loc)?,
})
})
.collect(),
providers: n
.providers
.iter()
.filter_map(|p| {
Some(WireArchProvider {
capability: p.capability.clone(),
provider_name: p.provider_name.clone(),
external: p.external,
loc: wire_loc(project_root, snapshots, &p.loc)?,
})
})
.collect(),
services: n
.services
.iter()
.filter_map(|s| {
Some(WireArchService {
name: s.name.clone(),
handler_count: s.handler_count,
loc: wire_loc(project_root, snapshots, &s.loc)?,
})
})
.collect(),
agents: n
.agents
.iter()
.filter_map(|a| {
Some(WireArchAgent {
name: a.name.clone(),
handler_count: a.handler_count,
loc: wire_loc(project_root, snapshots, &a.loc)?,
})
})
.collect(),
})
})
.collect(),
edges: model
.edges
.iter()
.filter_map(|e| {
Some(WireArchEdge {
from: e.from.clone(),
to: e.to.clone(),
capabilities: e.capabilities.clone(),
loc: wire_loc(project_root, snapshots, &e.loc)?,
})
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap as Map;
use std::fs;
fn setup_project(test_name: &str, files: &[(&str, &str)]) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"bynk-lsp-architecture-request-test-{test_name}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("create test root");
for (rel, contents) in files {
let p = root.join(rel);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(&p, contents).expect("write file");
}
root
}
const PROVIDER_SRC: &str = r#"context platformtime
exports capability { Clock }
capability Clock {
fn now() -> Effect[Int]
}
provides Clock = SystemClock {
fn now() -> Effect[Int] {
0
}
}
"#;
const CONSUMER_SRC: &str = r#"context ops.jobs
consumes platformtime { Clock }
service run {
on call() -> Effect[Int] given Clock {
let now <- Clock.now()
now
}
}
"#;
#[test]
fn params_deserialize_from_camel_case_wire_json() {
let json = serde_json::json!({
"textDocument": { "uri": "file:///a/b.bynk" }
});
let params: ArchitectureModelParams =
serde_json::from_value(json).expect("camelCase textDocument must deserialize");
assert_eq!(params.text_document.uri.as_str(), "file:///a/b.bynk");
}
#[test]
fn each_node_uri_points_at_its_own_declaring_file_not_the_others() {
let root = setup_project(
"twofile",
&[
("platformtime.bynk", PROVIDER_SRC),
("jobs.bynk", CONSUMER_SRC),
],
);
let diag = bynk_ide::diagnose_project(&root, &Map::new());
let snapshots: HashMap<PathBuf, String> = diag
.files
.iter()
.map(|f| (f.source_path.clone(), f.text.clone()))
.collect();
let model = architecture_model_for(&diag.unit_sources, &snapshots, &diag.sequence_info);
let wire = to_wire(&model, &root, &snapshots);
assert_eq!(wire.nodes.len(), 2);
let jobs = wire.nodes.iter().find(|n| n.name == "ops.jobs").unwrap();
let platformtime = wire
.nodes
.iter()
.find(|n| n.name == "platformtime")
.unwrap();
assert!(
jobs.loc.uri.as_str().ends_with("jobs.bynk"),
"got {}",
jobs.loc.uri
);
assert!(
platformtime.loc.uri.as_str().ends_with("platformtime.bynk"),
"got {}",
platformtime.loc.uri
);
assert_ne!(jobs.loc.uri, platformtime.loc.uri);
assert_eq!(jobs.capabilities.len(), 1);
assert!(jobs.capabilities[0].loc.uri.as_str().ends_with("jobs.bynk"));
assert_eq!(jobs.capabilities[0].from.as_deref(), Some("platformtime"));
assert_eq!(wire.edges.len(), 1);
assert_eq!(wire.edges[0].from, "ops.jobs");
assert_eq!(wire.edges[0].to, "platformtime");
assert!(wire.edges[0].loc.uri.as_str().ends_with("jobs.bynk"));
}
}