use std::cell::RefCell;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OptionsTypeSource {
PerCall,
LanguageDefault,
CallLevel,
Unset,
}
pub(crate) fn language_default_source(per_call: Option<&str>, file_level: Option<&str>) -> OptionsTypeSource {
match (per_call, file_level) {
(Some(_), _) => OptionsTypeSource::PerCall,
(None, Some(_)) => OptionsTypeSource::LanguageDefault,
(None, None) => OptionsTypeSource::Unset,
}
}
pub(crate) fn call_level_source(per_call: Option<&str>, call_level: Option<&str>) -> OptionsTypeSource {
match (per_call, call_level) {
(Some(_), _) => OptionsTypeSource::PerCall,
(None, Some(_)) => OptionsTypeSource::CallLevel,
(None, None) => OptionsTypeSource::Unset,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RefusalSite {
Argument,
Nested { via: String },
}
#[derive(Debug, Clone)]
pub(crate) struct FixtureKeyRefusal {
type_name: String,
key: String,
site: RefusalSite,
attribution: Option<Attribution>,
}
#[derive(Debug, Clone)]
struct Attribution {
language: String,
fixture_id: String,
call_key: Option<String>,
options_type_source: OptionsTypeSource,
}
thread_local! {
static LEDGER: RefCell<Vec<FixtureKeyRefusal>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn record(type_name: &str, key: &str, site: RefusalSite) {
LEDGER.with(|ledger| {
ledger.borrow_mut().push(FixtureKeyRefusal {
type_name: type_name.to_owned(),
key: key.to_owned(),
site,
attribution: None,
});
});
}
pub(crate) fn attribute(
language: &str,
fixture_id: &str,
call_key: Option<&str>,
options_type_source: OptionsTypeSource,
) {
LEDGER.with(|ledger| {
for refusal in ledger.borrow_mut().iter_mut() {
if refusal.attribution.is_none() {
refusal.attribution = Some(Attribution {
language: language.to_owned(),
fixture_id: fixture_id.to_owned(),
call_key: call_key.map(str::to_owned),
options_type_source,
});
}
}
});
}
pub(crate) fn take() -> Vec<FixtureKeyRefusal> {
LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()))
}
pub(crate) fn take_error(language: &str) -> Option<anyhow::Error> {
let refusals = take();
if refusals.is_empty() {
return None;
}
Some(anyhow::anyhow!(
"{language} e2e generator refused {} fixture value(s): a fixture key is not declared by the \
type the value is being built as.{}",
refusals.len(),
refusals
.iter()
.map(|refusal| format!("\n - {}", refusal.message(language)))
.collect::<String>()
))
}
impl FixtureKeyRefusal {
fn message(&self, fallback_language: &str) -> String {
let language = self
.attribution
.as_ref()
.map(|a| a.language.as_str())
.unwrap_or(fallback_language);
let type_name = &self.type_name;
let key = &self.key;
let mut message = match self.attribution.as_ref() {
Some(attribution) => format!(
"fixture `{}` (call {}, language `{language}`): key `{key}` is not declared as a field by `{type_name}`",
attribution.fixture_id,
call_reference(attribution.call_key.as_deref()),
),
None => format!("language `{language}`: key `{key}` is not declared as a field by `{type_name}`"),
};
if let RefusalSite::Nested { via } = &self.site {
message.push_str(&format!(" (reached through {via})"));
}
message.push('.');
if let Some(attribution) = self.attribution.as_ref() {
message.push(' ');
message.push_str(&self.lever(attribution, language));
}
message
}
fn lever(&self, attribution: &Attribution, language: &str) -> String {
let type_name = &self.type_name;
let per_call = override_table(attribution.call_key.as_deref(), language);
match attribution.options_type_source {
OptionsTypeSource::LanguageDefault if attribution.call_key.is_some() => format!(
"`{type_name}` is the options type this call resolves to for `{language}`, inherited from the \
file-level `[e2e.call.overrides.{language}].options_type` default because {per_call} declares \
no `options_type` of its own -- a file-level default applies to every call that does not \
override it. If this call takes a different type, declare `options_type` under {per_call} \
rather than changing the file-level default. If `{type_name}` is correct, remove or rename \
the fixture key, or add the field to `{type_name}`."
),
OptionsTypeSource::LanguageDefault | OptionsTypeSource::PerCall => format!(
"`{type_name}` is the options type this call resolves to for `{language}`, from \
{per_call}.options_type. Either that names the wrong type for this call, or the fixture \
key is wrong, or `{type_name}` is missing the field."
),
OptionsTypeSource::CallLevel => format!(
"`{type_name}` is the options type this call resolves to for `{language}`, from the \
call-level `options_type` on {call}. Either that names the wrong type, or the fixture key \
is wrong, or `{type_name}` is missing the field. A per-language `options_type` under \
{per_call} overrides it for `{language}` alone.",
call = call_reference(attribution.call_key.as_deref()),
),
OptionsTypeSource::Unset if attribution.call_key.is_some() => format!(
"No `options_type` is configured for `{language}` -- neither at {per_call} nor at the \
file-level `[e2e.call.overrides.{language}]` -- so `{type_name}` came from the argument's \
`element_type` or from the IR. Declare `options_type` under {per_call} to pin the type this \
call takes, or fix the fixture key or `{type_name}`."
),
OptionsTypeSource::Unset => format!(
"No `options_type` is configured for `{language}` at {per_call}, so `{type_name}` came from \
the argument's `element_type` or from the IR. Declare `options_type` under {per_call} to \
pin the type this call takes, or fix the fixture key or `{type_name}`."
),
}
}
}
fn call_reference(call_key: Option<&str>) -> String {
match call_key {
Some(key) => format!("`[e2e.calls.{key}]`"),
None => "`[e2e.call]` (the default call)".to_string(),
}
}
fn override_table(call_key: Option<&str>, language: &str) -> String {
match call_key {
Some(key) => format!("`[e2e.calls.{key}.overrides.{language}]`"),
None => format!("`[e2e.call.overrides.{language}]`"),
}
}
pub(crate) fn resolved_call_key<'a>(
e2e_config: &'a crate::e2e::config::E2eConfig,
call_config: &crate::core::config::e2e::CallConfig,
) -> Option<&'a str> {
e2e_config
.calls
.iter()
.find(|(_, candidate)| std::ptr::eq(*candidate, call_config))
.map(|(name, _)| name.as_str())
}
#[cfg(test)]
mod tests;