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 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 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}