use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::path::Path;
use crate::compat::suite_registration::word_boundary_contains;
use crate::compat::target_tree_index::TargetTreeIndex;
use crate::model::names::{ApplicationName, RelativePath, SchemaFeature};
use crate::model::verdict::Reason;
use crate::versions::version_cmp;
#[derive(Debug, Clone)]
pub struct IntroducedMapping {
pub schema_path: RelativePath,
pub conf_key: Option<String>,
pub mapping_target: Option<String>,
pub attr_names: Vec<String>,
}
pub const SCHEMA_FEATURE_FLOORS: &[(&str, &str)] = &[("alias", "3.7.0")];
pub fn check_schema_feature_unsupported(
mappings: &[IntroducedMapping],
pinned_version: Option<&str>,
) -> Vec<Reason> {
let Some(pinned) = pinned_version else {
return Vec::new();
};
let mut reasons = Vec::new();
for mapping in mappings {
for attr in &mapping.attr_names {
let Some((_, required)) = SCHEMA_FEATURE_FLOORS.iter().find(|(name, _)| name == attr)
else {
continue;
};
if version_cmp(pinned, required) != Ordering::Greater {
continue;
}
let Ok(feature) = SchemaFeature::new((*attr).clone()) else {
continue;
};
reasons.push(Reason::SchemaFeatureUnsupportedOnPin {
schema_path: mapping.schema_path.clone(),
conf_key: mapping.conf_key.clone(),
feature,
required_version: (*required).to_owned(),
pinned_version: pinned.to_owned(),
});
}
}
reasons
}
pub fn check_schema_key_reader_missing(
mappings: &[IntroducedMapping],
target: &TargetTreeIndex,
read_target: &dyn Fn(&RelativePath) -> Option<String>,
) -> Vec<Reason> {
let mut reasons = Vec::new();
let mut app_source_cache: BTreeMap<String, Vec<String>> = BTreeMap::new();
for mapping in mappings {
let Some(target_key) = &mapping.mapping_target else {
continue;
};
let mut segments = target_key.split('.');
let (Some(app), Some(env_key)) = (segments.next(), segments.next()) else {
continue;
};
let app_name = ApplicationName::new(app).ok();
let exists = app_has_any_path(target, app);
let searched_apps: Vec<ApplicationName> = app_name.clone().into_iter().collect();
let absent_apps: Vec<ApplicationName> = if exists {
Vec::new()
} else {
app_name.into_iter().collect()
};
let found = exists && {
let sources = app_source_cache
.entry(app.to_owned())
.or_insert_with(|| read_app_sources(target, app, read_target));
sources
.iter()
.any(|src| word_boundary_contains(src, env_key))
};
if found {
continue;
}
reasons.push(Reason::SchemaKeyReaderMissing {
schema_path: mapping.schema_path.clone(),
target_key: target_key.clone(),
searched_apps,
absent_apps,
});
}
reasons
}
fn app_has_any_path(target: &TargetTreeIndex, app: &str) -> bool {
let under_deps = format!("deps/{app}/");
let top_level = format!("{app}/");
target.present_paths().iter().any(|p| {
let s = p.to_string_lossy();
s.starts_with(under_deps.as_str()) || s.starts_with(top_level.as_str())
})
}
fn read_app_sources(
target: &TargetTreeIndex,
app: &str,
read_target: &dyn Fn(&RelativePath) -> Option<String>,
) -> Vec<String> {
let under_deps = format!("deps/{app}/src/");
let top_level = format!("{app}/src/");
target
.present_paths()
.iter()
.filter(|p| {
let s = p.to_string_lossy();
(s.starts_with(under_deps.as_str()) || s.starts_with(top_level.as_str()))
&& s.ends_with(".erl")
})
.filter_map(|p: &std::path::PathBuf| relative_path_of(p))
.filter_map(|p| read_target(&p))
.collect()
}
fn relative_path_of(p: &Path) -> Option<RelativePath> {
RelativePath::new(p.to_str()?.to_owned()).ok()
}