#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ImportEntry {
pub module: String,
pub name: String,
pub alias: Option<String>,
pub is_type_only: bool,
pub is_side_effect: bool,
pub is_wildcard: bool,
}
impl ImportEntry {
pub fn resolved_name(&self) -> &str {
self.alias.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Clone, Default)]
pub struct ImportGroup {
pub entries: Vec<ImportEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ImportRef {
pub module: String,
pub name: String,
pub is_type_only: bool,
pub alias: Option<String>,
}
impl ImportGroup {
pub fn new() -> Self {
Self::default()
}
pub fn resolve(refs: &[ImportRef]) -> Self {
let mut entries = Vec::new();
let mut claimed: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut seen: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
for import_ref in refs {
let key = (import_ref.module.clone(), import_ref.name.clone());
if seen.contains(&key) {
continue;
}
seen.insert(key);
let alias = if let Some(preferred) = &import_ref.alias {
claimed.insert(preferred.clone(), import_ref.module.clone());
claimed
.entry(import_ref.name.clone())
.or_insert_with(|| import_ref.module.clone());
Some(preferred.clone())
} else if let Some(existing_module) = claimed.get(&import_ref.name) {
if *existing_module == import_ref.module {
None
} else {
let module_prefix = module_to_prefix(&import_ref.module);
let auto_alias = format!("{}{}", module_prefix, import_ref.name);
claimed.insert(auto_alias.clone(), import_ref.module.clone());
Some(auto_alias)
}
} else {
claimed.insert(import_ref.name.clone(), import_ref.module.clone());
None
};
entries.push(ImportEntry {
module: import_ref.module.clone(),
name: import_ref.name.clone(),
alias,
is_type_only: import_ref.is_type_only,
is_side_effect: false,
is_wildcard: false,
});
}
Self { entries }
}
pub fn resolve_with_explicit(refs: &[ImportRef], explicit: Vec<ImportEntry>) -> Self {
let mut entries = Vec::new();
let mut claimed: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut seen: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
for entry in explicit {
if entry.is_side_effect || entry.is_wildcard {
entries.push(entry);
continue;
}
let key = (entry.module.clone(), entry.name.clone());
seen.insert(key);
let resolved = entry.alias.as_deref().unwrap_or(&entry.name);
claimed.insert(resolved.to_string(), entry.module.clone());
if entry.alias.is_some() {
claimed.insert(entry.name.clone(), entry.module.clone());
}
entries.push(entry);
}
for import_ref in refs {
let key = (import_ref.module.clone(), import_ref.name.clone());
if seen.contains(&key) {
continue;
}
seen.insert(key);
let alias = if let Some(preferred) = &import_ref.alias {
claimed.insert(preferred.clone(), import_ref.module.clone());
claimed
.entry(import_ref.name.clone())
.or_insert_with(|| import_ref.module.clone());
Some(preferred.clone())
} else if let Some(existing_module) = claimed.get(&import_ref.name) {
if *existing_module == import_ref.module {
None
} else {
let module_prefix = module_to_prefix(&import_ref.module);
let auto_alias = format!("{}{}", module_prefix, import_ref.name);
claimed.insert(auto_alias.clone(), import_ref.module.clone());
Some(auto_alias)
}
} else {
claimed.insert(import_ref.name.clone(), import_ref.module.clone());
None
};
entries.push(ImportEntry {
module: import_ref.module.clone(),
name: import_ref.name.clone(),
alias,
is_type_only: import_ref.is_type_only,
is_side_effect: false,
is_wildcard: false,
});
}
Self { entries }
}
pub fn resolved_name(&self, module: &str, name: &str) -> Option<&str> {
self.entries
.iter()
.find(|e| e.module == module && e.name == name)
.map(|e| e.resolved_name())
}
}
fn module_to_prefix(module: &str) -> String {
let last_segment = module
.rsplit(['/', ':', '.'])
.find(|s| !s.is_empty())
.unwrap_or(module);
let mut chars = last_segment.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let upper: String = first.to_uppercase().collect();
format!("{upper}{}", chars.as_str())
}
}
}
pub fn validate_module_path(path: &str) -> Result<(), crate::error::SigilStitchError> {
if path.is_empty() {
return Err(crate::error::SigilStitchError::InvalidModulePath {
message: "Module path cannot be empty".to_string(),
});
}
for ch in path.chars() {
match ch {
'\n' | '\r' | '\'' | '"' | '`' | ';' | '{' | '}' | '(' | ')' => {
return Err(crate::error::SigilStitchError::InvalidModulePath {
message: format!("Module path contains invalid character: {:?}", ch),
});
}
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dedup_same_module() {
let refs = vec![
ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
];
let group = ImportGroup::resolve(&refs);
assert_eq!(group.entries.len(), 1);
assert_eq!(group.entries[0].name, "User");
assert!(group.entries[0].alias.is_none());
}
#[test]
fn test_conflict_different_modules() {
let refs = vec![
ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
ImportRef {
module: "./other".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
];
let group = ImportGroup::resolve(&refs);
assert_eq!(group.entries.len(), 2);
assert!(group.entries[0].alias.is_none());
assert_eq!(group.entries[0].name, "User");
assert_eq!(group.entries[1].alias.as_deref(), Some("OtherUser"));
}
#[test]
fn test_resolved_name_lookup() {
let refs = vec![
ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
ImportRef {
module: "./other".into(),
name: "User".into(),
is_type_only: true,
alias: None,
},
];
let group = ImportGroup::resolve(&refs);
assert_eq!(group.resolved_name("./models", "User"), Some("User"));
assert_eq!(group.resolved_name("./other", "User"), Some("OtherUser"));
}
#[test]
fn test_module_to_prefix() {
assert_eq!(module_to_prefix("./models"), "Models");
assert_eq!(module_to_prefix("std::collections"), "Collections");
assert_eq!(module_to_prefix("github.com/foo/bar"), "Bar");
assert_eq!(module_to_prefix("net/http"), "Http");
}
#[test]
fn test_validate_module_path() {
assert!(validate_module_path("./models").is_ok());
assert!(validate_module_path("std::collections").is_ok());
assert!(validate_module_path("").is_err());
assert!(validate_module_path("foo\nbar").is_err());
assert!(validate_module_path("foo'bar").is_err());
}
#[test]
fn test_preferred_alias_from_ref() {
let refs = vec![ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: false,
alias: Some("MyUser".into()),
}];
let group = ImportGroup::resolve(&refs);
assert_eq!(group.entries.len(), 1);
assert_eq!(group.entries[0].alias.as_deref(), Some("MyUser"));
assert_eq!(group.resolved_name("./models", "User"), Some("MyUser"));
}
#[test]
fn test_preferred_alias_with_conflict() {
let refs = vec![
ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: false,
alias: Some("ModelUser".into()),
},
ImportRef {
module: "./other".into(),
name: "User".into(),
is_type_only: false,
alias: None,
},
];
let group = ImportGroup::resolve(&refs);
assert_eq!(group.entries.len(), 2);
assert_eq!(group.entries[0].alias.as_deref(), Some("ModelUser"));
assert!(group.entries[1].alias.is_some());
}
#[test]
fn test_preferred_alias_in_resolve_with_explicit() {
let refs = vec![ImportRef {
module: "./models".into(),
name: "User".into(),
is_type_only: false,
alias: Some("MyUser".into()),
}];
let group = ImportGroup::resolve_with_explicit(&refs, vec![]);
assert_eq!(group.entries.len(), 1);
assert_eq!(group.entries[0].alias.as_deref(), Some("MyUser"));
assert_eq!(group.resolved_name("./models", "User"), Some("MyUser"));
}
}