pub mod cbindgen;
use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
pub enum Language {
#[value(name = "c", alias = "C")]
C,
#[value(name = "c++", alias = "C++", alias = "cpp")]
Cxx,
#[value(name = "cython", alias = "Cython")]
Cython,
}
impl Language {
pub fn extension(&self) -> &'static str {
match self {
Language::C => "h",
Language::Cxx => "hpp",
Language::Cython => "pyx",
}
}
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SortKey {
#[default]
SourceOrder,
Name,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DocumentationStyle {
#[default]
Auto,
C,
C99,
Doxy,
Cxx,
}
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DocumentationLength {
#[default]
Full,
Short,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawFnSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<SortKey>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawStaticSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<SortKey>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawConstantSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<SortKey>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawEnumSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_with_name: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawHeaderSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_guard: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preamble: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trailer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autogen_warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pragma_once: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub includes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_includes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_includes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_style: Option<DocumentationStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_length: Option<DocumentationLength>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<SortKey>,
#[serde(rename = "fn", skip_serializing_if = "Option::is_none")]
pub fn_: Option<RawFnSection>,
#[serde(rename = "static", skip_serializing_if = "Option::is_none")]
pub static_: Option<RawStaticSection>,
#[serde(rename = "constant", skip_serializing_if = "Option::is_none")]
pub constant_: Option<RawConstantSection>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_: Option<RawEnumSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub c: Option<RawCSection>,
#[serde(alias = "c++", alias = "cpp", skip_serializing_if = "Option::is_none")]
pub cxx: Option<RawCxxSection>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PackageTypeMode {
Opaque,
Skip,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawPackageConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub types: Option<PackageTypeMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usize_is_size_t: Option<bool>,
}
#[derive(Debug, Clone, ValueEnum, Deserialize, Serialize)]
pub enum Style {
#[value(name = "both", alias = "Both")]
#[serde(alias = "both")]
Both,
#[value(name = "tag", alias = "Tag")]
#[serde(alias = "tag")]
Tag,
#[value(name = "type", alias = "Type")]
#[serde(alias = "type")]
Type,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub preamble: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trailer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autogen_warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pragma_once: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub includes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_includes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_includes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_style: Option<DocumentationStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_length: Option<DocumentationLength>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<SortKey>,
#[serde(rename = "fn", skip_serializing_if = "Option::is_none")]
pub fn_: Option<RawFnSection>,
#[serde(rename = "static", skip_serializing_if = "Option::is_none")]
pub static_: Option<RawStaticSection>,
#[serde(rename = "constant", skip_serializing_if = "Option::is_none")]
pub constant_: Option<RawConstantSection>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_: Option<RawEnumSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bundle: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usize_is_size_t: Option<bool>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub package: BTreeMap<String, RawPackageConfig>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub header: HashMap<String, RawHeaderSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub c: Option<RawCSection>,
#[serde(alias = "c++", alias = "cpp", skip_serializing_if = "Option::is_none")]
pub cxx: Option<RawCxxSection>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawCSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<Style>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpp_compat: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preamble: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trailer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autogen_warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pragma_once: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub includes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_includes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_includes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_style: Option<DocumentationStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_length: Option<DocumentationLength>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RawCxxSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub preamble: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trailer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autogen_warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pragma_once: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub includes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_includes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_includes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_style: Option<DocumentationStyle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_length: Option<DocumentationLength>,
}
#[derive(Debug, Clone)]
pub enum Config {
C(CConfig),
#[allow(dead_code)]
Cxx(CxxConfig),
}
#[derive(Debug, Clone)]
pub struct ConfigSet {
pub default: Config,
pub per_header: HashMap<String, Config>,
pub bundle: bool,
pub header_renames: HashMap<String, String>,
}
impl ConfigSet {
pub fn for_header(&self, base_name: &str) -> &Config {
self.per_header.get(base_name).unwrap_or(&self.default)
}
pub fn header_names(&self) -> impl Iterator<Item = &str> {
self.per_header.keys().map(|s| s.as_str())
}
}
#[derive(Debug, Clone)]
pub struct CommonConfig {
pub preamble: Option<String>,
pub trailer: Option<String>,
pub autogen_warning: Option<String>,
pub include_guard: Option<String>,
pub pragma_once: bool,
pub includes: Vec<String>,
pub no_includes: bool,
pub after_includes: Option<String>,
pub fn_sort_by: SortKey,
pub static_sort_by: SortKey,
pub constant_sort_by: SortKey,
pub documentation: bool,
pub documentation_style: DocumentationStyle,
pub documentation_length: DocumentationLength,
pub package_configs: HashMap<String, PackageConfig>,
pub usize_is_size_t: bool,
}
#[derive(Debug, Clone)]
pub struct PackageConfig {
pub types: Option<PackageTypeMode>,
pub usize_is_size_t: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct CConfig {
pub common: CommonConfig,
pub style: Style,
pub cpp_compat: bool,
pub enum_prefix_with_name: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CxxConfig {
pub common: CommonConfig,
}
#[derive(Debug)]
pub struct ConfigError {
pub message: String,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for ConfigError {}
#[derive(Clone)]
struct RawCommonFields {
preamble: Option<String>,
trailer: Option<String>,
autogen_warning: Option<String>,
pragma_once: Option<bool>,
includes: Vec<String>,
no_includes: Option<bool>,
after_includes: Option<String>,
sort_by: Option<SortKey>,
fn_sort_by: Option<SortKey>,
static_sort_by: Option<SortKey>,
constant_sort_by: Option<SortKey>,
documentation: Option<bool>,
documentation_style: Option<DocumentationStyle>,
documentation_length: Option<DocumentationLength>,
package_configs: HashMap<String, PackageConfig>,
usize_is_size_t: Option<bool>,
}
struct RawCommonOverrides {
preamble: Option<String>,
trailer: Option<String>,
autogen_warning: Option<String>,
pragma_once: Option<bool>,
includes: Option<Vec<String>>,
no_includes: Option<bool>,
after_includes: Option<String>,
documentation: Option<bool>,
documentation_style: Option<DocumentationStyle>,
documentation_length: Option<DocumentationLength>,
}
impl RawCommonOverrides {
fn merge(self, other: RawCommonOverrides) -> RawCommonOverrides {
RawCommonOverrides {
preamble: other.preamble.or(self.preamble),
trailer: other.trailer.or(self.trailer),
autogen_warning: other.autogen_warning.or(self.autogen_warning),
pragma_once: other.pragma_once.or(self.pragma_once),
includes: other.includes.or(self.includes),
no_includes: other.no_includes.or(self.no_includes),
after_includes: other.after_includes.or(self.after_includes),
documentation: other.documentation.or(self.documentation),
documentation_style: other.documentation_style.or(self.documentation_style),
documentation_length: other.documentation_length.or(self.documentation_length),
}
}
}
impl RawCommonFields {
fn resolve(self, overrides: RawCommonOverrides, include_guard: Option<String>) -> CommonConfig {
CommonConfig {
preamble: overrides.preamble.or(self.preamble),
trailer: overrides.trailer.or(self.trailer),
autogen_warning: overrides.autogen_warning.or(self.autogen_warning),
include_guard,
pragma_once: overrides.pragma_once.or(self.pragma_once).unwrap_or(false),
includes: overrides.includes.unwrap_or(self.includes),
no_includes: overrides.no_includes.or(self.no_includes).unwrap_or(false),
after_includes: overrides.after_includes.or(self.after_includes),
fn_sort_by: self.fn_sort_by.or(self.sort_by).unwrap_or_default(),
static_sort_by: self.static_sort_by.or(self.sort_by).unwrap_or_default(),
constant_sort_by: self.constant_sort_by.or(self.sort_by).unwrap_or_default(),
documentation: overrides
.documentation
.or(self.documentation)
.unwrap_or(true),
documentation_style: overrides
.documentation_style
.or(self.documentation_style)
.unwrap_or_default(),
documentation_length: overrides
.documentation_length
.or(self.documentation_length)
.unwrap_or_default(),
package_configs: self.package_configs,
usize_is_size_t: self.usize_is_size_t.unwrap_or(false),
}
}
}
trait IntoCommonOverrides {
fn into_common_overrides(self) -> RawCommonOverrides;
}
impl IntoCommonOverrides for RawCSection {
fn into_common_overrides(self) -> RawCommonOverrides {
RawCommonOverrides {
preamble: self.preamble,
trailer: self.trailer,
autogen_warning: self.autogen_warning,
pragma_once: self.pragma_once,
includes: self.includes,
no_includes: self.no_includes,
after_includes: self.after_includes,
documentation: self.documentation,
documentation_style: self.documentation_style,
documentation_length: self.documentation_length,
}
}
}
impl IntoCommonOverrides for RawCxxSection {
fn into_common_overrides(self) -> RawCommonOverrides {
RawCommonOverrides {
preamble: self.preamble,
trailer: self.trailer,
autogen_warning: self.autogen_warning,
pragma_once: self.pragma_once,
includes: self.includes,
no_includes: self.no_includes,
after_includes: self.after_includes,
documentation: self.documentation,
documentation_style: self.documentation_style,
documentation_length: self.documentation_length,
}
}
}
impl IntoCommonOverrides for RawHeaderSection {
fn into_common_overrides(self) -> RawCommonOverrides {
RawCommonOverrides {
preamble: self.preamble,
trailer: self.trailer,
autogen_warning: self.autogen_warning,
pragma_once: self.pragma_once,
includes: self.includes,
no_includes: self.no_includes,
after_includes: self.after_includes,
documentation: self.documentation,
documentation_style: self.documentation_style,
documentation_length: self.documentation_length,
}
}
}
#[derive(Debug, Default)]
pub struct CliOverrides {
pub style: Option<Style>,
pub cpp_compat: bool,
}
impl RawConfig {
pub fn from_toml_file(path: &Path) -> Result<Self, ConfigError> {
let contents = fs_err::read_to_string(path).map_err(|e| ConfigError {
message: format!("failed to read config file: {e}"),
})?;
toml::from_str(&contents).map_err(|e| ConfigError {
message: format!("failed to parse config file: {e}"),
})
}
pub fn into_config(
self,
language: &Language,
overrides: &CliOverrides,
) -> Result<ConfigSet, ConfigError> {
match language {
Language::Cython => {
return Err(ConfigError {
message: "Cython output is not yet supported".to_string(),
});
}
Language::Cxx => {
return Err(ConfigError {
message: "C++ output is not yet supported".to_string(),
});
}
Language::C => {}
}
let bundle = self.bundle.unwrap_or(false);
let mut package_configs: HashMap<String, PackageConfig> = HashMap::new();
let mut header_renames: HashMap<String, String> = HashMap::new();
let mut rename_targets: HashMap<String, String> = HashMap::new();
for (key, raw) in self.package {
if raw.types.is_some() || raw.usize_is_size_t.is_some() {
package_configs.insert(
key.clone(),
PackageConfig {
types: raw.types,
usize_is_size_t: raw.usize_is_size_t,
},
);
}
if let Some(header_name) = raw.header_name {
if bundle {
return Err(ConfigError {
message: format!(
"`header_name` is not supported in bundle mode \
(set on `[package.\"{key}\"]`)"
),
});
}
if header_name.is_empty() {
return Err(ConfigError {
message: format!(
"`header_name` on `[package.\"{key}\"]` must not be empty"
),
});
}
if header_name.contains(['/', '\\']) {
return Err(ConfigError {
message: format!(
"`header_name` on `[package.\"{key}\"]` must not contain \
path separators: `{header_name}`"
),
});
}
if header_name.contains('.') {
return Err(ConfigError {
message: format!(
"`header_name` on `[package.\"{key}\"]` must not include \
a file extension (got `{header_name}`); the language extension \
is appended automatically"
),
});
}
if let Some(prev_key) = rename_targets.insert(header_name.clone(), key.clone()) {
return Err(ConfigError {
message: format!(
"`header_name = \"{header_name}\"` is set on both \
`[package.\"{prev_key}\"]` and `[package.\"{key}\"]`"
),
});
}
header_renames.insert(key, header_name);
}
}
let base = RawCommonFields {
preamble: self.preamble,
trailer: self.trailer,
autogen_warning: self.autogen_warning,
pragma_once: self.pragma_once,
includes: self.includes,
no_includes: self.no_includes,
after_includes: self.after_includes,
sort_by: self.sort_by,
fn_sort_by: self.fn_.and_then(|s| s.sort_by),
static_sort_by: self.static_.and_then(|s| s.sort_by),
constant_sort_by: self.constant_.and_then(|s| s.sort_by),
documentation: self.documentation,
documentation_style: self.documentation_style,
documentation_length: self.documentation_length,
package_configs,
usize_is_size_t: self.usize_is_size_t,
};
let default = Self::build_config(
language,
overrides,
&base,
&self.c,
&self.cxx,
self.enum_.as_ref(),
None, None, None, None, )?;
let mut per_header = HashMap::new();
for (name, mut header_section) in self.header {
let include_guard = header_section.include_guard.take();
let header_sort_by = header_section.sort_by;
let header_fn_sort_by = header_section.fn_.as_ref().and_then(|s| s.sort_by);
let header_static_sort_by = header_section.static_.as_ref().and_then(|s| s.sort_by);
let header_constant_sort_by = header_section.constant_.as_ref().and_then(|s| s.sort_by);
let mut header_base = base.clone();
if let Some(sort_by) = header_sort_by {
header_base.sort_by = Some(sort_by);
}
if let Some(fn_sort_by) = header_fn_sort_by {
header_base.fn_sort_by = Some(fn_sort_by);
}
if let Some(static_sort_by) = header_static_sort_by {
header_base.static_sort_by = Some(static_sort_by);
}
if let Some(constant_sort_by) = header_constant_sort_by {
header_base.constant_sort_by = Some(constant_sort_by);
}
let header_c = header_section.c.take();
let header_cxx = header_section.cxx.take();
let header_enum = header_section.enum_.take();
let effective_enum = header_enum.as_ref().or(self.enum_.as_ref());
let header_config = Self::build_config(
language,
overrides,
&header_base,
&self.c,
&self.cxx,
effective_enum,
Some(header_section),
header_c,
header_cxx,
include_guard,
)?;
per_header.insert(name, header_config);
}
Ok(ConfigSet {
default,
per_header,
bundle,
header_renames,
})
}
#[allow(clippy::too_many_arguments)]
fn build_config(
language: &Language,
cli_overrides: &CliOverrides,
base: &RawCommonFields,
c_section: &Option<RawCSection>,
cxx_section: &Option<RawCxxSection>,
enum_section: Option<&RawEnumSection>,
header_section: Option<RawHeaderSection>,
header_c_section: Option<RawCSection>,
header_cxx_section: Option<RawCxxSection>,
include_guard: Option<String>,
) -> Result<Config, ConfigError> {
match language {
Language::C => {
let global_section = c_section.clone().unwrap_or_default();
let style = cli_overrides
.style
.clone()
.or_else(|| header_c_section.as_ref().and_then(|s| s.style.clone()))
.or(global_section.style.clone())
.unwrap_or(Style::Both);
let cpp_compat = if cli_overrides.cpp_compat {
true
} else {
header_c_section
.as_ref()
.and_then(|s| s.cpp_compat)
.or(global_section.cpp_compat)
.unwrap_or(false)
};
let enum_prefix_with_name = enum_section
.and_then(|s| s.prefix_with_name)
.unwrap_or(false);
let mut merged = global_section.into_common_overrides();
if let Some(hs) = header_section {
merged = merged.merge(hs.into_common_overrides());
}
if let Some(hc) = header_c_section {
merged = merged.merge(hc.into_common_overrides());
}
let common = base.clone().resolve(merged, include_guard);
Ok(Config::C(CConfig {
common,
style,
cpp_compat,
enum_prefix_with_name,
}))
}
Language::Cxx => {
let global_section = cxx_section.clone().unwrap_or_default();
let mut merged = global_section.into_common_overrides();
if let Some(hs) = header_section {
merged = merged.merge(hs.into_common_overrides());
}
if let Some(hcxx) = header_cxx_section {
merged = merged.merge(hcxx.into_common_overrides());
}
let common = base.clone().resolve(merged, include_guard);
Ok(Config::Cxx(CxxConfig { common }))
}
Language::Cython => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config() {
let raw: RawConfig = toml::from_str("").unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
assert!(matches!(config_set.default, Config::C(_)));
}
#[test]
fn full_c_config() {
let toml_str = r#"
preamble = "/* License */"
trailer = "/* End */"
autogen_warning = "// Auto-generated"
pragma_once = false
includes = ["<stdint.h>", "<stdbool.h>", "my_types.h"]
no_includes = false
after_includes = "/* after includes */"
[c]
style = "Tag"
cpp_compat = true
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert!(matches!(c.style, Style::Tag));
assert!(c.cpp_compat);
assert_eq!(c.common.preamble.as_deref(), Some("/* License */"));
assert_eq!(c.common.trailer.as_deref(), Some("/* End */"));
assert_eq!(
c.common.autogen_warning.as_deref(),
Some("// Auto-generated")
);
assert!(c.common.include_guard.is_none());
assert!(!c.common.pragma_once);
assert_eq!(
c.common.includes,
vec!["<stdint.h>", "<stdbool.h>", "my_types.h"]
);
assert!(!c.common.no_includes);
assert_eq!(
c.common.after_includes.as_deref(),
Some("/* after includes */")
);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn full_cxx_config_rejected() {
let toml_str = r#"
preamble = "/* C++ License */"
pragma_once = true
[cxx]
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let err = raw
.into_config(&Language::Cxx, &CliOverrides::default())
.unwrap_err();
assert!(err.message.contains("C++ output is not yet supported"));
}
#[test]
fn section_overrides_common() {
let toml_str = r#"
preamble = "/* Shared */"
pragma_once = true
[c]
pragma_once = false
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert_eq!(c.common.preamble.as_deref(), Some("/* Shared */"));
assert!(!c.common.pragma_once);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn multi_language_config() {
let toml_str = r#"
preamble = "/* Shared License */"
[c]
style = "Tag"
cpp_compat = true
[cxx]
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let c_config_set = raw
.clone()
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match &c_config_set.default {
Config::C(c) => {
assert!(matches!(c.style, Style::Tag));
assert!(c.cpp_compat);
assert_eq!(c.common.preamble.as_deref(), Some("/* Shared License */"));
assert!(c.common.include_guard.is_none());
}
_ => panic!("expected Config::C"),
}
let err = raw
.into_config(&Language::Cxx, &CliOverrides::default())
.unwrap_err();
assert!(err.message.contains("C++ output is not yet supported"));
}
#[test]
fn cxx_rejected() {
let raw: RawConfig = toml::from_str("").unwrap();
let err = raw
.into_config(&Language::Cxx, &CliOverrides::default())
.unwrap_err();
assert!(err.message.contains("C++ output is not yet supported"));
}
#[test]
fn cli_overrides_style() {
let toml_str = r#"
[c]
style = "Tag"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let overrides = CliOverrides {
style: Some(Style::Type),
cpp_compat: false,
};
let config_set = raw.into_config(&Language::C, &overrides).unwrap();
match config_set.default {
Config::C(c) => {
assert!(matches!(c.style, Style::Type));
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn cli_overrides_cpp_compat() {
let raw: RawConfig = toml::from_str("").unwrap();
let overrides = CliOverrides {
style: None,
cpp_compat: true,
};
let config_set = raw.into_config(&Language::C, &overrides).unwrap();
match config_set.default {
Config::C(c) => {
assert!(c.cpp_compat);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn cxx_alias_parses() {
let toml_str = r#"
[cxx]
preamble = "/* C++ */"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert!(raw.cxx.is_some());
}
#[test]
fn package_opaque_mode() {
let toml_str = r#"
[package.my-dep]
types = "opaque"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
raw.package["my-dep"].types,
Some(PackageTypeMode::Opaque)
);
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert_eq!(
c.common.package_configs["my-dep"].types,
Some(PackageTypeMode::Opaque)
);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn package_skip_mode() {
let toml_str = r#"
[package.other-dep]
types = "skip"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert_eq!(
raw.package["other-dep"].types,
Some(PackageTypeMode::Skip)
);
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert_eq!(
c.common.package_configs["other-dep"].types,
Some(PackageTypeMode::Skip)
);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn package_versioned_key() {
let toml_str = r#"
[package."foo@1.0"]
types = "opaque"
[package."foo@2.0"]
types = "skip"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert_eq!(raw.package.len(), 2);
assert_eq!(
raw.package["foo@1.0"].types,
Some(PackageTypeMode::Opaque)
);
assert_eq!(
raw.package["foo@2.0"].types,
Some(PackageTypeMode::Skip)
);
}
#[test]
fn package_empty_section_accepted() {
let toml_str = r#"
[package.my-dep]
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert!(raw.package.contains_key("my-dep"));
assert_eq!(raw.package["my-dep"].types, None);
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert!(c.common.package_configs.is_empty());
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn package_unknown_field_rejected() {
let toml_str = r#"
[package.my-dep]
typos = "opaque"
"#;
let result: Result<RawConfig, _> = toml::from_str(toml_str);
assert!(result.is_err());
}
#[test]
fn package_with_other_config() {
let toml_str = r#"
preamble = "/* License */"
[package.my-dep]
types = "opaque"
[c]
style = "Tag"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
assert_eq!(c.common.preamble.as_deref(), Some("/* License */"));
assert!(matches!(c.style, Style::Tag));
assert_eq!(
c.common.package_configs["my-dep"].types,
Some(PackageTypeMode::Opaque)
);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn usize_is_size_t_global_default() {
let raw: RawConfig = toml::from_str("").unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => assert!(!c.common.usize_is_size_t),
_ => panic!("expected Config::C"),
}
}
#[test]
fn usize_is_size_t_global_set() {
let raw: RawConfig = toml::from_str("usize_is_size_t = true").unwrap();
assert_eq!(raw.usize_is_size_t, Some(true));
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => assert!(c.common.usize_is_size_t),
_ => panic!("expected Config::C"),
}
}
#[test]
fn usize_is_size_t_per_package() {
let toml_str = r#"
[package.my-dep]
usize_is_size_t = true
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
assert_eq!(raw.package["my-dep"].usize_is_size_t, Some(true));
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
let pkg = c
.common
.package_configs
.get("my-dep")
.expect("package_configs entry should exist");
assert_eq!(pkg.types, None);
assert_eq!(pkg.usize_is_size_t, Some(true));
assert!(!c.common.usize_is_size_t);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn usize_is_size_t_per_package_with_types() {
let toml_str = r#"
[package.my-dep]
types = "opaque"
usize_is_size_t = false
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.default {
Config::C(c) => {
let pkg = &c.common.package_configs["my-dep"];
assert_eq!(pkg.types, Some(PackageTypeMode::Opaque));
assert_eq!(pkg.usize_is_size_t, Some(false));
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn include_guard_rejected_at_global_level() {
let toml_str = r#"
include_guard = "FOO_H"
"#;
let result: Result<RawConfig, _> = toml::from_str(toml_str);
assert!(result.is_err());
}
#[test]
fn header_section_basic() {
let toml_str = r#"
preamble = "/* Global */"
[header.my-lib]
include_guard = "MY_LIB_H"
preamble = "/* My Lib */"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match &config_set.default {
Config::C(c) => {
assert!(c.common.include_guard.is_none());
assert_eq!(c.common.preamble.as_deref(), Some("/* Global */"));
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("my-lib") {
Config::C(c) => {
assert_eq!(c.common.include_guard.as_deref(), Some("MY_LIB_H"));
assert_eq!(c.common.preamble.as_deref(), Some("/* My Lib */"));
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("unknown") {
Config::C(c) => {
assert!(c.common.include_guard.is_none());
assert_eq!(c.common.preamble.as_deref(), Some("/* Global */"));
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn header_section_inherits_global_language() {
let toml_str = r#"
[c]
style = "Tag"
cpp_compat = true
[header.my-lib]
include_guard = "MY_LIB_H"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.for_header("my-lib") {
Config::C(c) => {
assert!(matches!(c.style, Style::Tag));
assert!(c.cpp_compat);
assert_eq!(c.common.include_guard.as_deref(), Some("MY_LIB_H"));
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn header_section_language_overrides_global() {
let toml_str = r#"
[c]
style = "Tag"
cpp_compat = false
[header.my-lib]
include_guard = "MY_LIB_H"
[header.my-lib.c]
cpp_compat = true
style = "Type"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match &config_set.default {
Config::C(c) => {
assert!(matches!(c.style, Style::Tag));
assert!(!c.cpp_compat);
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("my-lib") {
Config::C(c) => {
assert!(matches!(c.style, Style::Type));
assert!(c.cpp_compat);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn header_section_common_overrides_global() {
let toml_str = r#"
pragma_once = true
documentation = false
[header.my-lib]
pragma_once = false
documentation = true
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match &config_set.default {
Config::C(c) => {
assert!(c.common.pragma_once);
assert!(!c.common.documentation);
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("my-lib") {
Config::C(c) => {
assert!(!c.common.pragma_once);
assert!(c.common.documentation);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn header_section_override_priority() {
let toml_str = r#"
preamble = "/* top-level */"
documentation = false
[c]
preamble = "/* global-c */"
[header.my-lib]
preamble = "/* header-common */"
documentation = true
[header.my-lib.c]
preamble = "/* header-c */"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match &config_set.default {
Config::C(c) => {
assert_eq!(c.common.preamble.as_deref(), Some("/* global-c */"));
assert!(!c.common.documentation);
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("my-lib") {
Config::C(c) => {
assert_eq!(c.common.preamble.as_deref(), Some("/* header-c */"));
assert!(c.common.documentation);
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn header_section_unknown_field_rejected() {
let toml_str = r#"
[header.my-lib]
typo_field = "value"
"#;
let result: Result<RawConfig, _> = toml::from_str(toml_str);
assert!(result.is_err());
}
#[test]
fn header_section_empty_is_valid() {
let toml_str = r#"
[header.my-lib]
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
assert!(config_set.per_header.contains_key("my-lib"));
}
#[test]
fn multiple_header_sections() {
let toml_str = r#"
preamble = "/* Global */"
[header.lib-a]
include_guard = "LIB_A_H"
preamble = "/* Lib A */"
[header.lib-b]
include_guard = "LIB_B_H"
"#;
let raw: RawConfig = toml::from_str(toml_str).unwrap();
let config_set = raw
.into_config(&Language::C, &CliOverrides::default())
.unwrap();
match config_set.for_header("lib-a") {
Config::C(c) => {
assert_eq!(c.common.include_guard.as_deref(), Some("LIB_A_H"));
assert_eq!(c.common.preamble.as_deref(), Some("/* Lib A */"));
}
_ => panic!("expected Config::C"),
}
match config_set.for_header("lib-b") {
Config::C(c) => {
assert_eq!(c.common.include_guard.as_deref(), Some("LIB_B_H"));
assert_eq!(c.common.preamble.as_deref(), Some("/* Global */"));
}
_ => panic!("expected Config::C"),
}
}
#[test]
fn config_reference_documents_every_field() {
const PACKAGE_DUMMY: &str = "cheadergen-test-package-dummy";
const HEADER_DUMMY: &str = "cheadergen-test-header-dummy";
let raw_c = RawCSection {
style: Some(Style::Both),
cpp_compat: Some(true),
preamble: Some("p".into()),
trailer: Some("t".into()),
autogen_warning: Some("w".into()),
pragma_once: Some(true),
includes: Some(vec!["x".into()]),
no_includes: Some(true),
after_includes: Some("a".into()),
documentation: Some(true),
documentation_style: Some(DocumentationStyle::Auto),
documentation_length: Some(DocumentationLength::Full),
};
let raw_cxx = RawCxxSection {
preamble: Some("p".into()),
trailer: Some("t".into()),
autogen_warning: Some("w".into()),
pragma_once: Some(true),
includes: Some(vec!["x".into()]),
no_includes: Some(true),
after_includes: Some("a".into()),
documentation: Some(true),
documentation_style: Some(DocumentationStyle::Auto),
documentation_length: Some(DocumentationLength::Full),
};
let raw_header = RawHeaderSection {
include_guard: Some("G".into()),
preamble: Some("p".into()),
trailer: Some("t".into()),
autogen_warning: Some("w".into()),
pragma_once: Some(true),
includes: Some(vec!["x".into()]),
no_includes: Some(true),
after_includes: Some("a".into()),
documentation: Some(true),
documentation_style: Some(DocumentationStyle::Auto),
documentation_length: Some(DocumentationLength::Full),
sort_by: Some(SortKey::Name),
fn_: Some(RawFnSection {
sort_by: Some(SortKey::Name),
}),
static_: Some(RawStaticSection {
sort_by: Some(SortKey::Name),
}),
constant_: Some(RawConstantSection {
sort_by: Some(SortKey::Name),
}),
enum_: Some(RawEnumSection {
prefix_with_name: Some(true),
}),
c: Some(raw_c.clone()),
cxx: Some(raw_cxx.clone()),
};
let raw_package = RawPackageConfig {
types: Some(PackageTypeMode::Opaque),
header_name: Some("name".into()),
usize_is_size_t: Some(true),
};
let max = RawConfig {
preamble: Some("p".into()),
trailer: Some("t".into()),
autogen_warning: Some("w".into()),
pragma_once: Some(true),
includes: vec!["x".into()],
no_includes: Some(true),
after_includes: Some("a".into()),
documentation: Some(true),
documentation_style: Some(DocumentationStyle::Auto),
documentation_length: Some(DocumentationLength::Full),
sort_by: Some(SortKey::Name),
fn_: Some(RawFnSection {
sort_by: Some(SortKey::Name),
}),
static_: Some(RawStaticSection {
sort_by: Some(SortKey::Name),
}),
constant_: Some(RawConstantSection {
sort_by: Some(SortKey::Name),
}),
enum_: Some(RawEnumSection {
prefix_with_name: Some(true),
}),
bundle: Some(true),
usize_is_size_t: Some(true),
package: BTreeMap::from([(PACKAGE_DUMMY.to_string(), raw_package)]),
header: HashMap::from([(HEADER_DUMMY.to_string(), raw_header)]),
c: Some(raw_c),
cxx: Some(raw_cxx),
};
let serialized = toml::to_string(&max).expect("serialize max RawConfig");
let value: toml::Value = toml::from_str(&serialized).expect("re-parse serialized RawConfig");
let mut keys: std::collections::HashSet<String> = std::collections::HashSet::new();
let dynamic = [PACKAGE_DUMMY, HEADER_DUMMY];
collect_table_keys(&value, &mut keys, &dynamic);
let reference_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("cheadergen")
.join("src")
.join("config_reference.rs");
let reference = std::fs::read_to_string(&reference_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", reference_path.display()));
let mut missing: Vec<&String> = keys
.iter()
.filter(|k| !contains_word(&reference, k))
.collect();
missing.sort();
assert!(
missing.is_empty(),
"{} does not mention these TOML keys: {:?}.\n\
Did you add a field to a Raw* config struct without updating the user-facing reference?",
reference_path.display(),
missing,
);
}
fn collect_table_keys(
value: &toml::Value,
out: &mut std::collections::HashSet<String>,
skip: &[&str],
) {
match value {
toml::Value::Table(table) => {
for (key, child) in table {
if !skip.contains(&key.as_str()) {
out.insert(key.clone());
}
collect_table_keys(child, out, skip);
}
}
toml::Value::Array(items) => {
for item in items {
collect_table_keys(item, out, skip);
}
}
_ => {}
}
}
fn contains_word(haystack: &str, word: &str) -> bool {
let bytes = haystack.as_bytes();
let len = word.len();
let mut start = 0;
while let Some(rel) = haystack[start..].find(word) {
let i = start + rel;
let before_is_word = i > 0 && is_ident_byte(bytes[i - 1]);
let after = i + len;
let after_is_word = after < bytes.len() && is_ident_byte(bytes[after]);
if !before_is_word && !after_is_word {
return true;
}
start = i + 1;
}
false
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
}