use std::collections::HashSet;
fn cap_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn lower_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
pub fn objc_selector_for_swift_method(
base_name: &str,
external_labels: &[Option<String>],
explicit_objc_name: Option<&str>,
) -> Option<String> {
if base_name.is_empty() {
return None;
}
if let Some(name) = explicit_objc_name.filter(|name| !name.is_empty()) {
return Some(name.to_string());
}
if external_labels.is_empty() {
return Some(base_name.to_string());
}
let first = external_labels.first().and_then(|label| label.as_deref());
let first_keyword = match first {
None | Some("_") | Some("") => format!("{base_name}:"),
Some(label) => format!("{base_name}With{}:", cap_first(label)),
};
let rest = external_labels
.iter()
.skip(1)
.map(|label| format!("{}:", label.as_deref().unwrap_or("")))
.collect::<String>();
Some(first_keyword + &rest)
}
pub fn objc_selector_for_swift_init(
external_labels: &[Option<String>],
internal_names: &[String],
explicit_objc_name: Option<&str>,
) -> Option<String> {
if let Some(name) = explicit_objc_name.filter(|name| !name.is_empty()) {
return Some(name.to_string());
}
if external_labels.is_empty() {
return Some("init".to_string());
}
let first_ext = external_labels.first().and_then(|label| label.as_deref());
let first_int = internal_names.first().map(String::as_str);
let first_label = match first_ext {
None | Some("_") | Some("") => first_int,
Some(label) => Some(label),
}?;
if first_label.is_empty() {
return None;
}
let rest = external_labels
.iter()
.skip(1)
.enumerate()
.map(|(idx, label)| {
let internal = internal_names
.get(idx + 1)
.map(String::as_str)
.unwrap_or("");
let name = match label.as_deref() {
Some(label) if label != "_" && !label.is_empty() => label,
_ => internal,
};
format!("{name}:")
})
.collect::<String>();
Some(format!("initWith{}:{rest}", cap_first(first_label)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjcAccessors {
pub getter: String,
pub setter: String,
}
pub fn objc_accessors_for_swift_property(
swift_name: &str,
explicit_objc_name: Option<&str>,
) -> Option<ObjcAccessors> {
if swift_name.is_empty() {
return None;
}
let getter = explicit_objc_name
.filter(|name| !name.is_empty())
.unwrap_or(swift_name)
.to_string();
Some(ObjcAccessors {
setter: format!("set{}:", cap_first(&getter)),
getter,
})
}
pub fn swift_base_names_for_objc_selector(selector: &str) -> Vec<String> {
if selector.is_empty() {
return Vec::new();
}
let trimmed = selector.trim_end_matches(':');
let keywords = trimmed.split(':').collect::<Vec<_>>();
let Some(first_keyword) = keywords.first().copied().filter(|s| !s.is_empty()) else {
return Vec::new();
};
let mut candidates = HashSet::new();
candidates.insert(first_keyword.to_string());
if first_keyword.starts_with("initWith") {
candidates.insert("init".to_string());
}
for prep in [
"With", "For", "By", "In", "On", "At", "From", "To", "Of", "As",
] {
if let Some(idx) = first_keyword.find(prep)
&& idx > 0
&& first_keyword[..idx]
.chars()
.next()
.map(|ch| ch.is_ascii_lowercase())
.unwrap_or(false)
&& first_keyword[idx + prep.len()..]
.chars()
.next()
.map(|ch| ch.is_ascii_uppercase())
.unwrap_or(false)
{
candidates.insert(first_keyword[..idx].to_string());
}
}
if keywords.len() == 1
&& selector.ends_with(':')
&& first_keyword.starts_with("set")
&& first_keyword
.chars()
.nth(3)
.map(|ch| ch.is_ascii_uppercase())
.unwrap_or(false)
{
let prop_name = lower_first(&first_keyword[3..]);
if !prop_name.is_empty() {
candidates.insert(prop_name);
}
}
let mut out = candidates.into_iter().collect::<Vec<_>>();
out.sort();
out
}
pub fn detect_explicit_objc_name(source_slice: &str) -> Option<String> {
let marker = source_slice.find("@objc")?;
let rest = &source_slice[marker + "@objc".len()..];
let open = rest.find('(')?;
let after_open = &rest[open + 1..];
let close = after_open.find(')')?;
let name = after_open[..close].trim();
if name.is_empty() || name.chars().any(char::is_whitespace) {
None
} else {
Some(name.to_string())
}
}
pub fn is_objc_exposed(source_slice: &str) -> bool {
!source_slice.contains("@nonobjc") && source_slice.contains("@objc")
}