Skip to main content

sim_lib_topology/
patch.rs

1//! Topology patch parsing and clone-apply support.
2
3use sim_kernel::{Cx, Error, Expr, Result, Symbol};
4
5use crate::{
6    Budget, Cell, Edge, EdgeId, Graph, Node, NodeId, PortRef, Scheduler, TopologyConnection,
7    capability::topology_write_capability, compile_graph, site::connection_from_graph,
8};
9
10mod data;
11
12/// A deterministic sequence of topology patch operations.
13#[derive(Clone, Debug)]
14pub struct TopologyPatch {
15    /// Patch operations in application order.
16    pub ops: Vec<PatchOp>,
17}
18
19impl TopologyPatch {
20    /// Parses patch data from the public Lisp operation forms.
21    pub fn from_expr(expr: &Expr) -> Result<Self> {
22        let ops = data::parse_patch_ops(expr)?;
23        if ops.is_empty() {
24            return Err(patch_error("patch requires at least one operation"));
25        }
26        Ok(Self { ops })
27    }
28
29    /// Converts patch data back to the public Lisp operation forms.
30    pub fn to_expr(&self) -> Expr {
31        data::patch_ops_to_expr(&self.ops)
32    }
33}
34
35/// One topology patch operation.
36#[derive(Clone, Debug)]
37pub enum PatchOp {
38    /// Add a graph node.
39    AddNode(Node),
40    /// Remove a graph node by id.
41    RemoveNode(NodeId),
42    /// Replace an existing graph node.
43    ReplaceNode {
44        /// The node id to replace.
45        id: NodeId,
46        /// The replacement node.
47        node: Node,
48    },
49    /// Add a graph edge.
50    AddEdge {
51        /// The edge to add.
52        edge: Edge,
53        /// Whether the edge id was given explicitly rather than assigned.
54        explicit_id: bool,
55    },
56    /// Remove an edge identified by source and destination endpoints.
57    RemoveEdge {
58        /// The source endpoint.
59        from: PortRef,
60        /// The destination endpoint.
61        to: PortRef,
62    },
63    /// Replace an edge identified by source and destination endpoints.
64    ReplaceEdge {
65        /// The source endpoint of the edge to replace.
66        from: PortRef,
67        /// The destination endpoint of the edge to replace.
68        to: PortRef,
69        /// The replacement edge.
70        edge: Edge,
71        /// Whether the edge id was given explicitly rather than assigned.
72        explicit_id: bool,
73    },
74    /// Add a graph state cell.
75    AddCell(Cell),
76    /// Replace graph budget settings.
77    SetBudget(Budget),
78    /// Replace graph scheduler settings.
79    SetScheduler(Scheduler),
80    /// Set or insert one graph metadata entry.
81    SetMetadata {
82        /// The metadata key.
83        key: Symbol,
84        /// The metadata value.
85        value: Expr,
86    },
87}
88
89/// Applies a patch to a clone, validates, compiles, and returns the new graph.
90pub fn apply_topology_patch(cx: &mut Cx, source: &Graph, patch: &TopologyPatch) -> Result<Graph> {
91    cx.require(&topology_write_capability())?;
92    let graph = apply_topology_patch_ops(source, patch)?;
93    compile_graph(cx, &graph)?;
94    Ok(graph)
95}
96
97/// Applies a patch's operations to a copy of `source` without compiling or
98/// validating the result. This is the editing surface for tools that build a
99/// topology incrementally (for example the Web-UI composer), where intermediate
100/// graphs are legitimately incomplete; validation runs later at save or run.
101/// Capability gating is the caller's responsibility.
102///
103/// # Examples
104///
105/// ```rust
106/// use sim_kernel::{Expr, Symbol};
107/// use sim_lib_topology::{PatchOp, TopologyPatch, apply_topology_patch_ops, parse_package};
108///
109/// let package = parse_package(
110///     "graph:\ntopology flow\nnode in verb=in\nnode out verb=out\nwire in -> out\n",
111/// )
112/// .unwrap();
113///
114/// let patch = TopologyPatch {
115///     ops: vec![PatchOp::SetMetadata {
116///         key: Symbol::new("note"),
117///         value: Expr::String("edited".to_owned()),
118///     }],
119/// };
120/// let edited = apply_topology_patch_ops(&package.graph, &patch).unwrap();
121///
122/// assert!(
123///     edited
124///         .metadata
125///         .iter()
126///         .any(|(key, _)| key == &Symbol::new("note"))
127/// );
128/// ```
129pub fn apply_topology_patch_ops(source: &Graph, patch: &TopologyPatch) -> Result<Graph> {
130    let mut graph = source.clone();
131    for op in &patch.ops {
132        apply_op(&mut graph, op)?;
133    }
134    Ok(graph)
135}
136
137/// Applies a patch and returns a new runnable topology connection.
138pub fn patched_connection(
139    cx: &mut Cx,
140    source: &Graph,
141    patch: &TopologyPatch,
142) -> Result<TopologyConnection> {
143    let graph = apply_topology_patch(cx, source, patch)?;
144    connection_from_graph(cx, &graph)
145}
146
147fn apply_op(graph: &mut Graph, op: &PatchOp) -> Result<()> {
148    match op {
149        PatchOp::AddNode(node) => graph.nodes.push(node.clone()),
150        PatchOp::RemoveNode(id) => remove_node(graph, id)?,
151        PatchOp::ReplaceNode { id, node } => replace_node(graph, id, node)?,
152        PatchOp::AddEdge { edge, explicit_id } => add_edge(graph, edge, *explicit_id),
153        PatchOp::RemoveEdge { from, to } => remove_edge(graph, from, to)?,
154        PatchOp::ReplaceEdge {
155            from,
156            to,
157            edge,
158            explicit_id,
159        } => replace_edge(graph, from, to, edge, *explicit_id)?,
160        PatchOp::AddCell(cell) => graph.cells.push(cell.clone()),
161        PatchOp::SetBudget(budget) => graph.budget = budget.clone(),
162        PatchOp::SetScheduler(scheduler) => graph.scheduler = scheduler.clone(),
163        PatchOp::SetMetadata { key, value } => set_metadata(graph, key, value.clone()),
164    }
165    Ok(())
166}
167
168fn remove_node(graph: &mut Graph, id: &NodeId) -> Result<()> {
169    let before = graph.nodes.len();
170    graph.nodes.retain(|node| &node.id != id);
171    if graph.nodes.len() == before {
172        return Err(patch_error(format!(
173            "remove-node target {} does not exist",
174            id.as_symbol()
175        )));
176    }
177    Ok(())
178}
179
180fn replace_node(graph: &mut Graph, id: &NodeId, node: &Node) -> Result<()> {
181    if &node.id != id {
182        return Err(patch_error(format!(
183            "replace-node replacement id {} does not match target {}",
184            node.id.as_symbol(),
185            id.as_symbol()
186        )));
187    }
188    let Some(slot) = graph.nodes.iter_mut().find(|existing| &existing.id == id) else {
189        return Err(patch_error(format!(
190            "replace-node target {} does not exist",
191            id.as_symbol()
192        )));
193    };
194    *slot = node.clone();
195    Ok(())
196}
197
198fn add_edge(graph: &mut Graph, edge: &Edge, explicit_id: bool) {
199    let mut edge = edge.clone();
200    if !explicit_id {
201        edge.id = next_edge_id(graph);
202    }
203    graph.edges.push(edge);
204}
205
206fn remove_edge(graph: &mut Graph, from: &PortRef, to: &PortRef) -> Result<()> {
207    let before = graph.edges.len();
208    graph
209        .edges
210        .retain(|edge| &edge.from != from || &edge.to != to);
211    if graph.edges.len() == before {
212        return Err(patch_error("remove-edge target does not exist"));
213    }
214    Ok(())
215}
216
217fn replace_edge(
218    graph: &mut Graph,
219    from: &PortRef,
220    to: &PortRef,
221    edge: &Edge,
222    explicit_id: bool,
223) -> Result<()> {
224    let Some(slot) = graph
225        .edges
226        .iter_mut()
227        .find(|existing| &existing.from == from && &existing.to == to)
228    else {
229        return Err(patch_error("replace-edge target does not exist"));
230    };
231    let mut replacement = edge.clone();
232    if !explicit_id {
233        replacement.id = slot.id;
234    }
235    *slot = replacement;
236    Ok(())
237}
238
239fn set_metadata(graph: &mut Graph, key: &Symbol, value: Expr) {
240    if let Some((_, existing)) = graph.metadata.iter_mut().find(|(name, _)| name == key) {
241        *existing = value;
242    } else {
243        graph.metadata.push((key.clone(), value));
244    }
245}
246
247fn next_edge_id(graph: &Graph) -> EdgeId {
248    EdgeId::new(
249        graph
250            .edges
251            .iter()
252            .map(|edge| edge.id.0)
253            .max()
254            .unwrap_or(0)
255            .saturating_add(1),
256    )
257}
258
259fn patch_error(message: impl Into<String>) -> Error {
260    Error::Eval(format!("topology patch error: {}", message.into()))
261}