fmi-rs 0.3.0

Functional Mock-up Interface (FMI) for Rust
#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)]

use std::{fs::File, io::BufReader, path::Path};

use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use thiserror::Error;

const FMI_LS_NAME: &str = "org.fmi-standard.fmi-ls-dae";
const FMI_LS_VERSION: &str = "1.0.0-alpha.1";

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DaeManifest {
    #[serde(rename = "@fmi-ls-name")]
    pub fmiLsName: String,

    #[serde(rename = "@fmi-ls-version")]
    pub fmiLsVersion: String,

    #[serde(rename = "EnableDAEParameter")]
    pub enableDaeParameter: EnableDAEParameter,

    #[serde(rename = "AlgebraicVariables")]
    pub algebraicVariables: AlgebraicVariables,

    #[serde(rename = "ModelStructure")]
    pub modelStructure: ModelStructure,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnableDAEParameter {
    #[serde(rename = "@valueReference")]
    pub valueReference: u32,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlgebraicVariables {
    #[serde(rename = "AlgebraicVariable")]
    pub algebraicVariables: Vec<AlgebraicVariable>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlgebraicVariable {
    #[serde(rename = "@valueReference")]
    pub valueReference: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumString)]
#[strum(serialize_all = "camelCase")]
pub enum DependencyKind {
    Dependent,
    Constant,
    Fixed,
    Tunable,
    Discrete,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Unknown {
    #[serde(rename = "@valueReference")]
    pub valueReference: u32,

    #[serde(rename = "@dependencies")]
    #[serde(with = "space_separated")]
    pub dependencies: Option<Vec<u32>>,

    #[serde(rename = "@dependenciesKind")]
    #[serde(with = "space_separated")]
    pub dependenciesKind: Option<Vec<DependencyKind>>,
}

mod space_separated {
    use serde::{Deserialize, Deserializer, Serializer, de::Error};
    use std::fmt::Display;
    use std::str::FromStr;

    pub fn serialize<T, S>(value: &Option<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error>
    where
        T: Display,
        S: Serializer,
    {
        match value {
            Some(values) => {
                let joined = values
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join(" ");
                serializer.serialize_some(&joined)
            }
            None => serializer.serialize_none(),
        }
    }

    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
    where
        T: FromStr,
        T::Err: Display,
        D: Deserializer<'de>,
    {
        Option::<String>::deserialize(deserializer)?
            .map(|value| {
                value
                    .split_whitespace()
                    .map(str::parse)
                    .collect::<Result<Vec<_>, _>>()
                    .map_err(D::Error::custom)
            })
            .transpose()
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelStructure {
    #[serde(rename = "Output", default)]
    pub outputs: Vec<Unknown>,

    #[serde(rename = "ContinuousStateDerivative", default)]
    pub continuousStateDerivatives: Vec<Unknown>,

    #[serde(rename = "ClockedState", default)]
    pub clockedStates: Vec<Unknown>,

    #[serde(rename = "InitialUnknown", default)]
    pub initialUnknowns: Vec<Unknown>,

    #[serde(rename = "EventIndicator", default)]
    pub eventIndicators: Vec<Unknown>,

    #[serde(rename = "Residual", default)]
    pub residuals: Vec<Unknown>,
}

#[derive(Error, Debug)]
pub enum DaeManifestError {
    #[error("Failed to open the file")]
    Io(#[from] std::io::Error),

    #[error("Failed to parse the file: {0}")]
    Parse(String),
}

impl DaeManifest {
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DaeManifestError> {
        let file = File::open(path)?;
        let reader = BufReader::new(file);
        let manifest: DaeManifest = quick_xml::de::from_reader(reader)
            .map_err(|e| DaeManifestError::Parse(e.to_string()))?;

        if manifest.fmiLsName != FMI_LS_NAME {
            return Err(DaeManifestError::Parse(format!(
                "Illegal value for attribute 'fmi-ls-name': expected '{FMI_LS_NAME}' but was '{}'",
                manifest.fmiLsName
            )));
        }

        if manifest.fmiLsVersion != FMI_LS_VERSION {
            return Err(DaeManifestError::Parse(format!(
                "Illegal value for attribute 'fmi-ls-version': expected '{FMI_LS_VERSION}' but was '{}'",
                manifest.fmiLsVersion
            )));
        }

        Ok(manifest)
    }
}