use std::path::Path;
use super::types::{
BuildConfig, ComposeFile, EnvFile, EnvFileEntry, Service, VolumeMount, VolumeType,
};
pub(super) fn anchor_compose_file(file: &mut ComposeFile, dir: &Path) {
for svc in file.services.values_mut() {
anchor_service(svc, dir);
}
for secret in file.secrets.values_mut() {
if let Some(f) = secret.file.as_deref().and_then(|f| anchor_fs(f, dir)) {
secret.file = Some(f);
}
}
for config in file.configs.values_mut() {
if let Some(f) = config.file.as_deref().and_then(|f| anchor_fs(f, dir)) {
config.file = Some(f);
}
}
}
pub(super) fn anchor_service(svc: &mut Service, dir: &Path) {
if let Some(build) = svc.build.as_mut() {
match build {
BuildConfig::Context(c) => {
if let Some(a) = anchor_context(c, dir) {
*c = a;
}
}
BuildConfig::Config { context, .. } => {
if let Some(a) = anchor_context(context, dir) {
*context = a;
}
}
}
}
match &mut svc.env_file {
EnvFile::Empty => {}
EnvFile::Single(e) => anchor_env_entry(e, dir),
EnvFile::List(list) => {
for e in list.iter_mut() {
anchor_env_entry(e, dir);
}
}
}
for v in svc.volumes.iter_mut() {
match v {
VolumeMount::Short(s) => {
if let Some(a) = anchor_short_volume(s, dir) {
*s = a;
}
}
VolumeMount::Long {
volume_type: VolumeType::Bind,
source: Some(src),
..
} => {
if let Some(a) = anchor_bind(src, dir) {
*src = a;
}
}
VolumeMount::Long { .. } => {}
}
}
}
fn anchor_fs(path: &str, dir: &Path) -> Option<String> {
if path.is_empty() || path.starts_with('~') || Path::new(path).is_absolute() {
return None;
}
Some(dir.join(path).to_string_lossy().into_owned())
}
fn anchor_context(context: &str, dir: &Path) -> Option<String> {
if context.contains("://") || context.starts_with("git@") {
return None;
}
anchor_fs(context, dir)
}
fn anchor_bind(src: &str, dir: &Path) -> Option<String> {
src.starts_with('.')
.then(|| dir.join(src).to_string_lossy().into_owned())
}
fn anchor_short_volume(s: &str, dir: &Path) -> Option<String> {
let parts: Vec<&str> = s.splitn(3, ':').collect();
let src = parts.first()?;
if !src.starts_with('.') {
return None;
}
let mut rebuilt = dir.join(src).to_string_lossy().into_owned();
for part in &parts[1..] {
rebuilt.push(':');
rebuilt.push_str(part);
}
Some(rebuilt)
}
fn anchor_env_entry(entry: &mut EnvFileEntry, dir: &Path) {
match entry {
EnvFileEntry::Path(p) => {
if let Some(a) = anchor_fs(p, dir) {
*p = a;
}
}
EnvFileEntry::Config { path, .. } => {
if let Some(a) = anchor_fs(path, dir) {
*path = a;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compose::types::Service;
use std::path::Path;
fn dir() -> &'static Path {
Path::new("/proj/sub")
}
#[test]
#[cfg(unix)]
fn anchors_relative_build_context() {
let mut svc = Service {
build: Some(BuildConfig::Context("./app".into())),
..Default::default()
};
anchor_service(&mut svc, dir());
match svc.build.unwrap() {
BuildConfig::Context(c) => assert_eq!(c, "/proj/sub/./app"),
_ => panic!("expected context"),
}
}
#[test]
fn leaves_url_build_context() {
let mut svc = Service {
build: Some(BuildConfig::Context("https://github.com/x/y.git".into())),
..Default::default()
};
anchor_service(&mut svc, dir());
match svc.build.unwrap() {
BuildConfig::Context(c) => assert_eq!(c, "https://github.com/x/y.git"),
_ => panic!("expected context"),
}
}
#[test]
#[cfg(unix)]
fn anchors_env_file_list() {
let mut svc = Service {
env_file: EnvFile::List(vec![
EnvFileEntry::Path("./a.env".into()),
EnvFileEntry::Path("/abs/b.env".into()),
]),
..Default::default()
};
anchor_service(&mut svc, dir());
let EnvFile::List(list) = &svc.env_file else {
panic!("expected list");
};
assert_eq!(list[0].path(), "/proj/sub/./a.env");
assert_eq!(list[1].path(), "/abs/b.env");
}
#[test]
#[cfg(unix)]
fn anchors_relative_short_bind_only() {
let mut svc = Service {
volumes: vec![
VolumeMount::Short("./data:/data:ro".into()),
VolumeMount::Short("named:/v".into()),
VolumeMount::Short("/abs:/a".into()),
],
..Default::default()
};
anchor_service(&mut svc, dir());
let got: Vec<_> = svc
.volumes
.iter()
.map(|v| match v {
VolumeMount::Short(s) => s.clone(),
_ => unreachable!(),
})
.collect();
assert_eq!(got[0], "/proj/sub/./data:/data:ro");
assert_eq!(got[1], "named:/v");
assert_eq!(got[2], "/abs:/a");
}
#[test]
fn leaves_tilde_paths() {
let mut svc = Service {
env_file: EnvFile::Single(EnvFileEntry::Path("~/x.env".into())),
..Default::default()
};
anchor_service(&mut svc, dir());
let EnvFile::Single(e) = &svc.env_file else {
panic!("expected single");
};
assert_eq!(e.path(), "~/x.env");
}
}