Skip to main content

fmi_export/fmi3/
variable_builder.rs

1use fmi::fmi3::{binding, schema};
2
3use crate::fmi3::types::{Binary, Clock};
4
5/// Wrapper for start values that can be either scalar or vector
6pub enum StartValue<T> {
7    Scalar(T),
8    Vector(Vec<T>),
9}
10
11impl<T> From<T> for StartValue<T> {
12    fn from(value: T) -> Self {
13        StartValue::Scalar(value)
14    }
15}
16
17impl<T> From<Vec<T>> for StartValue<T> {
18    fn from(value: Vec<T>) -> Self {
19        StartValue::Vector(value)
20    }
21}
22
23impl<T, const N: usize> From<[T; N]> for StartValue<T> {
24    fn from(value: [T; N]) -> Self {
25        StartValue::Vector(value.into())
26    }
27}
28
29impl<T: Clone> From<StartValue<T>> for Vec<T> {
30    fn from(value: StartValue<T>) -> Self {
31        match value {
32            StartValue::Scalar(v) => vec![v],
33            StartValue::Vector(v) => v,
34        }
35    }
36}
37
38// Special implementation for byte arrays to support Binary start values
39impl<const N: usize> From<&[u8; N]> for StartValue<Vec<u8>> {
40    fn from(value: &[u8; N]) -> Self {
41        StartValue::Scalar(value.to_vec())
42    }
43}
44
45impl From<&[u8]> for StartValue<Vec<u8>> {
46    fn from(value: &[u8]) -> Self {
47        StartValue::Scalar(value.to_vec())
48    }
49}
50
51/// A builder for creating FMI variables with a fluent interface.
52///
53/// This builder holds all possible FMI variable attributes. The generic type `T`
54/// determines which concrete FMI variable type will be created by `finish()`.
55/// Type-specific `finish()` implementations will extract only the relevant fields.
56pub struct VariableBuilder<T>
57where
58    T: FmiVariableBuilder,
59{
60    // Common fields for all variable types
61    name: String,
62    value_reference: binding::fmi3ValueReference,
63    description: Option<String>,
64    causality: Option<schema::Causality>,
65    variability: Option<schema::Variability>,
66    can_handle_multiple_set_per_time_instant: Option<bool>,
67    intermediate_update: Option<bool>,
68    previous: Option<u32>,
69    declared_type: Option<String>,
70    initial: Option<schema::Initial>,
71
72    // Type-specific start value
73    start: Option<T::Start>,
74
75    // Float-specific fields (for continuous variables)
76    derivative: Option<u32>, // Value reference of the state variable this is a derivative of
77    reinit: Option<bool>,
78
79    // Float attributes
80    min: Option<f64>,
81    max: Option<f64>,
82    nominal: Option<f64>,
83
84    // Integer attributes
85    quantity: Option<String>,
86
87    // Binary attributes
88    max_size: Option<usize>,
89    mime_type: Option<String>,
90
91    // Clock attributes
92    clocks: Option<Vec<u32>>,
93    interval_variability: Option<schema::IntervalVariability>,
94
95    // Dimensions for array variables
96    dimensions: Vec<schema::Dimension>,
97
98    _phantom: std::marker::PhantomData<T>,
99}
100
101impl<T> VariableBuilder<T>
102where
103    T: FmiVariableBuilder,
104{
105    /// Create a new variable builder with the given name.
106    pub fn new(name: impl Into<String>, value_reference: binding::fmi3ValueReference) -> Self {
107        Self {
108            name: name.into(),
109            value_reference,
110            description: None,
111            causality: None,
112            variability: None,
113            can_handle_multiple_set_per_time_instant: None,
114            intermediate_update: None,
115            previous: None,
116            declared_type: None,
117            initial: None,
118            start: None,
119            derivative: None,
120            reinit: None,
121            min: None,
122            max: None,
123            nominal: None,
124            quantity: None,
125            max_size: None,
126            mime_type: None,
127            clocks: None,
128            interval_variability: None,
129            dimensions: Vec::new(),
130            _phantom: std::marker::PhantomData,
131        }
132    }
133
134    // Common attribute setters
135
136    /// Set the description for the variable.
137    pub fn with_description(mut self, description: impl Into<String>) -> Self {
138        self.description = Some(description.into());
139        self
140    }
141
142    /// Set the causality for the variable.
143    pub fn with_causality(mut self, causality: schema::Causality) -> Self {
144        self.causality = Some(causality);
145        self
146    }
147
148    /// Set the variability for the variable.
149    pub fn with_variability(mut self, variability: schema::Variability) -> Self {
150        self.variability = Some(variability);
151        self
152    }
153
154    /// Set whether the variable can handle multiple set operations per time instant.
155    pub fn with_can_handle_multiple_set_per_time_instant(mut self, value: bool) -> Self {
156        self.can_handle_multiple_set_per_time_instant = Some(value);
157        self
158    }
159
160    /// Set whether the variable can be updated during intermediate update mode.
161    pub fn with_intermediate_update(mut self, value: bool) -> Self {
162        self.intermediate_update = Some(value);
163        self
164    }
165
166    /// Set the value reference of the variable that provides the previous value.
167    pub fn with_previous(mut self, value_reference: u32) -> Self {
168        self.previous = Some(value_reference);
169        self
170    }
171
172    /// Set the declared type name from TypeDefinitions.
173    pub fn with_declared_type(mut self, type_name: impl Into<String>) -> Self {
174        self.declared_type = Some(type_name.into());
175        self
176    }
177
178    /// Set the initial attribute for the variable.
179    pub fn with_initial(mut self, initial: schema::Initial) -> Self {
180        self.initial = Some(initial);
181        self
182    }
183
184    /// Set the start value for the variable.
185    pub fn with_start(mut self, start: impl Into<T::Start>) -> Self {
186        self.start = Some(start.into());
187        self
188    }
189
190    // Float-specific attribute setters
191
192    /// Set the derivative relationship for the variable (value reference of state variable).
193    pub fn with_derivative(mut self, derivative_vr: u32) -> Self {
194        self.derivative = Some(derivative_vr);
195        self
196    }
197
198    /// Set whether the variable can be reinitialized during event mode.
199    pub fn with_reinit(mut self, reinit: bool) -> Self {
200        self.reinit = Some(reinit);
201        self
202    }
203
204    /// Set the minimum value for the variable (float types).
205    pub fn with_min(mut self, min: f64) -> Self {
206        self.min = Some(min);
207        self
208    }
209
210    /// Set the maximum value for the variable (float types).
211    pub fn with_max(mut self, max: f64) -> Self {
212        self.max = Some(max);
213        self
214    }
215
216    /// Set the nominal value for the variable (float types).
217    pub fn with_nominal(mut self, nominal: f64) -> Self {
218        self.nominal = Some(nominal);
219        self
220    }
221
222    // Integer-specific attribute setters
223
224    /// Set the quantity for the variable (integer types).
225    pub fn with_quantity(mut self, quantity: impl Into<String>) -> Self {
226        self.quantity = Some(quantity.into());
227        self
228    }
229
230    // Array attribute setters
231
232    /// Add dimensions to the variable (for array types).
233    pub fn with_dimensions(mut self, dimensions: Vec<schema::Dimension>) -> Self {
234        self.dimensions = dimensions;
235        self
236    }
237
238    // Binary-specific attribute setters
239
240    /// Set the maximum size for binary variables.
241    pub fn with_max_size(mut self, max_size: usize) -> Self {
242        self.max_size = Some(max_size);
243        self
244    }
245
246    /// Set the MIME type for binary variables.
247    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
248        self.mime_type = Some(mime_type.into());
249        self
250    }
251
252    // Clock-related attribute setters
253
254    /// Set the clocks that this variable belongs to.
255    pub fn with_clocks(mut self, clocks: Vec<u32>) -> Self {
256        self.clocks = Some(clocks);
257        self
258    }
259
260    /// Set the interval variability for clock variables.
261    pub fn with_interval_variability(
262        mut self,
263        interval_variability: schema::IntervalVariability,
264    ) -> Self {
265        self.interval_variability = Some(interval_variability);
266        self
267    }
268
269    /// Build the final FMI variable.
270    ///
271    /// This delegates to the type-specific `finish` implementation which will
272    /// extract only the relevant fields for the concrete variable type.
273    pub fn finish(self) -> T::Var {
274        T::finish(self)
275    }
276}
277
278/// Trait for types that can be used to build FMI variables.
279///
280/// Implementors define:
281/// - `Var`: The concrete FMI variable type to create (e.g., `FmiFloat64`, `FmiBoolean`)
282/// - `Start`: The type for start values (typically `StartValue<T>`)
283/// - `variable()`: Creates a new builder (default implementation provided)
284/// - `finish()`: Type-specific method to extract relevant fields and create the concrete variable
285pub trait FmiVariableBuilder: Sized {
286    type Var: schema::AbstractVariableTrait;
287    type Start;
288
289    /// Create a new variable builder with the given name.
290    fn variable(name: impl Into<String>, value_reference: u32) -> VariableBuilder<Self> {
291        VariableBuilder::new(name, value_reference)
292    }
293
294    /// Type-specific finish method that creates the concrete variable type.
295    ///
296    /// This method receives the complete builder and extracts only the fields
297    /// relevant to the specific variable type being created.
298    fn finish(builder: VariableBuilder<Self>) -> Self::Var;
299}
300
301// Macro for implementing FmiVariableBuilder for float types (f32, f64)
302macro_rules! impl_fmi_variable_builder_float {
303    ($primitive_type:ty, $fmi_type:ty, $default_variability:expr) => {
304        impl FmiVariableBuilder for $primitive_type {
305            type Var = $fmi_type;
306            type Start = StartValue<Self>;
307
308            fn finish(builder: VariableBuilder<Self>) -> Self::Var {
309                let mut var = <$fmi_type>::new(
310                    builder.name,
311                    builder.value_reference,
312                    if builder.description.as_ref().map_or(true, |d| d.is_empty()) {
313                        None
314                    } else {
315                        builder.description
316                    },
317                    builder.causality.unwrap_or(schema::Causality::Local),
318                    builder.variability.unwrap_or($default_variability),
319                    builder.start.map(Into::into),
320                    builder.initial,
321                );
322
323                // Set float-specific attributes if present
324                if let Some(derivative) = builder.derivative {
325                    var.derivative = Some(derivative);
326                }
327                if let Some(reinit) = builder.reinit {
328                    var.reinit = Some(reinit);
329                }
330
331                // Apply dimensions if any
332                if !builder.dimensions.is_empty() {
333                    var.dimensions = builder.dimensions;
334                }
335
336                var
337            }
338        }
339    };
340}
341
342// Macro for implementing FmiVariableBuilder for integer types
343macro_rules! impl_fmi_variable_builder_int {
344    ($primitive_type:ty, $fmi_type:ty) => {
345        impl FmiVariableBuilder for $primitive_type {
346            type Var = $fmi_type;
347            type Start = StartValue<Self>;
348
349            fn finish(builder: VariableBuilder<Self>) -> Self::Var {
350                let mut var = <$fmi_type>::new(
351                    builder.name,
352                    builder.value_reference,
353                    if builder.description.as_ref().map_or(true, |d| d.is_empty()) {
354                        None
355                    } else {
356                        builder.description
357                    },
358                    builder.causality.unwrap_or(schema::Causality::Local),
359                    builder.variability.unwrap_or(schema::Variability::Discrete),
360                    builder.start.map(Into::into),
361                    builder.initial,
362                );
363
364                // Apply dimensions if any
365                if !builder.dimensions.is_empty() {
366                    var.dimensions = builder.dimensions;
367                }
368
369                var
370            }
371        }
372    };
373}
374
375impl_fmi_variable_builder_float!(f32, schema::FmiFloat32, schema::Variability::Continuous);
376impl_fmi_variable_builder_float!(f64, schema::FmiFloat64, schema::Variability::Continuous);
377
378impl_fmi_variable_builder_int!(i8, schema::FmiInt8);
379impl_fmi_variable_builder_int!(u8, schema::FmiUInt8);
380impl_fmi_variable_builder_int!(i16, schema::FmiInt16);
381impl_fmi_variable_builder_int!(u16, schema::FmiUInt16);
382impl_fmi_variable_builder_int!(i32, schema::FmiInt32);
383impl_fmi_variable_builder_int!(u32, schema::FmiUInt32);
384
385// Boolean type
386impl FmiVariableBuilder for bool {
387    type Var = schema::FmiBoolean;
388    type Start = StartValue<Self>;
389
390    fn finish(builder: VariableBuilder<Self>) -> Self::Var {
391        let mut var = schema::FmiBoolean::new(
392            builder.name,
393            builder.value_reference,
394            if builder.description.as_ref().map_or(true, |d| d.is_empty()) {
395                None
396            } else {
397                builder.description
398            },
399            builder.causality.unwrap_or(schema::Causality::Local),
400            builder.variability.unwrap_or(schema::Variability::Discrete),
401            builder.start.map(Into::into),
402            builder.initial,
403        );
404
405        // Apply dimensions if any
406        if !builder.dimensions.is_empty() {
407            var.dimensions = builder.dimensions;
408        }
409
410        var
411    }
412}
413
414// String type
415impl FmiVariableBuilder for String {
416    type Var = schema::FmiString;
417    type Start = StartValue<Self>;
418
419    fn finish(builder: VariableBuilder<Self>) -> Self::Var {
420        let mut var = schema::FmiString::new(
421            builder.name,
422            builder.value_reference,
423            if builder.description.as_ref().map_or(true, |d| d.is_empty()) {
424                None
425            } else {
426                builder.description
427            },
428            builder.causality.unwrap_or(schema::Causality::Local),
429            builder.variability.unwrap_or(schema::Variability::Discrete),
430            builder.start.map(Into::into),
431            builder.initial,
432        );
433
434        // Apply dimensions if any
435        if !builder.dimensions.is_empty() {
436            var.dimensions = builder.dimensions;
437        }
438
439        var
440    }
441}
442
443// Array type support - delegates to element type's finish() and adds dimensions
444impl<const N: usize, T> FmiVariableBuilder for [T; N]
445where
446    T: FmiVariableBuilder,
447    T::Var: schema::ArrayableVariableTrait,
448    T::Start: Into<Vec<T>>,
449{
450    type Var = T::Var;
451    type Start = T::Start;
452
453    fn finish(mut builder: VariableBuilder<Self>) -> Self::Var {
454        // Add the fixed dimension for this array
455        builder.dimensions.push(schema::Dimension::Fixed(N as _));
456
457        // Delegate to the element type's finish implementation
458        // We need to transmute the builder to the element type
459        let element_builder = VariableBuilder::<T> {
460            name: builder.name,
461            value_reference: builder.value_reference,
462            description: builder.description,
463            causality: builder.causality,
464            variability: builder.variability,
465            can_handle_multiple_set_per_time_instant: builder
466                .can_handle_multiple_set_per_time_instant,
467            intermediate_update: builder.intermediate_update,
468            previous: builder.previous,
469            declared_type: builder.declared_type,
470            initial: builder.initial,
471            start: builder.start,
472            derivative: builder.derivative,
473            reinit: builder.reinit,
474            min: builder.min,
475            max: builder.max,
476            nominal: builder.nominal,
477            quantity: builder.quantity,
478            max_size: builder.max_size,
479            mime_type: builder.mime_type,
480            clocks: builder.clocks,
481            interval_variability: builder.interval_variability,
482            dimensions: builder.dimensions,
483            _phantom: std::marker::PhantomData,
484        };
485
486        T::finish(element_builder)
487    }
488}
489
490// Vec type support - similar to array but with variable dimension
491impl<T> FmiVariableBuilder for Vec<T>
492where
493    T: FmiVariableBuilder,
494    T::Var: schema::ArrayableVariableTrait,
495    T::Start: Into<Vec<T>>,
496{
497    type Var = T::Var;
498    type Start = T::Start;
499
500    fn finish(mut builder: VariableBuilder<Self>) -> Self::Var {
501        // Add a variable dimension
502        builder.dimensions.push(schema::Dimension::Variable(0));
503
504        // Delegate to the element type's finish implementation
505        let element_builder = VariableBuilder::<T> {
506            name: builder.name,
507            value_reference: builder.value_reference,
508            description: builder.description,
509            causality: builder.causality,
510            variability: builder.variability,
511            can_handle_multiple_set_per_time_instant: builder
512                .can_handle_multiple_set_per_time_instant,
513            intermediate_update: builder.intermediate_update,
514            previous: builder.previous,
515            declared_type: builder.declared_type,
516            initial: builder.initial,
517            start: builder.start,
518            derivative: builder.derivative,
519            reinit: builder.reinit,
520            min: builder.min,
521            max: builder.max,
522            nominal: builder.nominal,
523            quantity: builder.quantity,
524            max_size: builder.max_size,
525            mime_type: builder.mime_type,
526            clocks: builder.clocks,
527            interval_variability: builder.interval_variability,
528            dimensions: builder.dimensions,
529            _phantom: std::marker::PhantomData,
530        };
531
532        T::finish(element_builder)
533    }
534}
535
536impl FmiVariableBuilder for Clock {
537    type Var = schema::FmiClock;
538
539    type Start = ();
540
541    fn finish(builder: VariableBuilder<Self>) -> Self::Var {
542        let var = schema::FmiClock::new(
543            builder.name,
544            builder.value_reference,
545            builder.description,
546            builder.causality.unwrap_or(schema::Causality::Local),
547            builder.variability.unwrap_or_default(),
548        );
549
550        let mut var = var;
551        var.interval_variability = Some(
552            builder
553                .interval_variability
554                .unwrap_or(schema::IntervalVariability::Triggered),
555        );
556
557        var
558    }
559}
560
561impl FmiVariableBuilder for Binary {
562    type Var = schema::FmiBinary;
563    type Start = StartValue<Vec<u8>>;
564
565    fn finish(builder: VariableBuilder<Self>) -> Self::Var {
566        let start_values = builder.start.map(|start| {
567            let vec_values: Vec<Vec<u8>> = start.into();
568            vec_values
569                .into_iter()
570                .map(|bytes| {
571                    // Simple hex encoding since base64 is not available
572                    bytes
573                        .iter()
574                        .map(|b| format!("{:02x}", b))
575                        .collect::<String>()
576                })
577                .collect()
578        });
579
580        let mut var = schema::FmiBinary::new(
581            builder.name,
582            builder.value_reference,
583            builder.description,
584            builder.causality.unwrap_or(schema::Causality::Local),
585            builder.variability.unwrap_or(schema::Variability::Discrete),
586            start_values,
587            builder.initial,
588        );
589
590        // Set max_size if provided
591        if let Some(max_size) = builder.max_size {
592            var.max_size = Some(max_size as u32);
593        }
594
595        // Set mime_type if provided
596        if let Some(mime_type) = builder.mime_type {
597            var.mime_type = Some(mime_type);
598        }
599
600        // Set clocks if provided
601        if let Some(clocks) = builder.clocks {
602            var.clocks = Some(fmi::schema::utils::AttrList(clocks));
603        }
604
605        // Apply dimensions if any
606        if !builder.dimensions.is_empty() {
607            var.dimensions = builder.dimensions;
608        }
609
610        var
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use fmi::schema::fmi3::{AbstractVariableTrait, ArrayableVariableTrait};
617
618    use super::*;
619
620    #[test]
621    fn test_scalar() {
622        let var_f1 = <f64 as FmiVariableBuilder>::variable("f1", 0)
623            .with_description("Description for f1")
624            .with_causality(schema::Causality::Parameter)
625            .with_variability(schema::Variability::Tunable)
626            .with_start(0.0)
627            .finish();
628        assert_eq!(var_f1.dimensions(), &[]);
629    }
630
631    #[test]
632    fn test_array1() {
633        let var_f2 = <[u16; 2] as FmiVariableBuilder>::variable("f2", 0)
634            .with_description("Description for f2")
635            .with_causality(schema::Causality::Parameter)
636            .with_variability(schema::Variability::Tunable)
637            .with_start(vec![0u16, 1])
638            .finish();
639        assert_eq!(var_f2.dimensions(), &[schema::Dimension::Fixed(2)]);
640    }
641
642    #[test]
643    fn test_builder_pattern() {
644        // Test using the builder pattern with method chaining
645        let var = <f64 as FmiVariableBuilder>::variable("test_var", 42)
646            .with_description("A test variable")
647            .with_causality(schema::Causality::Output)
648            .with_variability(schema::Variability::Continuous)
649            .with_start(1.5)
650            .with_initial(schema::Initial::Exact)
651            .finish();
652
653        assert_eq!(var.name(), "test_var");
654        assert_eq!(var.value_reference(), 42);
655        assert_eq!(var.description(), Some("A test variable"));
656        assert_eq!(var.causality(), schema::Causality::Output);
657        assert_eq!(var.variability(), schema::Variability::Continuous);
658    }
659
660    #[test]
661    fn test_builder_with_defaults() {
662        // Test that builder provides sensible defaults
663        let var = <f64 as FmiVariableBuilder>::variable("minimal", 0)
664            .with_start(0.0)
665            .finish();
666
667        assert_eq!(var.name(), "minimal");
668        assert_eq!(var.value_reference(), 0);
669        assert_eq!(var.causality(), schema::Causality::Local);
670        assert_eq!(var.variability(), schema::Variability::Continuous);
671    }
672
673    #[test]
674    fn test_float_specific_attributes() {
675        // Test float-specific attributes like derivative and reinit
676        let var = <f64 as FmiVariableBuilder>::variable("state_derivative", 10)
677            .with_causality(schema::Causality::Local)
678            .with_variability(schema::Variability::Continuous)
679            .with_derivative(5)  // Value reference of the state variable
680            .with_reinit(true)
681            .with_min(-100.0)
682            .with_max(100.0)
683            .with_nominal(10.0)
684            .with_start(0.0)
685            .with_initial(schema::Initial::Calculated)
686            .finish();
687
688        assert_eq!(var.name(), "state_derivative");
689        assert_eq!(var.value_reference(), 10);
690        assert_eq!(var.derivative(), Some(5));
691        assert_eq!(var.reinit(), Some(true));
692    }
693
694    #[test]
695    fn test_bool_type_creates_fmi_boolean() {
696        // Test that bool type creates FmiBoolean
697        let var = <bool as FmiVariableBuilder>::variable("flag", 20)
698            .with_causality(schema::Causality::Output)
699            .with_variability(schema::Variability::Discrete)
700            .with_start(false)
701            .finish();
702
703        assert_eq!(var.name(), "flag");
704        assert_eq!(var.value_reference(), 20);
705        assert_eq!(var.causality(), schema::Causality::Output);
706        assert_eq!(var.variability(), schema::Variability::Discrete);
707    }
708
709    #[test]
710    fn test_comprehensive_builder() {
711        // Test builder with many optional fields
712        let var = <f64 as FmiVariableBuilder>::variable("comprehensive", 100)
713            .with_description("A comprehensive test variable")
714            .with_causality(schema::Causality::Parameter)
715            .with_variability(schema::Variability::Tunable)
716            .with_can_handle_multiple_set_per_time_instant(true)
717            .with_intermediate_update(false)
718            .with_declared_type("CustomFloat64Type")
719            .with_start(42.0)
720            .with_initial(schema::Initial::Exact)
721            .with_min(0.0)
722            .with_max(1000.0)
723            .with_nominal(100.0)
724            .finish();
725
726        assert_eq!(var.name(), "comprehensive");
727        assert_eq!(var.value_reference(), 100);
728        assert_eq!(var.description(), Some("A comprehensive test variable"));
729        assert_eq!(var.causality(), schema::Causality::Parameter);
730        assert_eq!(var.variability(), schema::Variability::Tunable);
731    }
732}