use super::diagnostic_log::{DiagnosticLog, unreported};
use super::validate::{Severity, ValidationError};
use crate::core::config::ResolvedCrateConfig;
use crate::core::config::e2e::{CallConfig, E2eConfig};
const CONFIG_FILE_LABEL: &str = "alef.toml";
pub fn enforce_call_module_overrides(
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
languages: &[String],
log: &DiagnosticLog,
) -> anyhow::Result<()> {
let diagnostics = validate_call_module_overrides(e2e_config, config, languages);
for diag in unreported(&diagnostics, log) {
tracing::warn!("{}: {}", diag.file, diag.message);
}
let errors: Vec<_> = diagnostics
.iter()
.filter(|diag| diag.severity == Severity::Error)
.collect();
if errors.is_empty() {
return Ok(());
}
anyhow::bail!(
"e2e call module validation failed: {}",
errors
.iter()
.map(|diag| format!("{}: {}", diag.file, diag.message))
.collect::<Vec<_>>()
.join("; ")
);
}
pub fn validate_call_module_overrides(
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
languages: &[String],
) -> Vec<ValidationError> {
let mut errors = Vec::new();
let java_active = languages.iter().any(|lang| lang == "java");
let go_active = languages.iter().any(|lang| lang == "go");
if !java_active && !go_active {
return errors;
}
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 {
if java_active {
check_java_module(config_key, call, &mut errors);
}
if go_active {
check_go_module(config_key, call, &e2e_config.call, config, &mut errors);
}
}
errors
}
fn check_java_module(config_key: &str, call: &CallConfig, errors: &mut Vec<ValidationError>) {
let Some(value) = call.overrides.get("java").and_then(|o| o.module.as_deref()) else {
return;
};
if !java_module_looks_like_a_class(value) {
return;
}
errors.push(ValidationError {
file: CONFIG_FILE_LABEL.to_string(),
message: format!(
"{config_key}.overrides.java.module = \"{value}\" looks like a Java class, not a package \
(its last segment starts with an uppercase letter) — the java e2e generator emits \
`import {value}.*;`, which will not compile against a class"
),
severity: Severity::Warning,
});
}
fn java_module_looks_like_a_class(value: &str) -> bool {
let trimmed = value.trim();
if trimmed.is_empty() {
return false;
}
let last_segment = trimmed.rsplit('.').next().unwrap_or(trimmed);
last_segment.chars().next().is_some_and(|c| c.is_ascii_uppercase())
}
fn effective_go_module(
call: &CallConfig,
base_call: &CallConfig,
config: &ResolvedCrateConfig,
) -> Option<(String, GoModuleSource)> {
if let Some(value) = call.overrides.get("go").and_then(|o| o.module.clone()) {
return Some((value, GoModuleSource::OwnOverride));
}
if let Some(value) = base_call.overrides.get("go").and_then(|o| o.module.clone()) {
return Some((value, GoModuleSource::BaseOverride));
}
if config.go.as_ref().and_then(|go| go.module.as_ref()).is_some() {
return None;
}
let base = call.module.trim();
if base.is_empty() {
None
} else {
Some((base.to_string(), GoModuleSource::OwnModuleField))
}
}
enum GoModuleSource {
OwnOverride,
BaseOverride,
OwnModuleField,
}
fn check_go_module(
config_key: &str,
call: &CallConfig,
base_call: &CallConfig,
config: &ResolvedCrateConfig,
errors: &mut Vec<ValidationError>,
) {
let Some((value, source)) = effective_go_module(call, base_call, config) else {
return;
};
if !go_module_is_a_bare_word(&value) {
return;
}
let location = match source {
GoModuleSource::OwnOverride => format!("{config_key}.overrides.go.module"),
GoModuleSource::BaseOverride => {
format!(
"[e2e.call].overrides.go.module (inherited by {config_key}, which declares no go override of its own)"
)
}
GoModuleSource::OwnModuleField => format!("{config_key}.module"),
};
errors.push(ValidationError {
file: CONFIG_FILE_LABEL.to_string(),
message: format!(
"{location} = \"{value}\" is a bare word, not a resolvable Go import path (no \".\" \
or \"/\") — the go e2e generator imports this value verbatim as the crate's own package \
path; set `[go] module = \"github.com/<org>/<repo>\"` or `overrides.go.module` to a real \
import path"
),
severity: Severity::Error,
});
}
fn go_module_is_a_bare_word(value: &str) -> bool {
let trimmed = value.trim();
!trimmed.is_empty() && !trimmed.contains('.') && !trimmed.contains('/')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::e2e::CallOverride;
fn make_config(crate_name: &str) -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: crate_name.to_string(),
..ResolvedCrateConfig::default()
}
}
fn resolved_one(toml: &str) -> ResolvedCrateConfig {
use crate::core::config::new_config::NewAlefConfig;
let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
cfg.resolve().unwrap().remove(0)
}
fn call_with_java_override(module: &str) -> CallConfig {
let mut call = CallConfig::default();
call.overrides.insert(
"java".to_string(),
CallOverride {
module: Some(module.to_string()),
..CallOverride::default()
},
);
call
}
fn call_with_go_override(module: &str) -> CallConfig {
let mut call = CallConfig::default();
call.overrides.insert(
"go".to_string(),
CallOverride {
module: Some(module.to_string()),
..CallOverride::default()
},
);
call
}
#[test]
fn a_java_package_override_passes() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig {
call: call_with_java_override("io.example.widget"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_java_override_naming_a_class_fails() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig {
call: call_with_java_override("io.sample.Sample"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["java".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert_eq!(errors[0].severity, Severity::Warning);
assert!(
errors[0]
.message
.contains("[e2e.call].overrides.java.module = \"io.sample.Sample\""),
"got: {}",
errors[0].message
);
assert!(
errors[0].message.contains("looks like a Java class, not a package"),
"got: {}",
errors[0].message
);
}
#[test]
fn an_absent_java_override_is_a_no_op_regardless_of_the_base_module() {
let config = make_config("sample_crate");
let mut e2e_config = E2eConfig::default();
e2e_config.call.module = "TotallyWrongBaseValue".to_string();
let errors = validate_call_module_overrides(&e2e_config, &config, &["java".to_string()]);
assert_eq!(errors.len(), 0, "java never reads the base `module` field: {errors:?}");
}
#[test]
fn a_single_segment_lowercase_java_module_passes() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig {
call: call_with_java_override("widget"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["java".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn java_is_not_checked_when_java_is_not_an_active_language() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig {
call: call_with_java_override("io.sample.Sample"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["python".to_string()]);
assert_eq!(errors.len(), 0, "java is not active: {errors:?}");
}
#[test]
fn a_resolvable_go_module_override_passes() {
let config = make_config("sample-widget-rs");
let e2e_config = E2eConfig {
call: call_with_go_override("github.com/example/sample-widget"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_bare_word_go_module_override_fails() {
let config = make_config("sample-widget-rs");
let e2e_config = E2eConfig {
call: call_with_go_override("widget"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert_eq!(errors[0].severity, Severity::Error);
assert!(
errors[0]
.message
.contains("[e2e.call].overrides.go.module = \"widget\""),
"got: {}",
errors[0].message
);
assert!(
errors[0].message.contains("not a resolvable Go import path"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_bare_word_base_module_fails_when_go_module_config_is_absent() {
let config = make_config("sample-widget-rs");
let mut e2e_config = E2eConfig::default();
e2e_config.call.module = "widget".to_string();
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(errors.len(), 1, "expected exactly one error, got: {errors:?}");
assert!(
errors[0].message.contains("[e2e.call].module = \"widget\""),
"got: {}",
errors[0].message
);
}
fn config_with_go_module(crate_name: &str, go_module: &str) -> ResolvedCrateConfig {
resolved_one(&format!(
r#"
[workspace]
languages = ["go"]
[[crates]]
name = "{crate_name}"
sources = ["src/lib.rs"]
[crates.go]
module = "{go_module}"
"#
))
}
#[test]
fn a_bare_word_base_module_is_a_no_op_when_go_module_config_is_set() {
let config = config_with_go_module("sample-widget-rs", "github.com/example/sample-widget");
let mut e2e_config = E2eConfig::default();
e2e_config.call.module = "widget".to_string();
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(
errors.len(),
0,
"`[go] module` wins over the base field, so the base value is inert: {errors:?}"
);
}
#[test]
fn a_bare_word_go_override_still_fails_even_when_go_module_config_is_set() {
let config = config_with_go_module("sample-widget-rs", "github.com/example/sample-widget");
let e2e_config = E2eConfig {
call: call_with_go_override("widget"),
..E2eConfig::default()
};
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(
errors.len(),
1,
"an explicit override still wins over `[go] module` and is still checked: {errors:?}"
);
}
#[test]
fn go_is_not_checked_when_go_is_not_an_active_language() {
let config = make_config("sample-widget-rs");
let mut e2e_config = E2eConfig::default();
e2e_config.call.module = "widget".to_string();
let errors = validate_call_module_overrides(&e2e_config, &config, &["python".to_string()]);
assert_eq!(errors.len(), 0, "go is not active: {errors:?}");
}
#[test]
fn a_named_call_override_names_its_own_config_key() {
let config = make_config("sample_crate");
let mut e2e_config = E2eConfig::default();
e2e_config
.calls
.insert("summarize".to_string(), call_with_java_override("io.sample.Sample"));
let errors = validate_call_module_overrides(&e2e_config, &config, &["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.module"),
"got: {}",
errors[0].message
);
}
#[test]
fn a_named_call_with_no_go_override_inherits_the_bad_base_override() {
let config = make_config("sample-widget-rs");
let mut e2e_config = E2eConfig::default();
e2e_config.call.overrides.insert(
"go".to_string(),
CallOverride {
module: Some("widget".to_string()),
..CallOverride::default()
},
);
e2e_config.calls.insert("summarize".to_string(), CallConfig::default());
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(
errors.len(),
2,
"expected the base call and its inheritor both flagged: {errors:?}"
);
let summarize_error = errors
.iter()
.find(|e| e.message.contains("inherited by [e2e.calls.summarize]"))
.unwrap_or_else(|| panic!("no error named the inherited base override: {errors:?}"));
assert!(
summarize_error
.message
.starts_with("[e2e.call].overrides.go.module (inherited by [e2e.calls.summarize]"),
"got: {}",
summarize_error.message
);
assert!(
summarize_error.message.contains("= \"widget\""),
"got: {}",
summarize_error.message
);
}
#[test]
fn a_named_call_inheriting_a_valid_base_go_override_passes() {
let config = make_config("sample-widget-rs");
let mut e2e_config = E2eConfig::default();
e2e_config.call.overrides.insert(
"go".to_string(),
CallOverride {
module: Some("github.com/example/sample-widget".to_string()),
..CallOverride::default()
},
);
e2e_config.calls.insert("summarize".to_string(), CallConfig::default());
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn a_named_calls_own_go_override_wins_over_the_base_calls() {
let config = make_config("sample-widget-rs");
let mut e2e_config = E2eConfig::default();
e2e_config.call.overrides.insert(
"go".to_string(),
CallOverride {
module: Some("widget".to_string()),
..CallOverride::default()
},
);
e2e_config.calls.insert(
"summarize".to_string(),
call_with_go_override("github.com/example/sample-widget"),
);
let errors = validate_call_module_overrides(&e2e_config, &config, &["go".to_string()]);
assert_eq!(
errors.len(),
1,
"only the base call's own bad override should be flagged: {errors:?}"
);
assert!(
errors[0].message.starts_with("[e2e.call].overrides.go.module"),
"got: {}",
errors[0].message
);
}
#[test]
fn an_empty_module_value_is_a_no_op() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig::default();
let errors = validate_call_module_overrides(&e2e_config, &config, &["java".to_string(), "go".to_string()]);
assert_eq!(errors.len(), 0, "expected no errors, got: {errors:?}");
}
#[test]
fn enforce_bails_on_a_bare_word_go_module() {
let config = make_config("sample-widget-rs");
let e2e_config = E2eConfig {
call: call_with_go_override("widget"),
..E2eConfig::default()
};
let result = enforce_call_module_overrides(&e2e_config, &config, &["go".to_string()], &DiagnosticLog::new());
let err = result.expect_err("a bare-word go module must abort generation");
assert!(
err.to_string().contains("not a resolvable Go import path"),
"got: {err}"
);
}
#[test]
fn enforce_does_not_bail_on_a_real_go_import_path() {
let config = make_config("sample-widget-rs");
let e2e_config = E2eConfig {
call: call_with_go_override("github.com/example/sample-widget"),
..E2eConfig::default()
};
let result = enforce_call_module_overrides(&e2e_config, &config, &["go".to_string()], &DiagnosticLog::new());
assert!(result.is_ok(), "expected Ok(()), got: {result:?}");
}
#[test]
fn enforce_does_not_bail_on_a_flagged_java_module() {
let config = make_config("sample_crate");
let e2e_config = E2eConfig {
call: call_with_java_override("io.sample.Sample"),
..E2eConfig::default()
};
let result = enforce_call_module_overrides(&e2e_config, &config, &["java".to_string()], &DiagnosticLog::new());
assert!(
result.is_ok(),
"a flagged java module is still only a warning: {result:?}"
);
}
}