fmi-export-derive 0.3.0

Proc-macro support for fmi-export
Documentation
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};

use crate::model::{Field, FieldAttributeOuter, Model};

pub struct BuildTerminalsGen<'a> {
    model: &'a Model,
}

impl<'a> BuildTerminalsGen<'a> {
    pub fn new(model: &'a Model) -> Self {
        Self { model }
    }
}

impl ToTokens for BuildTerminalsGen<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let struct_name = &self.model.ident;
        let terminal_name = self.model.ident.to_string();
        let terminal_name_lit = syn::LitStr::new(&terminal_name, self.model.ident.span());
        let prefix_binding = quote! { prefix.unwrap_or("") };

        if self.model.get_terminal_attr().is_some() {
            tokens.extend(quote! {
                let terminal = <#struct_name as ::fmi_export::fmi3::TerminalProvider>::terminal(#terminal_name_lit, prefix);
                terminals.push(terminal);
            });
            return;
        }

        let mut child_tokens = Vec::new();
        for field in &self.model.fields {
            if self.is_child_field(field) {
                let field_type = &field.rust_type;
                let field_name = field.ident.to_string();
                let child_prefix_lit = self.child_prefix_literal(field, &field_name);
                if let Some(terminal_attr) = self.terminal_attr(field) {
                    let child_terminal_name = terminal_attr
                        .name
                        .as_ref()
                        .cloned()
                        .unwrap_or_else(|| field_name.clone());
                    let child_terminal_name_lit =
                        syn::LitStr::new(&child_terminal_name, field.ident.span());
                    child_tokens.push(quote! {
                        let child_prefix = format!("{}{}.", #prefix_binding, #child_prefix_lit);
                        let child_terminal = if child_prefix.is_empty() {
                            <#field_type as ::fmi_export::fmi3::TerminalProvider>::terminal(#child_terminal_name_lit, None)
                        } else {
                            <#field_type as ::fmi_export::fmi3::TerminalProvider>::terminal(#child_terminal_name_lit, Some(child_prefix.as_str()))
                        };
                        terminals.push(child_terminal);
                    });
                } else {
                    child_tokens.push(quote! {
                        let child_prefix = format!("{}{}.", #prefix_binding, #child_prefix_lit);
                        <#field_type as ::fmi_export::fmi3::Model>::build_terminals(
                            terminals,
                            Some(child_prefix.as_str()),
                        );
                    });
                }
            } else if self.has_no_variable_attributes(field) {
                let field_type = &field.rust_type;
                child_tokens.push(quote! {
                    <#field_type as ::fmi_export::fmi3::Model>::build_terminals(
                        terminals,
                        Some(#prefix_binding),
                    );
                });
            }
        }

        tokens.extend(quote! {
            #(#child_tokens)*
        });
    }
}

impl BuildTerminalsGen<'_> {
    fn has_no_variable_attributes(&self, field: &Field) -> bool {
        !field
            .attrs
            .iter()
            .any(|attr| matches!(attr, FieldAttributeOuter::Variable(_)))
    }

    fn is_child_field(&self, field: &Field) -> bool {
        field
            .attrs
            .iter()
            .any(|attr| matches!(attr, FieldAttributeOuter::Child(_)))
    }

    fn child_prefix_literal(&self, field: &Field, default_name: &str) -> syn::LitStr {
        let prefix = field.attrs.iter().find_map(|attr| {
            if let FieldAttributeOuter::Child(child_attr) = attr {
                child_attr.prefix.as_deref()
            } else {
                None
            }
        });
        let prefix = prefix.unwrap_or(default_name);
        syn::LitStr::new(prefix, field.ident.span())
    }

    fn terminal_attr<'a>(&self, field: &'a Field) -> Option<&'a crate::model::TerminalAttribute> {
        field.attrs.iter().find_map(|attr| {
            if let FieldAttributeOuter::Terminal(terminal_attr) = attr {
                Some(terminal_attr)
            } else {
                None
            }
        })
    }
}

pub struct TerminalProviderImpl<'a> {
    struct_name: &'a syn::Ident,
    model: &'a Model,
}

impl<'a> TerminalProviderImpl<'a> {
    pub fn new(struct_name: &'a syn::Ident, model: &'a Model) -> Self {
        Self { struct_name, model }
    }
}

