1use indexmap::IndexMap;
33
34use crate::document::edit::resolve_field_write;
35use crate::document::{Card, Document, EditError};
36use crate::quill::{CardSchema, FieldSchema, QuillConfig};
37use crate::value::QuillValue;
38use crate::Delta;
39
40pub struct TypedWriter<'a> {
44 config: &'a QuillConfig,
45 doc: &'a mut Document,
46}
47
48impl<'a> TypedWriter<'a> {
49 pub fn new(config: &'a QuillConfig, doc: &'a mut Document) -> Self {
51 Self { config, doc }
52 }
53
54 pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
61 let config = self.config;
62 match config.main.fields.get(name) {
63 Some(schema) => self.doc.main_mut().commit_field(name, value, schema),
64 None => Err(EditError::UnknownField(name.to_string())),
65 }
66 }
67
68 pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
76 where
77 K: Into<String>,
78 V: Into<QuillValue>,
79 I: IntoIterator<Item = (K, V)>,
80 {
81 let schema = Some(&self.config.main.fields);
82 set_all_impl(self.doc.main_mut(), schema, fields)
83 }
84
85 pub fn revise_body(&mut self, markdown: &str) -> Result<Delta, EditError> {
93 self.doc.main_mut().revise_body(markdown)
94 }
95
96 pub fn revise_field(&mut self, name: &str, text: &str) -> Result<Delta, EditError> {
109 match self.config.main.fields.get(name) {
110 Some(schema) => self.doc.main_mut().revise_field_checked(name, text, schema),
111 None => Err(EditError::UnknownField(name.to_string())),
112 }
113 }
114
115 pub fn add_card<K, V, I>(
131 &mut self,
132 kind: &str,
133 fields: I,
134 body: Option<&str>,
135 at: Option<usize>,
136 ) -> Result<(), Vec<(String, EditError)>>
137 where
138 K: Into<String>,
139 V: Into<QuillValue>,
140 I: IntoIterator<Item = (K, V)>,
141 {
142 let mut card = Card::new(kind).map_err(|e| vec![("$kind".to_string(), e)])?;
143 let schema = self.config.card_kind(kind).map(|s| &s.fields);
144 set_all_impl(&mut card, schema, fields)?;
145 if let Some(md) = body {
146 card.revise_body(md)
147 .map_err(|e| vec![("$body".to_string(), e)])?;
148 }
149 match at {
150 Some(index) => self.doc.insert_card(index, card),
151 None => self.doc.push_card(card),
152 }
153 .map_err(|e| vec![("$kind".to_string(), e)])?;
154 Ok(())
155 }
156
157 pub fn remove_card(&mut self, index: usize) -> Option<Card> {
161 self.doc.remove_card(index)
162 }
163
164 pub fn card(&mut self, index: usize) -> Result<CardWriter<'_>, EditError> {
171 let config = self.config;
172 let len = self.doc.cards().len();
173 let card = self
174 .doc
175 .card_mut(index)
176 .ok_or(EditError::IndexOutOfRange { index, len })?;
177 let schema = card.kind().and_then(|k| config.card_kind(k));
178 Ok(CardWriter { schema, card })
179 }
180}
181
182pub struct CardWriter<'a> {
185 schema: Option<&'a CardSchema>,
186 card: &'a mut Card,
187}
188
189impl CardWriter<'_> {
190 pub fn kind(&self) -> Option<&str> {
192 self.card.kind()
193 }
194
195 pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
200 match self.schema.and_then(|s| s.fields.get(name)) {
201 Some(schema) => self.card.commit_field(name, value, schema),
202 None => Err(EditError::UnknownField(name.to_string())),
203 }
204 }
205
206 pub fn revise_body(&mut self, markdown: &str) -> Result<Delta, EditError> {
209 self.card.revise_body(markdown)
210 }
211
212 pub fn revise_field(&mut self, name: &str, text: &str) -> Result<Delta, EditError> {
219 match self.schema.and_then(|s| s.fields.get(name)) {
220 Some(schema) => self.card.revise_field_checked(name, text, schema),
221 None => Err(EditError::UnknownField(name.to_string())),
222 }
223 }
224
225 pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
229 where
230 K: Into<String>,
231 V: Into<QuillValue>,
232 I: IntoIterator<Item = (K, V)>,
233 {
234 set_all_impl(self.card, self.schema.map(|s| &s.fields), fields)
235 }
236}
237
238fn set_all_impl<K, V, I>(
245 card: &mut Card,
246 fields_schema: Option<&IndexMap<String, FieldSchema>>,
247 fields: I,
248) -> Result<(), Vec<(String, EditError)>>
249where
250 K: Into<String>,
251 V: Into<QuillValue>,
252 I: IntoIterator<Item = (K, V)>,
253{
254 let fields: Vec<(String, QuillValue)> = fields
255 .into_iter()
256 .map(|(k, v)| (k.into(), v.into()))
257 .collect();
258
259 let mut resolved: Vec<(String, QuillValue)> = Vec::with_capacity(fields.len());
260 let mut errors: Vec<(String, EditError)> = Vec::new();
261 for (name, value) in fields {
262 match fields_schema.and_then(|m| m.get(&name)) {
263 Some(schema) => match resolve_field_write(&name, value, schema) {
264 Ok(stored) => resolved.push((name, stored)),
265 Err(e) => errors.push((name, e)),
266 },
267 None => errors.push((name.clone(), EditError::UnknownField(name))),
268 }
269 }
270 if !errors.is_empty() {
271 return Err(errors);
272 }
273 for (name, stored) in resolved {
275 card.payload_mut().insert_unchecked(name, stored);
276 }
277 Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::document::{Card, Document};
284 use crate::version::QuillReference;
285 use std::str::FromStr;
286
287 const QUILL_YAML: &str = "\
288quill:
289 name: memo
290 backend: typst
291 version: 1.0.0
292 description: Editor test quill
293main:
294 fields:
295 subject:
296 type: richtext
297 inline: true
298 qty:
299 type: integer
300card_kinds:
301 note:
302 fields:
303 body:
304 type: richtext
305";
306
307 fn config() -> QuillConfig {
308 QuillConfig::from_yaml(QUILL_YAML).expect("valid quill")
309 }
310
311 fn blank_doc() -> Document {
312 Document::new(QuillReference::from_str("memo@1.0.0").unwrap())
313 }
314
315 #[test]
316 fn set_resolves_schema_field_as_typed_commit() {
317 let config = config();
318 let mut doc = blank_doc();
319 let mut ed = TypedWriter::new(&config, &mut doc);
320
321 ed.set("qty", "3").unwrap();
323 ed.set("subject", "Hello").unwrap();
324 assert_eq!(
325 doc.main().payload().get("qty").unwrap().as_json(),
326 &serde_json::json!(3)
327 );
328 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
329 }
330
331 #[test]
332 fn set_rejects_unknown_field() {
333 let config = config();
334 let mut doc = blank_doc();
335 let mut ed = TypedWriter::new(&config, &mut doc);
336 let err = ed.set("notafield", "x").unwrap_err();
339 assert_eq!(err.code(), "edit::unknown_field");
340 assert!(doc.main().payload().get("notafield").is_none());
341 }
342
343 #[test]
344 fn set_all_is_all_or_nothing() {
345 let config = config();
346 let mut doc = blank_doc();
347 let mut ed = TypedWriter::new(&config, &mut doc);
348 let errs = ed
350 .set_all([("qty", "5"), ("subject", "bad\n\nblock")])
351 .unwrap_err();
352 assert_eq!(errs.len(), 1);
353 assert_eq!(errs[0].0, "subject");
354 assert!(doc.main().payload().get("qty").is_none());
355
356 let mut ed = TypedWriter::new(&config, &mut doc);
358 ed.set_all([("qty", "5"), ("subject", "ok")]).unwrap();
359 assert_eq!(
360 doc.main().payload().get("qty").unwrap().as_json(),
361 &serde_json::json!(5)
362 );
363 }
364
365 #[test]
366 fn set_all_rejects_unknown_field() {
367 let config = config();
368 let mut doc = blank_doc();
369 let mut ed = TypedWriter::new(&config, &mut doc);
370 let errs = ed.set_all([("qty", "3"), ("titel", "oops")]).unwrap_err();
374 assert_eq!(errs.len(), 1);
375 assert_eq!(errs[0].0, "titel");
376 assert_eq!(errs[0].1.code(), "edit::unknown_field");
377 assert!(doc.main().payload().get("qty").is_none());
378 }
379
380 #[test]
381 fn revise_body_revises_main_body() {
382 let config = config();
383 let mut doc = blank_doc();
384 let mut ed = TypedWriter::new(&config, &mut doc);
385 ed.revise_body("**hi**").unwrap();
386 assert_eq!(doc.main().body_markdown(), "**hi**");
387 }
388
389 #[test]
390 fn add_card_fuses_new_commit_push() {
391 let config = config();
392 let mut doc = blank_doc();
393 let mut ed = TypedWriter::new(&config, &mut doc);
394 ed.add_card("note", [("body", "**hi**")], Some("card body"), None)
395 .unwrap();
396 assert_eq!(doc.cards().len(), 1);
397 assert_eq!(doc.cards()[0].kind(), Some("note"));
398 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
399 assert_eq!(doc.cards()[0].body_markdown(), "card body");
400 }
401
402 #[test]
403 fn add_card_at_inserts_and_remove_card_returns() {
404 let config = config();
405 let mut doc = blank_doc();
406 {
407 let mut ed = TypedWriter::new(&config, &mut doc);
408 ed.add_card("note", [("body", "a")], None, None).unwrap();
409 ed.add_card("note", [("body", "c")], None, None).unwrap();
410 ed.add_card("note", [("body", "b")], None, Some(1)).unwrap();
412 }
413 let bodies: Vec<String> = doc
414 .cards()
415 .iter()
416 .map(|c| c.field_markdown("body").unwrap().unwrap())
417 .collect();
418 assert_eq!(bodies, ["a", "b", "c"]);
419
420 {
422 let mut ed = TypedWriter::new(&config, &mut doc);
423 let errs = ed
424 .add_card("note", [("body", "x")], None, Some(9))
425 .unwrap_err();
426 assert_eq!(errs[0].0, "$kind");
427 }
428 assert_eq!(doc.cards().len(), 3);
429
430 {
432 let mut ed = TypedWriter::new(&config, &mut doc);
433 let removed = ed.remove_card(1).unwrap();
434 assert_eq!(removed.field_markdown("body").unwrap().unwrap(), "b");
435 assert!(ed.remove_card(5).is_none());
436 }
437 assert_eq!(doc.cards().len(), 2);
438 }
439
440 #[test]
441 fn add_card_is_transactional_on_bad_field() {
442 let config = config();
443 let mut doc = blank_doc();
444 let mut ed = TypedWriter::new(&config, &mut doc);
445 let errs = ed
447 .add_card("note", [("stray", "x")], None, None)
448 .unwrap_err();
449 assert_eq!(errs[0].0, "stray");
450 assert_eq!(errs[0].1.code(), "edit::unknown_field");
451 assert_eq!(doc.cards().len(), 0);
452 }
453
454 #[test]
455 fn add_card_reports_invalid_kind() {
456 let config = config();
457 let mut doc = blank_doc();
458 let mut ed = TypedWriter::new(&config, &mut doc);
459 let errs = ed
461 .add_card("$reserved", [] as [(&str, &str); 0], None, None)
462 .unwrap_err();
463 assert_eq!(errs[0].0, "$kind");
464 assert_eq!(doc.cards().len(), 0);
465 }
466
467 #[test]
468 fn card_writer_resolves_card_kind_schema() {
469 let config = config();
470 let mut doc = blank_doc();
471 doc.push_card(Card::new("note").unwrap()).unwrap();
472
473 let mut ed = TypedWriter::new(&config, &mut doc);
474 let mut card_ed = ed.card(0).unwrap();
475 card_ed.set("body", "**hi**").unwrap();
476 let err = card_ed.set("stray", "v").unwrap_err();
478 assert_eq!(err.code(), "edit::unknown_field");
479
480 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
481
482 let mut ed = TypedWriter::new(&config, &mut doc);
484 assert!(matches!(
485 ed.card(9),
486 Err(EditError::IndexOutOfRange { .. })
487 ));
488 }
489
490 #[test]
491 fn revise_field_is_typed_and_rejects_unknown_and_non_inline() {
492 let config = config();
493 let mut doc = blank_doc();
494 let mut ed = TypedWriter::new(&config, &mut doc);
495 let _delta = ed.revise_field("subject", "Hello").unwrap();
497 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
498
499 let mut ed = TypedWriter::new(&config, &mut doc);
501 assert_eq!(
502 ed.revise_field("nope", "x").unwrap_err().code(),
503 "edit::unknown_field"
504 );
505 let err = ed.revise_field("subject", "a\n\nb").unwrap_err();
507 assert_eq!(err.code(), "edit::field_not_inline");
508 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
509 }
510
511 #[test]
512 fn card_writer_revise_field_resolves_card_schema() {
513 let config = config();
514 let mut doc = blank_doc();
515 doc.push_card(Card::new("note").unwrap()).unwrap();
516
517 let mut ed = TypedWriter::new(&config, &mut doc);
518 ed.card(0).unwrap().revise_field("body", "**hi**").unwrap();
519 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
520
521 let mut ed = TypedWriter::new(&config, &mut doc);
522 assert_eq!(
523 ed.card(0)
524 .unwrap()
525 .revise_field("stray", "x")
526 .unwrap_err()
527 .code(),
528 "edit::unknown_field"
529 );
530 }
531}