use weaveffi_ir::ir::Api;
pub const DEFAULT_VERSION: &str = "0.1.0";
pub const DEFAULT_NAME: &str = "weaveffi";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPackage {
pub name: String,
pub version: String,
pub description: Option<String>,
pub license: Option<String>,
pub authors: Vec<String>,
pub homepage: Option<String>,
pub repository: Option<String>,
}
impl ResolvedPackage {
pub fn description_or_default(&self) -> String {
self.description
.clone()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| format!("{} bindings generated by WeaveFFI", self.name))
}
pub fn ident_name(&self) -> String {
sanitize_ident(&self.name)
}
pub fn module_name(&self) -> String {
pascal_ident(&self.name)
}
}
pub fn pascal_ident(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut start_word = true;
for ch in name.chars() {
if ch.is_ascii_alphanumeric() {
if start_word {
out.push(ch.to_ascii_uppercase());
} else {
out.push(ch);
}
start_word = false;
} else {
start_word = true;
}
}
if out.is_empty() {
pascal_ident(DEFAULT_NAME)
} else {
out
}
}
pub fn sanitize_ident(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut prev_us = false;
for ch in name.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
prev_us = false;
} else if !prev_us && !out.is_empty() {
out.push('_');
prev_us = true;
}
}
let trimmed = out.trim_end_matches('_');
if trimmed.is_empty() {
DEFAULT_NAME.to_string()
} else {
trimmed.to_string()
}
}
pub fn name_from_basename(basename: Option<&str>) -> String {
basename
.and_then(|b| b.rsplit(['/', '\\']).next())
.map(|b| b.split('.').next().unwrap_or(b))
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_NAME)
.to_string()
}
pub fn resolve(
api: &Api,
name_override: Option<&str>,
input_basename: Option<&str>,
) -> ResolvedPackage {
let pkg = api.package.as_ref();
let name = name_override
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.or_else(|| {
pkg.map(|p| p.name.trim().to_string())
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| name_from_basename(input_basename));
let version = pkg
.map(|p| p.version.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| DEFAULT_VERSION.to_string());
ResolvedPackage {
name,
version,
description: pkg
.and_then(|p| p.description.clone())
.filter(|s| !s.is_empty()),
license: pkg
.and_then(|p| p.license.clone())
.filter(|s| !s.is_empty()),
authors: pkg.map(|p| p.authors.clone()).unwrap_or_default(),
homepage: pkg
.and_then(|p| p.homepage.clone())
.filter(|s| !s.is_empty()),
repository: pkg
.and_then(|p| p.repository.clone())
.filter(|s| !s.is_empty()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use weaveffi_ir::ir::Package;
fn api_with(pkg: Option<Package>) -> Api {
Api {
version: "0.3.0".into(),
package: pkg,
modules: vec![],
generators: None,
}
}
fn full_pkg() -> Package {
Package {
name: "kvstore".into(),
version: "1.2.0".into(),
description: Some("KV store".into()),
license: Some("MIT".into()),
authors: vec!["Ada".into()],
homepage: Some("https://example.com".into()),
repository: Some("https://github.com/x/kvstore".into()),
}
}
#[test]
fn package_block_drives_identity() {
let api = api_with(Some(full_pkg()));
let r = resolve(&api, None, Some("ignored.yml"));
assert_eq!(r.name, "kvstore");
assert_eq!(r.version, "1.2.0");
assert_eq!(r.license.as_deref(), Some("MIT"));
assert_eq!(r.authors, vec!["Ada".to_string()]);
}
#[test]
fn target_override_beats_package_name() {
let api = api_with(Some(full_pkg()));
let r = resolve(&api, Some("kvstore_py"), Some("kvstore.yml"));
assert_eq!(r.name, "kvstore_py");
assert_eq!(r.version, "1.2.0");
}
#[test]
fn falls_back_to_file_stem_then_default() {
let api = api_with(None);
let r = resolve(&api, None, Some("path/to/contacts.yml"));
assert_eq!(r.name, "contacts");
assert_eq!(r.version, DEFAULT_VERSION);
let r2 = resolve(&api, None, None);
assert_eq!(r2.name, DEFAULT_NAME);
}
#[test]
fn description_default_is_generated() {
let api = api_with(None);
let r = resolve(&api, Some("widgets"), None);
assert_eq!(
r.description_or_default(),
"widgets bindings generated by WeaveFFI"
);
}
#[test]
fn ident_name_sanitizes() {
assert_eq!(sanitize_ident("my-kv.store"), "my_kv_store");
assert_eq!(sanitize_ident("Kvstore"), "kvstore");
assert_eq!(sanitize_ident("--"), DEFAULT_NAME);
}
#[test]
fn pascal_ident_upper_camels() {
assert_eq!(pascal_ident("my-kv.store"), "MyKvStore");
assert_eq!(pascal_ident("kvstore"), "Kvstore");
assert_eq!(pascal_ident("contacts"), "Contacts");
assert_eq!(pascal_ident("--"), "Weaveffi");
}
}