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