use indexmap::IndexMap;
use crate::document::{Card, Document, EditError, RichtextDecodeError};
use crate::quill::{CardSchema, FieldSchema, FieldType, QuillConfig};
use crate::value::QuillValue;
#[derive(Debug, Clone, PartialEq)]
pub enum ReadValue {
Markdown(String),
Plaintext(String),
Value(QuillValue),
}
impl ReadValue {
pub fn as_text(&self) -> Option<&str> {
match self {
ReadValue::Markdown(s) | ReadValue::Plaintext(s) => Some(s),
ReadValue::Value(_) => None,
}
}
pub fn as_value(&self) -> Option<&QuillValue> {
match self {
ReadValue::Value(v) => Some(v),
ReadValue::Markdown(_) | ReadValue::Plaintext(_) => None,
}
}
}
pub struct TypedReader<'a> {
config: &'a QuillConfig,
doc: &'a Document,
}
impl<'a> TypedReader<'a> {
pub fn new(config: &'a QuillConfig, doc: &'a Document) -> Self {
Self { config, doc }
}
pub fn get(&self, name: &str) -> Result<Option<ReadValue>, EditError> {
read_field(self.doc.main(), Some(&self.config.main.fields), name)
}
pub fn get_body(&self) -> String {
self.doc.main().body_markdown()
}
pub fn card(&self, index: usize) -> Result<CardReader<'_>, EditError> {
let len = self.doc.cards().len();
let card = self
.doc
.card(index)
.ok_or(EditError::IndexOutOfRange { index, len })?;
let schema = card.kind().and_then(|k| self.config.card_kind(k));
Ok(CardReader { schema, card })
}
}
pub struct CardReader<'a> {
schema: Option<&'a CardSchema>,
card: &'a Card,
}
impl CardReader<'_> {
pub fn kind(&self) -> Option<&str> {
self.card.kind()
}
pub fn get(&self, name: &str) -> Result<Option<ReadValue>, EditError> {
read_field(self.card, self.schema.map(|s| &s.fields), name)
}
pub fn get_body(&self) -> String {
self.card.body_markdown()
}
}
fn read_field(
card: &Card,
fields_schema: Option<&IndexMap<String, FieldSchema>>,
name: &str,
) -> Result<Option<ReadValue>, EditError> {
let schema = fields_schema
.and_then(|m| m.get(name))
.ok_or_else(|| EditError::UnknownField(name.to_string()))?;
match schema.r#type {
FieldType::RichText { .. } => project(card.field_markdown(name), name, ReadValue::Markdown),
FieldType::PlainText { .. } => {
project(card.field_plaintext(name), name, ReadValue::Plaintext)
}
_ => Ok(card
.payload()
.get(name)
.map(|v| ReadValue::Value(v.clone()))),
}
}
fn project(
projection: Option<Result<String, RichtextDecodeError>>,
name: &str,
wrap: fn(String) -> ReadValue,
) -> Result<Option<ReadValue>, EditError> {
match projection {
None => Ok(None),
Some(Ok(text)) => Ok(Some(wrap(text))),
Some(Err(e)) => Err(EditError::FieldRichtextDecode {
field: name.to_string(),
message: e.into_message(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use crate::version::QuillReference;
use std::str::FromStr;
const QUILL_YAML: &str = "\
quill:
name: memo
backend: typst
version: 1.0.0
description: Reader test quill
main:
fields:
subject:
type: richtext
inline: true
note:
type: plaintext
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())
}
fn seeded_doc(config: &QuillConfig) -> Document {
let mut doc = blank_doc();
{
let mut w = crate::TypedWriter::new(config, &mut doc);
w.set("subject", "Hello **world**").unwrap();
w.set("qty", "3").unwrap();
w.add_card("note", [("body", "a *card*")], None, None).unwrap();
}
doc
}
#[test]
fn richtext_field_projects_to_markdown() {
let config = config();
let doc = seeded_doc(&config);
let view = TypedReader::new(&config, &doc);
assert_eq!(
view.get("subject").unwrap(),
Some(ReadValue::Markdown("Hello **world**".to_string()))
);
}
#[test]
fn plaintext_field_projects_to_literal_text() {
let config = config();
let mut doc = blank_doc();
{
let mut w = crate::TypedWriter::new(&config, &mut doc);
w.set("note", "a *literal* line").unwrap();
}
let view = TypedReader::new(&config, &doc);
assert_eq!(
view.get("note").unwrap(),
Some(ReadValue::Plaintext("a *literal* line".to_string()))
);
}
#[test]
fn scalar_field_returns_canonical_value() {
let config = config();
let doc = seeded_doc(&config);
let view = TypedReader::new(&config, &doc);
assert_eq!(
view.get("qty").unwrap(),
Some(ReadValue::Value(QuillValue::from_json(serde_json::json!(3))))
);
}
#[test]
fn absent_field_returns_none() {
let config = config();
let doc = blank_doc();
let view = TypedReader::new(&config, &doc);
assert_eq!(view.get("subject").unwrap(), None);
assert_eq!(view.get("qty").unwrap(), None);
}
#[test]
fn unknown_field_name_raises() {
let config = config();
let doc = blank_doc();
let view = TypedReader::new(&config, &doc);
assert!(matches!(
view.get("nope"),
Err(EditError::UnknownField(name)) if name == "nope"
));
}
#[test]
fn richtext_field_holding_scalar_raises_mismatch() {
let config = config();
let mut doc = blank_doc();
doc.main_mut()
.store_field("subject", QuillValue::from_json(serde_json::json!(3)))
.unwrap();
let view = TypedReader::new(&config, &doc);
assert!(matches!(
view.get("subject"),
Err(EditError::FieldRichtextDecode { field, .. }) if field == "subject"
));
}
#[test]
fn card_field_reads_through_kind_schema() {
let config = config();
let doc = seeded_doc(&config);
let view = TypedReader::new(&config, &doc);
let card = view.card(0).unwrap();
assert_eq!(card.kind(), Some("note"));
assert_eq!(
card.get("body").unwrap(),
Some(ReadValue::Markdown("a *card*".to_string()))
);
assert!(matches!(card.get("nope"), Err(EditError::UnknownField(_))));
}
#[test]
fn card_out_of_range_raises() {
let config = config();
let doc = blank_doc();
let view = TypedReader::new(&config, &doc);
assert!(matches!(
view.card(9),
Err(EditError::IndexOutOfRange { index: 9, len: 0 })
));
}
#[test]
fn body_read_is_quill_free() {
let config = config();
let mut doc = blank_doc();
doc.main_mut().revise_body("A **body**.").unwrap();
let view = TypedReader::new(&config, &doc);
assert_eq!(view.get_body(), "A **body**.");
}
}