1#![allow(non_camel_case_types)]
36
37use std::str::FromStr;
38
39use serde::{Deserialize, Serialize};
40
41use super::meta::validate_composable_kind;
42use super::payload::{MetaKey, Payload, PayloadItem};
43use super::prescan::{CommentPathSegment, NestedComment};
44use super::{Card, Document};
45use crate::value::QuillValue;
46use crate::version::QuillReference;
47
48pub const SCHEMA_V0_92_0: &str = "quillmark/document@0.92.0";
53
54pub fn peek_schema_version(json: &str) -> Option<String> {
63 #[derive(Deserialize)]
64 struct Peek {
65 schema: Option<String>,
66 }
67 serde_json::from_str::<Peek>(json).ok()?.schema
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76#[serde(tag = "schema")]
77pub enum StoredDocument {
78 #[serde(rename = "quillmark/document@0.92.0")]
81 V0_92_0(DocumentV0_92_0),
82 #[serde(rename = "quillmark/document@0.82.0")]
85 V0_82_0(DocumentV0_82_0),
86 #[serde(rename = "quillmark/document@0.81.0")]
89 V0_81_0(DocumentV0_81_0),
90}
91
92#[derive(Debug, Clone, PartialEq)]
101pub enum StorageError {
102 InvalidQuillReference {
104 value: String,
106 reason: String,
108 },
109 Malformed(String),
112}
113
114impl std::fmt::Display for StorageError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 StorageError::InvalidQuillReference { value, reason } => {
118 write!(f, "invalid quill reference {value:?}: {reason}")
119 }
120 StorageError::Malformed(msg) => f.write_str(msg),
121 }
122 }
123}
124
125impl std::error::Error for StorageError {}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct DocumentV0_82_0 {
132 pub main: CardV0_82_0,
133 #[serde(default)]
134 pub cards: Vec<CardV0_82_0>,
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct CardV0_82_0 {
140 pub payload: PayloadV0_82_0,
141 #[serde(default)]
142 pub body: String,
143}
144
145#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
147pub struct PayloadV0_82_0 {
148 #[serde(default)]
149 pub items: Vec<PayloadItemV0_82_0>,
150 #[serde(default)]
151 pub nested_comments: Vec<NestedCommentV0_82_0>,
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160#[serde(tag = "type", rename_all = "lowercase")]
161pub enum PayloadItemV0_82_0 {
162 Quill { value: String },
164 Kind { value: String },
166 Id { value: String },
168 Ext {
172 value: serde_json::Map<String, serde_json::Value>,
173 },
174 Field {
176 key: String,
177 value: serde_json::Value,
178 #[serde(default)]
179 fill: bool,
180 },
181 Comment {
183 text: String,
184 #[serde(default)]
185 inline: bool,
186 },
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct NestedCommentV0_82_0 {
192 pub container_path: Vec<CommentPathSegmentV0_82_0>,
193 pub position: usize,
194 pub text: String,
195 pub inline: bool,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub enum CommentPathSegmentV0_82_0 {
201 Key(String),
202 Index(usize),
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct DocumentV0_92_0 {
210 pub main: CardV0_92_0,
211 #[serde(default)]
212 pub cards: Vec<CardV0_92_0>,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct CardV0_92_0 {
218 pub payload: PayloadV0_92_0,
219 #[serde(default)]
220 pub body: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
225pub struct PayloadV0_92_0 {
226 #[serde(default)]
227 pub items: Vec<PayloadItemV0_92_0>,
228 #[serde(default)]
229 pub nested_comments: Vec<NestedCommentV0_92_0>,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
237#[serde(tag = "type", rename_all = "lowercase")]
238pub enum PayloadItemV0_92_0 {
239 Quill { value: String },
241 Kind { value: String },
243 Id { value: String },
245 Ext {
248 value: serde_json::Map<String, serde_json::Value>,
249 },
250 Seed {
253 value: serde_json::Map<String, serde_json::Value>,
254 },
255 Field {
257 key: String,
258 value: serde_json::Value,
259 #[serde(default)]
260 fill: bool,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
262 nested_fills: Vec<Vec<CommentPathSegmentV0_92_0>>,
263 },
264 Comment {
266 text: String,
267 #[serde(default)]
268 inline: bool,
269 },
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct NestedCommentV0_92_0 {
275 pub container_path: Vec<CommentPathSegmentV0_92_0>,
276 pub position: usize,
277 pub text: String,
278 pub inline: bool,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284pub enum CommentPathSegmentV0_92_0 {
285 Key(String),
286 Index(usize),
287}
288
289impl From<Document> for StoredDocument {
292 fn from(doc: Document) -> Self {
293 StoredDocument::V0_92_0(DocumentV0_92_0::from(&doc))
294 }
295}
296
297impl From<&Document> for DocumentV0_92_0 {
298 fn from(doc: &Document) -> Self {
299 DocumentV0_92_0 {
300 main: CardV0_92_0::from(doc.main()),
301 cards: doc.cards().iter().map(CardV0_92_0::from).collect(),
302 }
303 }
304}
305
306impl From<&Card> for CardV0_92_0 {
307 fn from(card: &Card) -> Self {
308 CardV0_92_0 {
309 payload: PayloadV0_92_0::from(card.payload()),
310 body: card.body().to_string(),
311 }
312 }
313}
314
315impl From<&Payload> for PayloadV0_92_0 {
316 fn from(payload: &Payload) -> Self {
317 let nested_comments = payload
321 .flat_nested_comments()
322 .iter()
323 .map(NestedCommentV0_92_0::from)
324 .collect();
325 PayloadV0_92_0 {
326 items: payload
327 .items()
328 .iter()
329 .map(PayloadItemV0_92_0::from)
330 .collect(),
331 nested_comments,
332 }
333 }
334}
335
336impl From<&PayloadItem> for PayloadItemV0_92_0 {
337 fn from(item: &PayloadItem) -> Self {
338 match item {
339 PayloadItem::Quill { reference } => PayloadItemV0_92_0::Quill {
340 value: reference.to_string(),
341 },
342 PayloadItem::Kind { value } => PayloadItemV0_92_0::Kind {
343 value: value.clone(),
344 },
345 PayloadItem::Id { value } => PayloadItemV0_92_0::Id {
346 value: value.clone(),
347 },
348 PayloadItem::Meta {
354 key: MetaKey::Ext,
355 value,
356 ..
357 } => PayloadItemV0_92_0::Ext {
358 value: value.clone(),
359 },
360 PayloadItem::Meta {
361 key: MetaKey::Seed,
362 value,
363 ..
364 } => PayloadItemV0_92_0::Seed {
365 value: value.clone(),
366 },
367 PayloadItem::Field {
371 key, value, fill, ..
372 } => PayloadItemV0_92_0::Field {
373 key: key.clone(),
374 value: value.as_json().clone(),
375 fill: *fill,
376 nested_fills: value
377 .nonroot_fill_paths()
378 .map(|p| p.iter().map(CommentPathSegmentV0_92_0::from).collect())
379 .collect(),
380 },
381 PayloadItem::Comment { text, inline } => PayloadItemV0_92_0::Comment {
382 text: text.clone(),
383 inline: *inline,
384 },
385 }
386 }
387}
388
389impl From<&NestedComment> for NestedCommentV0_92_0 {
390 fn from(nc: &NestedComment) -> Self {
391 NestedCommentV0_92_0 {
392 container_path: nc
393 .container_path
394 .iter()
395 .map(CommentPathSegmentV0_92_0::from)
396 .collect(),
397 position: nc.position,
398 text: nc.text.clone(),
399 inline: nc.inline,
400 }
401 }
402}
403
404impl From<&CommentPathSegment> for CommentPathSegmentV0_92_0 {
405 fn from(seg: &CommentPathSegment) -> Self {
406 match seg {
407 CommentPathSegment::Key(k) => CommentPathSegmentV0_92_0::Key(k.clone()),
408 CommentPathSegment::Index(i) => CommentPathSegmentV0_92_0::Index(*i),
409 }
410 }
411}
412
413impl TryFrom<StoredDocument> for Document {
414 type Error = StorageError;
415
416 fn try_from(stored: StoredDocument) -> Result<Self, Self::Error> {
417 match stored {
420 StoredDocument::V0_92_0(payload) => Document::try_from(payload),
421 StoredDocument::V0_82_0(payload) => Document::try_from(DocumentV0_92_0::from(payload)),
422 StoredDocument::V0_81_0(payload) => {
423 Document::try_from(DocumentV0_92_0::from(DocumentV0_82_0::from(payload)))
424 }
425 }
426 }
427}
428
429impl TryFrom<DocumentV0_92_0> for Document {
430 type Error = StorageError;
431
432 fn try_from(payload: DocumentV0_92_0) -> Result<Self, Self::Error> {
433 let main = Card::try_from(payload.main)?;
434 if main.quill().is_none() {
435 return Err(StorageError::Malformed(
436 "main card must carry a $quill entry".into(),
437 ));
438 }
439 let cards = payload
440 .cards
441 .into_iter()
442 .map(Card::try_from)
443 .collect::<Result<Vec<_>, _>>()?;
444 for card in &cards {
445 if card.quill().is_some() {
446 return Err(StorageError::Malformed(
447 "composable cards must not carry a $quill entry".into(),
448 ));
449 }
450 if card.seed().is_some() {
451 return Err(StorageError::Malformed(
452 "composable cards must not carry a $seed entry".into(),
453 ));
454 }
455 if let Some(kind) = card.kind() {
456 match validate_composable_kind(kind) {
457 Ok(()) => {}
458 Err(super::meta::CardKindError::InvalidName) => {
459 return Err(StorageError::Malformed(format!(
460 "invalid composable card kind {kind:?}: must match \
461 [a-z_][a-z0-9_]*"
462 )));
463 }
464 Err(super::meta::CardKindError::Reserved) => {
465 return Err(StorageError::Malformed(format!(
466 "composable card kind {kind:?} is reserved (root only)"
467 )));
468 }
469 }
470 }
471 }
472 Ok(Document::from_main_and_cards(main, cards, Vec::new()))
473 }
474}
475
476impl TryFrom<CardV0_92_0> for Card {
477 type Error = StorageError;
478
479 fn try_from(card: CardV0_92_0) -> Result<Self, Self::Error> {
480 let payload = Payload::try_from(card.payload)?;
481 validate_dto_payload(&payload)?;
482 Ok(Card::from_parts(payload, card.body))
483 }
484}
485
486impl TryFrom<PayloadV0_92_0> for Payload {
487 type Error = StorageError;
488
489 fn try_from(p: PayloadV0_92_0) -> Result<Self, Self::Error> {
490 let mut items = Vec::with_capacity(p.items.len());
491 for item in p.items {
492 items.push(PayloadItem::try_from(item)?);
493 }
494 let nested = p
495 .nested_comments
496 .into_iter()
497 .map(NestedComment::from)
498 .collect();
499 Ok(Payload::from_items_with_flat_nested(items, nested))
502 }
503}
504
505impl TryFrom<PayloadItemV0_92_0> for PayloadItem {
506 type Error = StorageError;
507
508 fn try_from(item: PayloadItemV0_92_0) -> Result<Self, Self::Error> {
509 Ok(match item {
510 PayloadItemV0_92_0::Quill { value } => {
511 let reference = QuillReference::from_str(&value).map_err(|reason| {
512 StorageError::InvalidQuillReference {
513 value: value.clone(),
514 reason,
515 }
516 })?;
517 PayloadItem::Quill { reference }
518 }
519 PayloadItemV0_92_0::Kind { value } => PayloadItem::Kind { value },
520 PayloadItemV0_92_0::Id { value } => PayloadItem::Id { value },
521 PayloadItemV0_92_0::Ext { value } => PayloadItem::Meta {
522 key: MetaKey::Ext,
523 value: depth_check_meta_map(value, "$ext")?,
524 nested_comments: Vec::new(),
525 },
526 PayloadItemV0_92_0::Seed { value } => PayloadItem::Meta {
527 key: MetaKey::Seed,
528 value: depth_check_meta_map(value, "$seed")?,
529 nested_comments: Vec::new(),
530 },
531 PayloadItemV0_92_0::Field {
532 key,
533 value,
534 fill,
535 nested_fills,
536 } => {
537 use super::edit::{validate_field, FieldViolation};
538 validate_field(&key, &value).map_err(|v| {
539 StorageError::Malformed(match v {
540 FieldViolation::InvalidName => {
541 format!("invalid field name {key:?}: must match [A-Za-z_][A-Za-z0-9_]*")
542 }
543 FieldViolation::TooDeep => format!(
544 "field {key:?} nests deeper than the maximum of {} levels",
545 crate::document::limits::MAX_YAML_DEPTH
546 ),
547 })
548 })?;
549 let mut qv = QuillValue::from_json(value);
550 for path in nested_fills {
551 let segs: Vec<CommentPathSegment> =
552 path.into_iter().map(CommentPathSegment::from).collect();
553 qv.set_fill_at(&segs);
554 }
555 PayloadItem::Field {
556 key,
557 value: qv,
558 fill,
559 nested_comments: Vec::new(),
560 }
561 }
562 PayloadItemV0_92_0::Comment { text, inline } => PayloadItem::Comment { text, inline },
563 })
564 }
565}
566
567fn depth_check_meta_map(
570 value: serde_json::Map<String, serde_json::Value>,
571 key: &str,
572) -> Result<serde_json::Map<String, serde_json::Value>, StorageError> {
573 let as_value = serde_json::Value::Object(value);
574 if crate::value::json_depth_exceeds(&as_value, crate::document::limits::MAX_YAML_DEPTH) {
575 return Err(StorageError::Malformed(format!(
576 "{key} nests deeper than the maximum of {} levels",
577 crate::document::limits::MAX_YAML_DEPTH
578 )));
579 }
580 let serde_json::Value::Object(value) = as_value else {
581 unreachable!("constructed as Object above")
582 };
583 Ok(value)
584}
585
586impl From<NestedCommentV0_92_0> for NestedComment {
587 fn from(nc: NestedCommentV0_92_0) -> Self {
588 NestedComment {
589 container_path: nc
590 .container_path
591 .into_iter()
592 .map(CommentPathSegment::from)
593 .collect(),
594 position: nc.position,
595 text: nc.text,
596 inline: nc.inline,
597 }
598 }
599}
600
601impl From<CommentPathSegmentV0_92_0> for CommentPathSegment {
602 fn from(seg: CommentPathSegmentV0_92_0) -> Self {
603 match seg {
604 CommentPathSegmentV0_92_0::Key(k) => CommentPathSegment::Key(k),
605 CommentPathSegmentV0_92_0::Index(i) => CommentPathSegment::Index(i),
606 }
607 }
608}
609
610impl From<DocumentV0_82_0> for DocumentV0_92_0 {
617 fn from(d: DocumentV0_82_0) -> Self {
618 DocumentV0_92_0 {
619 main: CardV0_92_0::from(d.main),
620 cards: d.cards.into_iter().map(CardV0_92_0::from).collect(),
621 }
622 }
623}
624
625impl From<CardV0_82_0> for CardV0_92_0 {
626 fn from(c: CardV0_82_0) -> Self {
627 CardV0_92_0 {
628 payload: PayloadV0_92_0::from(c.payload),
629 body: c.body,
630 }
631 }
632}
633
634impl From<PayloadV0_82_0> for PayloadV0_92_0 {
635 fn from(p: PayloadV0_82_0) -> Self {
636 PayloadV0_92_0 {
637 items: p.items.into_iter().map(PayloadItemV0_92_0::from).collect(),
638 nested_comments: p
639 .nested_comments
640 .into_iter()
641 .map(NestedCommentV0_92_0::from)
642 .collect(),
643 }
644 }
645}
646
647impl From<PayloadItemV0_82_0> for PayloadItemV0_92_0 {
648 fn from(item: PayloadItemV0_82_0) -> Self {
649 match item {
650 PayloadItemV0_82_0::Quill { value } => PayloadItemV0_92_0::Quill { value },
651 PayloadItemV0_82_0::Kind { value } => PayloadItemV0_92_0::Kind { value },
652 PayloadItemV0_82_0::Id { value } => PayloadItemV0_92_0::Id { value },
653 PayloadItemV0_82_0::Ext { value } => PayloadItemV0_92_0::Ext { value },
654 PayloadItemV0_82_0::Field { key, value, fill } => PayloadItemV0_92_0::Field {
655 key,
656 value,
657 fill,
658 nested_fills: Vec::new(),
659 },
660 PayloadItemV0_82_0::Comment { text, inline } => {
661 PayloadItemV0_92_0::Comment { text, inline }
662 }
663 }
664 }
665}
666
667impl From<NestedCommentV0_82_0> for NestedCommentV0_92_0 {
668 fn from(nc: NestedCommentV0_82_0) -> Self {
669 NestedCommentV0_92_0 {
670 container_path: nc
671 .container_path
672 .into_iter()
673 .map(CommentPathSegmentV0_92_0::from)
674 .collect(),
675 position: nc.position,
676 text: nc.text,
677 inline: nc.inline,
678 }
679 }
680}
681
682impl From<CommentPathSegmentV0_82_0> for CommentPathSegmentV0_92_0 {
683 fn from(seg: CommentPathSegmentV0_82_0) -> Self {
684 match seg {
685 CommentPathSegmentV0_82_0::Key(k) => CommentPathSegmentV0_92_0::Key(k),
686 CommentPathSegmentV0_82_0::Index(i) => CommentPathSegmentV0_92_0::Index(i),
687 }
688 }
689}
690
691fn validate_dto_payload(payload: &Payload) -> Result<(), StorageError> {
695 if payload.len() > crate::error::MAX_FIELD_COUNT {
696 return Err(StorageError::Malformed(format!(
697 "card has {} user fields, exceeding the maximum of {}",
698 payload.len(),
699 crate::error::MAX_FIELD_COUNT
700 )));
701 }
702 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
703 for key in payload.keys() {
704 if !seen.insert(key.as_str()) {
705 return Err(StorageError::Malformed(format!(
706 "duplicate user-field key {key:?}"
707 )));
708 }
709 }
710 Ok(())
711}
712
713#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
717pub struct DocumentV0_81_0 {
718 pub main: CardV0_81_0,
719 #[serde(default)]
720 pub cards: Vec<CardV0_81_0>,
721}
722
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
725pub struct CardV0_81_0 {
726 pub sentinel: SentinelV0_81_0,
727 #[serde(default)]
728 pub frontmatter: FrontmatterV0_81_0,
729 #[serde(default)]
730 pub body: String,
731}
732
733#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735#[serde(tag = "kind", rename_all = "lowercase")]
736pub enum SentinelV0_81_0 {
737 Main { quill: String },
738 Card { tag: String },
739}
740
741#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
743pub struct FrontmatterV0_81_0 {
744 #[serde(default)]
745 pub items: Vec<FrontmatterItemV0_81_0>,
746 #[serde(default)]
747 pub nested_comments: Vec<NestedCommentV0_81_0>,
748}
749
750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
752#[serde(tag = "kind", rename_all = "lowercase")]
753pub enum FrontmatterItemV0_81_0 {
754 Field {
755 key: String,
756 value: serde_json::Value,
757 #[serde(default)]
758 fill: bool,
759 },
760 Comment {
761 text: String,
762 #[serde(default)]
763 inline: bool,
764 },
765}
766
767#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
769pub struct NestedCommentV0_81_0 {
770 pub container_path: Vec<CommentPathSegmentV0_81_0>,
771 pub position: usize,
772 pub text: String,
773 pub inline: bool,
774}
775
776#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
778pub enum CommentPathSegmentV0_81_0 {
779 Key(String),
780 Index(usize),
781}
782
783impl From<DocumentV0_81_0> for DocumentV0_82_0 {
791 fn from(d: DocumentV0_81_0) -> Self {
792 DocumentV0_82_0 {
793 main: CardV0_82_0::from(d.main),
794 cards: d.cards.into_iter().map(CardV0_82_0::from).collect(),
795 }
796 }
797}
798
799impl From<CardV0_81_0> for CardV0_82_0 {
800 fn from(c: CardV0_81_0) -> Self {
801 let mut items: Vec<PayloadItemV0_82_0> = Vec::new();
802
803 match c.sentinel {
808 SentinelV0_81_0::Main { quill } => {
809 items.push(PayloadItemV0_82_0::Quill { value: quill });
810 items.push(PayloadItemV0_82_0::Kind {
811 value: "main".into(),
812 });
813 }
814 SentinelV0_81_0::Card { tag } => {
815 items.push(PayloadItemV0_82_0::Kind { value: tag });
816 }
817 }
818
819 for item in c.frontmatter.items {
823 items.push(match item {
824 FrontmatterItemV0_81_0::Field { key, value, fill } => {
825 PayloadItemV0_82_0::Field { key, value, fill }
826 }
827 FrontmatterItemV0_81_0::Comment { text, inline } => {
828 PayloadItemV0_82_0::Comment { text, inline }
829 }
830 });
831 }
832
833 let nested_comments = c
834 .frontmatter
835 .nested_comments
836 .into_iter()
837 .map(NestedCommentV0_82_0::from)
838 .collect();
839
840 CardV0_82_0 {
841 payload: PayloadV0_82_0 {
842 items,
843 nested_comments,
844 },
845 body: c.body,
846 }
847 }
848}
849
850impl From<NestedCommentV0_81_0> for NestedCommentV0_82_0 {
851 fn from(nc: NestedCommentV0_81_0) -> Self {
852 NestedCommentV0_82_0 {
853 container_path: nc
854 .container_path
855 .into_iter()
856 .map(CommentPathSegmentV0_82_0::from)
857 .collect(),
858 position: nc.position,
859 text: nc.text,
860 inline: nc.inline,
861 }
862 }
863}
864
865impl From<CommentPathSegmentV0_81_0> for CommentPathSegmentV0_82_0 {
866 fn from(seg: CommentPathSegmentV0_81_0) -> Self {
867 match seg {
868 CommentPathSegmentV0_81_0::Key(k) => CommentPathSegmentV0_82_0::Key(k),
869 CommentPathSegmentV0_81_0::Index(i) => CommentPathSegmentV0_82_0::Index(i),
870 }
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 fn sample() -> Document {
879 Document::from_markdown(
880 "\
881~~~card-yaml
882$quill: usaf_memo@0.1
883$kind: main
884# a top-level comment
885memo_for:
886 - ORG/SYMBOL # inline comment inside a sequence
887date: 2504-10-05
888subject: !must_fill Subject of the Memorandum
889~~~
890
891The body of the memorandum.
892
893~~~card-yaml
894$kind: indorsement
895for: ORG/SYMBOL
896from: ORG/SYMBOL
897~~~
898
899This body and the metadata above are an indorsement card.
900",
901 )
902 .unwrap()
903 }
904
905 #[test]
906 fn round_trips_through_serde_json() {
907 let doc = sample();
908 let json = serde_json::to_string(&doc).unwrap();
909 let restored: Document = serde_json::from_str(&json).unwrap();
910 assert_eq!(doc, restored);
911 assert_eq!(doc.to_markdown(), restored.to_markdown());
912 }
913
914 #[test]
915 fn serialization_uses_current_schema() {
916 let doc = sample();
917 let value: serde_json::Value = serde_json::to_value(&doc).unwrap();
918 assert_eq!(value["schema"], SCHEMA_V0_92_0);
919 }
920
921 #[test]
922 fn nested_fill_survives_storage_round_trip() {
923 let doc = Document::from_markdown(
926 "~~~card-yaml\n$quill: q@0.1\n$kind: main\naddr:\n street: !must_fill\n city: Anytown\n~~~\n",
927 )
928 .unwrap();
929 let json = serde_json::to_string(&doc).unwrap();
930 let restored: Document = serde_json::from_str(&json).unwrap();
931 assert_eq!(doc, restored, "nested fill must survive storage round-trip");
932 assert!(
933 restored.to_markdown().contains("street: !must_fill"),
934 "Got:\n{}",
935 restored.to_markdown()
936 );
937 }
938
939 #[test]
940 fn v0_82_0_payload_migrates_forward() {
941 let json = r#"{
944 "schema": "quillmark/document@0.82.0",
945 "main": {
946 "payload": {
947 "items": [
948 {"type": "quill", "value": "usaf_memo@0.1"},
949 {"type": "kind", "value": "main"},
950 {"type": "field", "key": "title", "value": "Hello", "fill": false}
951 ]
952 },
953 "body": "Body."
954 },
955 "cards": []
956 }"#;
957 let doc: Document = serde_json::from_str(json).unwrap();
958 assert_eq!(doc.main().kind(), Some("main"));
959 assert_eq!(
960 doc.main().payload().get("title").unwrap().as_str(),
961 Some("Hello")
962 );
963 }
964
965 #[test]
966 fn root_kind_is_main_through_round_trip() {
967 let doc = Document::from_markdown(
968 "~~~card-yaml\n$quill: usaf_memo@0.1\n$kind: main\ntitle: \"Hi\"\n~~~\n",
969 )
970 .unwrap();
971 assert_eq!(doc.main().kind(), Some("main"));
972 let restored: Document =
973 serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
974 assert_eq!(doc, restored);
975 assert_eq!(restored.main().kind(), Some("main"));
976 }
977
978 #[test]
979 fn serialization_is_byte_deterministic() {
980 let doc = sample();
984 let first = serde_json::to_string(&doc).unwrap();
985 let second = serde_json::to_string(&doc).unwrap();
986 assert_eq!(first, second, "to_string must be deterministic");
987 let restored: Document = serde_json::from_str(&first).unwrap();
988 let third = serde_json::to_string(&restored).unwrap();
989 assert_eq!(first, third, "byte-equality must survive a round-trip");
990 }
991
992 #[test]
993 fn rejects_unknown_schema_version() {
994 let json = r#"{"schema":"quillmark/document@0.99.0","main":{}}"#;
995 assert!(serde_json::from_str::<Document>(json).is_err());
996 }
997
998 #[test]
999 fn peek_schema_version_reads_field_without_full_parse() {
1000 let doc = sample();
1001 let json = serde_json::to_string(&doc).unwrap();
1002 assert_eq!(peek_schema_version(&json).as_deref(), Some(SCHEMA_V0_92_0));
1003
1004 let future = r#"{"schema":"quillmark/document@0.99.0","main":{}}"#;
1006 assert_eq!(
1007 peek_schema_version(future).as_deref(),
1008 Some("quillmark/document@0.99.0")
1009 );
1010 assert_eq!(peek_schema_version("not json"), None);
1011 assert_eq!(peek_schema_version(r#"{"foo":"bar"}"#), None);
1012 }
1013
1014 #[test]
1015 fn comment_on_dollar_line_round_trips() {
1016 let src = "\
1019~~~card-yaml
1020$quill: q@1.0
1021$kind: main # required for root
1022title: Hi
1023~~~
1024";
1025 let doc = Document::from_markdown(src).unwrap();
1026 let json = serde_json::to_string(&doc).unwrap();
1027 let restored: Document = serde_json::from_str(&json).unwrap();
1028 assert_eq!(doc, restored);
1029 assert!(restored
1031 .to_markdown()
1032 .contains("$kind: main # required for root"));
1033 }
1034
1035 #[test]
1036 fn v0_81_0_payload_loads_via_migration() {
1037 let json = r#"{
1038 "schema": "quillmark/document@0.81.0",
1039 "main": {
1040 "sentinel": {"kind": "main", "quill": "usaf_memo@0.1"},
1041 "frontmatter": {
1042 "items": [{"kind": "field", "key": "title", "value": "Hello"}]
1043 },
1044 "body": "Body."
1045 },
1046 "cards": []
1047 }"#;
1048 let doc: Document = serde_json::from_str(json).unwrap();
1049 assert_eq!(doc.main().kind(), Some("main"));
1050 assert_eq!(doc.quill_reference().to_string(), "usaf_memo@0.1");
1051 assert_eq!(
1052 doc.main().payload().get("title").unwrap().as_str(),
1053 Some("Hello")
1054 );
1055 }
1056
1057 #[test]
1058 fn v0_81_0_with_composable_card_migrates() {
1059 let json = r#"{
1060 "schema": "quillmark/document@0.81.0",
1061 "main": {
1062 "sentinel": {"kind": "main", "quill": "q@1.0"},
1063 "frontmatter": {"items": []},
1064 "body": ""
1065 },
1066 "cards": [
1067 {
1068 "sentinel": {"kind": "card", "tag": "indorsement"},
1069 "frontmatter": {"items": [{"kind": "field", "key": "for", "value": "X"}]},
1070 "body": "C body"
1071 }
1072 ]
1073 }"#;
1074 let doc: Document = serde_json::from_str(json).unwrap();
1075 assert_eq!(doc.cards().len(), 1);
1076 assert_eq!(doc.cards()[0].kind(), Some("indorsement"));
1077 assert_eq!(
1078 doc.cards()[0].payload().get("for").unwrap().as_str(),
1079 Some("X")
1080 );
1081 }
1082
1083 #[test]
1084 fn rejects_main_card_without_quill() {
1085 let json = r#"{
1086 "schema": "quillmark/document@0.82.0",
1087 "main": {"payload": {"items": [{"type": "kind", "value": "main"}]}, "body": ""},
1088 "cards": []
1089 }"#;
1090 let err = serde_json::from_str::<Document>(json).unwrap_err();
1091 assert!(err.to_string().contains("$quill"));
1092 }
1093
1094 #[test]
1095 fn rejects_composable_card_tagged_main() {
1096 let json = r#"{
1097 "schema": "quillmark/document@0.82.0",
1098 "main": {
1099 "payload": {"items": [
1100 {"type": "quill", "value": "q@1.0"},
1101 {"type": "kind", "value": "main"}
1102 ]},
1103 "body": ""
1104 },
1105 "cards": [
1106 {"payload": {"items": [{"type": "kind", "value": "main"}]}, "body": ""}
1107 ]
1108 }"#;
1109 let err = serde_json::from_str::<Document>(json).unwrap_err();
1110 assert!(err.to_string().contains("reserved (root only)"));
1111 }
1112
1113 #[test]
1114 fn rejects_invalid_quill_reference() {
1115 let json = r#"{
1116 "schema": "quillmark/document@0.82.0",
1117 "main": {
1118 "payload": {"items": [
1119 {"type": "quill", "value": "not a valid ref!!"},
1120 {"type": "kind", "value": "main"}
1121 ]},
1122 "body": ""
1123 },
1124 "cards": []
1125 }"#;
1126 let err = serde_json::from_str::<Document>(json).unwrap_err();
1127 assert!(err.to_string().contains("invalid quill reference"));
1128 }
1129
1130 #[test]
1131 fn v0_82_0_payload_loads_via_migration() {
1132 let json = r#"{
1135 "schema": "quillmark/document@0.82.0",
1136 "main": {
1137 "payload": {"items": [
1138 {"type": "quill", "value": "usaf_memo@0.1"},
1139 {"type": "kind", "value": "main"},
1140 {"type": "field", "key": "title", "value": "Hello"}
1141 ]},
1142 "body": "Body."
1143 },
1144 "cards": []
1145 }"#;
1146 let doc: Document = serde_json::from_str(json).unwrap();
1147 assert_eq!(doc.main().kind(), Some("main"));
1148 assert_eq!(
1149 doc.main().payload().get("title").unwrap().as_str(),
1150 Some("Hello")
1151 );
1152 let reser = serde_json::to_string(&doc).unwrap();
1153 assert_eq!(peek_schema_version(&reser).as_deref(), Some(SCHEMA_V0_92_0));
1154 }
1155
1156 #[test]
1157 fn v0_82_0_blob_with_seed_item_is_rejected() {
1158 let json = r#"{
1162 "schema": "quillmark/document@0.82.0",
1163 "main": {
1164 "payload": {"items": [
1165 {"type": "quill", "value": "q@1.0"},
1166 {"type": "kind", "value": "main"},
1167 {"type": "seed", "value": {"indorsement": {"from": "X"}}}
1168 ]},
1169 "body": ""
1170 },
1171 "cards": []
1172 }"#;
1173 assert!(serde_json::from_str::<Document>(json).is_err());
1174 }
1175
1176 #[test]
1177 fn rejects_composable_card_with_seed() {
1178 let json = r#"{
1181 "schema": "quillmark/document@0.92.0",
1182 "main": {
1183 "payload": {"items": [
1184 {"type": "quill", "value": "q@1.0"},
1185 {"type": "kind", "value": "main"}
1186 ]},
1187 "body": ""
1188 },
1189 "cards": [
1190 {"payload": {"items": [
1191 {"type": "kind", "value": "indorsement"},
1192 {"type": "seed", "value": {"note": {"from": "X"}}}
1193 ]}, "body": ""}
1194 ]
1195 }"#;
1196 let err = serde_json::from_str::<Document>(json).unwrap_err();
1197 assert!(err
1198 .to_string()
1199 .contains("composable cards must not carry a $seed entry"));
1200 }
1201
1202 #[test]
1203 fn v0_92_0_seed_item_round_trips() {
1204 let json = r#"{
1205 "schema": "quillmark/document@0.92.0",
1206 "main": {
1207 "payload": {"items": [
1208 {"type": "quill", "value": "q@1.0"},
1209 {"type": "kind", "value": "main"},
1210 {"type": "seed", "value": {"indorsement": {"from": "49 FW/CC"}}}
1211 ]},
1212 "body": ""
1213 },
1214 "cards": []
1215 }"#;
1216 let doc: Document = serde_json::from_str(json).unwrap();
1217 let overlay = doc
1218 .main()
1219 .seed()
1220 .and_then(|m| m.get("indorsement"))
1221 .and_then(crate::SeedOverlay::from_json)
1222 .expect("overlay present");
1223 assert_eq!(
1224 overlay.fields.get("from").and_then(|v| v.as_str()),
1225 Some("49 FW/CC")
1226 );
1227 let reser: Document = serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1228 assert_eq!(doc, reser);
1229 }
1230}