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