Skip to main content

datui_lib/analysis/
intent_modal.rs

1//! The form Data Quality Setup opens on one column to declare what it must hold:
2//! part of the key, required, read as a number, an allowed set, a range. It stages
3//! into Setup's draft, which only Run reads with.
4
5use crate::analysis::data_quality::TimeInterpretation;
6use crate::analysis::quality_intent::{
7    ColumnIntent, DeclaredIntent, NumberReading, ValueKind, allows_set, check_intent,
8    format_allowed, parse_allowed, reads_as_number,
9};
10use crate::widgets::text_input::TextInput;
11use polars::prelude::DataType;
12
13/// The form's rows, top to bottom. Which are shown depends on the column's type.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum IntentField {
16    Key,
17    Required,
18    ReadAs,
19    Allowed,
20    Minimum,
21    Maximum,
22}
23
24impl IntentField {
25    pub fn label(self) -> &'static str {
26        match self {
27            Self::Key => "Key:",
28            Self::Required => "Required:",
29            Self::ReadAs => "Read as:",
30            Self::Allowed => "Allowed:",
31            Self::Minimum => "Minimum:",
32            Self::Maximum => "Maximum:",
33        }
34    }
35
36    pub fn is_text(self) -> bool {
37        matches!(self, Self::Allowed | Self::Minimum | Self::Maximum)
38    }
39}
40
41/// One column's declaration, being edited.
42pub struct IntentForm {
43    pub column: String,
44    pub dtype: DataType,
45    /// How Text as time reads the column, when it does: the reading here, set there.
46    pub time: Option<TimeInterpretation>,
47    pub field: IntentField,
48    pub in_key: bool,
49    pub required: bool,
50    pub number: Option<NumberReading>,
51    pub allowed: TextInput,
52    pub min: TextInput,
53    pub max: TextInput,
54    /// Why Enter did not apply, until the next edit.
55    pub error: Option<String>,
56}
57
58impl IntentForm {
59    pub fn new(
60        column: &str,
61        dtype: DataType,
62        time: Option<TimeInterpretation>,
63        declared: &DeclaredIntent,
64        theme: &crate::config::Theme,
65    ) -> Self {
66        let input = || TextInput::new().with_theme(theme);
67        let intent = declared
68            .column(column)
69            .cloned()
70            .unwrap_or_else(|| ColumnIntent::new(column));
71        let mut form = Self {
72            column: column.to_string(),
73            dtype,
74            time,
75            field: IntentField::Key,
76            in_key: declared.key.iter().any(|name| name == column),
77            required: intent.required,
78            number: intent.number,
79            allowed: input(),
80            min: input(),
81            max: input(),
82            error: None,
83        };
84        form.allowed.set_value(format_allowed(&intent.allowed));
85        form.min.set_value(intent.min.unwrap_or_default());
86        form.max.set_value(intent.max.unwrap_or_default());
87        form.sync_focus();
88        form
89    }
90
91    /// What the column's values are, read as the form says now.
92    pub fn value_kind(&self) -> ValueKind {
93        ValueKind::of(&self.dtype, self.number, self.time.as_ref())
94    }
95
96    /// The rows this column takes: a set where values are codes, a reading where it
97    /// is text, a range where values have an order.
98    pub fn fields(&self) -> Vec<IntentField> {
99        let mut fields = vec![IntentField::Key, IntentField::Required];
100        if reads_as_number(&self.dtype) {
101            fields.push(IntentField::ReadAs);
102        }
103        if allows_set(&self.dtype) {
104            fields.push(IntentField::Allowed);
105        }
106        if self.value_kind().ranges() {
107            fields.extend([IntentField::Minimum, IntentField::Maximum]);
108        }
109        fields
110    }
111
112    /// Space, or ←→, on a row that is a choice: the key and required boxes, and the
113    /// reading, which a Text as time format holds when there is one.
114    pub fn adjust(&mut self, forward: bool) {
115        match self.field {
116            IntentField::Key => self.in_key = !self.in_key,
117            IntentField::Required => self.required = !self.required,
118            IntentField::ReadAs if self.time.is_none() => {
119                let choices = [
120                    None,
121                    Some(NumberReading::Whole),
122                    Some(NumberReading::Decimal),
123                ];
124                let at = choices
125                    .iter()
126                    .position(|choice| *choice == self.number)
127                    .unwrap_or(0);
128                let next = if forward {
129                    (at + 1) % choices.len()
130                } else {
131                    (at + choices.len() - 1) % choices.len()
132                };
133                self.number = choices[next];
134            }
135            _ => {}
136        }
137        self.error = None;
138    }
139
140    /// The reading as the row echoes it.
141    pub fn reading_label(&self) -> String {
142        match (&self.time, self.number) {
143            (Some(time), _) => format!("{} (Text as time)", time.label()),
144            (None, Some(number)) => number.label().to_string(),
145            (None, None) => "as stored".to_string(),
146        }
147    }
148
149    pub fn input(&self, field: IntentField) -> Option<&TextInput> {
150        match field {
151            IntentField::Allowed => Some(&self.allowed),
152            IntentField::Minimum => Some(&self.min),
153            IntentField::Maximum => Some(&self.max),
154            _ => None,
155        }
156    }
157
158    pub fn input_mut(&mut self) -> Option<&mut TextInput> {
159        match self.field {
160            IntentField::Allowed => Some(&mut self.allowed),
161            IntentField::Minimum => Some(&mut self.min),
162            IntentField::Maximum => Some(&mut self.max),
163            _ => None,
164        }
165    }
166
167    /// The text field under the cursor shows its cursor; the others do not.
168    pub fn sync_focus(&mut self) {
169        let field = self.field;
170        self.allowed.set_focused(field == IntentField::Allowed);
171        self.min.set_focused(field == IntentField::Minimum);
172        self.max.set_focused(field == IntentField::Maximum);
173    }
174
175    /// Whether the cursor is in a text field, so typed keys are text.
176    pub fn typing(&self) -> bool {
177        self.field.is_text()
178    }
179
180    /// The declaration as the form stands, and whether the column is in the key; or
181    /// why it cannot be measured. Rows the type does not take are left out.
182    pub fn finish(&self) -> Result<(bool, ColumnIntent), String> {
183        let fields = self.fields();
184        let text = |field: IntentField, input: &TextInput| {
185            let value = input.value().trim();
186            (fields.contains(&field) && !value.is_empty()).then(|| value.to_string())
187        };
188        let allowed = if fields.contains(&IntentField::Allowed) {
189            parse_allowed(&self.dtype, self.allowed.value())?
190        } else {
191            Vec::new()
192        };
193        let intent = ColumnIntent {
194            column: self.column.clone(),
195            required: self.required,
196            allowed,
197            min: text(IntentField::Minimum, &self.min),
198            max: text(IntentField::Maximum, &self.max),
199            number: self.number.filter(|_| self.time.is_none()),
200        };
201        check_intent(&intent, &self.dtype, self.time.as_ref())?;
202        Ok((self.in_key, intent))
203    }
204
205    /// Put the declaration into `declared`, or say why not.
206    pub fn apply(&self, declared: &mut DeclaredIntent) -> Result<(), String> {
207        let (in_key, intent) = self.finish()?;
208        declared.set_key(&self.column, in_key);
209        declared.set(intent);
210        Ok(())
211    }
212}
213
214impl crate::app::form::Form for IntentForm {
215    type Field = IntentField;
216
217    fn fields(&self) -> Vec<(IntentField, crate::app::form::FieldKind)> {
218        use crate::app::form::FieldKind;
219        IntentForm::fields(self)
220            .into_iter()
221            .map(|field| {
222                let kind = match field {
223                    IntentField::Key | IntentField::Required => FieldKind::Checkbox,
224                    field if field.is_text() => FieldKind::Text,
225                    _ => FieldKind::Choice,
226                };
227                (field, kind)
228            })
229            .collect()
230    }
231
232    fn focused(&self) -> IntentField {
233        self.field
234    }
235
236    fn set_focused(&mut self, field: IntentField) {
237        self.field = field;
238        self.sync_focus();
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    fn theme() -> crate::config::Theme {
247        crate::config::Theme::from_config(&crate::config::AppConfig::default().theme).unwrap()
248    }
249
250    /// The rows follow the type: text takes a reading and a set, and a range once it
251    /// reads as a number; a float takes a range and no set.
252    #[test]
253    fn rows_follow_the_columns_type() {
254        let declared = DeclaredIntent::default();
255        let mut text = IntentForm::new("code", DataType::String, None, &declared, &theme());
256        assert_eq!(
257            text.fields(),
258            vec![
259                IntentField::Key,
260                IntentField::Required,
261                IntentField::ReadAs,
262                IntentField::Allowed
263            ]
264        );
265        text.field = IntentField::ReadAs;
266        text.adjust(true);
267        assert_eq!(text.number, Some(NumberReading::Whole));
268        assert!(text.fields().contains(&IntentField::Minimum));
269        let float = IntentForm::new("amount", DataType::Float64, None, &declared, &theme());
270        assert_eq!(
271            float.fields(),
272            vec![
273                IntentField::Key,
274                IntentField::Required,
275                IntentField::Minimum,
276                IntentField::Maximum
277            ]
278        );
279    }
280
281    /// Apply stages the declaration; a bound the type cannot read is refused with
282    /// the reason, and nothing changes.
283    #[test]
284    fn apply_stages_or_says_why_not() {
285        let mut declared = DeclaredIntent::default();
286        let mut form = IntentForm::new("amount", DataType::Float64, None, &declared, &theme());
287        form.adjust(true);
288        form.field = IntentField::Minimum;
289        form.min.set_value("ten");
290        assert!(form.apply(&mut declared).is_err());
291        assert!(declared.is_empty());
292        form.min.set_value("10");
293        form.apply(&mut declared).unwrap();
294        assert_eq!(declared.key, vec!["amount"]);
295        assert_eq!(declared.columns[0].min.as_deref(), Some("10"));
296        // Reopened, the form shows what was staged.
297        let again = IntentForm::new("amount", DataType::Float64, None, &declared, &theme());
298        assert!(again.in_key);
299        assert_eq!(again.min.value(), "10");
300    }
301}