use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::extras::Language;
use super::new_config::ResolveError;
use super::output::{OutputConfig, OutputTemplate, validate_output_path};
use super::raw_crate::RawCrateConfig;
pub(crate) fn resolve_output_paths(
krate: &RawCrateConfig,
template: &OutputTemplate,
languages: &[Language],
multi_crate: bool,
) -> Result<HashMap<String, PathBuf>, ResolveError> {
let mut paths = HashMap::new();
for lang in languages {
let lang_str = lang.to_string();
let explicit = per_crate_explicit_output(&krate.output, lang);
let path = match explicit {
Some(value) => {
let path = PathBuf::from(value);
validate_output_path(&path).map_err(|detail| {
ResolveError::InvalidConfig(format!("crate `{}`: invalid output.{lang_str}: {detail}", krate.name))
})?;
path
}
None => template
.try_resolve(&krate.name, &lang_str, multi_crate)
.map_err(|detail| {
ResolveError::InvalidConfig(format!("crate `{}`: invalid output.{lang_str}: {detail}", krate.name))
})?,
};
paths.insert(lang_str, path);
}
Ok(paths)
}
pub(crate) fn default_package_root(lang: &str) -> String {
match lang {
"kotlin_android" => crate::backends::kotlin_android::DEFAULT_AAR_ROOT.to_string(),
other => format!("packages/{other}"),
}
}
pub(crate) fn per_crate_explicit_output(output: &OutputConfig, lang: &Language) -> Option<String> {
let path = match lang {
Language::Python => output.python.as_ref(),
Language::Node => output.node.as_ref(),
Language::Ruby => output.ruby.as_ref(),
Language::Php => output.php.as_ref(),
Language::Elixir => output.elixir.as_ref(),
Language::Wasm => output.wasm.as_ref(),
Language::Ffi => output.ffi.as_ref(),
Language::Go => output.go.as_ref(),
Language::Java => output.java.as_ref(),
Language::Kotlin => output.kotlin.as_ref(),
Language::KotlinAndroid => output.kotlin_android.as_ref(),
Language::Dart => output.dart.as_ref(),
Language::Swift => output.swift.as_ref(),
Language::Gleam => output.gleam.as_ref(),
Language::Csharp => output.csharp.as_ref(),
Language::R => output.r.as_ref(),
Language::Zig => output.zig.as_ref(),
Language::Rust | Language::C | Language::Jni => None,
};
path.map(|p| p.to_string_lossy().into_owned())
}
pub(crate) fn merge_map<V: Clone>(
workspace: &HashMap<String, V>,
per_crate: &HashMap<String, V>,
) -> HashMap<String, V> {
let mut merged = workspace.clone();
for (k, v) in per_crate {
merged.insert(k.clone(), v.clone());
}
merged
}
pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
config_path
.map(|p| p.to_string_lossy().replace("{name}", crate_name))
.unwrap_or_else(|| default.replace("{name}", crate_name))
}
pub(crate) fn default_binding_crate_root(crate_name: &str, lang: &str) -> Option<String> {
let suffix = match lang {
"python" => "py",
"node" => "node",
"php" => "php",
"ffi" => "ffi",
"wasm" => "wasm",
_ => return None,
};
Some(format!("crates/{crate_name}-{suffix}"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputLayout {
pub root: PathBuf,
pub src: PathBuf,
}
impl OutputLayout {
#[must_use]
pub fn from_output_dir(output_dir: &str) -> Self {
let path = PathBuf::from(output_dir);
if path.file_name().and_then(|name| name.to_str()) == Some("src") {
let root = path.parent().map(std::path::Path::to_path_buf).unwrap_or_default();
Self { root, src: path }
} else {
let src = path.join("src");
Self { root: path, src }
}
}
}
#[must_use]
pub fn resolve_output_layout(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> OutputLayout {
OutputLayout::from_output_dir(&resolve_output_dir(config_path, crate_name, default))
}
pub fn detect_serde_available(output_dir: &str) -> bool {
let src_path = std::path::Path::new(output_dir);
let mut dir = src_path;
loop {
let cargo_toml = dir.join("Cargo.toml");
if cargo_toml.exists() {
return cargo_toml_has_serde(&cargo_toml);
}
match dir.parent() {
Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
_ => break,
}
}
false
}
fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
let has_serde_json = content.contains("serde_json");
let has_serde_dep = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("serde ")
|| trimmed.starts_with("serde=")
|| trimmed.starts_with("serde.")
|| trimmed == "[dependencies.serde]"
});
has_serde_json && has_serde_dep
}
pub(crate) fn find_after_crates_prefix(path: &str) -> Option<&str> {
if let Some(pos) = path.find("/crates/") {
return Some(&path[pos + "/crates/".len()..]);
}
if let Some(stripped) = path.strip_prefix("crates/") {
return Some(stripped);
}
None
}
pub(crate) fn relative_slash_path(base: &Path, target: &Path) -> String {
let base_components: Vec<_> = base.components().collect();
let target_components: Vec<_> = target.components().collect();
let common = base_components
.iter()
.zip(target_components.iter())
.take_while(|(a, b)| a == b)
.count();
let mut parts: Vec<String> = base_components[common..].iter().map(|_| "..".to_string()).collect();
parts.extend(
target_components[common..]
.iter()
.map(|component| component.as_os_str().to_string_lossy().into_owned()),
);
if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
}
pub(crate) fn strip_trailing_components(path: &Path, suffix: &Path) -> Option<PathBuf> {
let path_components: Vec<_> = path.components().collect();
let suffix_components: Vec<_> = suffix.components().collect();
if path_components.len() < suffix_components.len() {
return None;
}
let split = path_components.len() - suffix_components.len();
let tail_matches = path_components[split..]
.iter()
.zip(suffix_components.iter())
.all(|(a, b)| a == b);
if !tail_matches {
return None;
}
if split == 0 {
return Some(PathBuf::from("."));
}
Some(path_components[..split].iter().collect())
}
pub(crate) fn java_project_root(output_dir: &str, package_path: &str) -> PathBuf {
let source_dir = if output_dir.ends_with(package_path) || output_dir.ends_with(&format!("{package_path}/")) {
PathBuf::from(output_dir)
} else {
PathBuf::from(output_dir).join(package_path)
};
let without_package = strip_trailing_components(&source_dir, Path::new(package_path)).unwrap_or(source_dir);
strip_trailing_components(&without_package, Path::new("src/main/java")).unwrap_or(without_package)
}
pub(crate) fn kotlin_project_root(output_dir: &str, package_path: &str, is_explicitly_configured: bool) -> PathBuf {
let output_path = PathBuf::from(output_dir);
if !is_explicitly_configured {
return output_path;
}
let full_suffix = Path::new("src/main/kotlin").join(package_path);
strip_trailing_components(&output_path, &full_suffix).unwrap_or(output_path)
}
#[cfg(test)]
mod tests;