#![allow(dead_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::error::LoadError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DepsConfig {
pub envelopes: Vec<EnvelopeSpec>,
pub explicit_dependencies: Vec<EnvelopeDependency>,
pub explicit_test_dependencies: Vec<EnvelopeDependency>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EnvelopeSpec {
pub name: String,
pub path: PathBuf,
pub dependencies: Vec<EnvelopeDependency>,
pub test_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct EnvelopeDependency {
pub name: String,
pub used_as: String,
}
#[derive(serde::Deserialize)]
struct DepsConfigRaw {
envelopes: Vec<EnvelopeSpecRaw>,
dependencies: Vec<EnvelopeDependencyRaw>,
test_dependencies: Vec<EnvelopeDependencyRaw>,
}
#[derive(serde::Deserialize)]
struct EnvelopeSpecRaw {
name: String,
path: String,
dependencies: Vec<EnvelopeDependencyRaw>,
test_only: bool,
}
#[derive(serde::Deserialize)]
struct EnvelopeDependencyRaw {
name: String,
used_as: String,
}
pub(crate) fn load(path: &Path) -> Result<DepsConfig, LoadError> {
let text = std::fs::read_to_string(path).map_err(|source| LoadError::DepsConfigNotFound {
path: path.to_path_buf(),
source,
})?;
decode(&text).map_err(|message| LoadError::DepsConfigDecode {
path: path.to_path_buf(),
message,
})
}
pub(crate) fn make_used_as_map(deps: &[EnvelopeDependency]) -> HashMap<String, String> {
let mut map = HashMap::new();
for dep in deps {
map.insert(dep.used_as.clone(), dep.name.clone());
}
map
}
fn decode(text: &str) -> Result<DepsConfig, String> {
let raw: DepsConfigRaw = serde_yaml::from_str(text).map_err(|e| e.to_string())?;
convert(raw)
}
fn convert(raw: DepsConfigRaw) -> Result<DepsConfig, String> {
let envelopes = raw
.envelopes
.into_iter()
.enumerate()
.map(|(i, spec)| convert_envelope_spec(spec, &format!("envelopes.[{i}]")))
.collect::<Result<Vec<_>, _>>()?;
let explicit_dependencies = convert_dependency_list(raw.dependencies, "dependencies")?;
let explicit_test_dependencies =
convert_dependency_list(raw.test_dependencies, "test_dependencies")?;
Ok(DepsConfig {
envelopes,
explicit_dependencies,
explicit_test_dependencies,
})
}
fn convert_envelope_spec(raw: EnvelopeSpecRaw, ctx: &str) -> Result<EnvelopeSpec, String> {
let path = parse_abs_path(&raw.path)
.ok_or_else(|| format!("{ctx}.path: not an absolute path: `{}`", raw.path))?;
let dependencies = convert_dependency_list(raw.dependencies, &format!("{ctx}.dependencies"))?;
Ok(EnvelopeSpec {
name: raw.name,
path,
dependencies,
test_only: raw.test_only,
})
}
fn convert_dependency_list(
raw: Vec<EnvelopeDependencyRaw>,
ctx: &str,
) -> Result<Vec<EnvelopeDependency>, String> {
raw.into_iter()
.enumerate()
.map(|(i, dep)| convert_dependency(dep, &format!("{ctx}.[{i}]")))
.collect()
}
fn convert_dependency(raw: EnvelopeDependencyRaw, ctx: &str) -> Result<EnvelopeDependency, String> {
if !is_uppercased_identifier(&raw.used_as) {
return Err(format!(
"{ctx}.used_as: not an uppercased identifier: `{}`",
raw.used_as
));
}
Ok(EnvelopeDependency {
name: raw.name,
used_as: raw.used_as,
})
}
fn is_uppercased_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c0) if c0.is_ascii_uppercase() => chars.all(|c| c == '-' || c.is_ascii_alphanumeric()),
_ => false,
}
}
fn parse_abs_path(s: &str) -> Option<PathBuf> {
let rest = s.strip_prefix('/')?;
let mut components: Vec<&str> = Vec::new();
for part in rest.split('/') {
match part {
"" | "." => {}
".." => {
components.pop()?;
}
other => components.push(other),
}
}
let mut path = PathBuf::from("/");
path.extend(components);
Some(path)
}
#[cfg(test)]
mod tests {
use super::*;
struct TempFile(PathBuf);
impl TempFile {
fn write(tag: &str, content: &str) -> Self {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rustyfi-loader-deps-test-{tag}-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
n
));
std::fs::write(&path, content).expect("write temp deps config");
TempFile(path)
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
const DEPS_MINIMAL: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/v01x/deps/deps-minimal.yaml.tpl"
));
const DEPS_DEMO_SHAPED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/v01x/deps/deps-demo-shaped.yaml.tpl"
));
#[test]
fn deps_minimal_decodes() {
let f = TempFile::write("minimal", DEPS_MINIMAL);
let cfg = load(&f.0).expect("minimal deps config decodes");
assert!(cfg.envelopes.is_empty());
assert!(cfg.explicit_dependencies.is_empty());
assert!(cfg.explicit_test_dependencies.is_empty());
}
#[test]
fn deps_demo_shaped_decodes_and_used_as_map() {
let rendered = DEPS_DEMO_SHAPED.replace("{{ROOT}}", "/synthetic-root");
let f = TempFile::write("demo-shaped", &rendered);
let cfg = load(&f.0).expect("demo-shaped deps config decodes");
assert_eq!(cfg.envelopes.len(), 3);
let stdlib = &cfg.envelopes[0];
assert_eq!(
stdlib.name,
"registered.6f2b80e9bb7c4e8af2104999fc25dbb3.stdlib.0.0.1"
);
assert_eq!(
stdlib.path,
PathBuf::from("/synthetic-root/store/stdlib.0.0.1/rustyfi-envelope.yaml")
);
assert!(stdlib.dependencies.is_empty());
assert!(!stdlib.test_only);
let math = &cfg.envelopes[1];
assert_eq!(math.dependencies.len(), 1);
assert_eq!(math.dependencies[0].used_as, "Stdlib");
assert_eq!(cfg.explicit_dependencies.len(), 3);
let map = make_used_as_map(&cfg.explicit_dependencies);
assert_eq!(map.len(), 3);
assert_eq!(
map.get("Stdlib").map(String::as_str),
Some("registered.6f2b80e9bb7c4e8af2104999fc25dbb3.stdlib.0.0.1")
);
assert_eq!(
map.get("Math").map(String::as_str),
Some("registered.6f2b80e9bb7c4e8af2104999fc25dbb3.math.0.0.1")
);
assert_eq!(
map.get("Tabular").map(String::as_str),
Some("registered.6f2b80e9bb7c4e8af2104999fc25dbb3.tabular.0.0.1")
);
assert!(cfg.explicit_test_dependencies.is_empty());
}
#[test]
fn make_used_as_map_later_duplicate_wins() {
let deps = vec![
EnvelopeDependency {
name: "pkg.a".into(),
used_as: "Foo".into(),
},
EnvelopeDependency {
name: "pkg.b".into(),
used_as: "Foo".into(),
},
];
let map = make_used_as_map(&deps);
assert_eq!(map.len(), 1);
assert_eq!(map.get("Foo").map(String::as_str), Some("pkg.b"));
}
#[test]
fn deps_missing_test_dependencies_errors() {
let f = TempFile::write("missing-key", "envelopes: []\ndependencies: []\n");
let err = load(&f.0).expect_err("missing test_dependencies must error");
assert!(matches!(err, LoadError::DepsConfigDecode { .. }));
}
#[test]
fn deps_negative_relative_envelope_path() {
let f = TempFile::write(
"rel-path",
"envelopes:
- name: pkg
path: \"not/absolute\"
dependencies: []
test_only: false
dependencies: []
test_dependencies: []
",
);
let err = load(&f.0).expect_err("relative envelope path must error");
match err {
LoadError::DepsConfigDecode { message, .. } => {
assert!(message.contains("envelopes.[0].path"), "{message}");
}
other => panic!("expected DepsConfigDecode, got {other:?}"),
}
}
#[test]
fn deps_negative_dotdot_past_root() {
let f = TempFile::write(
"dotdot-root",
"envelopes:
- name: pkg
path: \"/a/../../b\"
dependencies: []
test_only: false
dependencies: []
test_dependencies: []
",
);
let err = load(&f.0).expect_err("`..` past root must error");
assert!(matches!(err, LoadError::DepsConfigDecode { .. }));
}
#[test]
fn deps_abs_path_normalizes() {
assert_eq!(
parse_abs_path("/a/./b/../c//d"),
Some(PathBuf::from("/a/c/d"))
);
assert_eq!(parse_abs_path("/"), Some(PathBuf::from("/")));
assert_eq!(parse_abs_path("relative"), None);
assert_eq!(parse_abs_path("/a/.."), Some(PathBuf::from("/")));
assert_eq!(parse_abs_path("/.."), None);
}
#[test]
fn deps_negative_lowercase_used_as() {
let f = TempFile::write(
"lowercase-used-as",
"envelopes: []
dependencies:
- name: pkg
used_as: stdlib
test_dependencies: []
",
);
let err = load(&f.0).expect_err("lowercase used_as must error");
match err {
LoadError::DepsConfigDecode { message, .. } => {
assert!(message.contains("dependencies.[0].used_as"), "{message}");
}
other => panic!("expected DepsConfigDecode, got {other:?}"),
}
}
#[test]
fn deps_negative_used_as_with_space() {
let f = TempFile::write(
"used-as-space",
"envelopes: []
dependencies:
- name: pkg
used_as: \"St dlib\"
test_dependencies: []
",
);
let err = load(&f.0).expect_err("used_as with a space must error");
assert!(matches!(err, LoadError::DepsConfigDecode { .. }));
}
#[test]
fn deps_duplicate_envelope_name_decodes_fine() {
let f = TempFile::write(
"dup-name",
"envelopes:
- name: pkg
path: \"/a/rustyfi-envelope.yaml\"
dependencies: []
test_only: false
- name: pkg
path: \"/b/rustyfi-envelope.yaml\"
dependencies: []
test_only: false
dependencies: []
test_dependencies: []
",
);
let cfg = load(&f.0).expect("duplicate envelope names decode fine at Ld3b-1");
assert_eq!(cfg.envelopes.len(), 2);
}
#[test]
fn deps_file_missing_is_not_found() {
let path = std::env::temp_dir().join("rustyfi-loader-deps-test-does-not-exist.yaml");
let err = load(&path).expect_err("missing file must error");
assert!(matches!(err, LoadError::DepsConfigNotFound { .. }));
}
}