Skip to main content

worktree_io/
repo_hooks_parse.rs

1//! Parser for `.worktree.toml` per-repo hook configuration.
2//!
3//! The user-facing schema uses flat keys named after each hook. Hook scripts
4//! and their optional `order` siblings may sit either at the document root or
5//! inside an explicit `[hooks]` table. Both layouts produce the same
6//! [`RepoConfig`].
7//!
8//! ```toml
9//! "pre:open" = "cargo build"
10//! "pre:open:order" = "before"   # optional; defaults to "before"
11//!
12//! [hooks]
13//! "post:open" = "npm install"
14//! ```
15
16use crate::repo_hooks::{HookOrder, RepoConfig, RepoHookEntry, RepoHooksConfig};
17
18/// Parse a `.worktree.toml` body into a [`RepoConfig`].
19///
20/// # Errors
21///
22/// Returns an error string when the document is not valid TOML, when a hook
23/// script is not a string, or when an `order` value is missing or unknown.
24pub fn parse(contents: &str) -> Result<RepoConfig, String> {
25    let table: toml::Table = toml::from_str(contents).map_err(|e| e.to_string())?;
26    let mut hooks = RepoHooksConfig::default();
27    take_hooks(&table, &mut hooks)?;
28    if let Some(h) = table.get("hooks") {
29        let h = h
30            .as_table()
31            .ok_or_else(|| "`hooks` must be a TOML table".to_owned())?;
32        take_hooks(h, &mut hooks)?;
33    }
34    Ok(RepoConfig { hooks })
35}
36
37fn take_hooks(table: &toml::Table, out: &mut RepoHooksConfig) -> Result<(), String> {
38    if let Some(e) = extract_hook(table, "pre:open")? {
39        out.pre_open = Some(e);
40    }
41    if let Some(e) = extract_hook(table, "post:open")? {
42        out.post_open = Some(e);
43    }
44    Ok(())
45}
46
47fn extract_hook(table: &toml::Table, name: &str) -> Result<Option<RepoHookEntry>, String> {
48    let Some(script) = table.get(name) else {
49        return Ok(None);
50    };
51    let script = script
52        .as_str()
53        .ok_or_else(|| format!("`{name}` must be a string"))?;
54    let order_key = format!("{name}:order");
55    let order = match table.get(&order_key) {
56        None => HookOrder::default(),
57        Some(v) => {
58            let s = v
59                .as_str()
60                .ok_or_else(|| format!("`{order_key}` must be a string"))?;
61            parse_order(s).ok_or_else(|| {
62                format!("invalid `{order_key}` value `{s}` (expected before/after/replace)")
63            })?
64        }
65    };
66    Ok(Some(RepoHookEntry {
67        script: script.to_owned(),
68        order,
69    }))
70}
71
72fn parse_order(s: &str) -> Option<HookOrder> {
73    match s {
74        "before" => Some(HookOrder::Before),
75        "after" => Some(HookOrder::After),
76        "replace" => Some(HookOrder::Replace),
77        _ => None,
78    }
79}
80
81#[cfg(test)]
82#[path = "repo_hooks_parse_tests.rs"]
83mod tests;