1use indexmap::IndexMap;
27
28use crate::document::edit::resolve_field_write;
29use crate::document::{Card, Document, EditError};
30use crate::quill::{CardSchema, FieldSchema, QuillConfig};
31use crate::value::QuillValue;
32use crate::Delta;
33
34pub struct TypedWriter<'a> {
38 config: &'a QuillConfig,
39 doc: &'a mut Document,
40}
41
42impl<'a> TypedWriter<'a> {
43 pub fn new(config: &'a QuillConfig, doc: &'a mut Document) -> Self {
45 Self { config, doc }
46 }
47
48 pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
55 let config = self.config;
56 match config.main.fields.get(name) {
57 Some(schema) => self.doc.main_mut().commit_field(name, value, schema),
58 None => Err(EditError::UnknownField(name.to_string())),
59 }
60 }
61
62 pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
70 where
71 K: Into<String>,
72 V: Into<QuillValue>,
73 I: IntoIterator<Item = (K, V)>,
74 {
75 let schema = Some(&self.config.main.fields);
76 set_all_impl(self.doc.main_mut(), schema, fields)
77 }
78
79 pub fn set_body(&mut self, markdown: &str) -> Result<(), EditError> {
84 self.doc.main_mut().revise_body(markdown).map(|_| ())
85 }
86
87 pub fn revise_field(&mut self, name: &str, markdown: &str) -> Result<Delta, EditError> {
95 match self.config.main.fields.get(name) {
96 Some(schema) => self.doc.main_mut().revise_field_checked(name, markdown, schema),
97 None => Err(EditError::UnknownField(name.to_string())),
98 }
99 }
100
101 pub fn add_card<K, V, I>(
117 &mut self,
118 kind: &str,
119 fields: I,
120 body: Option<&str>,
121 at: Option<usize>,
122 ) -> Result<(), Vec<(String, EditError)>>
123 where
124 K: Into<String>,
125 V: Into<QuillValue>,
126 I: IntoIterator<Item = (K, V)>,
127 {
128 let mut card = Card::new(kind).map_err(|e| vec![("$kind".to_string(), e)])?;
129 let schema = self.config.card_kind(kind).map(|s| &s.fields);
130 set_all_impl(&mut card, schema, fields)?;
131 if let Some(md) = body {
132 card.revise_body(md)
133 .map_err(|e| vec![("$body".to_string(), e)])?;
134 }
135 match at {
136 Some(index) => self.doc.insert_card(index, card),
137 None => self.doc.push_card(card),
138 }
139 .map_err(|e| vec![("$kind".to_string(), e)])?;
140 Ok(())
141 }
142
143 pub fn remove_card(&mut self, index: usize) -> Option<Card> {
147 self.doc.remove_card(index)
148 }
149
150 pub fn card(&mut self, index: usize) -> Result<CardWriter<'_>, EditError> {
157 let config = self.config;
158 let len = self.doc.cards().len();
159 let card = self
160 .doc
161 .card_mut(index)
162 .ok_or(EditError::IndexOutOfRange { index, len })?;
163 let schema = card.kind().and_then(|k| config.card_kind(k));
164 Ok(CardWriter { schema, card })
165 }
166}
167
168pub struct CardWriter<'a> {
171 schema: Option<&'a CardSchema>,
172 card: &'a mut Card,
173}
174
175impl CardWriter<'_> {
176 pub fn kind(&self) -> Option<&str> {
178 self.card.kind()
179 }
180
181 pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
186 match self.schema.and_then(|s| s.fields.get(name)) {
187 Some(schema) => self.card.commit_field(name, value, schema),
188 None => Err(EditError::UnknownField(name.to_string())),
189 }
190 }
191
192 pub fn set_body(&mut self, markdown: &str) -> Result<(), EditError> {
195 self.card.revise_body(markdown).map(|_| ())
196 }
197
198 pub fn revise_field(&mut self, name: &str, markdown: &str) -> Result<Delta, EditError> {
204 match self.schema.and_then(|s| s.fields.get(name)) {
205 Some(schema) => self.card.revise_field_checked(name, markdown, schema),
206 None => Err(EditError::UnknownField(name.to_string())),
207 }
208 }
209
210 pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
214 where
215 K: Into<String>,
216 V: Into<QuillValue>,
217 I: IntoIterator<Item = (K, V)>,
218 {
219 set_all_impl(self.card, self.schema.map(|s| &s.fields), fields)
220 }
221}
222
223fn set_all_impl<K, V, I>(
230 card: &mut Card,
231 fields_schema: Option<&IndexMap<String, FieldSchema>>,
232 fields: I,
233) -> Result<(), Vec<(String, EditError)>>
234where
235 K: Into<String>,
236 V: Into<QuillValue>,
237 I: IntoIterator<Item = (K, V)>,
238{
239 let fields: Vec<(String, QuillValue)> = fields
240 .into_iter()
241 .map(|(k, v)| (k.into(), v.into()))
242 .collect();
243
244 let mut resolved: Vec<(String, QuillValue)> = Vec::with_capacity(fields.len());
245 let mut errors: Vec<(String, EditError)> = Vec::new();
246 for (name, value) in fields {
247 match fields_schema.and_then(|m| m.get(&name)) {
248 Some(schema) => match resolve_field_write(&name, value, schema) {
249 Ok(stored) => resolved.push((name, stored)),
250 Err(e) => errors.push((name, e)),
251 },
252 None => errors.push((name.clone(), EditError::UnknownField(name))),
253 }
254 }
255 if !errors.is_empty() {
256 return Err(errors);
257 }
258 for (name, stored) in resolved {
260 card.payload_mut().insert_unchecked(name, stored);
261 }
262 Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::document::{Card, Document};
269 use crate::version::QuillReference;
270 use std::str::FromStr;
271
272 const QUILL_YAML: &str = "\
273quill:
274 name: memo
275 backend: typst
276 version: 1.0.0
277 description: Editor test quill
278main:
279 fields:
280 subject:
281 type: richtext
282 inline: true
283 qty:
284 type: integer
285card_kinds:
286 note:
287 fields:
288 body:
289 type: richtext
290";
291
292 fn config() -> QuillConfig {
293 QuillConfig::from_yaml(QUILL_YAML).expect("valid quill")
294 }
295
296 fn blank_doc() -> Document {
297 Document::new(QuillReference::from_str("memo@1.0.0").unwrap())
298 }
299
300 #[test]
301 fn set_resolves_schema_field_as_typed_commit() {
302 let config = config();
303 let mut doc = blank_doc();
304 let mut ed = TypedWriter::new(&config, &mut doc);
305
306 ed.set("qty", "3").unwrap();
308 ed.set("subject", "Hello").unwrap();
309 assert_eq!(
310 doc.main().payload().get("qty").unwrap().as_json(),
311 &serde_json::json!(3)
312 );
313 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
314 }
315
316 #[test]
317 fn set_rejects_unknown_field() {
318 let config = config();
319 let mut doc = blank_doc();
320 let mut ed = TypedWriter::new(&config, &mut doc);
321 let err = ed.set("notafield", "x").unwrap_err();
324 assert_eq!(err.variant_name(), "UnknownField");
325 assert!(doc.main().payload().get("notafield").is_none());
326 }
327
328 #[test]
329 fn set_all_is_all_or_nothing() {
330 let config = config();
331 let mut doc = blank_doc();
332 let mut ed = TypedWriter::new(&config, &mut doc);
333 let errs = ed
335 .set_all([("qty", "5"), ("subject", "bad\n\nblock")])
336 .unwrap_err();
337 assert_eq!(errs.len(), 1);
338 assert_eq!(errs[0].0, "subject");
339 assert!(doc.main().payload().get("qty").is_none());
340
341 let mut ed = TypedWriter::new(&config, &mut doc);
343 ed.set_all([("qty", "5"), ("subject", "ok")]).unwrap();
344 assert_eq!(
345 doc.main().payload().get("qty").unwrap().as_json(),
346 &serde_json::json!(5)
347 );
348 }
349
350 #[test]
351 fn set_all_rejects_unknown_field() {
352 let config = config();
353 let mut doc = blank_doc();
354 let mut ed = TypedWriter::new(&config, &mut doc);
355 let errs = ed.set_all([("qty", "3"), ("titel", "oops")]).unwrap_err();
359 assert_eq!(errs.len(), 1);
360 assert_eq!(errs[0].0, "titel");
361 assert_eq!(errs[0].1.variant_name(), "UnknownField");
362 assert!(doc.main().payload().get("qty").is_none());
363 }
364
365 #[test]
366 fn set_body_revises_main_body() {
367 let config = config();
368 let mut doc = blank_doc();
369 let mut ed = TypedWriter::new(&config, &mut doc);
370 ed.set_body("**hi**").unwrap();
371 assert_eq!(doc.main().body_markdown(), "**hi**");
372 }
373
374 #[test]
375 fn add_card_fuses_new_commit_push() {
376 let config = config();
377 let mut doc = blank_doc();
378 let mut ed = TypedWriter::new(&config, &mut doc);
379 ed.add_card("note", [("body", "**hi**")], Some("card body"), None)
380 .unwrap();
381 assert_eq!(doc.cards().len(), 1);
382 assert_eq!(doc.cards()[0].kind(), Some("note"));
383 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
384 assert_eq!(doc.cards()[0].body_markdown(), "card body");
385 }
386
387 #[test]
388 fn add_card_at_inserts_and_remove_card_returns() {
389 let config = config();
390 let mut doc = blank_doc();
391 {
392 let mut ed = TypedWriter::new(&config, &mut doc);
393 ed.add_card("note", [("body", "a")], None, None).unwrap();
394 ed.add_card("note", [("body", "c")], None, None).unwrap();
395 ed.add_card("note", [("body", "b")], None, Some(1)).unwrap();
397 }
398 let bodies: Vec<String> = doc
399 .cards()
400 .iter()
401 .map(|c| c.field_markdown("body").unwrap().unwrap())
402 .collect();
403 assert_eq!(bodies, ["a", "b", "c"]);
404
405 {
407 let mut ed = TypedWriter::new(&config, &mut doc);
408 let errs = ed
409 .add_card("note", [("body", "x")], None, Some(9))
410 .unwrap_err();
411 assert_eq!(errs[0].0, "$kind");
412 }
413 assert_eq!(doc.cards().len(), 3);
414
415 {
417 let mut ed = TypedWriter::new(&config, &mut doc);
418 let removed = ed.remove_card(1).unwrap();
419 assert_eq!(removed.field_markdown("body").unwrap().unwrap(), "b");
420 assert!(ed.remove_card(5).is_none());
421 }
422 assert_eq!(doc.cards().len(), 2);
423 }
424
425 #[test]
426 fn add_card_is_transactional_on_bad_field() {
427 let config = config();
428 let mut doc = blank_doc();
429 let mut ed = TypedWriter::new(&config, &mut doc);
430 let errs = ed
432 .add_card("note", [("stray", "x")], None, None)
433 .unwrap_err();
434 assert_eq!(errs[0].0, "stray");
435 assert_eq!(errs[0].1.variant_name(), "UnknownField");
436 assert_eq!(doc.cards().len(), 0);
437 }
438
439 #[test]
440 fn add_card_reports_invalid_kind() {
441 let config = config();
442 let mut doc = blank_doc();
443 let mut ed = TypedWriter::new(&config, &mut doc);
444 let errs = ed
446 .add_card("$reserved", [] as [(&str, &str); 0], None, None)
447 .unwrap_err();
448 assert_eq!(errs[0].0, "$kind");
449 assert_eq!(doc.cards().len(), 0);
450 }
451
452 #[test]
453 fn card_writer_resolves_card_kind_schema() {
454 let config = config();
455 let mut doc = blank_doc();
456 doc.push_card(Card::new("note").unwrap()).unwrap();
457
458 let mut ed = TypedWriter::new(&config, &mut doc);
459 let mut card_ed = ed.card(0).unwrap();
460 card_ed.set("body", "**hi**").unwrap();
461 let err = card_ed.set("stray", "v").unwrap_err();
463 assert_eq!(err.variant_name(), "UnknownField");
464
465 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
466
467 let mut ed = TypedWriter::new(&config, &mut doc);
469 assert!(matches!(
470 ed.card(9),
471 Err(EditError::IndexOutOfRange { .. })
472 ));
473 }
474
475 #[test]
476 fn revise_field_is_typed_and_rejects_unknown_and_non_inline() {
477 let config = config();
478 let mut doc = blank_doc();
479 let mut ed = TypedWriter::new(&config, &mut doc);
480 let _delta = ed.revise_field("subject", "Hello").unwrap();
482 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
483
484 let mut ed = TypedWriter::new(&config, &mut doc);
486 assert_eq!(
487 ed.revise_field("nope", "x").unwrap_err().variant_name(),
488 "UnknownField"
489 );
490 let err = ed.revise_field("subject", "a\n\nb").unwrap_err();
492 assert_eq!(err.variant_name(), "FieldRichtextNotInline");
493 assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
494 }
495
496 #[test]
497 fn card_writer_revise_field_resolves_card_schema() {
498 let config = config();
499 let mut doc = blank_doc();
500 doc.push_card(Card::new("note").unwrap()).unwrap();
501
502 let mut ed = TypedWriter::new(&config, &mut doc);
503 ed.card(0).unwrap().revise_field("body", "**hi**").unwrap();
504 assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
505
506 let mut ed = TypedWriter::new(&config, &mut doc);
507 assert_eq!(
508 ed.card(0)
509 .unwrap()
510 .revise_field("stray", "x")
511 .unwrap_err()
512 .variant_name(),
513 "UnknownField"
514 );
515 }
516}