#![forbid(unsafe_code)]
#![allow(clippy::print_stdout)] #![warn(missing_docs)]
use std::path::{Path, PathBuf};
mod dds_type;
use zerodds_ast::facet::Extensibility;
use zerodds_ast::{Ast, TypeKind, TypeNode};
use zerodds_ast_core::Id;
use zerodds_ast_layout::key::{Encoding, Endianness, LayoutKey, Purpose};
use zerodds_ast_layout::{AxisFlag, Dialect, DialectLowering, DialectSettings, PlCdr1Reading};
use zerodds_idl_compose::{ComposeOptions, PathLoader, compose_with};
use zerodds_lang_rules::adapter::cst::{RustMapFormScope, nest_by_module, rust_item_pairs};
pub use zerodds_lang_rules::adapter::cst::RustMapForm;
use zerodds_lang_rules::encode::{rust_codec, rust_codec_pair};
use zerodds_lang_rules::rule::rust;
use zerodds_lang_rules::{ModelGraph, apply};
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Read {
path: PathBuf,
detail: String,
},
Compose {
path: PathBuf,
diagnostics: Vec<String>,
},
Codegen {
path: PathBuf,
detail: String,
},
Write {
path: PathBuf,
detail: String,
},
NoOutDir,
Axis {
flag: &'static str,
value: String,
detail: String,
},
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Read { path, detail } => write!(f, "{}: {detail}", path.display()),
Self::Compose { path, diagnostics } => {
write!(f, "{}: {}", path.display(), diagnostics.join("; "))
}
Self::Codegen { path, detail } => write!(f, "{}: {detail}", path.display()),
Self::Write { path, detail } => write!(f, "{}: {detail}", path.display()),
Self::NoOutDir => write!(f, "OUT_DIR is not set and no out_dir() was given"),
Self::Axis {
flag,
value: _,
detail,
} => write!(f, "{flag}: {detail}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DefaultExt {
Final,
Appendable,
Mutable,
}
impl From<DefaultExt> for Extensibility {
fn from(value: DefaultExt) -> Self {
match value {
DefaultExt::Final => Self::Final,
DefaultExt::Appendable => Self::Appendable,
DefaultExt::Mutable => Self::Mutable,
}
}
}
impl From<Extensibility> for DefaultExt {
fn from(value: Extensibility) -> Self {
match value {
Extensibility::Final => Self::Final,
Extensibility::Appendable => Self::Appendable,
Extensibility::Mutable => Self::Mutable,
}
}
}
pub struct Config {
include_dirs: Vec<PathBuf>,
defines: Vec<(String, String)>,
out_dir: Option<PathBuf>,
default_extensibility: Option<DefaultExt>,
header_comment: Option<String>,
emit_cargo_directives: bool,
typeobject: bool,
type_identifier_constants: bool,
dds_type_impls: bool,
dialect: Dialect,
axis_overrides: Vec<(AxisFlag, String)>,
pl_cdr1_reading: PlCdr1Reading,
rust_map: RustMapForm,
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl Config {
#[must_use]
pub fn new() -> Self {
Self {
include_dirs: Vec::new(),
defines: Vec::new(),
out_dir: None,
default_extensibility: None,
header_comment: None,
emit_cargo_directives: true,
typeobject: true,
type_identifier_constants: false,
dds_type_impls: false,
dialect: Dialect::default(),
axis_overrides: Vec::new(),
pl_cdr1_reading: PlCdr1Reading::default(),
rust_map: RustMapForm::default(),
}
}
pub fn include_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
self.include_dirs.push(dir.into());
self
}
pub fn define(&mut self, name: impl AsRef<str>, value: Option<&str>) -> &mut Self {
self.defines.push((
name.as_ref().to_string(),
value.unwrap_or_default().to_string(),
));
self
}
pub fn out_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
self.out_dir = Some(dir.into());
self
}
pub fn default_extensibility(&mut self, ext: DefaultExt) -> &mut Self {
self.default_extensibility = Some(ext);
self
}
pub fn dialect(&mut self, dialect: Dialect) -> &mut Self {
self.dialect = dialect;
self
}
pub fn axis(&mut self, axis: AxisFlag, value: impl Into<String>) -> &mut Self {
let value = value.into();
if axis == AxisFlag::DefaultExtensibility
&& let Some(extensibility) = Extensibility::parse(&value)
{
self.default_extensibility = Some(extensibility.into());
}
self.axis_overrides.push((axis, value));
self
}
pub fn pl_cdr1_reading(&mut self, reading: PlCdr1Reading) -> &mut Self {
self.pl_cdr1_reading = reading;
self
}
fn layout_key(&self, encoding: Encoding, endianness: Endianness) -> Result<LayoutKey, Error> {
let mut settings =
DialectSettings::in_dialect(self.dialect, encoding, endianness, Purpose::Data);
for (axis, value) in &self.axis_overrides {
settings = axis
.parse_and_apply(settings, value)
.map_err(|detail| Error::Axis {
flag: axis.flag_name(),
value: value.clone(),
detail,
})?;
}
Ok(settings.key.with_pl_cdr1_reading(self.pl_cdr1_reading))
}
fn lowering(&self) -> DialectLowering {
DialectLowering::for_dialect(self.dialect)
}
fn compose_default_extensibility(&self) -> Option<Extensibility> {
self.default_extensibility.map(Into::into).or_else(|| {
(self.dialect != Dialect::default()).then(|| self.dialect.default_extensibility())
})
}
pub fn header_comment(&mut self, comment: impl Into<String>) -> &mut Self {
self.header_comment = Some(comment.into());
self
}
pub fn typeobject(&mut self, enabled: bool) -> &mut Self {
self.typeobject = enabled;
self
}
pub fn type_identifier_constants(&mut self, enabled: bool) -> &mut Self {
self.type_identifier_constants = enabled;
self
}
pub fn dds_type_impls(&mut self, enabled: bool) -> &mut Self {
self.dds_type_impls = enabled;
self
}
pub fn emit_cargo_directives(&mut self, enabled: bool) -> &mut Self {
self.emit_cargo_directives = enabled;
self
}
pub fn rust_map(&mut self, form: RustMapForm) -> &mut Self {
self.rust_map = form;
self
}
pub fn compile_bundle(&self, idls: &[impl AsRef<Path>], name: &str) -> Result<(), Error> {
let out_dir = self.resolve_out_dir()?;
let loader = PathLoader::new(self.include_dirs.clone());
let paths: Vec<PathBuf> = idls.iter().map(|p| p.as_ref().to_path_buf()).collect();
if self.emit_cargo_directives {
for path in &paths {
println!("cargo:rerun-if-changed={}", path.display());
}
}
let mut composed = zerodds_idl_compose::compose_all(
&paths,
&loader,
ComposeOptions {
default_extensibility: self.compose_default_extensibility(),
default_nested: None,
defines: self.defines.clone(),
hashed_id_advances_the_counter: if self.lowering().hashed_id_advances_the_counter {
zerodds_idl_compose::HashedIdCounter::Advances
} else {
zerodds_idl_compose::HashedIdCounter::Skipped
},
hashed_name_spelling: if self.lowering().hashid_reads_the_escaped_spelling {
zerodds_idl_compose::HashedNameSpelling::AsWritten
} else {
zerodds_idl_compose::HashedNameSpelling::Stripped
},
name_of_the_enclosing_scope: if self.lowering().admits_name_of_enclosing_scope {
zerodds_idl_compose::NameOfTheEnclosingScope::Admitted
} else {
zerodds_idl_compose::NameOfTheEnclosingScope::Refused
},
key_on_union_discriminator: zerodds_idl_compose::KeyOnUnionDiscriminator::Refused,
dialect: zerodds_idl_compose::ComposeOptions::default().dialect,
},
)
.map_err(|detail| Error::Read {
path: paths.first().cloned().unwrap_or_default(),
detail,
})?;
if self.emit_cargo_directives {
for opened in loader.opened() {
println!("cargo:rerun-if-changed={opened}");
}
}
let bundle = PathBuf::from(format!("{name}.idl"));
if composed.failed() {
return Err(Error::Compose {
path: bundle,
diagnostics: composed
.diagnostics
.iter()
.filter(|d| d.severity == zerodds_idl_parse::Severity::Error)
.map(|d| located(d, None))
.collect(),
});
}
self.leave_out_what_xtypes_has_no_type_for(&bundle, &mut composed.ast)?;
let code = self.render_with_stem(&bundle, &composed.ast, name)?;
let target = out_dir.join(format!("{name}.rs"));
std::fs::write(&target, code).map_err(|error| Error::Write {
path: target,
detail: error.to_string(),
})
}
fn resolve_out_dir(&self) -> Result<PathBuf, Error> {
match &self.out_dir {
Some(dir) => Ok(dir.clone()),
None => Ok(PathBuf::from(
std::env::var("OUT_DIR").map_err(|_| Error::NoOutDir)?,
)),
}
}
pub fn compile(&self, idls: &[impl AsRef<Path>]) -> Result<(), Error> {
let out_dir = self.resolve_out_dir()?;
for idl in idls {
self.compile_one(idl.as_ref(), &out_dir)?;
}
Ok(())
}
fn compile_one(&self, idl: &Path, out_dir: &Path) -> Result<(), Error> {
let loader = PathLoader::new(self.include_dirs.clone());
let path = idl.to_string_lossy().into_owned();
let mut composed = compose_with(
&path,
&loader,
ComposeOptions {
default_extensibility: self.compose_default_extensibility(),
default_nested: None,
defines: self.defines.clone(),
hashed_id_advances_the_counter: if self.lowering().hashed_id_advances_the_counter {
zerodds_idl_compose::HashedIdCounter::Advances
} else {
zerodds_idl_compose::HashedIdCounter::Skipped
},
hashed_name_spelling: if self.lowering().hashid_reads_the_escaped_spelling {
zerodds_idl_compose::HashedNameSpelling::AsWritten
} else {
zerodds_idl_compose::HashedNameSpelling::Stripped
},
name_of_the_enclosing_scope: if self.lowering().admits_name_of_enclosing_scope {
zerodds_idl_compose::NameOfTheEnclosingScope::Admitted
} else {
zerodds_idl_compose::NameOfTheEnclosingScope::Refused
},
key_on_union_discriminator: zerodds_idl_compose::KeyOnUnionDiscriminator::Refused,
dialect: zerodds_idl_compose::ComposeOptions::default().dialect,
},
)
.map_err(|detail| Error::Read {
path: idl.to_path_buf(),
detail,
})?;
if self.emit_cargo_directives {
println!("cargo:rerun-if-changed={path}");
for opened in loader.opened() {
println!("cargo:rerun-if-changed={opened}");
}
}
if composed.failed() {
return Err(Error::Compose {
path: idl.to_path_buf(),
diagnostics: composed
.diagnostics
.iter()
.filter(|d| d.severity == zerodds_idl_parse::Severity::Error)
.map(|d| located(d, Some(idl)))
.collect(),
});
}
let stem = idl
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("generated");
self.leave_out_what_xtypes_has_no_type_for(idl, &mut composed.ast)?;
let code = self.render_with_stem(idl, &composed.ast, stem)?;
let target = out_dir.join(format!("{stem}.rs"));
std::fs::write(&target, code).map_err(|error| Error::Write {
path: target,
detail: error.to_string(),
})
}
fn leave_out_what_xtypes_has_no_type_for(
&self,
idl: &Path,
ast: &mut Ast,
) -> Result<(), Error> {
let left_out = ast
.drop_outside_profile(zerodds_ast::Profile::XTypes13)
.map_err(|error| Error::Codegen {
path: idl.to_path_buf(),
detail: error.to_string(),
})?;
if self.emit_cargo_directives {
for dropped in &left_out {
println!(
"cargo:warning={}: {} reaches `{}`, for which DDS-XTypes 1.3 has no type \
({}); no code is generated for it ({})",
idl.display(),
dropped.name,
dropped.construct,
dropped.detail,
dropped.spec_reference
);
}
}
Ok(())
}
fn render_with_stem(&self, idl: &Path, ast: &Ast, stem: &str) -> Result<String, Error> {
let _map_form = RustMapFormScope::enter(self.rust_map);
let graph = ModelGraph::of(ast);
let applied = apply(&graph, &rust::catalogue()).map_err(|error| Error::Codegen {
path: idl.to_path_buf(),
detail: format!("{error:?}"),
})?;
let mut pairs = rust_item_pairs(ast, &applied).map_err(|error| Error::Codegen {
path: idl.to_path_buf(),
detail: format!("{error:?}"),
})?;
let mut xcdr1: Vec<Id<TypeNode>> = Vec::new();
for (id, node) in ast.types() {
if node.name.is_empty() || !has_codec(&node.kind) {
continue;
}
let key = self.layout_key(encoding_for(ast, id), Endianness::Little)?;
for item in rust_codec(ast, id, key).map_err(|error| Error::Codegen {
path: idl.to_path_buf(),
detail: format!("{error:?}"),
})? {
pairs.push((node.module, item));
}
if !self.dds_type_impls {
continue;
}
let v1 = self.layout_key(xcdr1_encoding_for(ast, id), Endianness::Little)?;
if let Ok(items) = rust_codec_pair(ast, id, v1) {
for item in items {
pairs.push((node.module, item));
}
xcdr1.push(id);
}
}
let items = nest_by_module(ast, pairs).map_err(|error| Error::Codegen {
path: idl.to_path_buf(),
detail: format!("{error:?}"),
})?;
let items = zerodds_lang_rules::verbatim::weave_file(
ast,
zerodds_lang_rules::verbatim::aliases::RUST,
items,
);
let mut doc = vec![
format!(
"Generated by zerodds-build from {}.",
idl.file_name()
.and_then(|n| n.to_str())
.unwrap_or("the IDL")
),
String::new(),
String::from("Do not edit: the point of this file is that nobody did."),
];
if let Some(comment) = &self.header_comment {
doc.push(String::new());
doc.push(comment.clone());
}
let doc: Vec<&str> = doc.iter().map(String::as_str).collect();
let probe = zerodds_lang_render::rust::file(&zerodds_lang_cst::rust::File {
inner_attrs: Vec::new(),
doc: zerodds_lang_cst::common::Doc::lines(&[]),
uses: Vec::new(),
items: items.clone(),
});
let names: Vec<zerodds_lang_cst::common::Ident> = RUNTIME_IMPORTS
.iter()
.filter(|name| mentions_word(&probe, name))
.map(|name| zerodds_lang_cst::common::Ident::new(name))
.collect();
let mut uses = Vec::new();
if !names.is_empty() {
uses.push(zerodds_lang_cst::rust::Use {
path: zerodds_lang_cst::common::Path::single("zerodds_wire"),
names,
visibility: zerodds_lang_cst::common::Visibility::Private,
});
}
if mentions_word(&probe, "BTreeMap") {
uses.push(zerodds_lang_cst::rust::Use {
path: zerodds_lang_cst::common::Path::new(&["std", "collections"]),
names: vec![zerodds_lang_cst::common::Ident::new("BTreeMap")],
visibility: zerodds_lang_cst::common::Visibility::Private,
});
}
let file = zerodds_lang_cst::rust::File {
inner_attrs: Vec::new(),
doc: zerodds_lang_cst::common::Doc::lines(&doc),
uses,
items,
};
let mut code = zerodds_lang_render::rust::file(&file);
if self.typeobject {
code.push_str(&type_objects_module(ast, stem));
}
if self.type_identifier_constants {
code.push_str(&type_identifiers_module(ast, stem));
}
if self.dds_type_impls {
let key_for = |encoding: Encoding| {
self.layout_key(encoding, Endianness::Little)
.map_err(|error| error.to_string())
};
code.push_str(
&dds_type::dds_type_impls(ast, &xcdr1, &key_for).map_err(|detail| {
Error::Codegen {
path: idl.to_path_buf(),
detail,
}
})?,
);
}
Ok(code)
}
}
fn type_identifiers_module(ast: &Ast, stem: &str) -> String {
use core::fmt::Write as _;
let mut body = String::new();
for (id, node) in ast.types() {
if node.name.is_empty() || !has_codec(&node.kind) {
continue;
}
let Ok(identifier) = zerodds_types::from_ast::type_identifier(
ast,
id,
zerodds_types::from_ast::Form::Complete,
) else {
let _ = writeln!(
body,
" // `{}`: its identifier could not be computed from the model.",
node.name
);
continue;
};
match identifier {
zerodds_types::TypeIdentifier::EquivalenceHashComplete(hash) => {
let octets: Vec<String> = hash.0.iter().map(|b| format!("0x{b:02x}")).collect();
let _ = write!(
body,
" /// Complete equivalence hash of `{}`, XTypes 1.3 \u{a7}7.3.4.6.\n \
pub const {}: zerodds_types::TypeIdentifier =\n \
zerodds_types::TypeIdentifier::EquivalenceHashComplete(\
zerodds_types::EquivalenceHash([{}]));\n",
node.name,
constant_name(&node.name),
octets.join(", ")
);
}
zerodds_types::TypeIdentifier::StronglyConnectedComponent(scc) => {
let octets: Vec<String> = scc.hash.0.iter().map(|b| format!("0x{b:02x}")).collect();
let _ = write!(
body,
" /// Complete component identifier of `{}`, XTypes 1.3 \u{a7}7.3.4.9.3.\n \
pub const {}: zerodds_types::TypeIdentifier =\n \
zerodds_types::TypeIdentifier::StronglyConnectedComponent(\n \
zerodds_types::StronglyConnectedComponentId {{\n \
equivalence_kind: 0x{:02X},\n hash: \
zerodds_types::EquivalenceHash([{}]),\n scc_length: {},\n \
scc_index: {},\n }});\n",
node.name,
constant_name(&node.name),
scc.equivalence_kind,
octets.join(", "),
scc.scc_length,
scc.scc_index
);
}
other => {
let _ = writeln!(
body,
" // `{}`: {other:?} is not an equivalence hash and this module \
does not spell it as a constant yet.",
node.name
);
}
}
}
if body.is_empty() {
return String::new();
}
format!(
"\n#[allow(dead_code, non_upper_case_globals)]\npub mod type_identifiers_{} {{\n{body}}}\n",
constant_name(stem).to_lowercase()
)
}
fn type_objects_module(ast: &Ast, stem: &str) -> String {
use core::fmt::Write as _;
let mut body = String::new();
for (id, node) in ast.types() {
if node.name.is_empty() || !has_codec(&node.kind) {
continue;
}
let Ok(object) = zerodds_types::from_ast::minimal_type_object(ast, id) else {
continue;
};
let Ok(bytes) = zerodds_types::type_object::TypeObject::Minimal(object).to_bytes_le()
else {
continue;
};
let octets: Vec<String> = bytes.iter().map(|b| format!("0x{b:02x}")).collect();
let _ = write!(
body,
" /// Minimal TypeObject for `{}`, XCDR2-LE.\n pub const {}: &[u8] = &[{}];\n",
node.name,
constant_name(&node.name),
octets.join(", ")
);
}
if body.is_empty() {
return String::new();
}
format!(
"\n#[allow(dead_code, non_upper_case_globals)]\npub mod type_objects_{} {{\n{body}}}\n",
constant_name(stem).to_lowercase()
)
}
fn constant_name(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub(crate) const fn has_codec(kind: &TypeKind) -> bool {
matches!(
kind,
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
)
}
fn xcdr1_encoding_for(ast: &Ast, id: Id<TypeNode>) -> Encoding {
dds_type::xcdr1_encoding(ast.effective_extensibility(id))
}
pub(crate) fn encoding_for(ast: &Ast, id: Id<TypeNode>) -> Encoding {
match ast.effective_extensibility(id) {
Extensibility::Final => Encoding::PlainCdr2,
Extensibility::Appendable => Encoding::DelimitedCdr,
Extensibility::Mutable => Encoding::PlCdr2,
}
}
fn located(diagnostic: &zerodds_idl_parse::Diagnostic, unit: Option<&Path>) -> String {
let path = match (diagnostic.file.as_deref(), unit) {
(Some(file), _) => PathBuf::from(file),
(None, Some(unit)) => unit.to_path_buf(),
(None, None) => return diagnostic.message.clone(),
};
let place = std::fs::read_to_string(&path).ok().map(|text| {
let offset = (diagnostic.primary.start as usize).min(text.len());
let before = text.get(..offset).unwrap_or(&text);
let line = before.matches('\n').count() + 1;
let column = before
.rsplit('\n')
.next()
.map_or(0, |last| last.chars().count())
+ 1;
format!("{line}:{column}")
});
match place {
Some(place) => format!("{}:{place}: {}", path.display(), diagnostic.message),
None => format!("{}: {}", path.display(), diagnostic.message),
}
}
pub fn compile(idls: &[impl AsRef<Path>]) -> Result<(), Error> {
Config::new().compile(idls)
}
const RUNTIME_IMPORTS: [&str; 6] = [
"Narrowed",
"ParameterRole",
"Reader",
"TryConstruct",
"WireError",
"Writer",
];
fn mentions_word(haystack: &str, word: &str) -> bool {
let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
let bytes = haystack.as_bytes();
let mut from = 0;
while let Some(found) = haystack[from..].find(word) {
let at = from + found;
let before_ok = at == 0 || !is_ident(char::from(bytes[at - 1]));
let after = at + word.len();
let after_ok = after >= bytes.len() || !is_ident(char::from(bytes[after]));
if before_ok && after_ok {
return true;
}
from = at + 1;
}
false
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use super::*;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("zerodds-build-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
dir
}
#[test]
fn one_unit_becomes_one_module_named_after_it() {
let dir = scratch("basic");
let idl = dir.join("Robot.idl");
std::fs::write(
&idl,
"module robot { @appendable struct Pose { @key long id; double x; }; };",
)
.expect("write idl");
Config::new()
.out_dir(&dir)
.emit_cargo_directives(false)
.compile(&[&idl])
.expect("compile");
let generated = std::fs::read_to_string(dir.join("Robot.rs")).expect("the module");
assert!(generated.contains("pub mod robot"), "{generated}");
assert!(generated.contains("pub struct Pose"), "{generated}");
assert!(generated.contains("fn encode"), "{generated}");
assert!(generated.contains("use super::*"), "{generated}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_struct_holding_an_any_is_left_out_and_the_rest_of_the_unit_builds() {
let dir = scratch("any");
let idl = dir.join("Mixed.idl");
std::fs::write(
&idl,
"module m { struct Plain { long a; }; struct Holds { any a; }; \
struct Deep { Holds h; }; struct Beside { Plain p; string s; }; };",
)
.expect("write idl");
Config::new()
.out_dir(&dir)
.emit_cargo_directives(false)
.compile(&[&idl])
.expect("a unit with an `any` member builds");
let generated = std::fs::read_to_string(dir.join("Mixed.rs")).expect("the module");
assert!(generated.contains("pub struct Plain"), "{generated}");
assert!(generated.contains("pub struct Beside"), "{generated}");
assert!(!generated.contains("Holds"), "{generated}");
assert!(!generated.contains("Deep"), "{generated}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_broken_unit_fails_and_names_what_is_wrong() {
let dir = scratch("broken");
let idl = dir.join("Bad.idl");
std::fs::write(&idl, "module m { struct S { long ; }; };").expect("write idl");
let Err(error) = Config::new()
.out_dir(&dir)
.emit_cargo_directives(false)
.compile(&[&idl])
else {
panic!("a unit with a syntax error must not compile")
};
assert!(matches!(error, Error::Compose { .. }), "{error}");
assert!(!dir.join("Bad.rs").exists(), "nothing was written");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_default_extensibility_reaches_the_generated_codec() {
let dir = scratch("ext");
let idl = dir.join("Ext.idl");
std::fs::write(&idl, "module m { struct Silent { long a; }; };").expect("write idl");
Config::new()
.out_dir(&dir)
.emit_cargo_directives(false)
.default_extensibility(DefaultExt::Final)
.compile(&[&idl])
.expect("compile");
let with_final = std::fs::read_to_string(dir.join("Ext.rs")).expect("the module");
Config::new()
.out_dir(&dir)
.emit_cargo_directives(false)
.compile(&[&idl])
.expect("compile");
let with_default = std::fs::read_to_string(dir.join("Ext.rs")).expect("the module");
assert_ne!(
with_final, with_default,
"@final and the specification's @appendable are different wire plans"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_include_dir_is_searched_and_the_comment_is_written() {
let dir = scratch("include");
let headers = dir.join("headers");
std::fs::create_dir_all(&headers).expect("headers");
std::fs::write(headers.join("common.idl"), "struct Shared { long a; };").expect("common");
let idl = dir.join("Main.idl");
std::fs::write(&idl, "#include \"common.idl\"\nstruct Main { long b; };").expect("main");
Config::new()
.out_dir(&dir)
.include_dir(&headers)
.header_comment("built for the test")
.emit_cargo_directives(false)
.compile(&[&idl])
.expect("compile");
let generated = std::fs::read_to_string(dir.join("Main.rs")).expect("the module");
assert!(generated.contains("Shared"), "the include came in");
assert!(generated.contains("built for the test"), "{generated}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn without_out_dir_and_without_cargo_it_says_so() {
let error = Error::NoOutDir;
assert!(error.to_string().contains("OUT_DIR"), "{error}");
}
}