1use serde::{Deserialize, Serialize};
7use crate::ir::{Edge, Graph, Node};
8use crate::patch::Operation;
9use crate::types::*;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(tag = "action", rename_all = "snake_case")]
15pub enum UserAction {
16 AddSource { name: String, #[serde(default = "default_channels")] channels: u32 },
17 AddProcessor { name: String, #[serde(default = "default_channels")] channels: u32 },
18 AddAnalyzer { name: String, #[serde(default = "default_channels")] channels: u32 },
19 AddSink { name: String, #[serde(default = "default_channels")] channels: u32 },
20 AddForeign { name: String, plugin: String, #[serde(default = "default_channels")] channels: u32 },
21 Connect { from: String, from_port: Option<String>, to: String, to_port: Option<String> },
22 Disconnect { from: String, from_port: Option<String>, to: String, to_port: Option<String> },
23 RemoveNode { name: String },
24 FreezeNode { name: String, length_secs: f32 },
25 SetConfig { node: String, key: String, value: serde_json::Value },
26 AddModulation { source_node: String, source_port: String, target_node: String, target_param: String, amount: f32 },
27 RemoveModulation { id: StableId },
28 AddSubGraph { name: String },
29 ReplaceNode { name: String, new_kind_name: String },
30 DuplicateNode { node_id: StableId },
31 CheckoutRevision { revision: PatchId },
32 SquashHistory,
33 CreateSnapshot { name: String },
34 RunMonteCarlo { node_name: String, count: usize },
35 RunSensitivity { node_name: String },
36 RunStabilityMap { node_name: String, param: String, range_start: f32, range_end: f32 },
37}
38
39fn default_channels() -> u32 { 2 }
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct UserPatchFile {
43 pub description: Option<String>,
44 pub intent: Option<String>,
45 pub constraints: Vec<UserConstraint>,
46 pub actions: Vec<UserAction>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct UserConstraint {
51 #[serde(rename = "type")]
52 pub kind: String,
53 pub description: String,
54}
55
56#[derive(Debug, thiserror::Error)]
57pub enum ActionError {
58 #[error("node '{0}' not found")] NodeNotFound(String),
59 #[error("ambiguous node name '{0}'")] AmbiguousName(String),
60 #[error("invalid config for '{0}': {1}")] InvalidConfig(String, String),
61}
62
63pub fn compile_actions(actions: &[UserAction], graph: &Graph) -> Result<Vec<Operation>, ActionError> {
64 let mut ops = Vec::new();
65 let mut created: std::collections::HashMap<String, StableId> = std::collections::HashMap::new();
66
67 for action in actions {
68 match action {
69 UserAction::AddSource { name, channels } => {
70 let node = make_node(NodeKind::Source, name, *channels);
71 created.insert(name.clone(), node.id);
72 ops.push(Operation::AddNode(node));
73 }
74 UserAction::AddProcessor { name, channels } => {
75 let node = make_node(NodeKind::Processor, name, *channels);
76 created.insert(name.clone(), node.id);
77 ops.push(Operation::AddNode(node));
78 }
79 UserAction::AddAnalyzer { name, channels } => {
80 let node = make_node(NodeKind::Analyzer, name, *channels);
81 created.insert(name.clone(), node.id);
82 ops.push(Operation::AddNode(node));
83 }
84 UserAction::AddSink { name, channels } => {
85 let node = make_node(NodeKind::Sink, name, *channels);
86 created.insert(name.clone(), node.id);
87 ops.push(Operation::AddNode(node));
88 }
89 UserAction::AddForeign { name, plugin, channels } => {
90 let node = make_node(NodeKind::Foreign(plugin.clone()), name, *channels);
91 created.insert(name.clone(), node.id);
92 ops.push(Operation::AddNode(node));
93 }
94 UserAction::Connect { from, from_port, to, to_port } => {
95 let src_id = resolve_name(from, graph, &created)?;
96 let tgt_id = resolve_name(to, graph, &created)?;
97 let edge = Edge::new(
98 PortRef { node_id: src_id, port_name: from_port.clone().unwrap_or_else(|| "out".into()) },
99 PortRef { node_id: tgt_id, port_name: to_port.clone().unwrap_or_else(|| "in".into()) },
100 );
101 ops.push(Operation::AddEdge(edge));
102 }
103 UserAction::Disconnect { from, from_port, to, to_port } => {
104 let src_id = resolve_name(from, graph, &created)?;
105 let tgt_id = resolve_name(to, graph, &created)?;
106 let src_port = from_port.clone().unwrap_or_else(|| "out".into());
107 let tgt_port = to_port.clone().unwrap_or_else(|| "in".into());
108 if let Some(edge_id) = graph.edges.values().find_map(|e| {
109 if e.source.node_id == src_id && e.source.port_name == src_port && e.target.node_id == tgt_id && e.target.port_name == tgt_port { Some(e.id) } else { None }
110 }) {
111 ops.push(Operation::RemoveEdge(edge_id));
112 }
113 }
114 UserAction::RemoveNode { name } => {
115 let id = resolve_name(name, graph, &created)?;
116 ops.push(Operation::RemoveNode(id));
117 }
118 UserAction::SetConfig { node, key, value } => {
119 let id = resolve_name(node, graph, &created)?;
120 let config_val = json_to_config_value(value).map_err(|e| ActionError::InvalidConfig(key.clone(), e))?;
121 let mut delta = std::collections::BTreeMap::new();
122 delta.insert(key.clone(), ConfigChange { old: None, new: Some(config_val) });
123 ops.push(Operation::ModifyConfig { node_id: id, delta });
124 }
125 UserAction::AddModulation { source_node, source_port, target_node, target_param, amount } => {
126 let src_id = resolve_name(source_node, graph, &created)?;
127 let tgt_id = resolve_name(target_node, graph, &created)?;
128 let mod_ir = crate::ir::Modulation::new(
129 PortRef { node_id: src_id, port_name: source_port.clone() },
130 tgt_id, target_param.clone(), *amount,
131 );
132 ops.push(Operation::AddModulation(mod_ir));
133 }
134 UserAction::RemoveModulation { id } => { ops.push(Operation::RemoveModulation(*id)); }
135 UserAction::AddSubGraph { name } => {
136 let node = crate::ir::Node::new_subgraph(name);
137 created.insert(name.clone(), node.id);
138 ops.push(Operation::AddNode(node));
139 }
140 UserAction::ReplaceNode { name, new_kind_name } => {
141 let id = resolve_name(name, graph, &created)?;
142 let mut delta = std::collections::BTreeMap::new();
143 delta.insert("name".to_string(), ConfigChange { old: None, new: Some(ConfigValue::String(new_kind_name.clone())) });
144 ops.push(Operation::ModifyConfig { node_id: id, delta });
145 }
146 UserAction::DuplicateNode { .. } | UserAction::FreezeNode { .. } | UserAction::CheckoutRevision { .. } | UserAction::SquashHistory | UserAction::CreateSnapshot { .. } => {}
147 UserAction::RunMonteCarlo { node_name, count } => {
148 let id = resolve_name(node_name, graph, &created)?;
149 if let Some(node) = graph.nodes.get(&id) {
150 for _ in 0..*count {
151 for (key, value) in &node.config {
152 if let ConfigValue::Float(f) = value {
153 let noise = (rand::random::<f64>() - 0.5) * 0.1;
154 let mut delta = std::collections::BTreeMap::new();
155 delta.insert(key.clone(), ConfigChange { old: None, new: Some(ConfigValue::Float(*f + noise)) });
156 ops.push(Operation::ModifyConfig { node_id: id, delta });
157 }
158 }
159 }
160 }
161 }
162 UserAction::RunSensitivity { .. } | UserAction::RunStabilityMap { .. } => {}
163 }
164 }
165 Ok(ops)
166}
167
168pub fn node_name(node: &Node) -> String {
169 node.config.get("name").and_then(|v| v.as_string()).cloned().unwrap_or_else(|| node.id.to_string())
170}
171
172fn make_node(kind: NodeKind, name: &str, _channels: u32) -> Node {
173 let mut n = match kind {
174 NodeKind::Source => Node::new_source(name),
175 NodeKind::Processor => Node::new_processor(name),
176 NodeKind::Sink => Node::new_sink(name),
177 _ => Node::new_processor(name),
178 };
179 n.kind = kind;
180 n
182}
183
184pub fn resolve_name(name: &str, graph: &Graph, created: &std::collections::HashMap<String, StableId>) -> Result<StableId, ActionError> {
185 if let Some(&id) = created.get(name) { return Ok(id); }
186 if let Ok(id) = name.parse::<StableId>() { if graph.nodes.contains_key(&id) { return Ok(id); } }
187 let matches: Vec<StableId> = graph.nodes.iter().filter(|(_, n)| node_name(n) == name).map(|(&id, _)| id).collect();
188 match matches.len() {
189 0 => Err(ActionError::NodeNotFound(name.into())),
190 1 => Ok(matches[0]),
191 _ => Err(ActionError::AmbiguousName(name.into())),
192 }
193}
194
195fn json_to_config_value(v: &serde_json::Value) -> Result<ConfigValue, String> {
196 match v {
197 serde_json::Value::Number(n) => Ok(ConfigValue::Float(n.as_f64().unwrap_or(0.0))),
198 serde_json::Value::Bool(b) => Ok(ConfigValue::Bool(*b)),
199 serde_json::Value::String(s) => Ok(ConfigValue::String(s.clone())),
200 serde_json::Value::Array(arr) => {
201 let items: Result<Vec<_>, _> = arr.iter().map(json_to_config_value).collect();
202 Ok(ConfigValue::List(items?))
203 }
204 serde_json::Value::Object(map) => {
205 let mut bmap = std::collections::BTreeMap::new();
206 for (k, v) in map { bmap.insert(k.clone(), json_to_config_value(v)?); }
207 Ok(ConfigValue::Map(bmap))
208 }
209 serde_json::Value::Null => Err("null is not allowed".into()),
210 }
211}