diff --git a/AGENTS.md b/AGENTS.md index c42faeb..5b458d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,8 @@ Do not open pull requests directly from the worktree branch. - Ariadne uses a flat Rust source layout under `src/ariadne/`. Do not add a Cargo-default nested `src/ariadne/src/` source directory. +- Use `bazel build` for non-test targets when no Bazel test target is part of the validation set. + `bazel test` exits non-zero when it finds no test targets. - In Bazel `BUILD` files, depend on vendored Rust crates through the public aliases under `//third_party/crates`, for example `//third_party/crates:serde`. - Ariadne extraction must preserve the game-root season prefix for archived source. diff --git a/src/ariadne/BUILD b/src/ariadne/BUILD index 0473c79..340bff1 100644 --- a/src/ariadne/BUILD +++ b/src/ariadne/BUILD @@ -1,26 +1,9 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") rust_library( name = "ariadne_lib", crate_name = "ariadne", - srcs = [ - "artifact.rs", - "graph.rs", - "lib.rs", - ], - deps = [ - "//third_party/crates:serde", - "//third_party/crates:serde_json", - ], -) - -rust_test( - name = "ariadne_lib_test", - srcs = ["lib_test.rs"], - deps = [ - ":ariadne_lib", - "//third_party/crates:serde_json", - ], + srcs = ["lib.rs"], ) rust_binary( diff --git a/src/ariadne/artifact.rs b/src/ariadne/artifact.rs deleted file mode 100644 index 0543173..0000000 --- a/src/ariadne/artifact.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::error::Error; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader, BufWriter, Write}; -use std::path::Path; - -use serde::{Deserialize, Serialize}; - -use crate::{Delta, Edge, Graph, Node}; - -pub fn write_graph_artifact( - directory: impl AsRef, - graph: &Graph, - delta: &[Delta], -) -> std::io::Result<()> { - let directory = directory.as_ref(); - fs::create_dir_all(directory)?; - - write_json_file(directory.join("metadata.json"), &graph.metadata)?; - write_jsonl_file(directory.join("nodes.jsonl"), graph.nodes.values())?; - write_jsonl_file(directory.join("edges.jsonl"), graph.edges.iter())?; - write_jsonl_file(directory.join("delta.jsonl"), delta.iter())?; - - Ok(()) -} - -pub fn read_graph_artifact(directory: impl AsRef) -> std::io::Result { - let directory = directory.as_ref(); - let metadata = read_json_file(directory.join("metadata.json"))?; - let nodes: Vec = read_jsonl_file(directory.join("nodes.jsonl"))?; - let edges: Vec = read_jsonl_file(directory.join("edges.jsonl"))?; - - let mut graph = Graph::new(metadata); - for node in nodes { - graph.add_node(node).map_err(invalid_data)?; - } - for edge in edges { - graph.add_edge(edge).map_err(invalid_data)?; - } - - Ok(graph) -} - -pub fn read_graph_delta(directory: impl AsRef) -> std::io::Result> { - read_jsonl_file(directory.as_ref().join("delta.jsonl")) -} - -fn write_json_file(path: impl AsRef, value: &impl Serialize) -> std::io::Result<()> { - let file = File::create(path)?; - let writer = BufWriter::new(file); - serde_json::to_writer_pretty(writer, value).map_err(invalid_data) -} - -fn read_json_file Deserialize<'de>>(path: impl AsRef) -> std::io::Result { - let file = File::open(path)?; - let reader = BufReader::new(file); - serde_json::from_reader(reader).map_err(invalid_data) -} - -fn write_jsonl_file<'a, T: 'a + Serialize>( - path: impl AsRef, - values: impl IntoIterator, -) -> std::io::Result<()> { - let file = File::create(path)?; - let mut writer = BufWriter::new(file); - - for value in values { - serde_json::to_writer(&mut writer, value).map_err(invalid_data)?; - writer.write_all(b"\n")?; - } - - Ok(()) -} - -fn read_jsonl_file Deserialize<'de>>( - path: impl AsRef, -) -> std::io::Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let mut values = Vec::new(); - - for line in reader.lines() { - let line = line?; - if line.trim().is_empty() { - continue; - } - values.push(serde_json::from_str(&line).map_err(invalid_data)?); - } - - Ok(values) -} - -fn invalid_data(error: impl Error + Send + Sync + 'static) -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::InvalidData, error) -} diff --git a/src/ariadne/graph.rs b/src/ariadne/graph.rs deleted file mode 100644 index 6826cd5..0000000 --- a/src/ariadne/graph.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::error::Error; -use std::fmt::{Display, Formatter}; - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -pub type Metadata = BTreeMap; - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] -#[serde(transparent)] -pub struct NodeId(String); - -impl NodeId { - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl From<&str> for NodeId { - fn from(value: &str) -> Self { - Self::new(value) - } -} - -impl From for NodeId { - fn from(value: String) -> Self { - Self::new(value) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct SourceRef { - pub path: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub column: Option, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Node { - pub id: NodeId, - pub kind: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - pub payload: Value, -} - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct Edge { - pub from: NodeId, - pub to: NodeId, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "op", rename_all = "snake_case")] -pub enum Delta { - AddNode { node: Node }, - RemoveNode { node: Node }, - ReplaceNode { old: Node, new: Node }, - AddEdge { edge: Edge }, - RemoveEdge { edge: Edge }, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Graph { - pub metadata: Metadata, - pub nodes: BTreeMap, - pub edges: BTreeSet, -} - -impl Graph { - pub fn new(metadata: Metadata) -> Self { - Self { - metadata, - nodes: BTreeMap::new(), - edges: BTreeSet::new(), - } - } - - pub fn add_node(&mut self, node: Node) -> Result { - if self.nodes.contains_key(&node.id) { - return Err(GraphError::DuplicateNode { id: node.id }); - } - - self.nodes.insert(node.id.clone(), node.clone()); - Ok(Delta::AddNode { node }) - } - - pub fn remove_node(&mut self, id: &NodeId) -> Result, GraphError> { - let node = self - .nodes - .remove(id) - .ok_or_else(|| GraphError::MissingNode { id: id.clone() })?; - - let incident_edges: Vec<_> = self - .edges - .iter() - .filter(|edge| edge.from == *id || edge.to == *id) - .cloned() - .collect(); - - let mut deltas = Vec::with_capacity(incident_edges.len() + 1); - for edge in incident_edges { - self.edges.remove(&edge); - deltas.push(Delta::RemoveEdge { edge }); - } - deltas.push(Delta::RemoveNode { node }); - - Ok(deltas) - } - - pub fn replace_node(&mut self, node: Node) -> Result { - let old = self - .nodes - .get_mut(&node.id) - .ok_or_else(|| GraphError::MissingNode { - id: node.id.clone(), - })?; - let previous = std::mem::replace(old, node.clone()); - - Ok(Delta::ReplaceNode { - old: previous, - new: node, - }) - } - - pub fn add_edge(&mut self, edge: Edge) -> Result { - self.require_node(&edge.from)?; - self.require_node(&edge.to)?; - - if !self.edges.insert(edge.clone()) { - return Err(GraphError::DuplicateEdge { edge }); - } - - Ok(Delta::AddEdge { edge }) - } - - pub fn remove_edge(&mut self, edge: &Edge) -> Result { - if !self.edges.remove(edge) { - return Err(GraphError::MissingEdge { edge: edge.clone() }); - } - - Ok(Delta::RemoveEdge { edge: edge.clone() }) - } - - fn require_node(&self, id: &NodeId) -> Result<(), GraphError> { - if self.nodes.contains_key(id) { - Ok(()) - } else { - Err(GraphError::MissingNode { id: id.clone() }) - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum GraphError { - DuplicateNode { id: NodeId }, - MissingNode { id: NodeId }, - DuplicateEdge { edge: Edge }, - MissingEdge { edge: Edge }, -} - -impl Display for GraphError { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - match self { - GraphError::DuplicateNode { id } => { - write!(formatter, "duplicate node `{}`", id.as_str()) - } - GraphError::MissingNode { id } => write!(formatter, "missing node `{}`", id.as_str()), - GraphError::DuplicateEdge { edge } => write!( - formatter, - "duplicate edge `{} -> {}`", - edge.from.as_str(), - edge.to.as_str() - ), - GraphError::MissingEdge { edge } => write!( - formatter, - "missing edge `{} -> {}`", - edge.from.as_str(), - edge.to.as_str() - ), - } - } -} - -impl Error for GraphError {} diff --git a/src/ariadne/lib.rs b/src/ariadne/lib.rs index a34bfc0..8f9e795 100644 --- a/src/ariadne/lib.rs +++ b/src/ariadne/lib.rs @@ -1,9 +1,3 @@ -mod artifact; -mod graph; - -pub use artifact::{read_graph_artifact, read_graph_delta, write_graph_artifact}; -pub use graph::{Delta, Edge, Graph, GraphError, Metadata, Node, NodeId, SourceRef}; - pub fn run() { println!("Ariadne"); } diff --git a/src/ariadne/lib_test.rs b/src/ariadne/lib_test.rs deleted file mode 100644 index 834a4c0..0000000 --- a/src/ariadne/lib_test.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fs; - -use ariadne::{ - Delta, Edge, Graph, GraphError, Metadata, Node, NodeId, read_graph_artifact, read_graph_delta, - write_graph_artifact, -}; -use serde_json::json; - -fn node(id: &str, kind: &str) -> Node { - Node { - id: NodeId::from(id), - kind: kind.to_owned(), - source: None, - payload: json!({}), - } -} - -#[test] -fn graph_records_node_and_edge_mutations() { - let mut graph = Graph::new(Metadata::new()); - - let add_start = graph.add_node(node("start", "label")).unwrap(); - let add_end = graph.add_node(node("end", "label")).unwrap(); - let edge = Edge { - from: NodeId::from("start"), - to: NodeId::from("end"), - }; - let add_edge = graph.add_edge(edge.clone()).unwrap(); - let remove_edge = graph.remove_edge(&edge).unwrap(); - let add_edge_again = graph.add_edge(edge.clone()).unwrap(); - let replace_start = graph.replace_node(node("start", "entry_label")).unwrap(); - - assert_eq!(graph.nodes.len(), 2); - assert_eq!(graph.edges.len(), 1); - assert!(matches!(add_start, Delta::AddNode { .. })); - assert!(matches!(add_end, Delta::AddNode { .. })); - assert_eq!(add_edge, Delta::AddEdge { edge: edge.clone() }); - assert_eq!(remove_edge, Delta::RemoveEdge { edge: edge.clone() }); - assert_eq!(add_edge_again, Delta::AddEdge { edge }); - assert_eq!( - replace_start, - Delta::ReplaceNode { - old: node("start", "label"), - new: node("start", "entry_label") - } - ); -} - -#[test] -fn graph_rejects_edges_with_missing_endpoints() { - let mut graph = Graph::new(Metadata::new()); - graph.add_node(node("start", "label")).unwrap(); - - let error = graph - .add_edge(Edge { - from: NodeId::from("start"), - to: NodeId::from("missing"), - }) - .unwrap_err(); - - assert_eq!( - error, - GraphError::MissingNode { - id: NodeId::from("missing") - } - ); -} - -#[test] -fn removing_node_records_incident_edge_removals_before_node_removal() { - let mut graph = Graph::new(Metadata::new()); - graph.add_node(node("start", "label")).unwrap(); - graph.add_node(node("end", "label")).unwrap(); - let edge = Edge { - from: NodeId::from("start"), - to: NodeId::from("end"), - }; - graph.add_edge(edge.clone()).unwrap(); - - let deltas = graph.remove_node(&NodeId::from("start")).unwrap(); - - assert_eq!( - deltas, - vec![ - Delta::RemoveEdge { edge }, - Delta::RemoveNode { - node: node("start", "label") - } - ] - ); - assert!(graph.edges.is_empty()); - assert!(!graph.nodes.contains_key(&NodeId::from("start"))); -} - -#[test] -fn graph_artifact_round_trips_complete_graph_and_delta() { - let directory = std::env::temp_dir().join(format!( - "ariadne-graph-artifact-test-{}", - std::process::id() - )); - let _ = fs::remove_dir_all(&directory); - - let mut metadata = Metadata::new(); - metadata.insert("phase".to_owned(), json!("source")); - let mut graph = Graph::new(metadata); - let mut delta = Vec::new(); - - delta.push(graph.add_node(node("start", "label")).unwrap()); - delta.push(graph.add_node(node("end", "label")).unwrap()); - delta.push( - graph - .add_edge(Edge { - from: NodeId::from("start"), - to: NodeId::from("end"), - }) - .unwrap(), - ); - - write_graph_artifact(&directory, &graph, &delta).unwrap(); - - assert_eq!(read_graph_artifact(&directory).unwrap(), graph); - assert_eq!(read_graph_delta(&directory).unwrap(), delta); - assert!(directory.join("metadata.json").is_file()); - assert!(directory.join("nodes.jsonl").is_file()); - assert!(directory.join("edges.jsonl").is_file()); - assert!(directory.join("delta.jsonl").is_file()); - - fs::remove_dir_all(directory).unwrap(); -}