use mavinspect::protocol::Dialect;
use quote::{format_ident, quote};
use crate::conventions::{
description_doc_comment_line, dialect_enum_name, dialect_mod_name, enum_rust_name,
message_mod_name, message_specta_name, message_struct_name, microservice_msg_specta_name,
rust_var_name,
};
use crate::conventions::{microservice_doc_mention, microservice_msg_enum_name};
use crate::specs::dialects::dialect::messages::{
MessageImplModuleSpec, MessageInheritedModuleSpec, MessagesRootModuleSpec,
};
use crate::specs::Spec;
use crate::templates::helpers::{make_serde_derive_annotation, make_specta_derive_annotation};
pub(crate) fn messages_root_module(spec: &MessagesRootModuleSpec) -> syn::File {
let module_doc_comment = format!(
" MAVLink messages of `{}` dialect.",
spec.dialect_canonical_name()
);
let module_doc_comment = match spec.msrv_name() {
None => module_doc_comment,
Some(msrv_name) => format!(
" MAVLink messages of {} microservice for `{}` dialect.",
microservice_doc_mention(msrv_name),
spec.dialect_canonical_name()
),
};
let message_modules_and_imports = spec.messages().iter().map(|msg| {
let message_mod_name = format_ident!("{}", message_mod_name(msg.name()));
let message_struct_name = format_ident!("{}", message_struct_name(msg.name()));
quote! {
pub mod #message_mod_name;
pub use #message_mod_name::#message_struct_name;
}
});
syn::parse2(quote! {
#![doc = #module_doc_comment]
#(#message_modules_and_imports)*
})
.unwrap()
}
pub(crate) fn message_module(spec: &MessageImplModuleSpec) -> syn::File {
let module_doc_comment = match spec.msrv_name() {
Some(msrv_name) => format!(
" MAVLink `{}` message implementation for {} microservice of `{}` dialect dialect.",
spec.name(),
microservice_doc_mention(msrv_name),
spec.dialect_canonical_name(),
),
None => format!(
" # MAVLink `{}` message implementation for `{}` dialect.",
spec.name(),
spec.dialect().name(),
),
};
let mavspec_import = spec.params().mavspec_import();
let message_id: syn::LitInt = syn::parse_str(format!("{}", spec.id()).as_str()).unwrap();
let crc_extra: syn::LitInt = syn::parse_str(format!("{}", spec.crc_extra()).as_str()).unwrap();
let message_leading_doc_comment = format!(" MAVLink `{}` message.", spec.name());
let min_supported_mavlink_version_number = if spec.is_v1_compatible() { 1 } else { 2 };
let min_supported_mavlink_version_doc_comment = format!(
" The minimum supported MAVLink version is `MAVLink {min_supported_mavlink_version_number}`."
);
let description_doc_comments = spec.description().iter().map(description_doc_comment_line);
let specta_name = match spec.msrv_name() {
Some(msrv_name) => microservice_msg_specta_name(msrv_name, spec.name(), None),
None => message_specta_name(spec.name(), spec.dialect_canonical_name()),
};
let derive_serde = make_serde_derive_annotation(spec.params().serde);
let derive_specta =
make_specta_derive_annotation(spec.params().specta, Some(specta_name.as_str()));
let message_struct_ident = spec.ident();
let message_encode_decode_doc_comment =
format!(" [`{message_struct_ident}`] (encoding) and [`IntoPayload`] (decoding) traits.");
let message_fields = spec.fields().iter().map(|field| {
let leading_doc_comment = format!(" MAVLink field `{}`.", field.name());
let description_doc_comments = field.description().iter().map(description_doc_comment_line);
let serde_arrays_attr = if field.requires_serde_arrays() {
quote! {
#[cfg_attr(feature = "serde", serde(with = "serde_arrays"))]
}
} else {
quote!()
};
let field_ident = format_ident!("{}", rust_var_name(field.name()));
let field_rust_type: syn::Type =
syn::parse_str(field.r#type().rust_type().as_str()).unwrap();
let enum_ident = format_ident!("{}", enum_rust_name(field.enum_name()));
let array_length = field.array_length();
let field_base_type: syn::Type =
syn::parse_str(field.r#type().base_type().rust_type().as_str()).unwrap();
let base_type_attr = if field.is_enum() {
quote! { #[base_type(#field_base_type)] }
} else {
quote!()
};
let enum_type: syn::Type = syn::parse_str(field.enum_type().rust_type().as_str()).unwrap();
let repr_type_attr = if field.requires_enum_casting() {
quote! { #[repr_type(#enum_type)] }
} else {
quote!()
};
let bitmask_attr = if field.is_bitmask() {
quote! { #[bitmask] }
} else {
quote!()
};
let extension_attr = if field.is_extension() {
quote! { #[extension] }
} else {
quote!()
};
let field_definition = if field.is_enum() {
if field.is_array() {
quote! {
pub #field_ident: [super::super::enums::#enum_ident; #array_length],
}
} else {
quote! {
pub #field_ident: super::super::enums::#enum_ident,
}
}
} else {
quote! {
pub #field_ident: #field_rust_type,
}
};
quote! {
#[doc = #leading_doc_comment]
#(#description_doc_comments)*
#serde_arrays_attr
#bitmask_attr
#base_type_attr
#repr_type_attr
#extension_attr
#field_definition
}
});
let conversions = generate_conversions(spec);
let tests = generate_tests(spec);
syn::parse2(quote! {
#![doc = #module_doc_comment]
#mavspec_import
use mavspec::rust::spec::{MessageInfo, MessageSpec};
use mavspec::rust::spec::types::{MessageId, CrcExtra};
pub(crate) const MESSAGE_ID: MessageId = #message_id;
pub(crate) const CRC_EXTRA: CrcExtra = #crc_extra;
pub(crate) const MESSAGE_INFO: MessageInfo = MessageInfo::new(MESSAGE_ID, CRC_EXTRA);
#[inline]
pub const fn spec() -> &'static dyn MessageSpec {
&MESSAGE_INFO
}
#[allow(rustdoc::bare_urls)]
#[allow(rustdoc::broken_intra_doc_links)]
#[allow(rustdoc::invalid_rust_codeblocks)]
#[doc = #message_leading_doc_comment]
#[doc = #min_supported_mavlink_version_doc_comment]
#(#description_doc_comments)*
#[doc = #message_encode_decode_doc_comment]
#[derive(mavspec::rust::derive::Message)]
#[derive(core::clone::Clone, core::fmt::Debug, core::cmp::PartialEq)]
#derive_specta
#derive_serde
#[message_id(#message_id)]
#[crc_extra(#crc_extra)]
pub struct #message_struct_ident {
#(#message_fields)*
}
#conversions
#tests
})
.unwrap()
}
fn generate_conversions(spec: &MessageImplModuleSpec) -> proc_macro2::TokenStream {
let message_struct_ident = spec.ident();
let dialect_enum_ident = make_dialect_enum_ident(spec);
let from_msg_to_dialect_enum = quote! {
impl core::convert::From<#message_struct_ident> for super::super::#dialect_enum_ident {
fn from(value: #message_struct_ident) -> Self {
Self::#message_struct_ident(value)
}
}
};
let conversions = quote! {
#from_msg_to_dialect_enum
};
let conversions = match spec.parent_dialect() {
Some(parent_dialect) => {
let parent_msg_struct_ident =
quote! { super::super::super::super::messages::#message_struct_ident };
let from_msrv_msg_field_assignments = spec.fields().iter().map(|field| {
let field_ident = format_ident!("{}", rust_var_name(field.name()));
quote! {
#field_ident: value.#field_ident.into()
}
});
let from_parent_msg_field_assignments = spec.fields().iter().map(|field| {
let field_ident = format_ident!("{}", rust_var_name(field.name()));
if is_same_field_type(spec.name(), field.name(), spec.dialect(), parent_dialect)
.is_some()
{
quote! {
#field_ident: value.#field_ident
}
} else {
quote! {
#field_ident: value.#field_ident.try_into()?
}
}
});
quote! {
#conversions
impl core::convert::From<#message_struct_ident> for #parent_msg_struct_ident {
fn from(value: #message_struct_ident) -> Self {
Self {
#(#from_msrv_msg_field_assignments),*
}
}
}
impl core::convert::TryFrom<#parent_msg_struct_ident> for #message_struct_ident {
type Error = mavspec::rust::spec::SpecError;
fn try_from(value: #parent_msg_struct_ident) -> Result<Self, Self::Error> {
Ok(Self {
#(#from_parent_msg_field_assignments),*
})
}
}
}
}
None => conversions,
};
conversions
}
fn generate_tests(spec: &MessageImplModuleSpec) -> proc_macro2::TokenStream {
if !spec.params().generate_tests {
return quote!();
}
let message_struct_ident = spec.ident();
let v2_tests = quote! {
#[test]
fn basic_v2() {
let message = #message_struct_ident::default();
let encoded_payload = message.encode(MavLinkVersion::V2).unwrap();
let decoded_message = #message_struct_ident::try_from(&encoded_payload).unwrap();
assert_eq!(decoded_message.id(), message.id());
assert_eq!(encoded_payload.id(), message.id());
assert!(matches!(encoded_payload.version(), MavLinkVersion::V2));
}
};
let v1_tests = if spec.is_v1_compatible() {
quote! {
#[test]
fn basic_v1() {
let message = #message_struct_ident::default();
let encoded_payload = message.encode(MavLinkVersion::V1).unwrap();
let decoded_message = #message_struct_ident::try_from(&encoded_payload).unwrap();
assert_eq!(decoded_message.id(), message.id());
assert_eq!(encoded_payload.id(), message.id());
assert!(matches!(encoded_payload.version(), MavLinkVersion::V1));
}
}
} else {
quote!()
};
quote! {
#[cfg(test)]
mod tests {
use mavspec::rust::spec::{IntoPayload, MavLinkVersion};
use super::*;
#v2_tests
#v1_tests
}
}
}
fn make_dialect_enum_ident(spec: &MessageImplModuleSpec) -> syn::Ident {
let dialect_enum_ident = format_ident!("{}", dialect_enum_name(spec.dialect_canonical_name()));
let dialect_enum_ident = match spec.msrv_name() {
Some(msrv_name) => {
format_ident!("{}", microservice_msg_enum_name(msrv_name))
}
None => dialect_enum_ident,
};
dialect_enum_ident
}
fn is_same_field_type(
msg_name: &str,
field_name: &str,
dialect: &Dialect,
parent_dialect: &Dialect,
) -> Option<()> {
let msg = dialect.get_message_by_name(msg_name)?;
let field = msg.get_field_by_name(field_name)?;
let parent_msg = parent_dialect.get_message_by_name(msg_name)?;
let parent_field = parent_msg.get_field_by_name(field_name)?;
if field.fingerprint_strict(Some(dialect))
== parent_field.fingerprint_strict(Some(parent_dialect))
{
return Some(());
}
None
}
pub(crate) fn inherited_message_module(spec: &MessageInheritedModuleSpec) -> syn::File {
let original_dialect_mod_name = dialect_mod_name(spec.original_dialect_name());
let module_doc_comment = format!(
" MAVLink message `{}` for `{}` dialect inherited from [`super::super::super::{}`] dialect.",
spec.message_name(),
spec.dialect_canonical_name(),
&original_dialect_mod_name
);
let module_doc_comment = match spec.msrv_name() {
Some(msrv_name) => format!(
" MAVLink message `{}` for {} microservice of `{}` dialect inherited from [`super::super::super::super::super::{}`] dialect.",
spec.message_name(),
microservice_doc_mention(msrv_name),
spec.dialect_canonical_name(),
&original_dialect_mod_name
),
None => module_doc_comment,
};
let original_dialect_mod_ident = format_ident!("{}", &original_dialect_mod_name);
let original_dialect_import_path = quote! { super::super::super::#original_dialect_mod_ident };
let original_dialect_import_path = match spec.msrv_name() {
Some(_) => quote! { super::super::#original_dialect_import_path },
None => original_dialect_import_path,
};
let message_mod_ident = format_ident!("{}", message_mod_name(spec.message_name()));
let message_struct_ident = format_ident!("{}", message_struct_name(spec.message_name()));
let message_doc_comment = format!(" Originally defined in [`{original_dialect_mod_name}::messages::{message_mod_ident}`](dialect::messages::{message_struct_ident}).");
let reexported_from_dialect_doc_comment =
format!(" Re-exported from [`{original_dialect_mod_name}`](dialect) dialect.");
syn::parse2(quote! {
#![doc = #module_doc_comment]
use #original_dialect_import_path as dialect;
#[doc = #message_doc_comment]
pub type #message_struct_ident = dialect::messages::#message_struct_ident;
#[doc = #reexported_from_dialect_doc_comment]
pub use dialect::messages::#message_mod_ident::spec;
})
.unwrap()
}