Skip to main content

contract_cli/commands/
clauses.rs

1use crate::cli::ClauseCmd;
2use crate::clauses;
3use crate::db::{self, ContractClauseRow};
4use crate::error::{AppError, Result};
5use crate::output::{print_success, Ctx};
6
7pub fn run(cmd: ClauseCmd, ctx: Ctx) -> Result<()> {
8    match cmd {
9        ClauseCmd::List { number } => list(&number, ctx),
10        ClauseCmd::Add {
11            number,
12            slug,
13            heading,
14            body,
15            from_file,
16            position,
17        } => add(&number, &slug, heading, body, from_file, position, ctx),
18        ClauseCmd::Edit {
19            number,
20            slug,
21            heading,
22            body,
23            from_file,
24        } => edit(&number, &slug, heading, body, from_file, ctx),
25        ClauseCmd::Remove { number, slug } => remove(&number, &slug, ctx),
26        ClauseCmd::Move {
27            number,
28            slug,
29            position,
30        } => move_clause(&number, &slug, position, ctx),
31        ClauseCmd::Reset { number } => reset(&number, ctx),
32    }
33}
34
35fn body_text(body: Option<String>, from_file: Option<String>) -> Result<Option<String>> {
36    match (body, from_file) {
37        (Some(_), Some(_)) => Err(AppError::InvalidInput(
38            "pass either --body or --from-file, not both".into(),
39        )),
40        (Some(b), None) => Ok(Some(b)),
41        (None, Some(p)) => Ok(Some(std::fs::read_to_string(&p).map_err(|e| {
42            AppError::InvalidInput(format!("could not read {p}: {e}"))
43        })?)),
44        (None, None) => Ok(None),
45    }
46}
47
48fn list(number: &str, ctx: Ctx) -> Result<()> {
49    let conn = db::open()?;
50    let c = db::contract_get_or_404(&conn, number)?;
51    let rows = db::clauses_for(&conn, c.id)?;
52    print_success(ctx, &rows, |rs| {
53        if rs.is_empty() {
54            println!("(no clauses)");
55        } else {
56            for r in rs {
57                let custom = if r.heading.is_some() || r.body.is_some() {
58                    " [custom]"
59                } else {
60                    ""
61                };
62                println!(
63                    "  {:>2}. {:<22} {}",
64                    r.position + 1,
65                    r.slug,
66                    r.heading.clone().unwrap_or_default() + custom
67                );
68            }
69        }
70    });
71    Ok(())
72}
73
74fn add(
75    number: &str,
76    slug: &str,
77    heading: Option<String>,
78    body: Option<String>,
79    from_file: Option<String>,
80    position: Option<i64>,
81    ctx: Ctx,
82) -> Result<()> {
83    let mut conn = db::open()?;
84    let body = body_text(body, from_file)?;
85    // Validate slug exists in the pack OR a custom body was provided.
86    let c = db::contract_get_or_404(&conn, number)?;
87    let pack = clauses::load_pack(&c.kind, &c.clause_pack)?;
88    if body.is_none() && !pack.clauses.contains_key(slug) {
89        return Err(AppError::NotFound(format!(
90            "clause '{slug}' is not in pack '{}/{}'. Either pick a known slug or pass --body / --from-file to define a custom clause.",
91            c.kind, c.clause_pack
92        )));
93    }
94    // If using a custom clause that's not in the pack, also require a heading.
95    let heading = if body.is_some() && !pack.clauses.contains_key(slug) && heading.is_none() {
96        Some(humanise_slug(slug))
97    } else {
98        heading
99    };
100    let row = db::clause_add(&mut conn, number, slug, heading.as_deref(), body.as_deref(), position)?;
101    print_success(ctx, &row, |r| {
102        println!("added clause '{}' at position {}", r.slug, r.position + 1);
103    });
104    Ok(())
105}
106
107fn humanise_slug(slug: &str) -> String {
108    slug.replace('_', " ")
109        .split(' ')
110        .map(|w| {
111            let mut c = w.chars();
112            match c.next() {
113                None => String::new(),
114                Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
115            }
116        })
117        .collect::<Vec<_>>()
118        .join(" ")
119}
120
121fn edit(
122    number: &str,
123    slug: &str,
124    heading: Option<String>,
125    body: Option<String>,
126    from_file: Option<String>,
127    ctx: Ctx,
128) -> Result<()> {
129    let conn = db::open()?;
130    let body = body_text(body, from_file)?;
131    if heading.is_none() && body.is_none() {
132        return Err(AppError::InvalidInput(
133            "pass at least one of --heading / --body / --from-file".into(),
134        ));
135    }
136    db::clause_edit(&conn, number, slug, heading.as_deref(), body.as_deref())?;
137    print_success(ctx, &slug, |s| println!("edited clause '{s}'"));
138    Ok(())
139}
140
141fn remove(number: &str, slug: &str, ctx: Ctx) -> Result<()> {
142    let mut conn = db::open()?;
143    db::clause_remove(&mut conn, number, slug)?;
144    print_success(ctx, &slug, |s| println!("removed clause '{s}'"));
145    Ok(())
146}
147
148fn move_clause(number: &str, slug: &str, position: i64, ctx: Ctx) -> Result<()> {
149    let mut conn = db::open()?;
150    db::clause_move(&mut conn, number, slug, position)?;
151    print_success(ctx, &slug, |s| {
152        println!("moved clause '{s}' to position {}", position + 1)
153    });
154    Ok(())
155}
156
157fn reset(number: &str, ctx: Ctx) -> Result<()> {
158    let mut conn = db::open()?;
159    let c = db::contract_get_or_404(&conn, number)?;
160    let pack = clauses::load_pack(&c.kind, &c.clause_pack)?;
161    let fresh: Vec<ContractClauseRow> = pack
162        .pack
163        .default_clauses
164        .iter()
165        .enumerate()
166        .map(|(i, slug)| ContractClauseRow {
167            id: 0,
168            contract_id: 0,
169            position: i as i64,
170            slug: slug.clone(),
171            heading: None,
172            body: None,
173        })
174        .collect();
175    db::clauses_reset(&mut conn, number, &fresh)?;
176    print_success(ctx, &fresh, |f| {
177        println!("reset to pack default ({} clauses)", f.len());
178    });
179    Ok(())
180}