1use std::collections::BTreeMap;
12
13use rust_embed::RustEmbed;
14use serde::{Deserialize, Serialize};
15
16use crate::error::{AppError, Result};
17
18#[derive(RustEmbed)]
19#[folder = "clauses/"]
20pub struct PackAssets;
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct Pack {
24 pub pack: PackMeta,
25 pub clauses: BTreeMap<String, ClauseDef>,
27}
28
29#[derive(Debug, Clone, Deserialize)]
30pub struct PackMeta {
31 pub slug: String,
32 pub name: String,
33 pub version: String,
34 pub kind: String,
35 pub default_clauses: Vec<String>,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize)]
40pub struct ClauseDef {
41 pub heading: String,
42 pub body: String,
43}
44
45#[derive(Debug, Clone, Serialize)]
46pub struct ResolvedClause {
47 pub position: i64,
48 pub slug: String,
49 pub heading: String,
50 pub body: String,
51}
52
53pub fn load_pack(kind: &str, pack_slug: &str) -> Result<Pack> {
54 let path = format!("{kind}/{pack_slug}.toml");
55 let file = PackAssets::get(&path)
56 .ok_or_else(|| AppError::NotFound(format!("clause pack '{kind}/{pack_slug}'")))?;
57 let text = std::str::from_utf8(&file.data)
58 .map_err(|e| AppError::Other(format!("non-utf8 pack {path}: {e}")))?;
59 toml::from_str::<Pack>(text)
60 .map_err(|e| AppError::Other(format!("invalid pack {path}: {e}")))
61}
62
63pub fn list_packs() -> Vec<(String, String)> {
64 PackAssets::iter()
65 .filter_map(|p| {
66 let p = p.as_ref();
67 let (kind, file) = p.split_once('/')?;
68 let pack_slug = file.strip_suffix(".toml")?;
69 Some((kind.to_string(), pack_slug.to_string()))
70 })
71 .collect()
72}
73
74pub fn expand(body: &str, vars: &BTreeMap<String, String>) -> String {
77 let mut out = body.to_string();
78 for (k, v) in vars {
79 let token = format!("{{{{{k}}}}}");
80 out = out.replace(&token, v);
81 }
82 out
83}
84
85pub fn resolve(
91 pack: &Pack,
92 included: &[String],
93 overrides: &BTreeMap<String, (Option<String>, Option<String>)>,
94 vars: &BTreeMap<String, String>,
95) -> Result<Vec<ResolvedClause>> {
96 let mut out = Vec::with_capacity(included.len());
97 for (i, slug) in included.iter().enumerate() {
98 let def = pack
99 .clauses
100 .get(slug)
101 .ok_or_else(|| AppError::NotFound(format!("clause '{slug}' not in pack")))?;
102 let (head_override, body_override) = overrides
103 .get(slug)
104 .cloned()
105 .unwrap_or((None, None));
106 let heading = head_override.unwrap_or_else(|| def.heading.clone());
107 let body_template = body_override.unwrap_or_else(|| def.body.clone());
108 out.push(ResolvedClause {
109 position: i as i64,
110 slug: slug.clone(),
111 heading: expand(&heading, vars),
112 body: expand(&body_template, vars),
113 });
114 }
115 Ok(out)
116}