impl ToTokens for TerminalProviderImpl<'_> {
    fn to_tokens(&self, tokens: &mut TokenStream2) {
        let struct_name = self.struct_name;
        let prefix_binding = quote! { prefix.unwrap_or("") };

        let terminal_attr = self.model.get_terminal_attr();
        let include_members = terminal_attr.is_some();
        let struct_name_lit =
            syn::LitStr::new(&self.model.ident.to_string(), self.model.ident.span());
        let terminal_name_override = terminal_attr
            .and_then(|attr| attr.name.as_deref())
            .map(|name| syn::LitStr::new(name, self.model.ident.span()));
        let matching_rule_override = terminal_attr
            .and_then(|attr| attr.matching_rule.as_deref())
            .map(|rule| syn::LitStr::new(rule, self.model.ident.span()));
        let terminal_kind_override = terminal_attr
            .and_then(|attr| attr.terminal_kind.as_deref())
            .map(|kind| syn::LitStr::new(kind, self.model.ident.span()));

        let terminal_name_tokens = terminal_name_override
            .map(|lit| {
                quote! {
                    if provided == #struct_name_lit { #lit } else { provided }
                }
            })
            .unwrap_or_else(|| quote! { provided });
        let matching_rule_tokens = matching_rule_override
            .map(|lit| quote! { #lit })
            .unwrap_or_else(|| quote! { "bus" });
        let terminal_kind_tokens = terminal_kind_override
            .map(|lit| quote! { Some(#lit.to_string()) })
            .unwrap_or_else(|| quote! { None });

        let mut member_tokens = Vec::new();
        if include_members {
            for field in &self.model.fields {
                for attr in &field.attrs {
                    if let FieldAttributeOuter::Variable(var_attr) = attr {
                        if var_attr.skip {
                            continue;
                        }
                        let variable_name = var_attr
                            .name
                            .as_ref()
                            .cloned()
                            .unwrap_or_else(|| field.ident.to_string());
                        let variable_name_lit =
                            syn::LitStr::new(&variable_name, field.ident.span());
                        member_tokens.push(quote! {
                            let variable_name = format!("{}{}", #prefix_binding, #variable_name_lit);
                            terminal_member_variables.push(::fmi::schema::fmi3::TerminalMemberVariable {
                                variable_name: variable_name.clone(),
                                member_name: Some(variable_name),
                                variable_kind: "signal".to_string(),
                                ..Default::default()
                            });
                        });
                    }
                }
            }
        }

        let mut child_tokens = Vec::new();
        for field in &self.model.fields {
            if self.is_child_field(field) || self.has_no_variable_attributes(field) {
                let field_type = &field.rust_type;
                let field_name = field.ident.to_string();
                let child_terminal_name = self
                    .terminal_attr(field)
                    .and_then(|attr| attr.name.as_ref())
                    .cloned()
                    .unwrap_or_else(|| field_name.clone());
                let child_terminal_name_lit =
                    syn::LitStr::new(&child_terminal_name, field.ident.span());
                let child_prefix_lit = self.child_prefix_literal(field, &field_name);
                child_tokens.push(quote! {
                    let child_prefix = format!("{}{}.", #prefix_binding, #child_prefix_lit);
                    let child_terminal = if child_prefix.is_empty() {
                        <#field_type as ::fmi_export::fmi3::TerminalProvider>::terminal(#child_terminal_name_lit, None)
                    } else {
                        <#field_type as ::fmi_export::fmi3::TerminalProvider>::terminal(#child_terminal_name_lit, Some(child_prefix.as_str()))
                    };
                    terminals.push(child_terminal);
                });
            }
        }

        tokens.extend(quote! {
            #[automatically_derived]
            impl ::fmi_export::fmi3::TerminalProvider for #struct_name {
                fn terminal(name: &str, prefix: Option<&str>) -> ::fmi::schema::fmi3::Terminal {
                    let terminal_name = {
                        let provided = name;
                        #terminal_name_tokens
                    };
                    let mut terminal_member_variables = Vec::new();
                    #(#member_tokens)*

                    let mut terminals = Vec::new();
                    #(#child_tokens)*

                    ::fmi::schema::fmi3::Terminal {
                        name: terminal_name.to_string(),
                        matching_rule: #matching_rule_tokens.to_string(),
                        terminal_kind: #terminal_kind_tokens,
                        terminal_member_variables,
                        terminals,
                        ..Default::default()
                    }
                }
            }
        });
    }
}

impl TerminalProviderImpl<'_> {
    fn has_no_variable_attributes(&self, field: &Field) -> bool {
        !field
            .attrs
            .iter()
            .any(|attr| matches!(attr, FieldAttributeOuter::Variable(_)))
    }

    fn is_child_field(&self, field: &Field) -> bool {
        field
            .attrs
            .iter()
            .any(|attr| matches!(attr, FieldAttributeOuter::Child(_)))
    }

    fn child_prefix_literal(&self, field: &Field, default_name: &str) -> syn::LitStr {
        let prefix = field.attrs.iter().find_map(|attr| {
            if let FieldAttributeOuter::Child(child_attr) = attr {
                child_attr.prefix.as_deref()
            } else {
                None
            }
        });
        let prefix = prefix.unwrap_or(default_name);
        syn::LitStr::new(prefix, field.ident.span())
    }

    fn terminal_attr<'a>(&self, field: &'a Field) -> Option<&'a crate::model::TerminalAttribute> {
        field.attrs.iter().find_map(|attr| {
            if let FieldAttributeOuter::Terminal(terminal_attr) = attr {
                Some(terminal_attr)
            } else {
                None
            }
        })
    }
}