Skip to main content

flutmax_objdb/
lib.rs

1pub mod parser;
2
3use std::collections::HashMap;
4
5/// Port (inlet/outlet) type for Max objects
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum PortType {
8    /// Signal only (audio rate only)
9    Signal,
10    /// Signal + Float dual (controlled by float when no signal connection)
11    SignalFloat,
12    /// Int + Signal dual
13    IntSignal,
14    /// Float only
15    Float,
16    /// Int only
17    Int,
18    /// Bang only
19    Bang,
20    /// List only
21    List,
22    /// Symbol only
23    Symbol,
24    /// Any message (bang, int, float, list, symbol)
25    Any,
26    /// Multi-channel signal
27    MultiChannelSignal,
28    /// Multi-channel signal + float
29    MultiChannelSignalFloat,
30    /// Depends on argument type (placeholder for variable objects)
31    Dynamic,
32    /// Inactive inlet/outlet
33    Inactive,
34}
35
36impl PortType {
37    /// Convert XML type attribute string to PortType.
38    /// Normalizes case differences and notation variants (/, " or ", ", ").
39    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    /// Whether this port accepts Signal
74    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    /// Whether this port accepts Control messages
86    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/// Module type
104#[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/// Inlet definition
122#[derive(Debug, Clone)]
123pub struct PortDef {
124    /// Port ID (0-based)
125    pub id: u32,
126    /// Port type
127    pub port_type: PortType,
128    /// Whether this is a hot inlet (triggers output on message receipt). Always false for outlets.
129    pub is_hot: bool,
130    /// Description (digest text)
131    pub description: String,
132}
133
134/// Inlet configuration (fixed or variable count)
135#[derive(Debug, Clone)]
136pub enum InletSpec {
137    /// Fixed number of inlets (e.g., cycle~ = 2, biquad~ = 6)
138    Fixed(Vec<PortDef>),
139    /// Variable inlets depending on argument count (e.g., pack)
140    Variable {
141        /// Representative inlet definitions described in XML
142        defaults: Vec<PortDef>,
143        /// Minimum number of inlets
144        min_inlets: u32,
145    },
146}
147
148/// Outlet configuration (fixed or variable count)
149#[derive(Debug, Clone)]
150pub enum OutletSpec {
151    /// Fixed number of outlets
152    Fixed(Vec<PortDef>),
153    /// Variable outlets depending on argument count (e.g., trigger)
154    Variable {
155        /// Representative outlet definitions described in XML
156        defaults: Vec<PortDef>,
157        /// Minimum number of outlets
158        min_outlets: u32,
159    },
160}
161
162/// Object argument definition
163#[derive(Debug, Clone)]
164pub struct ArgDef {
165    pub name: String,
166    pub arg_type: String,
167    pub optional: bool,
168}
169
170/// Object definition
171#[derive(Debug, Clone)]
172pub struct ObjectDef {
173    /// Object name (e.g., "cycle~", "pack", "trigger")
174    pub name: String,
175    /// Module (max, msp, etc.)
176    pub module: Module,
177    /// Category (e.g., "MSP Synthesis", "Lists")
178    pub category: String,
179    /// Short description
180    pub digest: String,
181    /// Inlet definitions
182    pub inlets: InletSpec,
183    /// Outlet definitions
184    pub outlets: OutletSpec,
185    /// Argument definitions
186    pub args: Vec<ArgDef>,
187}
188
189impl ObjectDef {
190    /// Whether this ObjectDef has variable inlets
191    pub fn has_variable_inlets(&self) -> bool {
192        matches!(self.inlets, InletSpec::Variable { .. })
193    }
194
195    /// Whether this ObjectDef has variable outlets
196    pub fn has_variable_outlets(&self) -> bool {
197        matches!(self.outlets, OutletSpec::Variable { .. })
198    }
199
200    /// Returns the inlet count in the default configuration
201    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    /// Returns the outlet count in the default configuration
209    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/// Object definition database
218#[derive(Debug)]
219pub struct ObjectDb {
220    objects: HashMap<String, ObjectDef>,
221}
222
223impl ObjectDb {
224    /// Create an empty database
225    pub fn new() -> Self {
226        ObjectDb {
227            objects: HashMap::new(),
228        }
229    }
230
231    /// Insert an ObjectDef
232    pub fn insert(&mut self, def: ObjectDef) {
233        self.objects.insert(def.name.clone(), def);
234    }
235
236    /// Look up by object name
237    pub fn lookup(&self, name: &str) -> Option<&ObjectDef> {
238        self.objects.get(name)
239    }
240
241    /// Number of registered objects
242    pub fn len(&self) -> usize {
243        self.objects.len()
244    }
245
246    /// Whether the database is empty
247    pub fn is_empty(&self) -> bool {
248        self.objects.is_empty()
249    }
250
251    /// Iterator over all object names
252    pub fn names(&self) -> impl Iterator<Item = &str> {
253        self.objects.keys().map(|s| s.as_str())
254    }
255
256    /// Iterator over ObjectDefs filtered by module
257    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}