1pub mod parser;
2
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum PortType {
8 Signal,
10 SignalFloat,
12 IntSignal,
14 Float,
16 Int,
18 Bang,
20 List,
22 Symbol,
24 Any,
26 MultiChannelSignal,
28 MultiChannelSignalFloat,
30 Dynamic,
32 Inactive,
34}
35
36impl PortType {
37 pub fn from_xml_type(type_str: &str) -> Self {
40 let normalized = type_str
41 .to_lowercase()
42 .replace(" / ", "/")
43 .replace(", ", "/")
44 .replace(" or ", "/");
45 let normalized = normalized.trim();
46
47 match normalized {
48 "signal" => PortType::Signal,
49 "signal/float" | "float/signal" | "signal/float/symbol" | "signal/float/timevalue" => {
50 PortType::SignalFloat
51 }
52 "int/signal" | "signal/int" => PortType::IntSignal,
53 "float" | "double" => PortType::Float,
54 "int" | "long" | "int/voice" => PortType::Int,
55 "bang" => PortType::Bang,
56 "list" => PortType::List,
57 "symbol" => PortType::Symbol,
58 "anything" | "message" | "bang/int" | "bang/anything" | "int/float"
59 | "int/float/list" | "int/list" | "float/list" | "int/float/sig" | "signal/msg"
60 | "signal/message" | "signal/list" | "dictionary" | "dict" | "setvalue"
61 | "midievent" | "matrix" => PortType::Any,
62 "multi-channel signal" | "signal/multi-channel signal" => PortType::MultiChannelSignal,
63 "multi-channel signal/float" | "multi-channel signal/message" => {
64 PortType::MultiChannelSignalFloat
65 }
66 "inlet_type" | "outlet_type" | "objarg_type" => PortType::Dynamic,
67 "inactive" => PortType::Inactive,
68 "" => PortType::Any,
69 _ => PortType::Any,
70 }
71 }
72
73 pub fn accepts_signal(&self) -> bool {
75 matches!(
76 self,
77 PortType::Signal
78 | PortType::SignalFloat
79 | PortType::IntSignal
80 | PortType::MultiChannelSignal
81 | PortType::MultiChannelSignalFloat
82 )
83 }
84
85 pub fn accepts_control(&self) -> bool {
87 matches!(
88 self,
89 PortType::SignalFloat
90 | PortType::IntSignal
91 | PortType::Float
92 | PortType::Int
93 | PortType::Bang
94 | PortType::List
95 | PortType::Symbol
96 | PortType::Any
97 | PortType::Dynamic
98 | PortType::MultiChannelSignalFloat
99 )
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Module {
106 Max,
107 Msp,
108 Other(String),
109}
110
111impl Module {
112 pub fn parse(s: &str) -> Self {
113 match s.to_lowercase().as_str() {
114 "max" => Module::Max,
115 "msp" => Module::Msp,
116 other => Module::Other(other.to_string()),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct PortDef {
124 pub id: u32,
126 pub port_type: PortType,
128 pub is_hot: bool,
130 pub description: String,
132}
133
134#[derive(Debug, Clone)]
136pub enum InletSpec {
137 Fixed(Vec<PortDef>),
139 Variable {
141 defaults: Vec<PortDef>,
143 min_inlets: u32,
145 },
146}
147
148#[derive(Debug, Clone)]
150pub enum OutletSpec {
151 Fixed(Vec<PortDef>),
153 Variable {
155 defaults: Vec<PortDef>,
157 min_outlets: u32,
159 },
160}
161
162#[derive(Debug, Clone)]
164pub struct ArgDef {
165 pub name: String,
166 pub arg_type: String,
167 pub optional: bool,
168}
169
170#[derive(Debug, Clone)]
172pub struct ObjectDef {
173 pub name: String,
175 pub module: Module,
177 pub category: String,
179 pub digest: String,
181 pub inlets: InletSpec,
183 pub outlets: OutletSpec,
185 pub args: Vec<ArgDef>,
187}
188
189impl ObjectDef {
190 pub fn has_variable_inlets(&self) -> bool {
192 matches!(self.inlets, InletSpec::Variable { .. })
193 }
194
195 pub fn has_variable_outlets(&self) -> bool {
197 matches!(self.outlets, OutletSpec::Variable { .. })
198 }
199
200 pub fn default_inlet_count(&self) -> usize {
202 match &self.inlets {
203 InletSpec::Fixed(ports) => ports.len(),
204 InletSpec::Variable { defaults, .. } => defaults.len(),
205 }
206 }
207
208 pub fn default_outlet_count(&self) -> usize {
210 match &self.outlets {
211 OutletSpec::Fixed(ports) => ports.len(),
212 OutletSpec::Variable { defaults, .. } => defaults.len(),
213 }
214 }
215}
216
217#[derive(Debug)]
219pub struct ObjectDb {
220 objects: HashMap<String, ObjectDef>,
221}
222
223impl ObjectDb {
224 pub fn new() -> Self {
226 ObjectDb {
227 objects: HashMap::new(),
228 }
229 }
230
231 pub fn insert(&mut self, def: ObjectDef) {
233 self.objects.insert(def.name.clone(), def);
234 }
235
236 pub fn lookup(&self, name: &str) -> Option<&ObjectDef> {
238 self.objects.get(name)
239 }
240
241 pub fn len(&self) -> usize {
243 self.objects.len()
244 }
245
246 pub fn is_empty(&self) -> bool {
248 self.objects.is_empty()
249 }
250
251 pub fn names(&self) -> impl Iterator<Item = &str> {
253 self.objects.keys().map(|s| s.as_str())
254 }
255
256 pub fn by_module(&self, module: &Module) -> Vec<&ObjectDef> {
258 self.objects
259 .values()
260 .filter(|def| &def.module == module)
261 .collect()
262 }
263}
264
265impl Default for ObjectDb {
266 fn default() -> Self {
267 Self::new()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_port_type_from_xml_signal() {
277 assert_eq!(PortType::from_xml_type("signal"), PortType::Signal);
278 assert_eq!(PortType::from_xml_type("Signal"), PortType::Signal);
279 }
280
281 #[test]
282 fn test_port_type_from_xml_signal_float_variants() {
283 assert_eq!(
284 PortType::from_xml_type("signal/float"),
285 PortType::SignalFloat
286 );
287 assert_eq!(
288 PortType::from_xml_type("Signal/Float"),
289 PortType::SignalFloat
290 );
291 assert_eq!(
292 PortType::from_xml_type("signal, float"),
293 PortType::SignalFloat
294 );
295 assert_eq!(
296 PortType::from_xml_type("signal or float"),
297 PortType::SignalFloat
298 );
299 assert_eq!(
300 PortType::from_xml_type("float/signal"),
301 PortType::SignalFloat
302 );
303 assert_eq!(
304 PortType::from_xml_type("float / signal"),
305 PortType::SignalFloat
306 );
307 }
308
309 #[test]
310 fn test_port_type_from_xml_int_signal_variants() {
311 assert_eq!(PortType::from_xml_type("int/signal"), PortType::IntSignal);
312 assert_eq!(PortType::from_xml_type("signal/int"), PortType::IntSignal);
313 assert_eq!(PortType::from_xml_type("signal, int"), PortType::IntSignal);
314 assert_eq!(PortType::from_xml_type("int / signal"), PortType::IntSignal);
315 }
316
317 #[test]
318 fn test_port_type_from_xml_dynamic() {
319 assert_eq!(PortType::from_xml_type("INLET_TYPE"), PortType::Dynamic);
320 assert_eq!(PortType::from_xml_type("OUTLET_TYPE"), PortType::Dynamic);
321 }
322
323 #[test]
324 fn test_port_type_from_xml_control_types() {
325 assert_eq!(PortType::from_xml_type("float"), PortType::Float);
326 assert_eq!(PortType::from_xml_type("int"), PortType::Int);
327 assert_eq!(PortType::from_xml_type("bang"), PortType::Bang);
328 assert_eq!(PortType::from_xml_type("list"), PortType::List);
329 assert_eq!(PortType::from_xml_type("symbol"), PortType::Symbol);
330 assert_eq!(PortType::from_xml_type("anything"), PortType::Any);
331 }
332
333 #[test]
334 fn test_port_type_accepts_signal() {
335 assert!(PortType::Signal.accepts_signal());
336 assert!(PortType::SignalFloat.accepts_signal());
337 assert!(PortType::IntSignal.accepts_signal());
338 assert!(!PortType::Float.accepts_signal());
339 assert!(!PortType::Any.accepts_signal());
340 assert!(!PortType::Dynamic.accepts_signal());
341 }
342
343 #[test]
344 fn test_port_type_accepts_control() {
345 assert!(!PortType::Signal.accepts_control());
346 assert!(PortType::SignalFloat.accepts_control());
347 assert!(PortType::IntSignal.accepts_control());
348 assert!(PortType::Float.accepts_control());
349 assert!(PortType::Any.accepts_control());
350 assert!(PortType::Dynamic.accepts_control());
351 }
352
353 #[test]
354 fn test_module_from_str() {
355 assert_eq!(Module::parse("max"), Module::Max);
356 assert_eq!(Module::parse("msp"), Module::Msp);
357 assert_eq!(Module::parse("jit"), Module::Other("jit".to_string()));
358 }
359
360 #[test]
361 fn test_object_db_basic_operations() {
362 let mut db = ObjectDb::new();
363 assert!(db.is_empty());
364 assert_eq!(db.len(), 0);
365
366 let def = ObjectDef {
367 name: "cycle~".to_string(),
368 module: Module::Msp,
369 category: "MSP Synthesis".to_string(),
370 digest: "Sinusoidal oscillator".to_string(),
371 inlets: InletSpec::Fixed(vec![
372 PortDef {
373 id: 0,
374 port_type: PortType::SignalFloat,
375 is_hot: true,
376 description: "Frequency".to_string(),
377 },
378 PortDef {
379 id: 1,
380 port_type: PortType::SignalFloat,
381 is_hot: false,
382 description: "Phase (0-1)".to_string(),
383 },
384 ]),
385 outlets: OutletSpec::Fixed(vec![PortDef {
386 id: 0,
387 port_type: PortType::Signal,
388 is_hot: false,
389 description: "Output".to_string(),
390 }]),
391 args: vec![],
392 };
393
394 db.insert(def);
395 assert_eq!(db.len(), 1);
396 assert!(!db.is_empty());
397
398 let looked_up = db.lookup("cycle~").unwrap();
399 assert_eq!(looked_up.name, "cycle~");
400 assert_eq!(looked_up.module, Module::Msp);
401 assert_eq!(looked_up.default_inlet_count(), 2);
402 assert_eq!(looked_up.default_outlet_count(), 1);
403 assert!(!looked_up.has_variable_inlets());
404
405 assert!(db.lookup("nonexistent").is_none());
406 }
407}