use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use crate::cli::GenerateArgs;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct JsBindingsConfig {
pub module_name: Option<String>,
pub rename: HashMap<String, String>,
pub exclude: Vec<String>,
pub external_packages: HashMap<String, String>,
pub custom_types: HashMap<String, CustomTypeConfig>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct CustomTypeConfig {
pub type_name: Option<String>,
pub imports: Option<Vec<String>>,
pub lift: Option<String>,
pub lower: Option<String>,
}
impl CustomTypeConfig {
pub fn lift_expr(&self, builtin_expr: &str) -> String {
match &self.lift {
Some(template) => template.replace("{}", builtin_expr),
None => builtin_expr.to_string(),
}
}
pub fn lower_expr(&self, custom_expr: &str) -> String {
match &self.lower {
Some(template) => template.replace("{}", custom_expr),
None => custom_expr.to_string(),
}
}
}
#[derive(Debug, Deserialize)]
struct RootConfig {
#[serde(default)]
bindings: BindingsConfig,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct BindingsConfig {
js: JsBindingsConfig,
}
pub fn load(args: &GenerateArgs) -> Result<JsBindingsConfig> {
let Some(config_path) = resolve_config_path(args) else {
return Ok(JsBindingsConfig::default());
};
let src = fs::read_to_string(&config_path)
.with_context(|| format!("failed to read config file: {}", config_path.display()))?;
let parsed: RootConfig = toml::from_str(&src)
.with_context(|| format!("failed to parse config file: {}", config_path.display()))?;
Ok(parsed.bindings.js)
}
fn resolve_config_path(args: &GenerateArgs) -> Option<PathBuf> {
if let Some(path) = &args.config {
return Some(path.clone());
}
find_uniffi_toml(&args.source)
}
fn find_uniffi_toml(source: &Path) -> Option<PathBuf> {
source.canonicalize().ok().and_then(|path| {
let mut cursor = if path.is_dir() {
path
} else {
path.parent()?.to_path_buf()
};
loop {
let candidate = cursor.join("uniffi.toml");
if candidate.exists() {
return Some(candidate);
}
if !cursor.pop() {
return None;
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_when_no_file() {
let args = GenerateArgs {
source: std::path::PathBuf::from("/nonexistent/path.udl"),
out_dir: std::path::PathBuf::from("/tmp"),
config: None,
crate_name: None,
};
let cfg = load(&args).unwrap();
assert!(cfg.module_name.is_none());
assert!(cfg.rename.is_empty());
assert!(cfg.exclude.is_empty());
assert!(cfg.custom_types.is_empty());
}
#[test]
fn custom_type_lift_expr_with_template() {
let ct = CustomTypeConfig {
type_name: Some("URL".to_string()),
imports: None,
lift: Some("new URL({})".to_string()),
lower: None,
};
assert_eq!(ct.lift_expr("rawValue"), "new URL(rawValue)");
}
#[test]
fn custom_type_lift_expr_identity_when_unset() {
let ct = CustomTypeConfig::default();
assert_eq!(ct.lift_expr("rawValue"), "rawValue");
}
#[test]
fn custom_type_lower_expr_with_template() {
let ct = CustomTypeConfig {
type_name: None,
imports: None,
lift: None,
lower: Some("{}.toString()".to_string()),
};
assert_eq!(ct.lower_expr("myUrl"), "myUrl.toString()");
}
#[test]
fn custom_type_lower_expr_identity_when_unset() {
let ct = CustomTypeConfig::default();
assert_eq!(ct.lower_expr("myUrl"), "myUrl");
}
#[test]
fn parse_shared_multi_language_toml() {
let toml_str = r#"
[bindings.js]
module_name = "MyModule"
[bindings.python]
package_name = "my_package"
[bindings.swift]
module_name = "MySwiftModule"
[bindings.kotlin]
package_name = "com.example"
"#;
let parsed: RootConfig = toml::from_str(toml_str).unwrap();
assert_eq!(parsed.bindings.js.module_name.as_deref(), Some("MyModule"));
}
#[test]
fn parse_custom_types_config() {
let toml_str = r#"
[bindings.js.custom_types.Url]
type_name = "URL"
imports = ["{ URL } from 'url'"]
lift = "new URL({})"
lower = "{}.toString()"
"#;
let parsed: RootConfig = toml::from_str(toml_str).unwrap();
let cfg = parsed.bindings.js;
let url_cfg = cfg.custom_types.get("Url").unwrap();
assert_eq!(url_cfg.type_name.as_deref(), Some("URL"));
assert_eq!(url_cfg.lift.as_deref(), Some("new URL({})"));
assert_eq!(url_cfg.lower.as_deref(), Some("{}.toString()"));
assert_eq!(
url_cfg.imports.as_ref().unwrap(),
&vec!["{ URL } from 'url'".to_string()]
);
}
}