本文将带你深入了解 Rust 语言,并通过一个具体的例子来学习如何使用 Rust 创建一个类似于 Graphviz 的 DOT DSL。
挑战
领域特定语言 (DSL) 是一种针对特定领域进行优化的语言。由于 DSL 的目标性,它可以通过让编写者声明“想要什么”而不是“如何实现”来极大地提高生产力/理解力。
DSL 在处理复杂的定制和配置方面有着广泛的应用。
例如,DOT 语言 允许你用文本描述一个图,然后由 Graphviz 工具(例如 dot
)将其转换为图像。一个简单的图看起来像这样:
graph {
graph [bgcolor="yellow"]
a [color="red"]
b [color="blue"]
a -- b [color="green"]
}
将这段代码保存到一个名为 example.dot
的文件中,然后运行 dot example.dot -T png -o example.png
命令,就可以生成一个名为 example.png
的图像,该图像包含一个红色圆圈和一个蓝色圆圈,它们用一条绿色线连接起来,背景为黄色。
挑战:编写一个类似于 Graphviz dot 语言的领域特定语言 (DSL)。
我们的 DSL 类似于 Graphviz dot 语言,因为它将用于创建图数据结构。但是,与 DOT 语言不同的是,我们的 DSL 将是一个内部 DSL,仅在我们自己的语言中使用。
关于内部 DSL 和外部 DSL 的差异,可以参考 这里。
建造者模式
本练习要求你使用建造者模式来构建多个结构体。简而言之,这种模式允许你将包含大量参数的结构体构造函数拆分成多个独立的函数。
这种方法使你可以进行紧凑但高度灵活的结构体构造和配置。你可以在 以下页面 中了解更多关于建造者模式的信息。
解决方法
为了实现这个解决方案,我们需要一个名为 maplit
的外部 crate。
pub mod graph {
pub use std::collections::HashMap;
use maplit;
pub type Attrs = [(&'static str, &'static str)];
pub mod graph_items {
pub use super::{Attrs, HashMap};
pub mod edge {
use super::{Attrs, HashMap};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Edge {
pub attrs: HashMap<&'static str, &'static str>,
}
impl Edge {
pub fn new(a: &'static str, b: &'static str) -> Self {
Edge {
attrs: maplit::hashmap! {
a => b
},
}
}
pub fn with_attrs(mut self, attrs: &Attrs) -> Self {
for &(attr, value) in attrs.iter() {
self.attrs.insert(attr, value);
}
self
}
pub fn attr(&self, a: &'static str) -> Option<&str> {
self.attrs.get(&a).copied()
}
}
}
pub mod node {
use super::{Attrs, HashMap};
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Node {
pub name: &'static str,
pub attrs: HashMap<&'static str, &'static str>,
}
impl Node {
pub fn new(name: &'static str) -> Self {
let attrs = HashMap::<&'static str, &'static str>::new();
Node { name, attrs }
}
pub fn with_attrs(mut self, attrs: &Attrs) -> Self {
self.attrs.extend(attrs.iter().cloned());
self
}
pub fn attr(&self, a: &'static str) -> Option<&str> {
self.attrs.get(&a).copied()
}
}
}
}
use graph_items::{edge::Edge, node::Node};
#[derive(Debug, PartialEq, Eq)]
pub struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub attrs: HashMap<&'static str, &'static str>,
}
impl Graph {
pub fn new() -> Self {
Graph {
edges: Vec::new(),
nodes: Vec::new(),
attrs: HashMap::new(),
}
}
pub fn with_attrs(mut self, attrs: &Attrs) -> Self {
for &(attr, value) in attrs.iter() {
self.attrs.insert(attr, value);
}
self
}
pub fn with_nodes(mut self, nodes: &[Node]) -> Self {
self.nodes.extend(nodes.iter().cloned());
self
}
pub fn with_edges(mut self, edges: &[Edge]) -> Self {
self.edges.extend(edges.iter().cloned());
self
}
pub fn node(&self, name: &'static str) -> Option<&Node> {
let mut node: Option<&Node> = None;
for n in self.nodes.iter() {
if n.name.eq(name) {
node = Some(n);
break;
}
}
node
}
}
}
#[cfg(test)]
mod tests {
use super::graph::{graph_items::edge::Edge, graph_items::node::Node, Attrs, Graph};
use std::collections::HashMap;
#[test]
fn test_graph_initialization() {
let graph = Graph::new();
assert!(graph.nodes.is_empty());
assert!(graph.edges.is_empty());
assert!(graph.attrs.is_empty());
}
#[test]
fn test_graph_with_nodes_and_edges() {
let nodes = vec![
Node::new("a").with_attrs(&[("color", "green")]),
Node::new("b").with_attrs(&[("label", "Node B")]),
];
let edges = vec![Edge::new("a", "b").with_attrs(&[("color", "blue")])];
let attrs = vec![("title", "Graph Testing")];
let graph = Graph::new()
.with_nodes(&nodes)
.with_edges(&edges)
.with_attrs(&attrs);
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.attrs.get("title"), Some(&"Graph Testing"));
let edge = &graph.edges[0];
assert_eq!(edge.attr("color"), Some("blue"));
let node = graph.node("a").expect("Node a should exist");
assert_eq!(node.attr("color"), Some("green"));
}
#[test]
fn test_graph_with_node_attributes() {
let nodes = vec![
Node::new("x").with_attrs(&[("color", "red")]),
Node::new("y").with_attrs(&[("size", "large")]),
];
let graph = Graph::new().with_nodes(&nodes);
let node_x = graph.node("x").expect("Node x should exist");
assert_eq!(node_x.attr("color"), Some("red"));
let node_y = graph.node("y").expect("Node y should exist");
assert_eq!(node_y.attr("size"), Some("large"));
}
#[test]
fn test_graph_with_edge_attributes() {
let edges = vec![Edge::new("start", "end").with_attrs(&[("weight", "5")])];
let graph = Graph::new().with_edges(&edges);
let edge = &graph.edges[0];
assert_eq!(edge.attr("weight"), Some("5"));
}
#[test]
fn test_graph_with_attrs() {
let attrs: &Attrs = &[("foo", "bar"), ("baz", "qux")];
let graph = Graph::new().with_attrs(&attrs);
assert_eq!(graph.attrs.get("foo"), Some(&"bar"));
assert_eq!(graph.attrs.get("baz"), Some(&"qux"));
}
#[test]
fn test_node_creation_with_attrs() {
let node = Node::new("test").with_attrs(&[("color", "blue"), ("size", "medium")]);
assert_eq!(node.attr("color"), Some("blue"));
assert_eq!(node.attr("size"), Some("medium"));
}
#[test]
fn test_edge_creation_with_attrs() {
let edge = Edge::new("a", "b").with_attrs(&[("color", "black")]);
assert_eq!(edge.attr("color"), Some("black"));
}
}
总结
通过这个例子,我们学习了如何使用 Rust 创建一个类似于 Graphviz 的 DOT DSL。我们使用了建造者模式来构建我们的结构体,并使用 maplit
crate 来简化代码编写。
希望这篇文章能帮助你更好地理解 Rust 语言,并激发你使用 Rust 创建自己的 DSL 的兴趣!
扩展
除了上面的例子,我们还可以扩展我们的 DSL 来支持更多功能,例如:
支持不同的图类型,例如有向图、无向图等。 支持自定义节点形状和边样式。 支持图的布局控制,例如指定节点排列方式等。 支持将生成的图数据结构保存到文件或数据库中。
这些扩展都可以通过添加新的结构体、函数和方法来实现。