use std::path::Path;
use path_clean::PathClean;
use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB;
use wdl_analysis::types::PrimitiveType;
use wdl_ast::Diagnostic;
use super::CallContext;
use super::Callback;
use super::Function;
use super::Signature;
use crate::PrimitiveValue;
use crate::Value;
use crate::diagnostics::function_call_failed;
const FUNCTION_NAME: &str = "join_paths";
fn join_paths_simple(context: CallContext<'_>) -> Result<Value, Diagnostic> {
debug_assert!(context.arguments.len() == 2);
debug_assert!(context.return_type_eq(PrimitiveType::String));
let first = context
.coerce_argument(0, PrimitiveType::Directory)
.unwrap_directory();
let second = context
.coerce_argument(1, PrimitiveType::String)
.unwrap_string();
let first = context.base_dir().join(first.as_str()).map_err(|_| {
function_call_failed(
FUNCTION_NAME,
format!("path `{first}` cannot be joined with the evaluation base path"),
context.arguments[0].span,
)
})?;
if first.is_local() {
let path = first.unwrap_local();
let second = Path::new(second.as_str());
if !second.is_relative() {
return Err(function_call_failed(
FUNCTION_NAME,
format!(
"path `{second}` is not a relative path",
second = second.display()
),
context.arguments[1].span,
));
}
Ok(PrimitiveValue::new_string(
path.join(second)
.clean()
.into_os_string()
.into_string()
.expect("should be UTF-8"),
)
.into())
} else {
let mut url = first.unwrap_remote();
if second.starts_with('/') || second.contains(":") {
return Err(function_call_failed(
FUNCTION_NAME,
format!("path `{second}` is not a relative path"),
context.arguments[1].span,
));
}
if let Ok(mut segments) = url.path_segments_mut() {
segments.pop_if_empty();
segments.push("");
}
url.join(&second)
.map(|u| PrimitiveValue::new_string(u.to_string()).into())
.map_err(|_| {
function_call_failed(
FUNCTION_NAME,
format!("path `{second}` cannot be joined with URL `{url}`"),
context.arguments[1].span,
)
})
}
}
fn join_paths(context: CallContext<'_>) -> Result<Value, Diagnostic> {
debug_assert!(!context.arguments.is_empty() && context.arguments.len() < 3);
debug_assert!(context.return_type_eq(PrimitiveType::String));
let (first, array, skip, array_span) = if context.arguments.len() == 1 {
let array = context
.coerce_argument(0, ANALYSIS_STDLIB.array_string_non_empty_type().clone())
.unwrap_array();
(
array.as_slice()[0].clone().unwrap_string(),
array,
true,
context.arguments[0].span,
)
} else {
let first = context
.coerce_argument(0, PrimitiveType::Directory)
.unwrap_directory()
.into();
let array = context
.coerce_argument(1, ANALYSIS_STDLIB.array_string_non_empty_type().clone())
.unwrap_array();
(first, array, false, context.arguments[1].span)
};
let first = context.base_dir().join(&first).map_err(|_| {
function_call_failed(
FUNCTION_NAME,
format!("path `{first}` cannot be joined with the evaluation base path"),
context.arguments[0].span,
)
})?;
if first.is_local() {
let mut path = first.unwrap_local();
for (i, element) in array
.as_slice()
.iter()
.enumerate()
.skip(if skip { 1 } else { 0 })
{
let next = element.as_string().expect("element should be string");
let p = Path::new(next.as_str());
if !p.is_relative() {
return Err(function_call_failed(
FUNCTION_NAME,
format!("path `{next}` (array index {i}) is not a relative path"),
array_span,
));
}
path.push(p);
}
Ok(PrimitiveValue::new_string(
path.clean()
.into_os_string()
.into_string()
.expect("should be UTF-8"),
)
.into())
} else {
let mut url = first.unwrap_remote();
for (i, element) in array
.as_slice()
.iter()
.enumerate()
.skip(if skip { 1 } else { 0 })
{
let next = element.as_string().expect("element should be string");
if next.starts_with('/') || next.contains(":") {
return Err(function_call_failed(
FUNCTION_NAME,
format!("path `{next}` (array index {i}) is not a relative path"),
array_span,
));
}
if let Ok(mut segments) = url.path_segments_mut() {
segments.pop_if_empty();
segments.push("");
}
url = url.join(next).map_err(|_| {
function_call_failed(
FUNCTION_NAME,
format!("path `{next}` (array index {i}) cannot be joined with URL `{url}`"),
context.arguments[1].span,
)
})?;
}
Ok(PrimitiveValue::new_string(url.to_string()).into())
}
}
pub const fn descriptor() -> Function {
Function::new(
const {
&[
Signature::new(
"(base: Directory, relative: String) -> String",
Callback::Sync(join_paths_simple),
),
Signature::new(
"(base: Directory, relative: Array[String]+) -> String",
Callback::Sync(join_paths),
),
Signature::new(
"(paths: Array[String]+) -> String",
Callback::Sync(join_paths),
),
]
},
)
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use wdl_ast::version::V1;
use crate::v1::test::TestEnv;
use crate::v1::test::eval_v1_expr;
#[tokio::test]
async fn join_paths() {
let env = TestEnv::default();
#[cfg(unix)]
{
use std::path::Path;
let value = eval_v1_expr(&env, V1::Two, "join_paths('/usr', ['bin', 'echo'])")
.await
.unwrap();
assert_eq!(value.unwrap_string().as_str(), "/usr/bin/echo");
let value = eval_v1_expr(&env, V1::Two, "join_paths(['/usr', 'bin', 'echo'])")
.await
.unwrap();
assert_eq!(value.unwrap_string().as_str(), "/usr/bin/echo");
let value = eval_v1_expr(&env, V1::Two, "join_paths('mydir', 'mydata.txt')")
.await
.unwrap();
assert_eq!(
Path::new(value.unwrap_string().as_str())
.strip_prefix(env.base_dir().as_local().unwrap())
.unwrap()
.to_str()
.unwrap(),
"mydir/mydata.txt"
);
let value = eval_v1_expr(&env, V1::Two, "join_paths('/usr', 'bin/echo')")
.await
.unwrap();
assert_eq!(value.unwrap_string().as_str(), "/usr/bin/echo");
let diagnostic = eval_v1_expr(&env, V1::Two, "join_paths('/usr', '/bin/echo')")
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `/bin/echo` is not a relative path"
);
let diagnostic =
eval_v1_expr(&env, V1::Two, "join_paths('/usr', ['foo', '/bin/echo'])")
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `/bin/echo` (array index 1) is not a \
relative path"
);
let diagnostic =
eval_v1_expr(&env, V1::Two, "join_paths(['/usr', 'foo', '/bin/echo'])")
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `/bin/echo` (array index 2) is not a \
relative path"
);
}
#[cfg(windows)]
{
let diagnostic = eval_v1_expr(&env, V1::Two, "join_paths('C:\\usr', 'C:\\bin\\echo')")
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `C:\\bin\\echo` is not a relative path"
);
let diagnostic = eval_v1_expr(
&env,
V1::Two,
"join_paths('C:\\usr', ['foo', 'C:\\bin\\echo'])",
)
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `C:\\bin\\echo` (array index 1) is \
not a relative path"
);
let diagnostic = eval_v1_expr(
&env,
V1::Two,
"join_paths(['C:\\usr', 'foo', 'C:\\bin\\echo'])",
)
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `C:\\bin\\echo` (array index 2) is \
not a relative path"
);
}
let diagnostic = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com', '/foo/bar')",
)
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `/foo/bar` is not a relative path"
);
let diagnostic = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com', '//wrong.org/foo')",
)
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `//wrong.org/foo` is not a relative path"
);
let diagnostic = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com', 'https://wrong.org/foo')",
)
.await
.unwrap_err();
assert_eq!(
diagnostic.message(),
"call to function `join_paths` failed: path `https://wrong.org/foo` is not a relative \
path"
);
let value = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com', 'foo/bar/baz')",
)
.await
.unwrap();
assert_eq!(
value.unwrap_string().as_str(),
"https://example.com/foo/bar/baz"
);
let value = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com/foo/bar/', 'baz')",
)
.await
.unwrap();
assert_eq!(
value.unwrap_string().as_str(),
"https://example.com/foo/bar/baz"
);
let value = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com/foo/bar', '../baz')",
)
.await
.unwrap();
assert_eq!(
value.unwrap_string().as_str(),
"https://example.com/foo/baz"
);
let value = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com/foo/bar', ['nope', '../baz', 'qux'])",
)
.await
.unwrap();
assert_eq!(
value.unwrap_string().as_str(),
"https://example.com/foo/bar/baz/qux"
);
let value = eval_v1_expr(
&env,
V1::Two,
"join_paths('https://example.com/foo/bar?foo=jam', 'baz?foo=qux')",
)
.await
.unwrap();
assert_eq!(
value.unwrap_string().as_str(),
"https://example.com/foo/bar/baz?foo=qux"
);
}
}