use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Via {
Sql { file: String },
Use { fragment: String },
Each { name: String, value: String },
}
impl std::fmt::Display for Via {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Via::Sql { file } => f.write_str(file),
Via::Use { fragment } => write!(f, "fragment '{fragment}'"),
Via::Each { name, value } => write!(f, "$each {name} = {value}"),
}
}
}
pub fn trail_suffix(trail: &[Via]) -> String {
trail
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" › ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Entry {
file: String,
path: Option<String>,
via: Vec<Via>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceMap {
entries: BTreeMap<String, Entry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceRef<'a> {
pub file: &'a str,
pub path: Option<String>,
pub via: &'a [Via],
}
impl SourceMap {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn record(&mut self, compiled_path: &str, file: &str, path: Option<&str>, via: Vec<Via>) {
self.entries.insert(
compiled_path.to_string(),
Entry {
file: file.to_string(),
path: path.map(str::to_string),
via,
},
);
}
pub fn resolve<'a>(&'a self, own_file: &'a str, compiled_path: &str) -> SourceRef<'a> {
let hit = self
.entries
.iter()
.filter(|(at, _)| is_prefix(at, compiled_path))
.max_by_key(|(at, _)| at.len());
match hit {
Some((at, entry)) => {
let rest = &compiled_path[at.len()..];
let path = entry.path.as_ref().map(|p| join(p, rest));
SourceRef {
file: &entry.file,
path,
via: &entry.via,
}
}
None => SourceRef {
file: own_file,
path: Some(compiled_path.to_string()),
via: &[],
},
}
}
}
fn is_prefix(prefix: &str, path: &str) -> bool {
path == prefix
|| prefix.is_empty()
|| path
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with('.') || rest.starts_with('['))
}
fn join(base: &str, rest: &str) -> String {
match (base.is_empty(), rest.strip_prefix('.')) {
(true, Some(rest)) => rest.to_string(),
_ => format!("{base}{rest}"),
}
}
impl SourceRef<'_> {
pub fn is_authored_here(&self) -> bool {
self.via.is_empty()
}
pub fn describe(&self) -> Option<String> {
if self.via.is_empty() {
return None;
}
Some(trail_suffix(self.via))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_no_entry_a_coordinate_is_its_own_source() {
let map = SourceMap::default();
let at = map.resolve("wf.json", "tasks[2].function.input.query");
assert_eq!(at.file, "wf.json");
assert_eq!(at.path.as_deref(), Some("tasks[2].function.input.query"));
assert!(at.is_authored_here());
assert_eq!(at.describe(), None);
}
#[test]
fn the_longest_prefix_wins_and_the_rest_is_rerooted() {
let mut map = SourceMap::default();
map.record(
"tasks[2].function.input.query",
"sql/settle.sql",
None,
vec![Via::Sql {
file: "sql/settle.sql".to_string(),
}],
);
map.record(
"tasks[1]",
"shared.json",
Some("fragments.f.tasks[0]"),
Vec::new(),
);
let sql = map.resolve("wf.json", "tasks[2].function.input.query");
assert_eq!(sql.file, "sql/settle.sql");
assert_eq!(sql.path, None);
assert_eq!(sql.describe().as_deref(), Some("sql/settle.sql"));
let inner = map.resolve("wf.json", "tasks[1].function.input");
assert_eq!(inner.file, "shared.json");
assert_eq!(
inner.path.as_deref(),
Some("fragments.f.tasks[0].function.input")
);
assert_eq!(map.resolve("wf.json", "tasks[10]").file, "wf.json");
}
}