1use serde::de::DeserializeOwned;
20use serde::{Deserialize, Serialize};
21use serde_json::{Map, Value};
22use std::collections::BTreeMap;
23
24#[derive(Deserialize)]
25pub struct Request {
26 pub action: String,
28 pub project: String,
29 #[serde(default)]
31 pub settings: Map<String, Value>,
32 #[serde(default)]
35 pub input: Map<String, Value>,
36 #[serde(default)]
37 pub context: Context,
38 #[serde(default)]
41 pub button: Option<String>,
42 #[serde(default)]
44 pub changed: Option<String>,
45 #[serde(default)]
47 pub state: Option<Value>,
48 #[serde(default)]
50 pub resumed: bool,
51 #[serde(default)]
53 pub locale: Option<String>,
54}
55
56impl Request {
57 pub fn input<T: DeserializeOwned>(&self) -> crate::Result<T> {
59 Ok(serde_json::from_value(Value::Object(self.input.clone()))?)
60 }
61
62 pub fn state<T: DeserializeOwned>(&self) -> Option<T> {
64 self.state.as_ref().and_then(|state| T::deserialize(state).ok())
65 }
66}
67
68#[derive(Deserialize)]
70pub struct Context {
71 pub place: String,
73 #[serde(default)]
74 pub id: Option<i64>,
75}
76
77impl Default for Context {
78 fn default() -> Self {
79 Context { place: "project".into(), id: None }
80 }
81}
82
83#[derive(Serialize, Default)]
86pub struct Reply {
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub page: Option<Page>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub message: Option<String>,
92 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
95 pub errors: BTreeMap<String, String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub open: Option<Open>,
99}
100
101impl Reply {
102 pub fn message(message: impl Into<String>) -> Reply {
104 Reply { message: Some(message.into()), ..Reply::default() }
105 }
106
107 pub fn document(markdown: impl Into<String>) -> Reply {
109 Reply::page(Page { markdown: Some(markdown.into()), ..Page::default() })
110 }
111
112 pub fn page(page: Page) -> Reply {
113 Reply { page: Some(page), ..Reply::default() }
114 }
115
116 pub fn error(key: impl Into<String>, message: impl Into<String>) -> Reply {
119 Reply { errors: BTreeMap::from([(key.into(), message.into())]), ..Reply::default() }
120 }
121
122 pub fn open(open: Open) -> Reply {
124 Reply { open: Some(open), ..Reply::default() }
125 }
126
127 pub fn to_json(&self) -> crate::Result<String> {
129 Ok(serde_json::to_string(self)?)
130 }
131}
132
133#[derive(Serialize, Default)]
135pub struct Page {
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub title: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub markdown: Option<String>,
140 #[serde(skip_serializing_if = "Vec::is_empty")]
141 pub fields: Vec<Field>,
142 #[serde(skip_serializing_if = "Vec::is_empty")]
143 pub buttons: Vec<Button>,
144 #[serde(skip_serializing_if = "Option::is_none")]
147 pub state: Option<Value>,
148}
149
150impl Page {
151 pub fn with_state(mut self, state: &impl Serialize) -> crate::Result<Page> {
153 self.state = Some(serde_json::to_value(state)?);
154 Ok(self)
155 }
156}
157
158#[derive(Serialize, Clone)]
164pub struct Field {
165 pub key: String,
166 #[serde(rename = "type")]
167 pub kind: FieldKind,
168 pub label: String,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub hint: Option<String>,
171 #[serde(skip_serializing_if = "std::ops::Not::not")]
173 pub required: bool,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub default: Option<Value>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub min: Option<i64>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub max: Option<i64>,
181 #[serde(skip_serializing_if = "Vec::is_empty")]
182 pub options: Vec<Choice>,
183 #[serde(skip_serializing_if = "TextFormat::is_plain")]
184 pub format: TextFormat,
185 #[serde(skip_serializing_if = "std::ops::Not::not")]
186 pub multiline: bool,
187 #[serde(skip_serializing_if = "Vec::is_empty")]
189 pub kinds: Vec<String>,
190 #[serde(skip_serializing_if = "Vec::is_empty")]
192 pub entry_types: Vec<String>,
193 #[serde(skip_serializing_if = "Vec::is_empty")]
195 pub tags: Vec<String>,
196 #[serde(skip_serializing_if = "Option::is_none")]
199 pub multiple: Option<bool>,
200}
201
202#[derive(Serialize, Clone, Copy, PartialEq, Eq, Debug)]
203#[serde(rename_all = "lowercase")]
204pub enum FieldKind {
205 Text,
206 Bool,
207 Int,
208 Choice,
209 Scenes,
210 Entity,
211}
212
213#[derive(Serialize, Clone, Copy, PartialEq, Eq, Default, Debug)]
216#[serde(rename_all = "kebab-case")]
217pub enum TextFormat {
218 #[default]
219 Plain,
220 Rich,
221 RichExtended,
222}
223
224impl TextFormat {
225 fn is_plain(&self) -> bool {
226 *self == TextFormat::Plain
227 }
228}
229
230#[derive(Serialize, Clone)]
232pub struct Choice {
233 pub value: String,
234 pub label: String,
235}
236
237impl Choice {
238 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Choice {
239 Choice { value: value.into(), label: label.into() }
240 }
241}
242
243impl Field {
244 fn new(key: impl Into<String>, kind: FieldKind, label: impl Into<String>) -> Field {
245 Field {
246 key: key.into(),
247 kind,
248 label: label.into(),
249 hint: None,
250 required: false,
251 default: None,
252 min: None,
253 max: None,
254 options: Vec::new(),
255 format: TextFormat::Plain,
256 multiline: false,
257 kinds: Vec::new(),
258 entry_types: Vec::new(),
259 tags: Vec::new(),
260 multiple: None,
261 }
262 }
263
264 pub fn text(key: impl Into<String>, label: impl Into<String>) -> Field {
266 Field::new(key, FieldKind::Text, label)
267 }
268
269 pub fn bool(key: impl Into<String>, label: impl Into<String>) -> Field {
270 Field::new(key, FieldKind::Bool, label)
271 }
272
273 pub fn int(key: impl Into<String>, label: impl Into<String>, min: Option<i64>, max: Option<i64>) -> Field {
274 Field { min, max, ..Field::new(key, FieldKind::Int, label) }
275 }
276
277 pub fn choice(key: impl Into<String>, label: impl Into<String>, options: Vec<Choice>) -> Field {
279 Field { options, ..Field::new(key, FieldKind::Choice, label) }
280 }
281
282 pub fn scenes(key: impl Into<String>, label: impl Into<String>) -> Field {
284 Field::new(key, FieldKind::Scenes, label)
285 }
286
287 pub fn entity(key: impl Into<String>, label: impl Into<String>) -> Field {
291 Field::new(key, FieldKind::Entity, label)
292 }
293
294 pub fn hint(mut self, hint: impl Into<String>) -> Field {
295 self.hint = Some(hint.into());
296 self
297 }
298
299 pub fn required(mut self) -> Field {
300 self.required = true;
301 self
302 }
303
304 pub fn default(mut self, value: impl Into<Value>) -> Field {
307 self.default = Some(value.into());
308 self
309 }
310
311 pub fn format(mut self, format: TextFormat) -> Field {
312 self.format = format;
313 self
314 }
315
316 pub fn multiline(mut self) -> Field {
318 self.multiline = true;
319 self
320 }
321
322 pub fn single(mut self) -> Field {
324 self.multiple = (self.kind != FieldKind::Entity).then_some(false);
325 self
326 }
327
328 pub fn multiple(mut self) -> Field {
330 self.multiple = (self.kind != FieldKind::Scenes).then_some(true);
331 self
332 }
333
334 pub fn kinds(mut self, kinds: &[&str]) -> Field {
336 self.kinds = kinds.iter().map(|it| it.to_string()).collect();
337 self
338 }
339
340 pub fn entry_types(mut self, types: &[&str]) -> Field {
343 self.entry_types = types.iter().map(|it| it.to_string()).collect();
344 self
345 }
346
347 pub fn tags(mut self, tags: &[&str]) -> Field {
349 self.tags = tags.iter().map(|it| it.to_string()).collect();
350 self
351 }
352}
353
354#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
358pub struct EntityRef {
359 pub kind: String,
360 pub id: i64,
361}
362
363impl EntityRef {
364 pub fn new(kind: impl Into<String>, id: i64) -> EntityRef {
365 EntityRef { kind: kind.into(), id }
366 }
367}
368
369impl From<EntityRef> for Value {
370 fn from(entity: EntityRef) -> Value {
371 serde_json::json!({"kind": entity.kind, "id": entity.id})
372 }
373}
374
375#[derive(Serialize, Clone)]
378pub struct Button {
379 pub id: String,
380 pub label: String,
381 #[serde(skip_serializing_if = "std::ops::Not::not")]
382 pub primary: bool,
383 #[serde(skip_serializing_if = "std::ops::Not::not")]
384 pub inline: bool,
385 #[serde(skip_serializing_if = "is_true")]
387 pub validate: bool,
388}
389
390impl Button {
391 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Button {
392 Button { id: id.into(), label: label.into(), primary: false, inline: false, validate: true }
393 }
394
395 pub fn primary(mut self) -> Button {
396 self.primary = true;
397 self
398 }
399
400 pub fn inline(mut self) -> Button {
401 self.inline = true;
402 self
403 }
404
405 pub fn skip_checks(mut self) -> Button {
407 self.validate = false;
408 self
409 }
410}
411
412#[derive(Serialize, Clone)]
415pub struct Open {
416 pub place: String,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub id: Option<i64>,
419}
420
421impl Open {
422 pub fn new(place: impl Into<String>, id: Option<i64>) -> Open {
423 Open { place: place.into(), id }
424 }
425}
426
427fn is_true(value: &bool) -> bool {
428 *value
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use serde_json::json;
435
436 #[test]
437 fn replies_leave_out_what_is_unset() {
438 assert_eq!(Reply::default().to_json().unwrap(), "{}");
439 assert_eq!(Reply::message("Hi").to_json().unwrap(), r#"{"message":"Hi"}"#);
440 assert_eq!(Reply::document("# Hi").to_json().unwrap(), r##"{"page":{"markdown":"# Hi"}}"##);
441 assert_eq!(Reply::error("", "No").to_json().unwrap(), r#"{"errors":{"":"No"}}"#);
442 let open = Reply { message: Some("Added".into()), ..Reply::open(Open::new("entry", Some(12))) };
443 assert_eq!(open.to_json().unwrap(), r#"{"message":"Added","open":{"place":"entry","id":12}}"#);
444 }
445
446 #[test]
447 fn a_page_has_its_fields_buttons_and_state() {
448 #[derive(Serialize)]
449 struct Step {
450 step: u32,
451 }
452 let page = Page {
453 title: Some("Who?".into()),
454 fields: vec![
455 Field::text("name", "Name").required(),
456 Field::text("want", "Wants").format(TextFormat::RichExtended).hint("A line"),
457 Field::text("notes", "Notes").multiline(),
458 Field::choice("set", "Set", vec![Choice::new("quick", "Quick")]).default("quick"),
459 Field::int("count", "Count", Some(1), None).default(8),
460 Field::bool("all", "All"),
461 Field::scenes("scene", "Scene").single(),
462 ],
463 buttons: vec![Button::new("back", "Back").skip_checks(), Button::new("next", "Next").primary().inline()],
464 ..Page::default()
465 }
466 .with_state(&Step { step: 2 })
467 .unwrap();
468 assert_eq!(
469 serde_json::to_value(Reply::page(page)).unwrap(),
470 json!({"page": {
471 "title": "Who?",
472 "fields": [
473 {"key": "name", "type": "text", "label": "Name", "required": true},
474 {"key": "want", "type": "text", "label": "Wants", "hint": "A line", "format": "rich-extended"},
475 {"key": "notes", "type": "text", "label": "Notes", "multiline": true},
476 {"key": "set", "type": "choice", "label": "Set", "default": "quick",
477 "options": [{"value": "quick", "label": "Quick"}]},
478 {"key": "count", "type": "int", "label": "Count", "default": 8, "min": 1},
479 {"key": "all", "type": "bool", "label": "All"},
480 {"key": "scene", "type": "scenes", "label": "Scene", "multiple": false},
481 ],
482 "buttons": [
483 {"id": "back", "label": "Back", "validate": false},
484 {"id": "next", "label": "Next", "primary": true, "inline": true},
485 ],
486 "state": {"step": 2},
487 }})
488 );
489 }
490
491 #[test]
492 fn a_request_carries_state() {
493 #[derive(Deserialize)]
494 struct Step {
495 step: u32,
496 }
497 let request: Request = serde_json::from_value(json!({
498 "action": "wave", "project": "Storm", "settings": {}, "locale": null,
499 "context": {"place": "project", "id": null}, "input": {}, "button": "next",
500 "changed": null, "state": {"step": 2}, "resumed": true,
501 }))
502 .unwrap();
503 assert_eq!(request.state::<Step>().map(|it| it.step), Some(2));
504 assert!(request.state::<String>().is_none());
505 assert!(request.resumed);
506 let first: Request = serde_json::from_value(json!({"action": "wave", "project": "Storm", "state": null})).unwrap();
507 assert!(first.state.is_none() && !first.resumed && first.button.is_none());
508 }
509
510 #[test]
511 fn an_entity_is_one_item_unless_multiple() {
512 let mara = EntityRef::new("entry", 12);
513 let fields = vec![
514 Field::entity("who", "Who"),
515 Field::entity("a", "First")
516 .kinds(&["entry"])
517 .entry_types(&["person"])
518 .tags(&["pov"])
519 .required()
520 .default(mara.clone()),
521 Field::entity("cast", "Cast").multiple().default(vec![mara.clone(), EntityRef::new("scene", 3)]),
522 Field::entity("one", "One").multiple().single(),
523 Field::scenes("scenes", "Scenes").single().multiple(),
524 ];
525 assert_eq!(
526 serde_json::to_value(&fields).unwrap(),
527 json!([
528 {"key": "who", "type": "entity", "label": "Who"},
529 {"key": "a", "type": "entity", "label": "First", "required": true,
530 "default": {"kind": "entry", "id": 12},
531 "kinds": ["entry"], "entry_types": ["person"], "tags": ["pov"]},
532 {"key": "cast", "type": "entity", "label": "Cast",
533 "default": [{"kind": "entry", "id": 12}, {"kind": "scene", "id": 3}], "multiple": true},
534 {"key": "one", "type": "entity", "label": "One"},
535 {"key": "scenes", "type": "scenes", "label": "Scenes"},
536 ])
537 );
538 }
539
540 #[test]
541 fn entity_input_reads_as_entity_refs() {
542 #[derive(Deserialize)]
543 struct Picks {
544 who: Option<EntityRef>,
545 #[serde(default)]
546 cast: Vec<EntityRef>,
547 }
548 let request = |input: Value| -> Request {
549 serde_json::from_value(json!({"action": "meet", "project": "Storm", "input": input})).unwrap()
550 };
551 let picked: Picks = request(json!({"who": {"kind": "entry", "id": 12}, "cast": [{"kind": "event", "id": 4}]}))
552 .input()
553 .unwrap();
554 assert_eq!(picked.who, Some(EntityRef::new("entry", 12)));
555 assert_eq!(picked.cast, vec![EntityRef::new("event", 4)]);
556 let none: Picks = request(json!({"who": null, "cast": []})).input().unwrap();
557 assert!(none.who.is_none() && none.cast.is_empty());
558 }
559}