use std::{collections::HashMap, fs};
use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use serde::Deserialize;
use crate::{merge_toml, BindgenPaths, BindgenPathsLayer};
pub struct CrateRootsLayer {
roots: HashMap<String, Utf8PathBuf>,
}
impl BindgenPathsLayer for CrateRootsLayer {
fn get_crate_root(&self, crate_name: &str) -> Option<Utf8PathBuf> {
self.roots.get(crate_name).cloned()
}
}
#[derive(Deserialize, Default)]
struct GlobalConfigFile {
#[serde(rename = "crate-roots", default)]
crate_roots: HashMap<String, String>,
#[serde(default)]
defaults: toml::value::Table,
#[serde(default)]
crates: HashMap<String, toml::value::Table>,
}
#[derive(Default)]
pub struct GlobalConfig {
defaults: toml::value::Table,
crate_overrides: HashMap<String, toml::value::Table>,
}
impl GlobalConfig {
pub fn from_file(path: &Utf8Path) -> Result<(Self, Option<CrateRootsLayer>)> {
let contents =
fs::read_to_string(path).with_context(|| format!("read file: {:?}", path))?;
let raw: toml::value::Table =
toml::de::from_str(&contents).with_context(|| format!("parse toml: {:?}", path))?;
let has_global_config_keys = raw.contains_key("crate-roots")
|| raw.contains_key("defaults")
|| raw.contains_key("crates");
if !has_global_config_keys && !raw.is_empty() {
eprintln!(
"warning: {path} looks like an old-style --config override file. \
The --config flag now expects a global config file with [defaults], \
[crates.<name>], and/or [crate-roots] sections. \
Old-style flat config files are no longer supported."
);
return Ok((Self::default(), None));
}
let file: GlobalConfigFile =
toml::de::from_str(&contents).with_context(|| format!("parse toml: {:?}", path))?;
let crate_roots_layer = if file.crate_roots.is_empty() {
None
} else {
let base_dir = path.parent().unwrap_or(Utf8Path::new("."));
let roots = file
.crate_roots
.into_iter()
.map(|(name, rel_path)| (name, base_dir.join(rel_path)))
.collect();
Some(CrateRootsLayer { roots })
};
Ok((
Self {
defaults: file.defaults,
crate_overrides: file.crates,
},
crate_roots_layer,
))
}
pub fn get_config(&self, paths: &BindgenPaths, crate_name: &str) -> Result<toml::value::Table> {
let mut config = self.defaults.clone();
if let Some(config_path) = paths.get_config_path(crate_name) {
if config_path.exists() {
let contents = fs::read_to_string(&config_path)
.with_context(|| format!("read file: {:?}", config_path))?;
let crate_config: toml::value::Table = toml::de::from_str(&contents)
.with_context(|| format!("parse toml: {:?}", config_path))?;
merge_toml(&mut config, crate_config)?;
}
}
if let Some(overrides) = self.crate_overrides.get(crate_name) {
merge_toml(&mut config, overrides.clone())?;
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use camino::Utf8PathBuf;
use tempfile::TempDir;
struct StaticLayer {
crate_name: String,
crate_root: Utf8PathBuf,
}
impl BindgenPathsLayer for StaticLayer {
fn get_crate_root(&self, crate_name: &str) -> Option<Utf8PathBuf> {
if crate_name == self.crate_name {
Some(self.crate_root.clone())
} else {
None
}
}
}
fn write_file(dir: &TempDir, name: &str, contents: &str) -> Utf8PathBuf {
let path = Utf8PathBuf::from_path_buf(dir.path().join(name)).unwrap();
fs::write(&path, contents).unwrap();
path
}
fn paths_for(crate_name: &str, root: &Utf8PathBuf) -> BindgenPaths {
let mut paths = BindgenPaths::default();
paths.add_layer(StaticLayer {
crate_name: crate_name.to_string(),
crate_root: root.clone(),
});
paths
}
#[test]
fn test_defaults_fill_in_absent_keys() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[defaults]
key_only_in_defaults = "sentinel"
"#,
);
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
let paths = paths_for("my_crate", &crate_root);
let (config, _) = GlobalConfig::from_file(&global_path).unwrap();
let table = config.get_config(&paths, "my_crate").unwrap();
assert_eq!(table["key_only_in_defaults"].as_str().unwrap(), "sentinel");
}
#[test]
fn test_empty_global_config() {
let dir = TempDir::new().unwrap();
let paths = paths_for(
"my_crate",
&Utf8PathBuf::from_path_buf(dir.path().to_owned()).unwrap(),
);
let config = GlobalConfig::default();
let table = config.get_config(&paths, "my_crate").unwrap();
assert!(table.is_empty());
}
#[test]
fn test_defaults_only() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[defaults.bindings.swift]
ffi_module_name = "MyFFI"
"#,
);
let (config, roots_layer) = GlobalConfig::from_file(&global_path).unwrap();
assert!(roots_layer.is_none());
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
let paths = paths_for("my_crate", &crate_root);
let table = config.get_config(&paths, "my_crate").unwrap();
assert_eq!(
table["bindings"]["swift"]["ffi_module_name"]
.as_str()
.unwrap(),
"MyFFI"
);
}
#[test]
fn test_crate_config_overrides_defaults() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[defaults]
shared_key = "default_value"
default_only_key = "sentinel"
"#,
);
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
fs::write(
crate_root.join("uniffi.toml"),
r#"
shared_key = "crate_value"
"#,
)
.unwrap();
let (config, _) = GlobalConfig::from_file(&global_path).unwrap();
let paths = paths_for("my_crate", &crate_root);
let table = config.get_config(&paths, "my_crate").unwrap();
assert_eq!(table["shared_key"].as_str().unwrap(), "crate_value");
assert_eq!(table["default_only_key"].as_str().unwrap(), "sentinel");
}
#[test]
fn test_per_crate_overrides_win() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[defaults.bindings.swift]
ffi_module_name = "DefaultFFI"
[crates.my_crate.bindings.swift]
ffi_module_name = "OverrideFFI"
ffi_module_filename = "my_crate_ffi"
"#,
);
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
fs::write(
crate_root.join("uniffi.toml"),
r#"
[bindings.swift]
ffi_module_name = "CrateFFI"
"#,
)
.unwrap();
let (config, _) = GlobalConfig::from_file(&global_path).unwrap();
let paths = paths_for("my_crate", &crate_root);
let table = config.get_config(&paths, "my_crate").unwrap();
assert_eq!(
table["bindings"]["swift"]["ffi_module_name"]
.as_str()
.unwrap(),
"OverrideFFI"
);
assert_eq!(
table["bindings"]["swift"]["ffi_module_filename"]
.as_str()
.unwrap(),
"my_crate_ffi"
);
}
#[test]
fn test_deep_merge() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[defaults.bindings.swift]
ffi_module_name = "SharedFFI"
[crates.my_crate.bindings.kotlin]
package_name = "com.example"
"#,
);
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
fs::write(
crate_root.join("uniffi.toml"),
r#"
[bindings.swift]
ffi_module_filename = "my_crate_ffi"
"#,
)
.unwrap();
let (config, _) = GlobalConfig::from_file(&global_path).unwrap();
let paths = paths_for("my_crate", &crate_root);
let table = config.get_config(&paths, "my_crate").unwrap();
let bindings = &table["bindings"];
assert_eq!(
bindings["swift"]["ffi_module_name"].as_str().unwrap(),
"SharedFFI"
);
assert_eq!(
bindings["swift"]["ffi_module_filename"].as_str().unwrap(),
"my_crate_ffi"
);
assert_eq!(
bindings["kotlin"]["package_name"].as_str().unwrap(),
"com.example"
);
}
#[test]
fn test_crate_roots_layer() {
let dir = TempDir::new().unwrap();
let sub = dir.path().join("crates").join("my_crate");
fs::create_dir_all(&sub).unwrap();
let global_path = write_file(
&dir,
"global.toml",
r#"
[crate-roots]
my_crate = "crates/my_crate"
"#,
);
let (config, roots_layer) = GlobalConfig::from_file(&global_path).unwrap();
let roots_layer = roots_layer.expect("expected CrateRootsLayer");
let mut paths = BindgenPaths::default();
paths.add_layer(roots_layer);
let root = paths.get_crate_root("my_crate").unwrap();
assert!(root.is_absolute());
assert!(root.ends_with("crates/my_crate"));
let config_path = paths.get_config_path("my_crate").unwrap();
assert!(config_path.ends_with("uniffi.toml"));
let table = config.get_config(&paths, "my_crate").unwrap();
assert!(table.is_empty());
}
#[test]
fn test_old_style_config_returns_default() {
let dir = TempDir::new().unwrap();
let global_path = write_file(
&dir,
"old.toml",
r#"
[bindings.swift]
ffi_module_name = "OldFFI"
"#,
);
let (config, roots_layer) = GlobalConfig::from_file(&global_path).unwrap();
assert!(roots_layer.is_none());
let crate_root = Utf8PathBuf::from_path_buf(dir.path().join("my_crate")).unwrap();
fs::create_dir_all(&crate_root).unwrap();
let paths = paths_for("my_crate", &crate_root);
let table = config.get_config(&paths, "my_crate").unwrap();
assert!(table.is_empty());
}
}