1#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Field {
14 pub label: &'static str,
15 pub value: String,
16 pub hint: &'static str,
18 pub required: bool,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Form {
23 pub title: &'static str,
24 pub fields: Vec<Field>,
25 pub cursor: usize,
27}
28
29impl Form {
30 pub fn new_schedule() -> Self {
36 let f = |label, hint, required| Field {
37 label,
38 value: String::new(),
39 hint,
40 required,
41 };
42 Form {
43 title: "new schedule",
44 fields: vec![
45 f("schedule id", "nightly-recon", true),
46 f("workflow id", "recon", true),
47 f("workflow type", "OrderWorkflow", true),
48 f("task queue", "demo-tq", true),
49 f("spec", "0 2 * * * or @every 1h", true),
50 f("input", "optional JSON", false),
51 ],
52 cursor: 0,
53 }
54 }
55
56 pub fn next(&mut self) {
58 if !self.fields.is_empty() {
59 self.cursor = (self.cursor + 1) % self.fields.len();
60 }
61 }
62
63 pub fn previous(&mut self) {
64 if !self.fields.is_empty() {
65 self.cursor = (self.cursor + self.fields.len() - 1) % self.fields.len();
66 }
67 }
68
69 pub fn push(&mut self, c: char) {
70 if let Some(f) = self.fields.get_mut(self.cursor) {
71 f.value.push(c);
72 }
73 }
74
75 pub fn backspace(&mut self) -> bool {
78 self.fields
79 .get_mut(self.cursor)
80 .and_then(|f| f.value.pop())
81 .is_some()
82 }
83
84 pub fn get(&self, label: &str) -> &str {
86 self.fields
87 .iter()
88 .find(|f| f.label == label)
89 .map(|f| f.value.trim())
90 .unwrap_or("")
91 }
92
93 pub fn missing(&self) -> Option<&'static str> {
98 self.fields
99 .iter()
100 .find(|f| f.required && f.value.trim().is_empty())
101 .map(|f| f.label)
102 }
103
104 pub fn focus(&mut self, label: &str) {
106 if let Some(i) = self.fields.iter().position(|f| f.label == label) {
107 self.cursor = i;
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn a_new_schedule_asks_for_everything_the_server_requires() {
118 let f = Form::new_schedule();
122 let required: Vec<&str> = f
123 .fields
124 .iter()
125 .filter(|f| f.required)
126 .map(|f| f.label)
127 .collect();
128 assert_eq!(
129 required,
130 [
131 "schedule id",
132 "workflow id",
133 "workflow type",
134 "task queue",
135 "spec"
136 ]
137 );
138 }
139
140 #[test]
141 fn typing_lands_on_the_focused_field_only() {
142 let mut f = Form::new_schedule();
143 for c in "nightly".chars() {
144 f.push(c);
145 }
146 f.next();
147 for c in "recon".chars() {
148 f.push(c);
149 }
150 assert_eq!(f.get("schedule id"), "nightly");
151 assert_eq!(f.get("workflow id"), "recon");
152 }
153
154 #[test]
155 fn the_caret_wraps_in_both_directions() {
156 let mut f = Form::new_schedule();
157 let last = f.fields.len() - 1;
158 f.previous();
159 assert_eq!(f.cursor, last, "back from the first goes to the last");
160 f.next();
161 assert_eq!(f.cursor, 0);
162 }
163
164 #[test]
165 fn backspace_says_whether_it_removed_anything() {
166 let mut f = Form::new_schedule();
167 assert!(!f.backspace(), "nothing to delete on an empty field");
168 f.push('x');
169 assert!(f.backspace());
170 assert_eq!(f.get("schedule id"), "");
171 }
172
173 #[test]
174 fn a_missing_required_field_is_named_rather_than_merely_counted() {
175 let mut f = Form::new_schedule();
177 assert_eq!(f.missing(), Some("schedule id"));
178 for (label, v) in [
179 ("schedule id", "nightly"),
180 ("workflow id", "recon"),
181 ("workflow type", "OrderWorkflow"),
182 ("task queue", "demo-tq"),
183 ] {
184 f.focus(label);
185 for c in v.chars() {
186 f.push(c);
187 }
188 }
189 assert_eq!(f.missing(), Some("spec"));
190 f.focus("spec");
191 for c in "0 2 * * *".chars() {
192 f.push(c);
193 }
194 assert_eq!(f.missing(), None, "input is optional");
195 }
196
197 #[test]
198 fn whitespace_alone_does_not_satisfy_a_required_field() {
199 let mut f = Form::new_schedule();
200 f.push(' ');
201 assert_eq!(f.missing(), Some("schedule id"));
202 }
203}