use std::ffi::OsStr;
use std::fs::read_to_string;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use serde::Serialize;
use crate::consts::{
MAV_CMD, MAV_CMD_CONDITION, MAV_CMD_DO, MAV_CMD_NAV, MAV_CMD_NAV_FENCE,
MAV_CMD_NAV_FENCE_PREFIX, MAV_CMD_NAV_PREFIX, MAV_CMD_NAV_RALLY, MAV_CMD_NAV_RALLY_PREFIX,
MISSION_CONDITION_COMMANDS, MISSION_DO_COMMANDS, MISSION_NAV_COMMANDS_EXCLUDES,
MISSION_NAV_COMMANDS_INCLUDES, MISSION_NAV_FENCE_COMMANDS, MISSION_NAV_RALLY_COMMANDS,
};
use crate::conventions;
use crate::error::RustGenResult;
use crate::specs::dialects::dialect::enums::{
EnumImplModuleSpec, EnumInheritedModuleSpec, EnumsRootModuleSpec,
};
use crate::specs::dialects::dialect::messages::{
MessageImplModuleSpec, MessageInheritedModuleSpec, MessagesRootModuleSpec,
};
use crate::specs::dialects::dialect::microservices::MicroservicesRootModuleSpec;
use crate::specs::dialects::dialect::DialectModuleSpec;
use crate::specs::dialects::dialect::MsrvSpec;
use crate::specs::dialects::DialectsRootModuleSpec;
use crate::specs::root::RootModuleSpec;
use crate::specs::Spec;
use crate::templates;
use mavinspect::protocol::{Dialect, Enum, Filter, Message, Microservices, Protocol};
use mavinspect::utils::{Buildable, Builder};
use quote::quote;
#[derive(Clone, Debug, Default, Serialize)]
pub struct GeneratorParams {
pub microservices: Microservices,
pub metadata: bool,
pub use_fingerprints: bool,
pub serde: bool,
pub specta: bool,
pub generate_tests: bool,
#[doc(hidden)]
pub internal: bool,
}
#[derive(Clone, Debug)]
pub struct Generator {
protocol: Arc<Protocol>,
out_path: PathBuf,
params: GeneratorParams,
}
impl GeneratorParams {
pub(crate) fn mavspec_import(&self) -> proc_macro2::TokenStream {
if self.internal {
quote! {
#[allow(dead_code)]
use crate as mavspec;
}
} else {
quote! {}
}
}
}
impl Generator {
pub fn new<T: ?Sized + AsRef<OsStr>>(
protocol: Protocol,
out_path: &T,
params: GeneratorParams,
) -> Self {
Self {
protocol: Arc::new(protocol),
out_path: PathBuf::from(out_path),
params,
}
}
pub(crate) fn make<T: ?Sized + AsRef<OsStr>>(
protocol: Arc<Protocol>,
out_path: &T,
params: GeneratorParams,
) -> Self {
Self {
protocol,
out_path: PathBuf::from(out_path),
params,
}
}
pub fn generate(&self) -> RustGenResult<()> {
log::info!("Generating Rust code from MAVLink protocol.");
if self.params.use_fingerprints && !self.fingerprint_has_updated()? {
log::info!("Fingerprint hasn't changed. Skipping.");
return Ok(());
}
self.generate_root_module()?;
self.generate_dialects()?;
if self.params.use_fingerprints {
self.generate_fingerprint()?;
}
log::info!(
"Generation results: {}",
self.out_path.canonicalize().unwrap().to_str().unwrap()
);
Ok(())
}
fn fingerprint(&self) -> String {
self.protocol.fingerprint().to_string()
}
fn fingerprint_has_updated(&self) -> RustGenResult<bool> {
if self.fingerprint_path().exists() {
let existing_fingerprint = read_to_string(self.fingerprint_path())?;
return Ok(existing_fingerprint != self.fingerprint());
}
Ok(true)
}
fn generate_fingerprint(&self) -> RustGenResult<()> {
let mut file = File::create(self.fingerprint_path())?;
file.write_all(self.fingerprint().as_bytes())?;
Ok(())
}
fn generate_root_module(&self) -> RustGenResult<()> {
create_dir_all(self.out_path.as_path())?;
let mut file = File::create(self.root_module_file_path("mod.rs"))?;
let content = prettyplease::unparse(&templates::root_module(&RootModuleSpec::new(
self.protocol.as_ref(),
)));
file.write_all(content.as_bytes())?;
log::debug!("Generated: root module.");
Ok(())
}
fn generate_dialects(&self) -> RustGenResult<()> {
create_dir_all(self.dialects_dir())?;
let mut file = File::create(self.dialects_mod_rs())?;
let content = prettyplease::unparse(&templates::dialects::dialects_root_module(
&DialectsRootModuleSpec::new(self.protocol.as_ref(), &self.params),
));
file.write_all(content.as_bytes())?;
log::debug!("Generated: 'dialects' root module.");
for dialect in self.protocol.dialects() {
let dialect_spec = DialectModuleSpec::new(
dialect,
&self.params,
self.should_have_microservices(dialect),
);
self.generate_dialect(&dialect_spec)?;
if self.should_have_microservices(dialect) {
self.generate_microservices(&dialect_spec)?;
}
}
Ok(())
}
fn generate_dialect(&self, dialect_spec: &DialectModuleSpec) -> RustGenResult<()> {
create_dir_all(self.dialect_dir(dialect_spec))?;
let mut file = File::create(self.dialect_mod_rs(dialect_spec))?;
let content =
prettyplease::unparse(&templates::dialects::dialect::dialect_module(dialect_spec));
file.write_all(content.as_bytes())?;
log::debug!(
"Generated: 'dialects::{}' root module.",
dialect_spec.module_name()
);
self.generate_enums(dialect_spec)?;
self.generate_messages(dialect_spec)?;
Ok(())
}
fn generate_enums(&self, dialect_spec: &DialectModuleSpec) -> RustGenResult<()> {
create_dir_all(self.enums_dir(dialect_spec))?;
let mut file = File::create(self.enums_mod_rs(dialect_spec))?;
let content =
prettyplease::unparse(&templates::dialects::dialect::enums::enums_root_module(
&EnumsRootModuleSpec::new(dialect_spec, &self.params),
));
file.write_all(content.as_bytes())?;
log::debug!(
"Generated: 'dialects::{}::enums' root module.",
dialect_spec.module_name()
);
for mav_enum in dialect_spec.enums() {
let mut file = File::create(self.enum_file(dialect_spec, mav_enum.name()))?;
let content = if let Some(inherited_from_dialect) =
self.enum_inherited_from(mav_enum, dialect_spec)
{
prettyplease::unparse(&templates::dialects::dialect::enums::enum_inherited_module(
&EnumInheritedModuleSpec::new(
mav_enum,
dialect_spec,
inherited_from_dialect.name(),
&self.params,
),
))
} else {
prettyplease::unparse(&templates::dialects::dialect::enums::enum_module(
&EnumImplModuleSpec::new(mav_enum, &dialect_spec),
))
};
file.write_all(content.as_bytes())?;
log::trace!(
"Generated: enum '{}' for dialect '{}'.",
mav_enum.name(),
dialect_spec.display_name(),
);
}
Ok(())
}
fn generate_messages(&self, dialect_spec: &DialectModuleSpec) -> RustGenResult<()> {
create_dir_all(self.messages_dir(dialect_spec))?;
let mut file = File::create(self.messages_mod_rs(dialect_spec))?;
let content = prettyplease::unparse(
&templates::dialects::dialect::messages::messages_root_module(
&MessagesRootModuleSpec::new(dialect_spec),
),
);
file.write_all(content.as_bytes())?;
log::debug!(
"Generated: 'dialects::{}::messages' root module.",
dialect_spec.module_name()
);
for message in dialect_spec.messages() {
let mut file = File::create(self.message_file(dialect_spec, message.name()))?;
match self.message_inherited_from(message, dialect_spec) {
Some(original_dialect) => {
let content = prettyplease::unparse(
&templates::dialects::dialect::messages::inherited_message_module(
&MessageInheritedModuleSpec::new(
dialect_spec,
original_dialect.canonical_name(),
message,
&self.params,
),
),
);
file.write_all(content.as_bytes())?;
log::trace!(
"Message '{}' in dialect '{}' is inherited from dialect '{}'.",
message.name(),
dialect_spec.display_name(),
original_dialect.name()
);
}
_ => {
let content = prettyplease::unparse(
&templates::dialects::dialect::messages::message_module(
&MessageImplModuleSpec::new(message, dialect_spec),
),
);
file.write_all(content.as_bytes())?;
log::trace!(
"Generated: message '{}' for dialect '{}'.",
message.name(),
dialect_spec.display_name(),
);
}
}
}
log::debug!(
"Generated: all '{}' dialect messages.",
dialect_spec.display_name()
);
Ok(())
}
fn should_have_microservices(&self, dialect: &Dialect) -> bool {
!self.params.microservices.is_empty() && self.protocol.is_default_dialect(dialect)
}
fn generate_microservices(&self, dialect_spec: &DialectModuleSpec) -> RustGenResult<()> {
let microservices = dialect_spec.params().microservices & dialect_spec.microservices();
if microservices.is_empty() {
return Ok(());
}
create_dir_all(self.microservices_dir(dialect_spec))?;
let mut file = File::create(self.microservices_mod_rs(dialect_spec))?;
let content = prettyplease::unparse(
&templates::dialects::dialect::microservices::microservices_root_module(
&MicroservicesRootModuleSpec::new(dialect_spec),
),
);
file.write_all(content.as_bytes())?;
log::debug!(
"Generated: 'dialects::{}::microservices' root module.",
dialect_spec.module_name()
);
for (msrv_name, msrv) in microservices.iter_names() {
let msrv_dialect = dialect_spec
.dialect()
.filtered(&Filter::by_microservices(msrv));
let msrv_dialect = if msrv_name == "MISSION" {
Self::update_mission_msrv(msrv_dialect)
} else {
msrv_dialect
};
let msrv_dialect_spec =
DialectModuleSpec::new(&msrv_dialect, dialect_spec.params(), false)
.with_msrv(MsrvSpec::new(msrv_name, dialect_spec.dialect()));
self.generate_dialect(&msrv_dialect_spec)?;
log::debug!(
"Generated: 'dialects::{}::microservices::{}' microservice module.",
dialect_spec.module_name(),
msrv_name,
);
}
Ok(())
}
fn fingerprint_path(&self) -> PathBuf {
self.out_path.join(".fingerprint")
}
fn root_module_file_path(&self, filename: &str) -> PathBuf {
self.out_path.join(filename)
}
fn dialects_dir(&self) -> PathBuf {
self.out_path.join("dialects")
}
fn dialects_mod_rs(&self) -> PathBuf {
self.dialects_dir().join("mod.rs")
}
fn dialect_dir(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
if let Some(msrv_name) = dialect_spec.msrv_name() {
self.dialects_dir()
.join(dialect_spec.module_name())
.join("microservices")
.join(conventions::dialect_mod_name(msrv_name))
} else {
self.dialects_dir().join(dialect_spec.module_name())
}
}
fn dialect_mod_rs(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.dialect_dir(dialect_spec).join("mod.rs")
}
fn enums_dir(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.dialect_dir(dialect_spec).join("enums")
}
fn enums_mod_rs(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.enums_dir(dialect_spec).join("mod.rs")
}
fn enum_file(&self, dialect_spec: &DialectModuleSpec, enum_name: &str) -> PathBuf {
self.enums_dir(dialect_spec)
.join(conventions::enum_file_name(enum_name))
}
fn messages_dir(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.dialect_dir(dialect_spec).join("messages")
}
fn messages_mod_rs(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.messages_dir(dialect_spec).join("mod.rs")
}
fn message_file(&self, dialect_spec: &DialectModuleSpec, message_name: &str) -> PathBuf {
self.messages_dir(dialect_spec)
.join(conventions::message_file_name(message_name))
}
fn enum_inherited_from<'a>(
&'a self,
mav_enum: &Enum,
dialect_spec: &'a DialectModuleSpec,
) -> Option<&'a Dialect> {
if let Some(parent_dialect) = dialect_spec.parent_dialect() {
if let Some(original_enum) = parent_dialect.get_enum_by_name(mav_enum.name()) {
if original_enum.fingerprint() == mav_enum.fingerprint() {
return Some(parent_dialect);
}
}
return None;
}
if mav_enum.defined_in() != dialect_spec.canonical_name() {
if let Some(defined_in_dialect) = self
.protocol
.get_dialect_by_canonical_name(mav_enum.defined_in())
{
if let Some(original_enum) = defined_in_dialect.get_enum_by_name(mav_enum.name()) {
if original_enum.fingerprint() == mav_enum.fingerprint() {
return Some(defined_in_dialect);
}
}
}
}
None
}
fn message_inherited_from<'a>(
&'a self,
msg: &Message,
dialect_spec: &'a DialectModuleSpec,
) -> Option<&'a Dialect> {
if let Some(parent_dialect) = dialect_spec.parent_dialect() {
if let Some(original_msg) = parent_dialect.get_message_by_name(msg.name()) {
if original_msg.fingerprint_strict(Some(parent_dialect))
== msg.fingerprint_strict(Some(dialect_spec.dialect()))
{
return Some(parent_dialect);
}
}
return None;
}
if msg.defined_in() != dialect_spec.canonical_name() {
if let Some(defined_in_dialect) = self
.protocol
.get_dialect_by_canonical_name(msg.defined_in())
{
if let Some(original_msg) = defined_in_dialect.get_message_by_name(msg.name()) {
if original_msg.fingerprint_strict(Some(defined_in_dialect))
== msg.fingerprint_strict(Some(dialect_spec.dialect()))
{
return Some(defined_in_dialect);
}
}
}
}
None
}
fn microservices_dir(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.dialect_dir(dialect_spec).join("microservices")
}
fn microservices_mod_rs(&self, dialect_spec: &DialectModuleSpec) -> PathBuf {
self.microservices_dir(dialect_spec).join("mod.rs")
}
fn update_mission_msrv(dialect: Dialect) -> Dialect {
if let Some(mav_cmd) = dialect.get_enum_by_name(MAV_CMD) {
let mav_cmd = mav_cmd.clone();
Self::add_mav_cmd_mission_versions(dialect, &mav_cmd)
} else {
dialect
}
}
fn add_mav_cmd_mission_versions(dialect: Dialect, mav_cmd: &Enum) -> Dialect {
let description = mav_cmd.description();
let mut dialect_builder = dialect.to_builder();
dialect_builder.insert_enum(
mav_cmd
.to_builder()
.set_name(MAV_CMD_NAV)
.set_description(format!(
"This is `MAV_CMD` enum version that contains `MAV_CMD_NAV_*` mission items\
(excluding fence and rally points).\n\n{}",
description
))
.keep_entries_with_names(MISSION_NAV_COMMANDS_INCLUDES)
.remove_entries_with_names(MISSION_NAV_COMMANDS_EXCLUDES)
.update_entries(|entry| {
let name_stripped = match entry.name().strip_prefix(MAV_CMD_NAV_PREFIX) {
None => entry.name_stripped(),
Some(stripped) => stripped,
};
entry
.to_builder()
.set_name_stripped(name_stripped.to_string())
.build()
})
.build(),
);
dialect_builder.insert_enum(
mav_cmd
.to_builder()
.set_name(MAV_CMD_DO)
.set_description(format!(
"This is `MAV_CMD` enum version that contains `MAV_CMD_DO_*` mission items.\n\n{}",
description
))
.keep_entries_with_names(MISSION_DO_COMMANDS)
.build(),
);
dialect_builder.insert_enum(
mav_cmd
.to_builder()
.set_name(MAV_CMD_CONDITION)
.set_description(format!(
"This is `MAV_CMD` enum version that contains `MAV_CMD_CONDITION_*` mission items.\n\n{}",
description
))
.keep_entries_with_names(MISSION_CONDITION_COMMANDS)
.build(),
);
dialect_builder.insert_enum(
mav_cmd
.to_builder()
.set_name(MAV_CMD_NAV_FENCE)
.set_description(format!(
"This is `MAV_CMD` enum version that contains `MAV_CMD_NAV_FENCE_*` mission items.\n\n{}",
description
))
.keep_entries_with_names(MISSION_NAV_FENCE_COMMANDS)
.update_entries(|entry| {
let name_stripped = match entry.name().strip_prefix(MAV_CMD_NAV_FENCE_PREFIX) {
None => entry.name_stripped(),
Some(stripped) => stripped,
};
entry.to_builder().set_name_stripped(name_stripped.to_string()).build()
})
.build(),
);
dialect_builder.insert_enum(
mav_cmd
.to_builder()
.set_name(MAV_CMD_NAV_RALLY)
.set_description(format!(
"This is `MAV_CMD` enum version that contains `MAV_CMD_NAV_RALLY` mission item.\n\n{}",
description
))
.keep_entries_with_names(MISSION_NAV_RALLY_COMMANDS)
.update_entries(|entry| {
let name_stripped = match entry.name().strip_prefix(MAV_CMD_NAV_RALLY_PREFIX) {
None => entry.name_stripped(),
Some(stripped) => stripped,
};
entry.to_builder().set_name_stripped(name_stripped.to_string()).build()
})
.build(),
);
dialect_builder.build()
}
}