sim_lib_view_agent/
editor.rs1use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
11use sim_lib_intent::{field, intent_kind_of};
12use sim_lib_topology::{
13 Edge, EdgeId, Graph, Node, NodeId, PatchOp, PortRef, TopologyPatch, apply_topology_patch_ops,
14};
15
16pub fn apply_composer_intent(cx: &mut Cx, graph: &Graph, intent: &Expr) -> Result<Graph> {
24 cx.require(&CapabilityName::new("topology-write"))?;
25 let ops = composer_ops(graph, intent)?;
26 let patch = TopologyPatch { ops };
27 apply_topology_patch_ops(graph, &patch)
28}
29
30fn composer_ops(graph: &Graph, intent: &Expr) -> Result<Vec<PatchOp>> {
31 let kind = intent_kind_of(intent)
32 .ok_or_else(|| Error::HostError("composer input is not an Intent".to_owned()))?;
33 match &*kind.name {
34 "create" => {
35 let verb = require_symbol(intent, "class")?;
36 let id = create_id(graph, intent, &verb);
37 let mut ops = vec![PatchOp::AddNode(Node::named(
38 NodeId(id.clone()),
39 verb.name.to_string(),
40 ))];
41 if let Some(at) = field(intent, "at") {
42 ops.push(PatchOp::SetMetadata {
43 key: pos_key(&id),
44 value: at.clone(),
45 });
46 }
47 Ok(ops)
48 }
49 "move" => {
50 let node = require_symbol(intent, "node")?;
51 let at = field(intent, "at").cloned().unwrap_or(Expr::Nil);
52 Ok(vec![PatchOp::SetMetadata {
53 key: pos_key(&node),
54 value: at,
55 }])
56 }
57 "wire" => {
58 let from = port_ref(field(intent, "from"))?;
59 let to = port_ref(field(intent, "to"))?;
60 Ok(vec![PatchOp::AddEdge {
61 edge: Edge::new(EdgeId(0), from, to),
62 explicit_id: false,
63 }])
64 }
65 "unwire" => {
66 let edge = field(intent, "edge")
67 .ok_or_else(|| Error::HostError("unwire is missing an 'edge'".to_owned()))?;
68 let from = port_ref(sub_field(edge, "from"))?;
69 let to = port_ref(sub_field(edge, "to"))?;
70 Ok(vec![PatchOp::RemoveEdge { from, to }])
71 }
72 "delete" => {
73 let targets = match field(intent, "targets") {
74 Some(Expr::List(items)) => items,
75 _ => {
76 return Err(Error::HostError(
77 "delete 'targets' must be a list".to_owned(),
78 ));
79 }
80 };
81 targets
82 .iter()
83 .map(|target| match target {
84 Expr::Symbol(symbol) => Ok(PatchOp::RemoveNode(NodeId(symbol.clone()))),
85 _ => Err(Error::HostError(
86 "delete target must be a node id".to_owned(),
87 )),
88 })
89 .collect()
90 }
91 other => Err(Error::HostError(format!(
92 "composer does not handle intent '{other}'"
93 ))),
94 }
95}
96
97fn create_id(graph: &Graph, intent: &Expr, verb: &Symbol) -> Symbol {
98 if let Some(args) = field(intent, "args")
99 && let Some(Expr::Symbol(id)) = sub_field(args, "id")
100 {
101 return id.clone();
102 }
103 Symbol::new(format!("{}{}", verb.name, graph.nodes.len()))
104}
105
106fn pos_key(node: &Symbol) -> Symbol {
107 Symbol::new(format!("pos:{}", node.name))
108}
109
110fn port_ref(field: Option<&Expr>) -> Result<PortRef> {
111 let map = field.ok_or_else(|| Error::HostError("missing a port reference".to_owned()))?;
112 let node = match sub_field(map, "node") {
113 Some(Expr::Symbol(symbol)) => symbol.clone(),
114 _ => {
115 return Err(Error::HostError(
116 "port ref 'node' must be a symbol".to_owned(),
117 ));
118 }
119 };
120 let port = match sub_field(map, "port") {
121 Some(Expr::Symbol(symbol)) => symbol.clone(),
122 _ => Symbol::new("out"),
123 };
124 Ok(PortRef::new(NodeId(node), port))
125}
126
127fn require_symbol(intent: &Expr, name: &str) -> Result<Symbol> {
128 match field(intent, name) {
129 Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
130 _ => Err(Error::HostError(format!(
131 "composer intent field '{name}' must be a symbol"
132 ))),
133 }
134}
135
136fn sub_field<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
137 let Expr::Map(entries) = map else {
138 return None;
139 };
140 entries.iter().find_map(|(key, value)| {
141 matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
142 .then_some(value)
143 })
144}