pub const KNOWN_DEPS: &[&str] = &[
"status",
"settings",
"claude_json",
"usage",
"sessions",
"git",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderError {
Malformed(String),
UnknownDep(String),
}
pub fn parse_data_deps_header(src: &str) -> Result<Vec<String>, HeaderError> {
let header_block = collect_header_block(src);
let Some(rhs) = find_data_deps_rhs(&header_block)? else {
return Ok(vec!["status".to_string()]);
};
let tokens = split_array_body(rhs)?;
let mut deps = vec!["status".to_string()];
for token in tokens {
if !KNOWN_DEPS.contains(&token.as_str()) {
return Err(HeaderError::UnknownDep(token));
}
if !deps.iter().any(|d| d == &token) {
deps.push(token);
}
}
Ok(deps)
}
fn collect_header_block(src: &str) -> String {
let mut buf = String::new();
for line in src.lines() {
let trimmed = line.trim_start();
if trimmed.is_empty() {
break;
}
let Some(rest) = trimmed.strip_prefix("//") else {
break;
};
let rest = rest.strip_prefix(' ').unwrap_or(rest);
buf.push_str(rest);
buf.push('\n');
}
buf
}
fn find_data_deps_rhs(header: &str) -> Result<Option<&str>, HeaderError> {
let Some(start) = header.find("@data_deps") else {
return Ok(None);
};
let after_key = &header[start + "@data_deps".len()..];
let Some(eq_pos) = after_key.find('=') else {
return Err(HeaderError::Malformed(
"@data_deps declaration missing `=`".to_string(),
));
};
let after_eq = after_key[eq_pos + 1..].trim_start();
let Some(open) = after_eq.strip_prefix('[') else {
return Err(HeaderError::Malformed(
"@data_deps RHS must be an array literal starting with `[`".to_string(),
));
};
Ok(Some(open))
}
fn split_array_body(body: &str) -> Result<Vec<String>, HeaderError> {
let Some(end) = body.find(']') else {
return Err(HeaderError::Malformed(
"missing closing `]` in @data_deps array".to_string(),
));
};
let inside = &body[..end];
let stripped: String = inside
.lines()
.map(|line| match line.find("//") {
Some(i) => &line[..i],
None => line,
})
.collect::<Vec<_>>()
.join(" ");
let mut tokens = Vec::new();
for raw in stripped.split(',') {
let s = raw.trim();
if s.is_empty() {
continue;
}
let unquoted = unquote(s)?;
tokens.push(unquoted);
}
Ok(tokens)
}
fn unquote(s: &str) -> Result<String, HeaderError> {
let bytes = s.as_bytes();
if bytes.len() >= 2
&& ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
|| (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''))
{
Ok(s[1..s.len() - 1].to_string())
} else {
Err(HeaderError::Malformed(format!(
"expected quoted string, got `{s}`"
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn deps(names: &[&str]) -> Vec<String> {
names.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn no_header_defaults_to_status_only() {
let src = "fn render(ctx) { () }";
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status"])));
}
#[test]
fn empty_array_defaults_to_status_only() {
let src = "// @data_deps = []\nfn render(ctx) {}";
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status"])));
}
#[test]
fn single_line_single_entry_unions_with_status() {
let src = r#"// @data_deps = ["usage"]
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status", "usage"])));
}
#[test]
fn single_line_multi_entry() {
let src = r#"// @data_deps = ["settings", "usage", "git"]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Ok(deps(&["status", "settings", "usage", "git"]))
);
}
#[test]
fn explicit_status_is_accepted_without_duplication() {
let src = r#"// @data_deps = ["status", "usage"]
fn render(ctx) {}"#;
let resolved = parse_data_deps_header(src).unwrap();
assert_eq!(resolved, deps(&["status", "usage"]));
assert_eq!(
resolved.iter().filter(|d| *d == "status").count(),
1,
"status must not be duplicated when listed explicitly"
);
}
#[test]
fn multi_line_array_accepted() {
let src = r#"// @data_deps = [
// "settings",
// "usage",
// "git",
// ]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Ok(deps(&["status", "settings", "usage", "git"]))
);
}
#[test]
fn trailing_comma_in_single_line_ok() {
let src = r#"// @data_deps = ["usage",]
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status", "usage"])));
}
#[test]
fn single_quotes_accepted() {
let src = "// @data_deps = ['usage']\nfn render(ctx) {}";
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status", "usage"])));
}
#[test]
fn unknown_dep_name_rejected() {
let src = r#"// @data_deps = ["usage", "mystery"]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Err(HeaderError::UnknownDep("mystery".to_string()))
);
}
#[test]
fn reserved_credentials_dep_rejected_as_unknown() {
let src = r#"// @data_deps = ["credentials"]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Err(HeaderError::UnknownDep("credentials".to_string()))
);
}
#[test]
fn reserved_jsonl_dep_rejected_as_unknown() {
let src = r#"// @data_deps = ["jsonl"]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Err(HeaderError::UnknownDep("jsonl".to_string()))
);
}
#[test]
fn blank_line_ends_header_block() {
let src = r#"// top comment
// @data_deps = ["usage"]
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status"])));
}
#[test]
fn non_comment_line_ends_header_block() {
let src = r#"// top comment
fn render(ctx) {}
// @data_deps = ["usage"]"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status"])));
}
#[test]
fn header_appearing_after_other_comments_still_parses() {
let src = r#"// Some plugin description
// Authored by me
// @data_deps = ["usage"]
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status", "usage"])));
}
#[test]
fn malformed_missing_equals_rejected() {
let src = r#"// @data_deps ["usage"]
fn render(ctx) {}"#;
assert!(matches!(
parse_data_deps_header(src),
Err(HeaderError::Malformed(_))
));
}
#[test]
fn malformed_scalar_rhs_rejected() {
let src = r#"// @data_deps = "usage"
fn render(ctx) {}"#;
assert!(matches!(
parse_data_deps_header(src),
Err(HeaderError::Malformed(_))
));
}
#[test]
fn malformed_missing_closing_bracket() {
let src = r#"// @data_deps = ["usage"
fn render(ctx) {}"#;
assert!(matches!(
parse_data_deps_header(src),
Err(HeaderError::Malformed(_))
));
}
#[test]
fn malformed_unquoted_token() {
let src = r#"// @data_deps = [usage]
fn render(ctx) {}"#;
assert!(matches!(
parse_data_deps_header(src),
Err(HeaderError::Malformed(_))
));
}
#[test]
fn block_comment_syntax_is_not_scanned() {
let src = r#"/* @data_deps = ["usage"] */
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status"])));
}
#[test]
fn inline_comment_on_array_line_accepted() {
let src = r#"// @data_deps = [
// "usage", // why we need it
// "git", // trailing comment too
// ]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Ok(deps(&["status", "usage", "git"]))
);
}
#[test]
fn inline_comment_after_last_entry_accepted() {
let src = r#"// @data_deps = [
// "usage", // ok
// "git"
// ]
fn render(ctx) {}"#;
assert_eq!(
parse_data_deps_header(src),
Ok(deps(&["status", "usage", "git"]))
);
}
#[test]
fn whitespace_before_double_slash_is_tolerated() {
let src = r#" // @data_deps = ["usage"]
fn render(ctx) {}"#;
assert_eq!(parse_data_deps_header(src), Ok(deps(&["status", "usage"])));
}
}