use indexmap::IndexMap;
use crate::document::edit::resolve_field_write;
use crate::document::{Card, Document, EditError};
use crate::quill::{CardSchema, FieldSchema, QuillConfig};
use crate::value::QuillValue;
use crate::Delta;
pub struct TypedWriter<'a> {
config: &'a QuillConfig,
doc: &'a mut Document,
}
impl<'a> TypedWriter<'a> {
pub fn new(config: &'a QuillConfig, doc: &'a mut Document) -> Self {
Self { config, doc }
}
pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
let config = self.config;
match config.main.fields.get(name) {
Some(schema) => self.doc.main_mut().commit_field(name, value, schema),
None => Err(EditError::UnknownField(name.to_string())),
}
}
pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
where
K: Into<String>,
V: Into<QuillValue>,
I: IntoIterator<Item = (K, V)>,
{
let schema = Some(&self.config.main.fields);
set_all_impl(self.doc.main_mut(), schema, fields)
}
pub fn set_body(&mut self, markdown: &str) -> Result<(), EditError> {
self.doc.main_mut().revise_body(markdown).map(|_| ())
}
pub fn revise_field(&mut self, name: &str, markdown: &str) -> Result<Delta, EditError> {
match self.config.main.fields.get(name) {
Some(schema) => self.doc.main_mut().revise_field_checked(name, markdown, schema),
None => Err(EditError::UnknownField(name.to_string())),
}
}
pub fn add_card<K, V, I>(
&mut self,
kind: &str,
fields: I,
body: Option<&str>,
at: Option<usize>,
) -> Result<(), Vec<(String, EditError)>>
where
K: Into<String>,
V: Into<QuillValue>,
I: IntoIterator<Item = (K, V)>,
{
let mut card = Card::new(kind).map_err(|e| vec![("$kind".to_string(), e)])?;
let schema = self.config.card_kind(kind).map(|s| &s.fields);
set_all_impl(&mut card, schema, fields)?;
if let Some(md) = body {
card.revise_body(md)
.map_err(|e| vec![("$body".to_string(), e)])?;
}
match at {
Some(index) => self.doc.insert_card(index, card),
None => self.doc.push_card(card),
}
.map_err(|e| vec![("$kind".to_string(), e)])?;
Ok(())
}
pub fn remove_card(&mut self, index: usize) -> Option<Card> {
self.doc.remove_card(index)
}
pub fn card(&mut self, index: usize) -> Result<CardWriter<'_>, EditError> {
let config = self.config;
let len = self.doc.cards().len();
let card = self
.doc
.card_mut(index)
.ok_or(EditError::IndexOutOfRange { index, len })?;
let schema = card.kind().and_then(|k| config.card_kind(k));
Ok(CardWriter { schema, card })
}
}
pub struct CardWriter<'a> {
schema: Option<&'a CardSchema>,
card: &'a mut Card,
}
impl CardWriter<'_> {
pub fn kind(&self) -> Option<&str> {
self.card.kind()
}
pub fn set(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
match self.schema.and_then(|s| s.fields.get(name)) {
Some(schema) => self.card.commit_field(name, value, schema),
None => Err(EditError::UnknownField(name.to_string())),
}
}
pub fn set_body(&mut self, markdown: &str) -> Result<(), EditError> {
self.card.revise_body(markdown).map(|_| ())
}
pub fn revise_field(&mut self, name: &str, markdown: &str) -> Result<Delta, EditError> {
match self.schema.and_then(|s| s.fields.get(name)) {
Some(schema) => self.card.revise_field_checked(name, markdown, schema),
None => Err(EditError::UnknownField(name.to_string())),
}
}
pub fn set_all<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
where
K: Into<String>,
V: Into<QuillValue>,
I: IntoIterator<Item = (K, V)>,
{
set_all_impl(self.card, self.schema.map(|s| &s.fields), fields)
}
}
fn set_all_impl<K, V, I>(
card: &mut Card,
fields_schema: Option<&IndexMap<String, FieldSchema>>,
fields: I,
) -> Result<(), Vec<(String, EditError)>>
where
K: Into<String>,
V: Into<QuillValue>,
I: IntoIterator<Item = (K, V)>,
{
let fields: Vec<(String, QuillValue)> = fields
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
let mut resolved: Vec<(String, QuillValue)> = Vec::with_capacity(fields.len());
let mut errors: Vec<(String, EditError)> = Vec::new();
for (name, value) in fields {
match fields_schema.and_then(|m| m.get(&name)) {
Some(schema) => match resolve_field_write(&name, value, schema) {
Ok(stored) => resolved.push((name, stored)),
Err(e) => errors.push((name, e)),
},
None => errors.push((name.clone(), EditError::UnknownField(name))),
}
}
if !errors.is_empty() {
return Err(errors);
}
for (name, stored) in resolved {
card.payload_mut().insert_unchecked(name, stored);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::{Card, Document};
use crate::version::QuillReference;
use std::str::FromStr;
const QUILL_YAML: &str = "\
quill:
name: memo
backend: typst
version: 1.0.0
description: Editor test quill
main:
fields:
subject:
type: richtext
inline: true
qty:
type: integer
card_kinds:
note:
fields:
body:
type: richtext
";
fn config() -> QuillConfig {
QuillConfig::from_yaml(QUILL_YAML).expect("valid quill")
}
fn blank_doc() -> Document {
Document::new(QuillReference::from_str("memo@1.0.0").unwrap())
}
#[test]
fn set_resolves_schema_field_as_typed_commit() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
ed.set("qty", "3").unwrap();
ed.set("subject", "Hello").unwrap();
assert_eq!(
doc.main().payload().get("qty").unwrap().as_json(),
&serde_json::json!(3)
);
assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
}
#[test]
fn set_rejects_unknown_field() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let err = ed.set("notafield", "x").unwrap_err();
assert_eq!(err.variant_name(), "UnknownField");
assert!(doc.main().payload().get("notafield").is_none());
}
#[test]
fn set_all_is_all_or_nothing() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let errs = ed
.set_all([("qty", "5"), ("subject", "bad\n\nblock")])
.unwrap_err();
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].0, "subject");
assert!(doc.main().payload().get("qty").is_none());
let mut ed = TypedWriter::new(&config, &mut doc);
ed.set_all([("qty", "5"), ("subject", "ok")]).unwrap();
assert_eq!(
doc.main().payload().get("qty").unwrap().as_json(),
&serde_json::json!(5)
);
}
#[test]
fn set_all_rejects_unknown_field() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let errs = ed.set_all([("qty", "3"), ("titel", "oops")]).unwrap_err();
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].0, "titel");
assert_eq!(errs[0].1.variant_name(), "UnknownField");
assert!(doc.main().payload().get("qty").is_none());
}
#[test]
fn set_body_revises_main_body() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
ed.set_body("**hi**").unwrap();
assert_eq!(doc.main().body_markdown(), "**hi**");
}
#[test]
fn add_card_fuses_new_commit_push() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
ed.add_card("note", [("body", "**hi**")], Some("card body"), None)
.unwrap();
assert_eq!(doc.cards().len(), 1);
assert_eq!(doc.cards()[0].kind(), Some("note"));
assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
assert_eq!(doc.cards()[0].body_markdown(), "card body");
}
#[test]
fn add_card_at_inserts_and_remove_card_returns() {
let config = config();
let mut doc = blank_doc();
{
let mut ed = TypedWriter::new(&config, &mut doc);
ed.add_card("note", [("body", "a")], None, None).unwrap();
ed.add_card("note", [("body", "c")], None, None).unwrap();
ed.add_card("note", [("body", "b")], None, Some(1)).unwrap();
}
let bodies: Vec<String> = doc
.cards()
.iter()
.map(|c| c.field_markdown("body").unwrap().unwrap())
.collect();
assert_eq!(bodies, ["a", "b", "c"]);
{
let mut ed = TypedWriter::new(&config, &mut doc);
let errs = ed
.add_card("note", [("body", "x")], None, Some(9))
.unwrap_err();
assert_eq!(errs[0].0, "$kind");
}
assert_eq!(doc.cards().len(), 3);
{
let mut ed = TypedWriter::new(&config, &mut doc);
let removed = ed.remove_card(1).unwrap();
assert_eq!(removed.field_markdown("body").unwrap().unwrap(), "b");
assert!(ed.remove_card(5).is_none());
}
assert_eq!(doc.cards().len(), 2);
}
#[test]
fn add_card_is_transactional_on_bad_field() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let errs = ed
.add_card("note", [("stray", "x")], None, None)
.unwrap_err();
assert_eq!(errs[0].0, "stray");
assert_eq!(errs[0].1.variant_name(), "UnknownField");
assert_eq!(doc.cards().len(), 0);
}
#[test]
fn add_card_reports_invalid_kind() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let errs = ed
.add_card("$reserved", [] as [(&str, &str); 0], None, None)
.unwrap_err();
assert_eq!(errs[0].0, "$kind");
assert_eq!(doc.cards().len(), 0);
}
#[test]
fn card_writer_resolves_card_kind_schema() {
let config = config();
let mut doc = blank_doc();
doc.push_card(Card::new("note").unwrap()).unwrap();
let mut ed = TypedWriter::new(&config, &mut doc);
let mut card_ed = ed.card(0).unwrap();
card_ed.set("body", "**hi**").unwrap();
let err = card_ed.set("stray", "v").unwrap_err();
assert_eq!(err.variant_name(), "UnknownField");
assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
let mut ed = TypedWriter::new(&config, &mut doc);
assert!(matches!(
ed.card(9),
Err(EditError::IndexOutOfRange { .. })
));
}
#[test]
fn revise_field_is_typed_and_rejects_unknown_and_non_inline() {
let config = config();
let mut doc = blank_doc();
let mut ed = TypedWriter::new(&config, &mut doc);
let _delta = ed.revise_field("subject", "Hello").unwrap();
assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
let mut ed = TypedWriter::new(&config, &mut doc);
assert_eq!(
ed.revise_field("nope", "x").unwrap_err().variant_name(),
"UnknownField"
);
let err = ed.revise_field("subject", "a\n\nb").unwrap_err();
assert_eq!(err.variant_name(), "FieldRichtextNotInline");
assert_eq!(doc.main().field_markdown("subject").unwrap().unwrap(), "Hello");
}
#[test]
fn card_writer_revise_field_resolves_card_schema() {
let config = config();
let mut doc = blank_doc();
doc.push_card(Card::new("note").unwrap()).unwrap();
let mut ed = TypedWriter::new(&config, &mut doc);
ed.card(0).unwrap().revise_field("body", "**hi**").unwrap();
assert_eq!(doc.cards()[0].field_markdown("body").unwrap().unwrap(), "**hi**");
let mut ed = TypedWriter::new(&config, &mut doc);
assert_eq!(
ed.card(0)
.unwrap()
.revise_field("stray", "x")
.unwrap_err()
.variant_name(),
"UnknownField"
);
}
}