1use serde_json::{Map, Value, json};
4
5const NOTES: [&str; 3] = ["title", "description", "$comment"];
7const NUMERIC: [&str; 6] = [
8 "type",
9 "minimum",
10 "maximum",
11 "exclusiveMinimum",
12 "exclusiveMaximum",
13 "multipleOf",
14];
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Key {
19 Char(char),
20 Backspace,
21 Delete,
22 Left,
23 Right,
24 Up,
25 Down,
26 Home,
27 End,
28 Enter,
29 Tab,
30 BackTab,
31 Other,
33}
34
35#[derive(Debug, Clone, PartialEq)]
36pub enum Input {
37 Fixed(Value),
39 Bool(Option<bool>),
41 Choice(Vec<String>, Option<usize>),
43 Text(String),
44 Integer(String),
45 Number(String),
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub struct Field {
50 pub name: String,
51 pub required: bool,
52 pub hint: String,
54 pub input: Input,
55}
56
57impl Field {
58 fn value(&self) -> Result<Option<Value>, String> {
60 let number = |text: &str, integer: bool| {
61 let value: Value = serde_json::from_str(text.trim()).unwrap_or_default();
62 let valid = if integer {
63 value.is_i64() || value.is_u64()
64 } else {
65 value.is_number()
66 };
67 let kind = if integer { "an integer" } else { "a number" };
68 valid
69 .then_some(value)
70 .ok_or_else(|| format!("{}: enter {kind}.", self.name))
71 };
72 Ok(match &self.input {
73 Input::Fixed(value) => Some(value.clone()),
74 Input::Bool(value) => value.map(Value::Bool),
75 Input::Choice(options, index) => {
76 index.map(|index| Value::String(options[index].clone()))
77 }
78 Input::Text(text) => (self.required || !text.is_empty()).then(|| json!(text)),
79 Input::Integer(text) | Input::Number(text) if text.trim().is_empty() => None,
80 Input::Integer(text) => Some(number(text, true)?),
81 Input::Number(text) => Some(number(text, false)?),
82 })
83 }
84
85 pub fn display(&self) -> String {
87 match &self.input {
88 Input::Fixed(value) => format!("{value} (fixed)"),
89 Input::Bool(None) | Input::Choice(_, None) => "–".into(),
90 Input::Bool(Some(value)) => value.to_string(),
91 Input::Choice(options, Some(index)) => {
92 Value::String(options[*index].clone()).to_string()
93 }
94 Input::Text(text) | Input::Integer(text) | Input::Number(text) => text.clone(),
95 }
96 }
97
98 fn key(&mut self, key: Key) {
99 let required = self.required;
100 match (&mut self.input, key) {
101 (Input::Text(text) | Input::Integer(text) | Input::Number(text), Key::Char(c)) => {
102 text.push(c)
103 }
104 (Input::Text(text) | Input::Integer(text) | Input::Number(text), Key::Backspace) => {
105 text.pop();
106 }
107 (Input::Bool(value), Key::Char('t' | 'y')) => *value = Some(true),
108 (Input::Bool(value), Key::Char('f' | 'n')) => *value = Some(false),
109 (Input::Bool(value), Key::Char(' ') | Key::Left | Key::Right) => {
110 *value = match value {
111 None => Some(true),
112 Some(true) => Some(false),
113 Some(false) if required => Some(true),
114 Some(false) => None,
115 }
116 }
117 (Input::Choice(options, index), Key::Char(' ') | Key::Right) => {
118 *index = match *index {
119 None => Some(0),
120 Some(i) if i + 1 < options.len() => Some(i + 1),
121 Some(_) if required => Some(0),
122 Some(_) => None,
123 }
124 }
125 (Input::Choice(options, index), Key::Left) => {
126 *index = match *index {
127 Some(0) if !required => None,
128 Some(0) | None => Some(options.len() - 1),
129 Some(i) => Some(i - 1),
130 }
131 }
132 _ => {}
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct Form {
139 pub verdict: crate::runtime::Verdict,
140 pub fields: Vec<Field>,
141 pub json: Option<String>,
143 pub selected: usize,
144 pub cursor: usize,
146 pub error: Option<String>,
148}
149
150impl Form {
151 pub fn new(verdict: crate::runtime::Verdict, schema: Option<&Value>) -> Self {
153 let fields = schema.map_or(Some(Vec::new()), fields);
154 let json = fields.is_none().then(|| {
155 let template = schema.map_or(json!({}), template);
156 serde_json::to_string_pretty(&template).expect("template is JSON")
157 });
158 Self {
159 verdict,
160 fields: fields.unwrap_or_default(),
161 cursor: json.as_ref().map_or(0, String::len),
162 json,
163 selected: 0,
164 error: None,
165 }
166 }
167
168 pub fn draft(&self) -> String {
170 if let Some(json) = &self.json {
171 return json.clone();
172 }
173 let fields = self.fields.iter().map(|field| {
174 let value = match (&field.input, field.value()) {
175 (Input::Text(text), _) => json!(text),
176 (_, Ok(Some(value))) => value,
177 _ => Value::Null,
178 };
179 (field.name.clone(), value)
180 });
181 serde_json::to_string_pretty(&Value::Object(fields.collect())).expect("draft is JSON")
182 }
183
184 pub fn result(&self) -> Result<Value, String> {
186 let mut object = match &self.json {
187 Some(text) => match serde_json::from_str(text) {
188 Ok(Value::Object(object)) => object,
189 Ok(_) => return Err("Owner fields must be a JSON object.".into()),
190 Err(error) => return Err(format!("Owner fields are not valid JSON: {error}")),
191 },
192 None => {
193 let mut object = Map::new();
194 for field in &self.fields {
195 if let Some(value) = field.value()? {
196 object.insert(field.name.clone(), value);
197 }
198 }
199 object
200 }
201 };
202 if object.contains_key("verdict") {
203 return Err("Choose the verdict in the review, not inside the owner fields.".into());
204 }
205 object.insert("verdict".into(), json!(self.verdict));
206 Ok(Value::Object(object))
207 }
208
209 pub fn set_json(&mut self, text: String) {
211 self.cursor = text.len();
212 self.json = Some(text);
213 }
214
215 pub fn paste(&mut self, text: &str) {
217 if let Some(json) = &mut self.json {
218 self.cursor = boundary(json, self.cursor);
219 if json.len().saturating_add(text.len()) <= crate::human::MAX_RESULT_BYTES {
220 json.insert_str(self.cursor, text);
221 self.cursor += text.len();
222 } else {
223 self.error = Some("Human fields exceed 256000 bytes.".into());
224 }
225 } else if let Some(Field {
226 input: Input::Text(value) | Input::Integer(value) | Input::Number(value),
227 ..
228 }) = self.fields.get_mut(self.selected)
229 {
230 if value.len().saturating_add(text.len()) <= crate::human::MAX_RESULT_BYTES {
231 value.push_str(text);
232 } else {
233 self.error = Some("Human fields exceed 256000 bytes.".into());
234 }
235 }
236 }
237
238 pub fn inline_key(&mut self, key: Key, modified: bool) {
240 if self.json.is_none() {
241 let text_field = self.fields.get(self.selected).is_some_and(|field| {
242 matches!(
243 field.input,
244 Input::Text(_) | Input::Integer(_) | Input::Number(_)
245 )
246 });
247 match key {
248 Key::Char(character) if text_field && !modified => {
249 self.paste(&character.to_string())
250 }
251 _ if !modified => self.key(key),
252 _ => {}
253 }
254 return;
255 }
256 let text = self.json.as_ref().expect("JSON mode");
257 self.cursor = boundary(text, self.cursor);
258 match key {
259 Key::Char(character) if !modified => self.paste(&character.to_string()),
260 Key::Enter => self.paste("\n"),
261 Key::Tab => self.paste(" "),
262 Key::Left | Key::Backspace if self.cursor > 0 => {
263 let previous = text[..self.cursor]
264 .char_indices()
265 .last()
266 .map_or(0, |(index, _)| index);
267 if key == Key::Backspace {
268 self.json
269 .as_mut()
270 .expect("JSON mode")
271 .drain(previous..self.cursor);
272 }
273 self.cursor = previous;
274 }
275 Key::Right | Key::Delete if self.cursor < text.len() => {
276 let next = self.cursor
277 + text[self.cursor..]
278 .chars()
279 .next()
280 .expect("not at end")
281 .len_utf8();
282 if key == Key::Delete {
283 self.json
284 .as_mut()
285 .expect("JSON mode")
286 .drain(self.cursor..next);
287 } else {
288 self.cursor = next;
289 }
290 }
291 Key::Home => self.cursor = text[..self.cursor].rfind('\n').map_or(0, |index| index + 1),
292 Key::End => {
293 self.cursor += text[self.cursor..]
294 .find('\n')
295 .unwrap_or(text.len() - self.cursor)
296 }
297 Key::Up | Key::Down => {
298 let start = text[..self.cursor].rfind('\n').map_or(0, |index| index + 1);
299 let column = text[start..self.cursor].chars().count();
300 let destination = if key == Key::Up {
301 start
302 .checked_sub(1)
303 .map(|end| (text[..end].rfind('\n').map_or(0, |index| index + 1), end))
304 } else {
305 text[self.cursor..].find('\n').map(|offset| {
306 let start = self.cursor + offset + 1;
307 let end = start + text[start..].find('\n').unwrap_or(text.len() - start);
308 (start, end)
309 })
310 };
311 if let Some((start, end)) = destination {
312 self.cursor = start
313 + text[start..end]
314 .char_indices()
315 .nth(column)
316 .map_or(end - start, |(index, _)| index);
317 }
318 }
319 _ => {}
320 }
321 }
322
323 pub fn key(&mut self, key: Key) {
324 let count = self.fields.len();
325 match key {
326 Key::Up | Key::BackTab if count > 0 => {
327 self.selected = (self.selected + count - 1) % count
328 }
329 Key::Down | Key::Tab if count > 0 => self.selected = (self.selected + 1) % count,
330 _ if self.json.is_none() => {
331 if let Some(field) = self.fields.get_mut(self.selected) {
332 field.key(key);
333 }
334 }
335 _ => {}
336 }
337 }
338}
339
340fn fields(schema: &Value) -> Option<Vec<Field>> {
342 let schema = schema.as_object()?;
343 let top = ["type", "properties", "required", "additionalProperties"];
344 if schema
345 .keys()
346 .any(|key| !top.contains(&key.as_str()) && !NOTES.contains(&key.as_str()))
347 {
348 return None;
349 }
350 let required: Vec<&str> = match schema.get("required") {
351 None => Vec::new(),
352 Some(names) => names
353 .as_array()?
354 .iter()
355 .map(Value::as_str)
356 .collect::<Option<_>>()?,
357 };
358 let empty = Map::new();
359 let properties = match schema.get("properties") {
360 None => &empty,
361 Some(properties) => properties.as_object()?,
362 };
363 properties
364 .iter()
365 .map(|(name, property)| {
366 let (input, hint) = input(property)?;
367 Some(Field {
368 name: name.clone(),
369 required: required.contains(&name.as_str()),
370 hint,
371 input,
372 })
373 })
374 .collect()
375}
376
377fn input(property: &Value) -> Option<(Input, String)> {
378 let property = property.as_object()?;
379 let only = |keys: &[&str]| {
380 property
381 .keys()
382 .all(|key| keys.contains(&key.as_str()) || NOTES.contains(&key.as_str()))
383 };
384 let kind = property.get("type").map(Value::as_str);
385 let input = if let Some(value) = property.get("const") {
386 only(&["const", "type"]).then(|| Input::Fixed(value.clone()))?
387 } else if let Some(options) = property.get("enum") {
388 let options = options.as_array()?;
389 let strings: Option<Vec<String>> = options
390 .iter()
391 .map(|option| option.as_str().map(str::to_owned))
392 .collect();
393 let strings = strings.filter(|strings| !strings.is_empty())?;
394 (kind.is_none_or(|kind| kind == Some("string")) && only(&["enum", "type"]))
395 .then_some(Input::Choice(strings, None))?
396 } else {
397 match kind?? {
398 "boolean" if only(&["type"]) => Input::Bool(None),
399 "string" if only(&["type", "minLength", "maxLength"]) => Input::Text(String::new()),
400 "integer" if only(&NUMERIC) => Input::Integer(String::new()),
401 "number" if only(&NUMERIC) => Input::Number(String::new()),
402 _ => return None,
403 }
404 };
405 let constraints = property
406 .iter()
407 .filter(|(key, _)| !["type", "const", "enum"].contains(&key.as_str()))
408 .filter(|(key, _)| !NOTES.contains(&key.as_str()))
409 .map(|(key, value)| format!("{key} {value}"));
410 let kind = kind.flatten().map(str::to_owned);
411 let mut hint = kind
412 .into_iter()
413 .chain(constraints)
414 .collect::<Vec<_>>()
415 .join(", ");
416 if let Some(description) = property.get("description").and_then(Value::as_str) {
417 hint = [hint.as_str(), description]
418 .into_iter()
419 .filter(|part| !part.is_empty())
420 .collect::<Vec<_>>()
421 .join(" — ");
422 }
423 Some((input, hint))
424}
425
426pub fn boundary(text: &str, cursor: usize) -> usize {
429 let mut cursor = cursor.min(text.len());
430 while !text.is_char_boundary(cursor) {
431 cursor -= 1;
432 }
433 cursor
434}
435
436pub fn template(schema: &Value) -> Value {
437 if let Some(value) = ["const", "default"].iter().find_map(|key| schema.get(*key)) {
438 return value.clone();
439 }
440 if let Some(value) = schema.get("enum").and_then(|options| options.get(0)) {
441 return value.clone();
442 }
443 let kind = match &schema["type"] {
444 Value::String(kind) => kind.as_str(),
445 Value::Array(kinds) => kinds.first().and_then(Value::as_str).unwrap_or_default(),
446 _ if schema.get("properties").is_some() => "object",
447 _ => "",
448 };
449 match kind {
450 "object" => Value::Object(
451 schema["properties"]
452 .as_object()
453 .into_iter()
454 .flatten()
455 .map(|(name, property)| (name.clone(), template(property)))
456 .collect(),
457 ),
458 "array" => json!([]),
459 "string" => json!(""),
460 "integer" | "number" => json!(0),
461 "boolean" => json!(false),
462 _ => Value::Null,
463 }
464}