1use 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#[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
41pub struct IntentForm {
43 pub column: String,
44 pub dtype: DataType,
45 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 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 pub fn value_kind(&self) -> ValueKind {
93 ValueKind::of(&self.dtype, self.number, self.time.as_ref())
94 }
95
96 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 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 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 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 pub fn typing(&self) -> bool {
177 self.field.is_text()
178 }
179
180 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 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 #[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 #[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 let again = IntentForm::new("amount", DataType::Float64, None, &declared, &theme());
298 assert!(again.in_key);
299 assert_eq!(again.min.value(), "10");
300 }
301}