fmi-rs 0.2.0

Functional Mock-up Interface (FMI) for Rust
use std::{any::TypeId, str::FromStr};

use roxmltree::Node;

use crate::model_description::{
    BaseUnit, Category, DefaultExperiment, DisplayUnit, ModelDescriptionError, Unit,
};

pub(crate) trait NodeExt<'a, 'input> {
    fn get_child(&self, name: &str) -> Option<Node<'a, 'input>>;
    fn get_required_child(&self, name: &str) -> Result<Node<'a, 'input>, ModelDescriptionError>;
    fn get_children(&self, name: &str) -> Vec<Node<'a, 'input>>;
    fn required_attribute(&self, name: &str) -> Result<String, ModelDescriptionError>;
    fn attribute_as<T: FromStr + 'static>(
        &self,
        name: &str,
    ) -> Result<Option<T>, ModelDescriptionError>;
    fn required_attribute_as<T: FromStr + 'static>(
        &self,
        name: &str,
    ) -> Result<T, ModelDescriptionError>;
}

impl<'a, 'input> NodeExt<'a, 'input> for Node<'a, 'input> {
    fn get_child(&self, name: &str) -> Option<Node<'a, 'input>> {
        self.children().find(|n| n.has_tag_name(name))
    }

    fn get_required_child(&self, name: &str) -> Result<Node<'a, 'input>, ModelDescriptionError> {
        self.children()
            .find(|n| n.has_tag_name(name))
            .ok_or_else(|| {
                let message = format!(
                    "Missing required element <{}> in <{}>",
                    name,
                    self.tag_name().name()
                );
                ModelDescriptionError::Parse(message)
            })
    }

    fn get_children(&self, name: &str) -> Vec<Node<'a, 'input>> {
        self.children().filter(|n| n.has_tag_name(name)).collect()
    }

    fn required_attribute(&self, name: &str) -> Result<String, ModelDescriptionError> {
        self.attribute(name)
            .ok_or_else(|| {
                let message = format!("Missing required attribute '{}'", name);
                ModelDescriptionError::Parse(message)
            })
            .map(|s| s.to_string())
    }

    fn attribute_as<T: FromStr + 'static>(
        &self,
        name: &str,
    ) -> Result<Option<T>, ModelDescriptionError> {
        if let Some(literal) = self.attribute(name) {
            let normalized = if TypeId::of::<T>() == TypeId::of::<bool>() {
                match literal {
                    "1" => "true",
                    "0" => "false",
                    _ => literal,
                }
            } else {
                literal
            };
            let result = normalized.parse::<T>().map_err(|_| {
                let message = format!(
                    "Illegal value '{}' for attribute '{}' in <{}>.",
                    literal,
                    name,
                    self.tag_name().name()
                );
                ModelDescriptionError::Parse(message)
            })?;
            Ok(Some(result))
        } else {
            Ok(None)
        }
    }

    fn required_attribute_as<T: FromStr + 'static>(
        &self,
        name: &str,
    ) -> Result<T, ModelDescriptionError> {
        if let Some(value) = self.attribute(name) {
            let normalized = if TypeId::of::<T>() == TypeId::of::<bool>() {
                match value {
                    "1" => "true",
                    "0" => "false",
                    _ => value,
                }
            } else {
                value
            };
            normalized.parse::<T>().map_err(|_| {
                let message = format!(
                    "Illegal value '{}' for attribute '{}' in <{}>.",
                    value,
                    name,
                    self.tag_name().name()
                );
                ModelDescriptionError::Parse(message)
            })
        } else {
            let message = format!(
                "Missing required attribute '{}' in <{}>.",
                name,
                self.tag_name().name()
            );
            Err(ModelDescriptionError::Parse(message))
        }
    }
}

impl BaseUnit {
    pub(crate) fn from_node(node: &roxmltree::Node) -> Result<Self, ModelDescriptionError> {
        Ok(BaseUnit {
            kg: node.attribute_as("kg")?.unwrap_or_default(),
            m: node.attribute_as("m")?.unwrap_or_default(),
            s: node.attribute_as("s")?.unwrap_or_default(),
            A: node.attribute_as("A")?.unwrap_or_default(),
            K: node.attribute_as("K")?.unwrap_or_default(),
            mol: node.attribute_as("mol")?.unwrap_or_default(),
            cd: node.attribute_as("cd")?.unwrap_or_default(),
            rad: node.attribute_as("rad")?.unwrap_or_default(),
            factor: node.attribute_as("factor")?.unwrap_or(1.0),
            offset: node.attribute_as("offset")?.unwrap_or_default(),
            range: node.range(),
        })
    }
}

impl DisplayUnit {
    pub(crate) fn from_node(node: &roxmltree::Node) -> Result<Self, ModelDescriptionError> {
        Ok(DisplayUnit {
            factor: node.attribute_as("factor")?.unwrap_or(1.0),
            offset: node.attribute_as("offset")?.unwrap_or_default(),
            inverse: node.attribute_as("inverse")?.unwrap_or_default(),
            name: node.required_attribute("name")?,
            range: node.range(),
        })
    }
}

impl Unit {
    pub(crate) fn from_node(node: &roxmltree::Node) -> Result<Self, ModelDescriptionError> {
        Ok(Unit {
            name: node.required_attribute("name")?,
            baseUnit: node
                .get_child("BaseUnit")
                .map(|n| BaseUnit::from_node(&n))
                .transpose()?,
            displayUnits: node
                .get_children("DisplayUnit")
                .into_iter()
                .map(|n| DisplayUnit::from_node(&n))
                .collect::<Result<Vec<_>, _>>()?,
            range: node.range(),
        })
    }
}

impl Category {
    pub(crate) fn from_node(node: &roxmltree::Node) -> Result<Self, ModelDescriptionError> {
        Ok(Category {
            name: node.required_attribute("name")?,
            description: node.attribute_as("description")?,
        })
    }
}

impl DefaultExperiment {
    pub(crate) fn from_node(node: &roxmltree::Node) -> Result<Self, ModelDescriptionError> {
        Ok(DefaultExperiment {
            startTime: node.attribute_as("startTime")?,
            stopTime: node.attribute_as("stopTime")?,
            tolerance: node.attribute_as("tolerance")?,
            stepSize: node.attribute_as("stepSize")?,
            range: node.range(),
        })
    }
}