Skip to main content

ravenrs_app/
record_action.rs

1//! Actions declared as data: on the records selected, they set fields to expressions, check the
2//! rules and keep what passes. No code runs but the expressions.
3
4use ravenrs_criteria::{Criteria, Scope};
5
6use crate::{
7    Action, ActionCategory, CONTEXT_SAVE, CriteriaMode, EntityInfo, ModelError, Module, Operation, RuleWords,
8    Selection, SpaceError, ViewController, Violation,
9};
10
11/// An action of an entity that sets fields of the records selected.
12///
13/// ```
14/// use ravenrs_app::{EntityInfo, FieldInfo, FieldKind, RecordAction};
15///
16/// let task = EntityInfo::new("Task")
17///     .field(FieldInfo::new("id", FieldKind::Integer).key())
18///     .field(FieldInfo::new("state", FieldKind::Text))
19///     .action(
20///         RecordAction::new("Finish")
21///             .enabled("[state] <> 'done'")
22///             .set("state = 'done'")
23///             .confirm("Finish the tasks?"),
24///     );
25/// assert!(task.check().is_ok());
26/// ```
27#[derive(Clone, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
30pub struct RecordAction {
31    /// What its button says.
32    pub caption: String,
33    /// Its id; `Entity.Caption` unless given.
34    pub id: Option<String>,
35    /// How many records it takes: one or more unless given.
36    pub selection: Selection,
37    /// An expression every record selected has to meet for it to be on.
38    pub enabled: Option<String>,
39    /// What it sets: `field = expression; ...`, each read from the record as it was.
40    pub set: String,
41    /// The question asked before it runs.
42    pub confirm: Option<String>,
43}
44
45#[cfg_attr(feature = "live", ravenrs_live::expose)]
46impl RecordAction {
47    /// An action with this caption, on one record or more, that sets nothing yet.
48    pub fn new(caption: impl Into<String>) -> Self {
49        Self {
50            caption: caption.into(),
51            id: None,
52            selection: Selection::RequireMultiple,
53            enabled: None,
54            set: String::new(),
55            confirm: None,
56        }
57    }
58
59    /// Gives it an id of its own.
60    pub fn id(mut self, id: impl Into<String>) -> Self {
61        self.id = Some(id.into());
62        self
63    }
64
65    /// How many records it takes.
66    pub fn selection(mut self, selection: Selection) -> Self {
67        self.selection = selection;
68        self
69    }
70
71    /// On only while every record selected meets this expression.
72    pub fn enabled(mut self, expression: impl Into<String>) -> Self {
73        self.enabled = Some(expression.into());
74        self
75    }
76
77    /// What it sets, as `field = expression; ...`.
78    pub fn set(mut self, assignments: impl Into<String>) -> Self {
79        self.set = assignments.into();
80        self
81    }
82
83    /// Asks this before it runs.
84    pub fn confirm(mut self, question: impl Into<String>) -> Self {
85        self.confirm = Some(question.into());
86        self
87    }
88
89    /// Its id on this entity.
90    pub fn id_on(&self, entity: &EntityInfo) -> String {
91        self.id.clone().unwrap_or_else(|| format!("{}.{}", entity.name, self.caption))
92    }
93
94    /// The action that does it on this entity.
95    pub fn to_action(&self, entity: &EntityInfo) -> Action {
96        let mut action = Action::simple(self.id_on(entity), self.caption.clone())
97            .category(ActionCategory::Edit)
98            .target_entity(entity.name.clone())
99            .selection(self.selection)
100            .requires(Operation::Write);
101        if let Some(enabled) = &self.enabled {
102            action = action.target_criteria(enabled.clone(), CriteriaMode::TrueForAll);
103        }
104        if let Some(question) = &self.confirm {
105            action = action.confirmation(question.clone());
106        }
107        action.assign(entity, &self.set)
108    }
109
110    /// Says what is wrong with it on this entity: an expression that does not parse, or a field
111    /// it has not.
112    pub(crate) fn check(&self, entity: &EntityInfo) -> Result<(), ModelError> {
113        let id = self.id_on(entity);
114        let wrong = |what: String| ModelError(format!("the action {id}: {what}"));
115        if let Some(enabled) = &self.enabled {
116            Criteria::parse(enabled).map_err(|e| wrong(e.to_string()))?;
117        }
118        assignments(entity, &self.set).map(drop).map_err(wrong)
119    }
120}
121
122/// The assignments of an action, with the fields checked against the entity.
123pub(crate) fn assignments(entity: &EntityInfo, text: &str) -> Result<Vec<(String, Criteria)>, String> {
124    let found = Criteria::assignments(text).map_err(|e| e.to_string())?;
125    if found.is_empty() {
126        return Err("it sets nothing".to_owned());
127    }
128    for (field, _) in &found {
129        if !entity.fields.iter().any(|known| &known.name == field) {
130            return Err(format!("{} has no field {field}", entity.name));
131        }
132    }
133    Ok(found)
134}
135
136/// Sets the fields of every record selected, checks each against its rules and keeps them all,
137/// or none.
138pub(crate) fn assign(
139    entity: &EntityInfo,
140    set: &[(String, Criteria)],
141    cx: &crate::ActionContext,
142) -> Result<(), String> {
143    let key = entity.key_field().map(|key| key.name.clone()).unwrap_or_default();
144    let words = cx.service::<RuleWords>().map(|words| (*words).clone()).unwrap_or_default();
145    let environment = crate::environment().at(crate::local_now());
146    let mut space = cx.space.borrow_mut();
147    let result = (|| {
148        for record in &cx.selected {
149            let mut stored =
150                space.find(&entity.name, &record.get(&key)).map_err(|e| e.to_string())?.ok_or_else(|| {
151                    SpaceError::Conflict(format!("a record of {} is gone", entity.name)).to_string()
152                })?;
153            let values = {
154                let scope = Scope::new(&environment).row(&stored);
155                set.iter()
156                    .map(|(field, expression)| {
157                        scope
158                            .evaluate(expression)
159                            .map(|value| (field, value))
160                            .map_err(|e| format!("{field}: {e}"))
161                    })
162                    .collect::<Result<Vec<_>, String>>()?
163            };
164            for (field, value) in values {
165                stored.set(field, value);
166            }
167            let wrong: Vec<String> = crate::validate(entity, &stored, CONTEXT_SAVE, &*space, &words)
168                .into_iter()
169                .filter(Violation::is_error)
170                .map(|violation| violation.message)
171                .collect();
172            if !wrong.is_empty() {
173                return Err(wrong.join("; "));
174            }
175            space.save(stored).map_err(|e| e.to_string())?;
176        }
177        space.commit().map_err(|e| e.to_string())
178    })();
179    if result.is_err() {
180        space.rollback();
181    }
182    drop(space);
183    if result.is_ok() {
184        cx.records_changed();
185    }
186    result
187}
188
189struct Declared {
190    actions: Vec<Action>,
191}
192
193impl ViewController for Declared {
194    fn id(&self) -> &str {
195        "RecordActions"
196    }
197
198    fn actions(&self) -> Vec<Action> {
199        self.actions.clone()
200    }
201}
202
203/// What gives the entities their declared actions. It comes by itself.
204#[derive(Debug)]
205pub(crate) struct RecordActionModule(pub(crate) Vec<EntityInfo>);
206
207impl Module for RecordActionModule {
208    fn name(&self) -> &str {
209        "RecordActions"
210    }
211
212    fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
213        let actions = self
214            .0
215            .iter()
216            .flat_map(|entity| entity.actions.iter().map(move |declared| declared.to_action(entity)))
217            .collect();
218        vec![Box::new(Declared { actions })]
219    }
220}