use super::validate::{Severity, ValidationError};
use crate::codegen::naming::{self, PublicIdentifierKind};
use crate::core::config::e2e::{CallConfig, E2eConfig};
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::{EnumDef, TypeDef};
const CONFIG_FILE_LABEL: &str = "alef.toml";
const CLASS_CONSUMING_LANGUAGES: &[(&str, Language)] = &[
("java", Language::Java),
("kotlin", Language::Kotlin),
("kotlin_android", Language::KotlinAndroid),
("php", Language::Php),
("ruby", Language::Ruby),
("dart", Language::Dart),
];
fn naming_language_for(lang: &str) -> Option<Language> {
CLASS_CONSUMING_LANGUAGES
.iter()
.find(|(name, _)| *name == lang)
.map(|(_, language)| *language)
}
pub fn validate_call_class_overrides(
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
languages: &[String],
) -> Vec<ValidationError> {
if type_defs.is_empty() && enums.is_empty() {
return Vec::new();
}
let mut errors = Vec::new();
let mut sources: Vec<(String, &CallConfig)> = vec![("[e2e.call]".to_string(), &e2e_config.call)];
let mut named_calls: Vec<(&String, &CallConfig)> = e2e_config.calls.iter().collect();
named_calls.sort_by_key(|(name, _)| (*name).clone());
for (name, call) in named_calls {
sources.push((format!("[e2e.calls.{name}]"), call));
}
for (config_key, call) in sources {
let mut override_langs: Vec<&String> = call.overrides.keys().collect();
override_langs.sort();
for lang in override_langs {
if !languages.iter().any(|resolved| resolved == lang) {
continue;
}
let Some(naming_lang) = naming_language_for(lang) else {
continue;
};
let Some(class_value) = call.overrides.get(lang).and_then(|o| o.class.as_ref()) else {
continue;
};
check_class_override(
&config_key,
lang,
naming_lang,
class_value,
config,
type_defs,
enums,
&mut errors,
);
}
}
errors
}
#[allow(clippy::too_many_arguments)]
fn check_class_override(
config_key: &str,
lang: &str,
naming_lang: Language,
class_value: &str,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
errors: &mut Vec<ValidationError>,
) {
let candidates = emitted_class_names(lang, naming_lang, config, type_defs, enums);
let simple_name = simple_class_name(class_value);
if candidates.iter().any(|candidate| candidate == simple_name) {
return;
}
let suggestion = closest_candidates(simple_name, &candidates);
let suggestion_text = if suggestion.is_empty() {
String::new()
} else {
format!(" (did you mean {}?)", suggestion.join(" or "))
};
errors.push(ValidationError {
file: CONFIG_FILE_LABEL.to_string(),
message: format!(
"{config_key}.overrides.{lang}.class = \"{class_value}\" does not match any class the {lang} backend \
emits for crate '{}'{suggestion_text}",
config.name
),
severity: Severity::Error,
});
}
fn emitted_class_names(
lang: &str,
naming_lang: Language,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
) -> Vec<String> {
let mut names = vec![naming::to_class_name(&config.name)];
for type_def in type_defs {
names.push(naming::public_host_identifier(
naming_lang,
PublicIdentifierKind::Type,
&type_def.name,
));
}
for enum_def in enums {
names.push(naming::public_host_identifier(
naming_lang,
PublicIdentifierKind::Type,
&enum_def.name,
));
}
for bridge in &config.trait_bridges {
if bridge.is_active_for(lang) {
names.push(format!("{}Bridge", bridge.trait_name));
}
}
names.sort();
names.dedup();
names
}
fn simple_class_name(raw: &str) -> &str {
let after_dot = raw.rsplit('.').next().unwrap_or(raw);
let after_namespace = after_dot.rsplit("::").next().unwrap_or(after_dot);
after_namespace.rsplit('\\').next().unwrap_or(after_namespace)
}
fn closest_candidates(value: &str, candidates: &[String]) -> Vec<String> {
let mut scored: Vec<(usize, &String)> = candidates
.iter()
.map(|candidate| (levenshtein_distance(value, candidate), candidate))
.collect();
scored.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(b.1)));
scored
.into_iter()
.take(2)
.map(|(_, candidate)| format!("\"{candidate}\""))
.collect()
}
fn levenshtein_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, &character_a) in a.iter().enumerate() {
let mut previous_diagonal = row[0];
row[0] = i + 1;
for (j, &character_b) in b.iter().enumerate() {
let above = row[j + 1];
let cost = usize::from(character_a != character_b);
let substitution = previous_diagonal + cost;
let insertion = row[j] + 1;
let deletion = above + 1;
previous_diagonal = above;
row[j + 1] = substitution.min(insertion).min(deletion);
}
}
row[b.len()]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::e2e::CallOverride;
use crate::core::config::trait_bridge::TraitBridgeConfig;
use crate::core::ir::FieldDef;
fn make_config(crate_name: &str) -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: crate_name.to_string(),
..ResolvedCrateConfig::default()
}
}
fn make_type(name: &str) -> TypeDef {
TypeDef {
name: name.to_string(),
fields: vec![FieldDef::default()],
..TypeDef::default()
}
}
fn make_e2e_config(class: &str, lang: &str) -> E2eConfig {
let mut call = CallConfig::default();
let override_config = CallOverride {
class: Some(class.to_string()),
..CallOverride::default()
};
call.overrides.insert(lang.to_string(), override_config);
E2eConfig {
call,
..E2eConfig::default()
}
}
#[test]
fn a_class_override_matching_an_emitted_struct_passes() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let e2e_config = make_e2e_config("DocumentApi", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_misspelled_class_override_fails_with_the_offending_language_and_value() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let e2e_config = make_e2e_config("DocumentAppi", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert_eq!(errors[0].severity, Severity::Error);
assert_eq!(errors[0].file, "alef.toml");
assert!(
errors[0]
.message
.contains("[e2e.call].overrides.java.class = \"DocumentAppi\""),
"got: {}",
errors[0].message
);
assert!(
errors[0]
.message
.contains("java backend emits for crate 'sample_crate'"),
"got: {}",
errors[0].message
);
}
#[test]
fn the_suggestion_names_the_closest_candidate() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let e2e_config = make_e2e_config("DocumentAppi", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("did you mean \"DocumentApi\""),
"got: {}",
errors[0].message
);
}
#[test]
fn an_absent_override_is_a_no_op() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let e2e_config = E2eConfig::default();
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_fully_qualified_class_override_is_compared_on_its_simple_name() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("SampleService")];
let e2e_config = make_e2e_config("dev.example.SampleService", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_crate_facade_override_passes_with_no_ir_types() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("Placeholder")];
let e2e_config = make_e2e_config("SampleCrate", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn an_active_trait_bridge_class_override_passes() {
let mut config = make_config("sample_crate");
config.trait_bridges = vec![TraitBridgeConfig {
trait_name: "Validator".to_string(),
..TraitBridgeConfig::default()
}];
let type_defs = vec![make_type("Placeholder")];
let e2e_config = make_e2e_config("ValidatorBridge", "kotlin_android");
let errors =
validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["kotlin_android".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn an_empty_ir_skips_validation_entirely() {
let config = make_config("sample_crate");
let e2e_config = make_e2e_config("TotallyWrongClassName", "java");
let errors = validate_call_class_overrides(&e2e_config, &config, &[], &[], &["java".to_string()]);
assert_eq!(errors.len(), 0, "an absent IR must license no claim: {errors:?}");
}
#[test]
fn a_language_that_does_not_consume_class_is_never_checked() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let e2e_config = make_e2e_config("TotallyWrongClassName", "python");
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["python".to_string()]);
assert_eq!(errors.len(), 0, "python does not consume `class`: {errors:?}");
}
#[test]
fn a_named_call_override_names_its_own_config_key() {
let config = make_config("sample_crate");
let type_defs = vec![make_type("DocumentApi")];
let mut e2e_config = E2eConfig::default();
let mut call = CallConfig::default();
let override_config = CallOverride {
class: Some("NotARealClass".to_string()),
..CallOverride::default()
};
call.overrides.insert("java".to_string(), override_config);
e2e_config.calls.insert("summarize".to_string(), call);
let errors = validate_call_class_overrides(&e2e_config, &config, &type_defs, &[], &["java".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0]
.message
.starts_with("[e2e.calls.summarize].overrides.java.class"),
"got: {}",
errors[0].message
);
}
#[test]
fn levenshtein_distance_matches_known_values() {
assert_eq!(levenshtein_distance("DocumentApi", "DocumentApi"), 0);
assert_eq!(levenshtein_distance("DocumentAppi", "DocumentApi"), 1);
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
}
}