Skip to main content

ravenrs_sheet/model/
macros.rs

1//! GridCraft macros: typed, declarative command lists that run as one undo step. They are not VBA:
2//! a VBA project in a file is carried through unchanged (`Workbook::vba_project`) and never run.
3
4use serde::{Deserialize, Serialize};
5
6/// The macro format version written by this build.
7pub const MACRO_VERSION: u32 = 1;
8/// The most macros a workbook keeps.
9pub const MAX_MACROS: usize = 1_000;
10/// The most steps in one macro, counting the steps inside Repeat and If.
11pub const MAX_MACRO_STEPS: usize = 10_000;
12/// How deeply Repeat and If may nest (a top-level step is at depth 1).
13pub const MAX_MACRO_DEPTH: usize = 4;
14/// The longest macro or input name, in characters.
15pub const MAX_MACRO_NAME: usize = 255;
16/// The longest macro description, step note, Stop message or If value (as JSON), in characters.
17pub const MAX_MACRO_TEXT: usize = 4_096;
18/// The longest reference or token in a macro (an output anchor, an input's default range, a
19/// Repeat's range, an If's cell), in characters.
20pub const MAX_MACRO_REF: usize = 1_024;
21/// The longest shortcut text (`Ctrl+Shift+M`), in characters.
22pub const MAX_MACRO_SHORTCUT: usize = 64;
23/// The most commands a macro's permissions list.
24pub const MAX_MACRO_PERMISSIONS: usize = 256;
25/// The most column types an input lists: one per column of a sheet.
26pub const MAX_MACRO_COLUMNS: usize = 16_384;
27
28/// A recorded or hand-made macro: a list of typed steps that runs as one undo step. Most steps run
29/// a GridCraft command; Repeat, If and Stop add bounded control flow. References in step
30/// parameters can be tokens (`"Input.col(2)"`) that are resolved against the declared inputs when
31/// the macro runs, so a macro recorded on one block of data works on another.
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33#[serde(default, rename_all = "camelCase")]
34pub struct Macro {
35    /// Format version ([`MACRO_VERSION`]).
36    pub v: u32,
37    pub name: String,
38    pub description: String,
39    /// Keyboard shortcut (`Ctrl+Shift+M`) for a user interface to offer; the engine only stores it.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub shortcut: Option<String>,
42    /// The ranges the macro works on, checked before it runs.
43    pub inputs: Vec<NamedInput>,
44    pub steps: Vec<Step>,
45    /// Where results go and what to do when they would land on existing data.
46    pub output: OutputRule,
47    /// Commands that need permission (reading or writing files) which the macro may run.
48    pub permissions: Vec<String>,
49}
50
51impl Default for Macro {
52    fn default() -> Self {
53        Macro {
54            v: MACRO_VERSION,
55            name: String::new(),
56            description: String::new(),
57            shortcut: None,
58            inputs: vec![],
59            steps: vec![],
60            output: OutputRule::default(),
61            permissions: vec![],
62        }
63    }
64}
65
66impl Macro {
67    /// Steps in the macro, counting those inside Repeat and If.
68    pub fn step_count(&self) -> usize {
69        fn count(steps: &[Step]) -> usize {
70            steps.iter().map(|s| 1usize.saturating_add(s.kind.children().map(|b| count(b)).sum::<usize>())).sum()
71        }
72        count(&self.steps)
73    }
74
75    /// Checks every field a file can set: the format version ([`check_version`]); the lengths of
76    /// the name, description, shortcut, notes, messages, references and command ids; the counts of
77    /// steps ([`MAX_MACRO_STEPS`]), inputs, permissions and column types; the nesting depth
78    /// ([`MAX_MACRO_DEPTH`]); and each input's declaration. Command parameters are bounded only by
79    /// the size of the whole macro, which [`MAX_MACRO_STEPS`] and the workbook's item limit cap.
80    /// Every struct is taken apart field by field, so a field added later doesn't compile here
81    /// until someone decides its limit.
82    pub fn check_limits(&self) -> Result<(), String> {
83        let Macro { v, name, description, shortcut, inputs, steps, output, permissions } = self;
84        check_version(u64::from(*v))?;
85        let name = name.trim();
86        if name.is_empty() {
87            return Err("A macro needs a name.".into());
88        }
89        if name.chars().count() > MAX_MACRO_NAME {
90            return Err(format!("A macro name can be at most {MAX_MACRO_NAME} characters long."));
91        }
92        if name.chars().any(char::is_control) {
93            return Err("A macro name can't hold line breaks or other control characters.".into());
94        }
95        check_text("A macro description", description, MAX_MACRO_TEXT, false)?;
96        if let Some(shortcut) = shortcut {
97            check_text("A shortcut", shortcut, MAX_MACRO_SHORTCUT, true)?;
98        }
99        let OutputRule { anchor, on_conflict: _ } = output;
100        if let Some(anchor) = anchor {
101            check_text("The output anchor", anchor, MAX_MACRO_REF, true)?;
102        }
103        if permissions.len() > MAX_MACRO_PERMISSIONS {
104            return Err(format!("A macro can list at most {MAX_MACRO_PERMISSIONS} permissions."));
105        }
106        for p in permissions {
107            check_id("A permission", p)?;
108        }
109        // Count without recursing past the depth limit (a hostile file can nest deeper).
110        // A Repeat or If at the top is nested 1 deep; one inside it, 2 deep.
111        fn walk(steps: &[Step], level: usize, count: &mut usize) -> Result<(), String> {
112            for s in steps {
113                *count = count.saturating_add(1);
114                if *count > MAX_MACRO_STEPS {
115                    return Err(format!("A macro can have at most {MAX_MACRO_STEPS} steps."));
116                }
117                let Step { kind, enabled: _, breakpoint: _, note } = s;
118                check_text("A step note", note, MAX_MACRO_TEXT, false)?;
119                match kind {
120                    StepKind::Command { id, params: _ } => check_id("A step's command", id)?,
121                    StepKind::Repeat { over, of: _, max: _, body: _ } => check_text("A Repeat step's range", over, MAX_MACRO_REF, true)?,
122                    StepKind::If { cell, op: _, value, then: _, otherwise: _ } => {
123                        check_text("An If step's cell", cell, MAX_MACRO_REF, true)?;
124                        // Empty and NotEmpty ignore the value, which is then null.
125                        if serde_json::to_string(value).map_or(usize::MAX, |t| t.chars().count()) > MAX_MACRO_TEXT {
126                            return Err(format!("An If step's value can be at most {MAX_MACRO_TEXT} characters long."));
127                        }
128                    }
129                    StepKind::Stop { message, rollback: _ } => check_text("A Stop message", message, MAX_MACRO_TEXT, false)?,
130                }
131                if kind.children().next().is_some() && level > MAX_MACRO_DEPTH {
132                    return Err(format!("Repeat and If steps can nest at most {MAX_MACRO_DEPTH} deep."));
133                }
134                for body in kind.children() {
135                    walk(body, level + 1, count)?;
136                }
137            }
138            Ok(())
139        }
140        walk(steps, 1, &mut 0)?;
141        if inputs.len() > 64 {
142            return Err("A macro can declare at most 64 inputs.".into());
143        }
144        for (k, i) in inputs.iter().enumerate() {
145            let NamedInput { name, kind, rows, cols, header, col_types, non_empty: _, default_ref } = i;
146            let n = name.as_str();
147            let ident = n.chars().next().is_some_and(char::is_alphabetic) && n.chars().all(|c| c.is_alphanumeric() || c == '_');
148            if !ident || n.chars().count() > MAX_MACRO_NAME {
149                return Err(format!("'{n}' can't name an input: use letters, digits and _, starting with a letter."));
150            }
151            if ["Output", "Row", "Column", "Name"].iter().any(|r| r.eq_ignore_ascii_case(n)) {
152                return Err(format!("'{n}' is a token of its own and can't name an input."));
153            }
154            // A step written with plain A1 text (a recorded one) must keep meaning the cell.
155            if crate::base::RangeRef::parse(n).is_some() {
156                return Err(format!("'{n}' reads as a cell reference and can't name an input."));
157            }
158            if inputs.iter().take(k).any(|o| o.name.eq_ignore_ascii_case(n)) {
159                return Err(format!("Two inputs are called '{n}'."));
160            }
161            for (what, [least, most]) in [("rows", rows), ("columns", cols)] {
162                if *most != 0 && least > most {
163                    return Err(format!("Input '{n}' allows {least} to {most} {what}: the least can't be more than the most."));
164                }
165            }
166            // A cell is one row and one column ([1, 1], or the default [1, 0]) with no header.
167            let one = |b: &[u32; 2]| *b == [1, 1] || *b == [1, 0];
168            if *kind == InputKind::Cell && (!one(rows) || !one(cols) || *header || col_types.len() > 1) {
169                return Err(format!("Input '{n}' is a single cell: one row and one column, no header, at most one column type."));
170            }
171            let most = if cols[1] == 0 { MAX_MACRO_COLUMNS } else { usize::try_from(cols[1]).unwrap_or(usize::MAX).min(MAX_MACRO_COLUMNS) };
172            if col_types.len() > most {
173                return Err(format!("Input '{n}' lists the types of {} columns, but has at most {most}.", col_types.len()));
174            }
175            if let Some(r) = default_ref {
176                check_text(&format!("The default range of input '{n}'"), r, MAX_MACRO_REF, true)?;
177            }
178        }
179        Ok(())
180    }
181}
182
183/// Refuses `text` longer than `max` characters, and, for a one-line field, one that holds line
184/// breaks or other control characters.
185fn check_text(what: &str, text: &str, max: usize, one_line: bool) -> Result<(), String> {
186    if text.chars().count() > max {
187        return Err(format!("{what} can be at most {max} characters long."));
188    }
189    if one_line && text.chars().any(char::is_control) {
190        return Err(format!("{what} can't hold line breaks or other control characters."));
191    }
192    Ok(())
193}
194
195/// Refuses a command id that isn't one: empty, longer than a name, or holding anything other than
196/// letters, digits, `.` and `_` (every command id is of that form).
197fn check_id(what: &str, id: &str) -> Result<(), String> {
198    let ok = !id.is_empty() && id.len() <= MAX_MACRO_NAME && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_');
199    if ok { Ok(()) } else { Err(format!("{what} must be a command id (letters, digits, . and _): '{}'", id.chars().take(64).collect::<String>())) }
200}
201
202/// The macros of a workbook. Workbook snapshots share the list until one of them changes it, so
203/// an undo entry or a copy made for a cell edit holds the same list rather than a copy of every
204/// step. Reads go through `Deref` to the list; a write through `DerefMut` first copies the list
205/// when another snapshot shares it.
206#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
207#[serde(transparent)]
208pub struct Macros(std::sync::Arc<Vec<Macro>>);
209
210impl Macros {
211    pub fn is_empty(&self) -> bool {
212        self.0.is_empty()
213    }
214
215    /// Whether both hold the one list, with no copy made between them.
216    pub fn shares(&self, other: &Macros) -> bool {
217        std::sync::Arc::ptr_eq(&self.0, &other.0)
218    }
219
220    /// A rough size of the list in memory, for the undo budget: each macro's JSON, which holds
221    /// every name, text and step parameter, plus its structs. An undo step that keeps a copy of
222    /// the list (after a save, rename or delete) is charged for all of it. The JSON is counted as
223    /// it is written, without building it.
224    pub fn estimated_bytes(&self) -> usize {
225        /// Counts the bytes written to it.
226        struct Count(usize);
227        impl std::io::Write for Count {
228            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
229                self.0 = self.0.saturating_add(buf.len());
230                Ok(buf.len())
231            }
232            fn flush(&mut self) -> std::io::Result<()> {
233                Ok(())
234            }
235        }
236        self.0.iter().fold(0usize, |bytes, m| {
237            let mut json = Count(0);
238            // Writing to a counter fails only for a value JSON can't hold, which a macro never is.
239            let _ = serde_json::to_writer(&mut json, m);
240            let steps = m.step_count().saturating_mul(std::mem::size_of::<Step>());
241            bytes.saturating_add(std::mem::size_of::<Macro>()).saturating_add(json.0).saturating_add(steps)
242        })
243    }
244}
245
246impl From<Vec<Macro>> for Macros {
247    fn from(list: Vec<Macro>) -> Macros {
248        Macros(std::sync::Arc::new(list))
249    }
250}
251
252impl std::ops::Deref for Macros {
253    type Target = Vec<Macro>;
254    fn deref(&self) -> &Vec<Macro> {
255        &self.0
256    }
257}
258
259impl std::ops::DerefMut for Macros {
260    fn deref_mut(&mut self) -> &mut Vec<Macro> {
261        std::sync::Arc::make_mut(&mut self.0)
262    }
263}
264
265impl IntoIterator for Macros {
266    type Item = Macro;
267    type IntoIter = std::vec::IntoIter<Macro>;
268    fn into_iter(self) -> Self::IntoIter {
269        std::sync::Arc::unwrap_or_clone(self.0).into_iter()
270    }
271}
272
273impl<'a> IntoIterator for &'a Macros {
274    type Item = &'a Macro;
275    type IntoIter = std::slice::Iter<'a, Macro>;
276    fn into_iter(self) -> Self::IntoIter {
277        self.0.iter()
278    }
279}
280
281impl FromIterator<Macro> for Macros {
282    fn from_iter<I: IntoIterator<Item = Macro>>(iter: I) -> Macros {
283        Macros(std::sync::Arc::new(iter.into_iter().collect()))
284    }
285}
286
287/// Refuses a format version newer than [`MACRO_VERSION`]: this build would drop the fields it
288/// doesn't know, and a save would then write the rest back as if it were the whole macro.
289pub fn check_version(v: u64) -> Result<(), String> {
290    if v > u64::from(MACRO_VERSION) {
291        return Err(format!("This macro was made by a newer version of GridCraft (macro format {v}); this version reads format {MACRO_VERSION}."));
292    }
293    Ok(())
294}
295
296/// Reads the `macros` field of a JSON workbook as raw entries, whatever they hold, so they can be
297/// checked one at a time.
298pub(crate) fn entries<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<serde_json::Value>, D::Error> {
299    Ok(match serde_json::Value::deserialize(d)? {
300        serde_json::Value::Array(list) => list,
301        serde_json::Value::Null => vec![],
302        other => vec![other],
303    })
304}
305
306/// A range the macro works on, with the shape and content it expects.
307#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
308#[serde(default, rename_all = "camelCase")]
309pub struct NamedInput {
310    /// The token that stands for it in step parameters (`Input`, `Rates`).
311    pub name: String,
312    pub kind: InputKind,
313    /// Allowed data rows (header excluded), `[min, max]`; a max of 0 means no limit.
314    pub rows: [u32; 2],
315    /// Allowed columns, `[min, max]`; a max of 0 means no limit.
316    pub cols: [u32; 2],
317    /// The first row holds column names.
318    pub header: bool,
319    /// What each column must hold, first column first; columns past the list may hold anything.
320    pub col_types: Vec<ColType>,
321    /// Every data cell must have something in it.
322    pub non_empty: bool,
323    /// The range used when a run doesn't name one (else the current selection, for the first input).
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub default_ref: Option<String>,
326}
327
328impl Default for NamedInput {
329    fn default() -> Self {
330        NamedInput {
331            name: "Input".into(),
332            kind: InputKind::Range,
333            rows: [1, 0],
334            cols: [1, 0],
335            header: false,
336            col_types: vec![],
337            non_empty: false,
338            default_ref: None,
339        }
340    }
341}
342
343#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub enum InputKind {
346    /// A single cell.
347    Cell,
348    #[default]
349    Range,
350}
351
352/// What a column of an input must hold. Empty cells pass unless the input is `non_empty`.
353#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub enum ColType {
356    Number,
357    Text,
358    /// A number shown with a date or time format.
359    Date,
360    Bool,
361    #[default]
362    Any,
363}
364
365impl ColType {
366    /// The plural noun used in messages ("must hold numbers").
367    pub fn noun(self) -> &'static str {
368        match self {
369            ColType::Number => "numbers",
370            ColType::Text => "text",
371            ColType::Date => "dates",
372            ColType::Bool => "TRUE or FALSE",
373            ColType::Any => "anything",
374        }
375    }
376}
377
378/// One macro step and its editor flags.
379#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
380pub struct Step {
381    #[serde(flatten)]
382    pub kind: StepKind,
383    /// A disabled step is skipped.
384    #[serde(default = "yes", skip_serializing_if = "is_true")]
385    pub enabled: bool,
386    /// The debugger stops before this step.
387    #[serde(default, skip_serializing_if = "is_false")]
388    pub breakpoint: bool,
389    #[serde(default, skip_serializing_if = "String::is_empty")]
390    pub note: String,
391}
392
393fn yes() -> bool {
394    true
395}
396fn is_true(b: &bool) -> bool {
397    *b
398}
399fn is_false(b: &bool) -> bool {
400    !*b
401}
402
403impl Step {
404    pub fn new(kind: StepKind) -> Step {
405        Step { kind, enabled: true, breakpoint: false, note: String::new() }
406    }
407
408    /// A step that runs command `id` with `params`.
409    pub fn command(id: impl Into<String>, params: serde_json::Value) -> Step {
410        Step::new(StepKind::Command { id: id.into(), params })
411    }
412}
413
414#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
415#[serde(tag = "type", rename_all = "camelCase")]
416pub enum StepKind {
417    /// Runs a GridCraft command. String parameters may hold tokens, resolved when the step runs.
418    Command {
419        id: String,
420        #[serde(default)]
421        params: serde_json::Value,
422    },
423    /// Runs `body` once for each row (or column) of the range `over`, a token or reference; inside
424    /// it, `Row` (or `Column`) stands for the current one. At most `max` times.
425    Repeat {
426        over: String,
427        #[serde(default)]
428        of: RepeatOf,
429        #[serde(default = "repeat_max")]
430        max: u32,
431        #[serde(default)]
432        body: Vec<Step>,
433    },
434    /// Compares the value of `cell` (a token or reference to one cell) with `value` and runs
435    /// `then` or `else`.
436    If {
437        cell: String,
438        op: CompareOp,
439        #[serde(default)]
440        value: serde_json::Value,
441        #[serde(default)]
442        then: Vec<Step>,
443        #[serde(default, rename = "else")]
444        otherwise: Vec<Step>,
445    },
446    /// Ends the run with a message. The changes made so far are kept, or undone with `rollback`.
447    Stop {
448        #[serde(default)]
449        message: String,
450        #[serde(default, skip_serializing_if = "is_false")]
451        rollback: bool,
452    },
453}
454
455fn repeat_max() -> u32 {
456    10_000
457}
458
459impl StepKind {
460    /// The step lists inside this step (Repeat's body, If's branches).
461    pub fn children(&self) -> impl Iterator<Item = &Vec<Step>> {
462        let (a, b) = match self {
463            StepKind::Repeat { body, .. } => (Some(body), None),
464            StepKind::If { then, otherwise, .. } => (Some(then), Some(otherwise)),
465            _ => (None, None),
466        };
467        a.into_iter().chain(b)
468    }
469}
470
471#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub enum RepeatOf {
474    #[default]
475    Rows,
476    Columns,
477}
478
479#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub enum CompareOp {
482    Eq,
483    Ne,
484    Gt,
485    Ge,
486    Lt,
487    Le,
488    /// The cell is empty (`value` is ignored).
489    Empty,
490    NotEmpty,
491    /// The cell's text contains `value`'s text, ignoring case.
492    Contains,
493}
494
495/// Where a macro's results go.
496#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
497#[serde(default, rename_all = "camelCase")]
498pub struct OutputRule {
499    /// What the `Output` token stands for: a token (`Input.offset(0,4)`), a reference or `Name:X`.
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub anchor: Option<String>,
502    /// What to do when the cells the macro writes, outside its inputs, already hold data.
503    pub on_conflict: OnConflict,
504}
505
506#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
507#[serde(rename_all = "camelCase")]
508pub enum OnConflict {
509    /// Ask first: overwrite, shift the data down or right, or cancel. Programmatic runs fail.
510    #[default]
511    Ask,
512    Fail,
513    Overwrite,
514    ShiftDown,
515    ShiftRight,
516}
517
518impl OnConflict {
519    pub fn parse(s: &str) -> Option<OnConflict> {
520        Some(match s.to_ascii_lowercase().replace(['_', '-', ' '], "").as_str() {
521            "ask" => OnConflict::Ask,
522            "fail" | "cancel" => OnConflict::Fail,
523            "overwrite" => OnConflict::Overwrite,
524            "shiftdown" => OnConflict::ShiftDown,
525            "shiftright" => OnConflict::ShiftRight,
526            _ => return None,
527        })
528    }
529
530    pub fn name(self) -> &'static str {
531        match self {
532            OnConflict::Ask => "ask",
533            OnConflict::Fail => "fail",
534            OnConflict::Overwrite => "overwrite",
535            OnConflict::ShiftDown => "shiftDown",
536            OnConflict::ShiftRight => "shiftRight",
537        }
538    }
539}