backhopper_core/compat/
cuttlefish_features.rs1use std::cmp::Ordering;
12use std::collections::BTreeMap;
13use std::path::Path;
14
15use crate::compat::suite_registration::word_boundary_contains;
16use crate::compat::target_tree_index::TargetTreeIndex;
17use crate::model::names::{ApplicationName, RelativePath, SchemaFeature};
18use crate::model::verdict::Reason;
19use crate::versions::version_cmp;
20
21#[derive(Debug, Clone)]
25pub struct IntroducedMapping {
26 pub schema_path: RelativePath,
27 pub conf_key: Option<String>,
29 pub mapping_target: Option<String>,
32 pub attr_names: Vec<String>,
34}
35
36pub const SCHEMA_FEATURE_FLOORS: &[(&str, &str)] = &[("alias", "3.7.0")];
38
39pub fn check_schema_feature_unsupported(
43 mappings: &[IntroducedMapping],
44 pinned_version: Option<&str>,
45) -> Vec<Reason> {
46 let Some(pinned) = pinned_version else {
47 return Vec::new();
48 };
49 let mut reasons = Vec::new();
50 for mapping in mappings {
51 for attr in &mapping.attr_names {
52 let Some((_, required)) = SCHEMA_FEATURE_FLOORS.iter().find(|(name, _)| name == attr)
53 else {
54 continue;
55 };
56 if version_cmp(pinned, required) != Ordering::Greater {
60 continue;
61 }
62 let Ok(feature) = SchemaFeature::new((*attr).clone()) else {
63 continue;
64 };
65 reasons.push(Reason::SchemaFeatureUnsupportedOnPin {
66 schema_path: mapping.schema_path.clone(),
67 conf_key: mapping.conf_key.clone(),
68 feature,
69 required_version: (*required).to_owned(),
70 pinned_version: pinned.to_owned(),
71 });
72 }
73 }
74 reasons
75}
76
77pub fn check_schema_key_reader_missing(
83 mappings: &[IntroducedMapping],
84 target: &TargetTreeIndex,
85 read_target: &dyn Fn(&RelativePath) -> Option<String>,
86) -> Vec<Reason> {
87 let mut reasons = Vec::new();
88 let mut app_source_cache: BTreeMap<String, Vec<String>> = BTreeMap::new();
89 for mapping in mappings {
90 let Some(target_key) = &mapping.mapping_target else {
91 continue;
92 };
93 let mut segments = target_key.split('.');
94 let (Some(app), Some(env_key)) = (segments.next(), segments.next()) else {
95 continue;
96 };
97 let app_name = ApplicationName::new(app).ok();
98 let exists = app_has_any_path(target, app);
99 let searched_apps: Vec<ApplicationName> = app_name.clone().into_iter().collect();
100 let absent_apps: Vec<ApplicationName> = if exists {
101 Vec::new()
102 } else {
103 app_name.into_iter().collect()
104 };
105 let found = exists && {
106 let sources = app_source_cache
107 .entry(app.to_owned())
108 .or_insert_with(|| read_app_sources(target, app, read_target));
109 sources
110 .iter()
111 .any(|src| word_boundary_contains(src, env_key))
112 };
113 if found {
114 continue;
115 }
116 reasons.push(Reason::SchemaKeyReaderMissing {
117 schema_path: mapping.schema_path.clone(),
118 target_key: target_key.clone(),
119 searched_apps,
120 absent_apps,
121 });
122 }
123 reasons
124}
125
126fn app_has_any_path(target: &TargetTreeIndex, app: &str) -> bool {
130 let under_deps = format!("deps/{app}/");
131 let top_level = format!("{app}/");
132 target.present_paths().iter().any(|p| {
133 let s = p.to_string_lossy();
134 s.starts_with(under_deps.as_str()) || s.starts_with(top_level.as_str())
135 })
136}
137
138fn read_app_sources(
140 target: &TargetTreeIndex,
141 app: &str,
142 read_target: &dyn Fn(&RelativePath) -> Option<String>,
143) -> Vec<String> {
144 let under_deps = format!("deps/{app}/src/");
145 let top_level = format!("{app}/src/");
146 target
147 .present_paths()
148 .iter()
149 .filter(|p| {
150 let s = p.to_string_lossy();
151 (s.starts_with(under_deps.as_str()) || s.starts_with(top_level.as_str()))
152 && s.ends_with(".erl")
153 })
154 .filter_map(|p: &std::path::PathBuf| relative_path_of(p))
155 .filter_map(|p| read_target(&p))
156 .collect()
157}
158
159fn relative_path_of(p: &Path) -> Option<RelativePath> {
160 RelativePath::new(p.to_str()?.to_owned()).ok()
161}