1use std::str::FromStr;
26
27use serde::{Deserialize, Serialize};
28use serde_json::{Map as JsonMap, Value as JsonValue};
29
30use super::payload::{MetaKey, Payload, PayloadItem};
31use super::Card;
32use crate::value::{PathSegment, QuillValue};
33use crate::version::QuillReference;
34use quillmark_content::Content;
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "lowercase")]
40#[non_exhaustive]
41pub enum PayloadItemWire {
42 Field {
44 key: String,
45 value: JsonValue,
46 #[serde(default)]
48 fill: bool,
49 #[serde(
54 default,
55 rename = "nestedFills",
56 alias = "nested_fills",
57 skip_serializing_if = "Vec::is_empty"
58 )]
59 nested_fills: Vec<Vec<PathStepWire>>,
60 },
61 Comment {
63 text: String,
64 #[serde(default)]
66 inline: bool,
67 },
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[serde(untagged)]
75#[non_exhaustive]
76pub enum PathStepWire {
77 Index(usize),
78 Key(String),
79}
80
81impl From<&PathSegment> for PathStepWire {
82 fn from(seg: &PathSegment) -> Self {
83 match seg {
84 PathSegment::Key(k) => PathStepWire::Key(k.clone()),
85 PathSegment::Index(i) => PathStepWire::Index(*i),
86 }
87 }
88}
89
90impl From<&PathStepWire> for PathSegment {
91 fn from(seg: &PathStepWire) -> Self {
92 match seg {
93 PathStepWire::Key(k) => PathSegment::Key(k.clone()),
94 PathStepWire::Index(i) => PathSegment::Index(*i),
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase", deny_unknown_fields)]
107#[non_exhaustive]
108pub struct CardWire {
109 #[serde(default)]
112 pub kind: String,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub quill: Option<String>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub ext: Option<JsonMap<String, JsonValue>>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub seed: Option<JsonMap<String, JsonValue>>,
124 #[serde(default, alias = "payload_items")]
126 pub payload_items: Vec<PayloadItemWire>,
127 #[serde(default)]
137 pub body: JsonValue,
138}
139
140impl CardWire {
141 pub fn new(kind: String, body: JsonValue) -> Self {
145 Self {
146 kind,
147 quill: None,
148 ext: None,
149 seed: None,
150 payload_items: Vec::new(),
151 body,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum WireError {
160 InvalidQuillReference { value: String, reason: String },
162 InvalidField { key: String, reason: String },
166}
167
168impl std::fmt::Display for WireError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 WireError::InvalidQuillReference { value, reason } => {
172 write!(f, "invalid `quill` reference {value:?}: {reason}")
173 }
174 WireError::InvalidField { key, reason } => {
175 write!(f, "invalid field {key:?}: {reason}")
176 }
177 }
178 }
179}
180
181impl std::error::Error for WireError {}
182
183impl From<&Card> for CardWire {
184 fn from(card: &Card) -> Self {
185 let mut wire = CardWire {
186 kind: String::new(),
187 quill: None,
188 ext: None,
189 seed: None,
190 payload_items: Vec::new(),
191 body: quillmark_content::serial::to_canonical_value(card.body()),
192 };
193 for item in card.payload().items() {
194 match item {
195 PayloadItem::Quill { reference } => wire.quill = Some(reference.to_string()),
196 PayloadItem::Kind { value } => wire.kind = value.clone(),
197 PayloadItem::Meta {
198 key: MetaKey::Ext,
199 value,
200 ..
201 } => wire.ext = Some(value.clone()),
202 PayloadItem::Meta {
203 key: MetaKey::Seed,
204 value,
205 ..
206 } => wire.seed = Some(value.clone()),
207 PayloadItem::Field {
208 key, value, fill, ..
209 } => {
210 let nested_fills = value
211 .nonroot_fill_paths()
212 .map(|p| p.iter().map(PathStepWire::from).collect())
213 .collect();
214 wire.payload_items.push(PayloadItemWire::Field {
215 key: key.clone(),
216 value: value.as_json().clone(),
217 fill: *fill,
218 nested_fills,
219 })
220 }
221 PayloadItem::Comment { text, inline } => {
222 wire.payload_items.push(PayloadItemWire::Comment {
223 text: text.clone(),
224 inline: *inline,
225 })
226 }
227 }
228 }
229 wire
230 }
231}
232
233impl TryFrom<CardWire> for Card {
234 type Error = WireError;
235
236 fn try_from(wire: CardWire) -> Result<Self, Self::Error> {
237 let items = wire
238 .payload_items
239 .into_iter()
240 .map(|item| match item {
241 PayloadItemWire::Field {
242 key,
243 value,
244 fill,
245 nested_fills,
246 } => {
247 validate_wire_field(&key, &value)?;
248 let mut qv = QuillValue::from_json(value);
249 for path in &nested_fills {
250 let segs: Vec<PathSegment> = path.iter().map(PathSegment::from).collect();
251 qv.set_fill_at(&segs);
252 }
253 Ok(PayloadItem::Field {
254 key,
255 value: qv,
256 fill,
257 nested_comments: Vec::new(),
258 })
259 }
260 PayloadItemWire::Comment { text, inline } => {
261 Ok(PayloadItem::Comment { text, inline })
262 }
263 })
264 .collect::<Result<Vec<_>, WireError>>()?;
265
266 let mut payload = Payload::from_items(items);
270 if let Some(value) = wire.quill {
271 let reference = QuillReference::from_str(&value)
272 .map_err(|reason| WireError::InvalidQuillReference { value, reason })?;
273 payload.set_quill(reference);
274 }
275 if !wire.kind.is_empty() {
287 payload.set_kind(wire.kind);
288 }
289 let too_deep = |key: &str| {
290 let key = key.to_string();
291 move |max| WireError::InvalidField {
292 key,
293 reason: format!("nests deeper than the maximum of {} levels", max),
294 }
295 };
296 if let Some(ext) = wire.ext {
297 payload.set_ext(crate::value::depth_check_meta_map(ext, too_deep("$ext"))?);
298 }
299 if let Some(seed) = wire.seed {
300 payload.set_seed(crate::value::depth_check_meta_map(seed, too_deep("$seed"))?);
301 }
302 let body = body_from_wire(&wire.body)?;
303 Ok(Card::from_parts(payload, body))
304 }
305}
306
307fn body_from_wire(body: &JsonValue) -> Result<Content, WireError> {
313 let invalid = |reason: String| WireError::InvalidField {
314 key: "$body".to_string(),
315 reason,
316 };
317 match super::decode_richtext_value(body) {
318 Some(result) => result.map_err(|e| invalid(e.into_message())),
319 None => match body {
322 JsonValue::Null => Ok(Content::empty()),
323 other => Err(invalid(format!(
324 "expected a richtext content object or a markdown string, got {}",
325 match other {
326 JsonValue::Bool(_) => "a boolean",
327 JsonValue::Number(_) => "a number",
328 JsonValue::Array(_) => "an array",
329 _ => "an unsupported value",
330 }
331 ))),
332 },
333 }
334}
335
336fn validate_wire_field(key: &str, value: &JsonValue) -> Result<(), WireError> {
339 use super::edit::{validate_field, FieldViolation};
340 validate_field(key, value).map_err(|v| WireError::InvalidField {
341 key: key.to_string(),
342 reason: match v {
343 FieldViolation::InvalidName => {
344 "field names must match [A-Za-z_][A-Za-z0-9_]*".to_string()
345 }
346 FieldViolation::TooDeep => format!(
347 "nests deeper than the maximum of {} levels",
348 crate::document::limits::MAX_YAML_DEPTH
349 ),
350 },
351 })
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use serde_json::json;
358
359 #[test]
362 fn card_wire_round_trips_nested_fill() {
363 let mut addr = QuillValue::from_json(json!({"street": null, "city": "Anytown"}));
364 assert!(addr.set_fill_at(&[PathSegment::Key("street".to_string())]));
365 let payload = Payload::from_items(vec![PayloadItem::Field {
366 key: "addr".to_string(),
367 value: addr,
368 fill: false,
369 nested_comments: Vec::new(),
370 }]);
371 let card = Card::from_parts(payload, quillmark_content::Content::empty());
372
373 let wire = CardWire::from(&card);
374 let as_json = serde_json::to_value(&wire).unwrap();
375 assert_eq!(
376 as_json["payloadItems"][0]["nestedFills"],
377 json!([["street"]]),
378 "nested fill path rides the wire as a JS array; JSON value stays fill-free"
379 );
380 assert_eq!(
381 as_json["payloadItems"][0]["value"],
382 json!({"street": null, "city": "Anytown"})
383 );
384
385 let back = Card::try_from(wire).expect("wire → card");
386 assert_eq!(back, card, "nested fill must survive Card → wire → Card");
387 }
388
389 #[test]
395 fn card_wire_round_trips_content_field_losslessly() {
396 use quillmark_content::model::{Mark, MarkKind};
397
398 let mut card = Card::new("note").unwrap();
399 let mut content = quillmark_content::import::from_markdown("underlined intro").unwrap();
400 content.marks.push(Mark::new(0, 10, MarkKind::Underline));
401 content.normalize();
402 let json = quillmark_content::serial::to_canonical_value(&content);
403 let schema = crate::quill::FieldSchema::new(
404 "intro".to_string(),
405 crate::quill::FieldType::RichText { inline: false },
406 None,
407 );
408 card.commit_field("intro", crate::QuillValue::from_json(json), &schema)
409 .unwrap();
410
411 let wire = CardWire::from(&card);
412 let as_json = serde_json::to_value(&wire).unwrap();
414 assert!(as_json["payloadItems"][0]["value"].is_object());
415
416 let back = Card::try_from(wire).expect("wire → card");
417 assert_eq!(back, card, "content field must survive Card → wire → Card");
418 let read = back.field_richtext("intro").unwrap().unwrap();
420 assert!(read.marks.iter().any(|m| matches!(m.kind, MarkKind::Underline)));
421 }
422
423 #[test]
425 fn card_wire_round_trips_fields_and_comment() {
426 let mut payload = Payload::from_items(vec![
427 PayloadItem::comment("a note"),
428 PayloadItem::field("title", QuillValue::from_json(json!("Hi"))),
429 PayloadItem::Field {
430 key: "count".to_string(),
431 value: QuillValue::from_json(json!(3)),
432 fill: true,
433 nested_comments: Vec::new(),
434 },
435 ]);
436 payload.set_kind("note");
437 let card = Card::from_parts(payload, crate::document::import_body("body text").unwrap());
438
439 let wire = CardWire::from(&card);
440 assert_eq!(wire.kind, "note");
441 assert_eq!(wire.payload_items.len(), 3);
442
443 let back = Card::try_from(wire).expect("wire → card");
444 assert_eq!(back, card, "Card → wire → Card must be identity");
445 }
446
447 #[test]
449 fn card_wire_round_trips_quill() {
450 let mut payload = Payload::from_index_map(Default::default());
451 payload.set_quill("memo@1.2.3".parse().unwrap());
452 payload.set_kind("main");
453 let card = Card::from_parts(payload, quillmark_content::Content::empty());
454
455 let wire = CardWire::from(&card);
456 assert_eq!(wire.quill.as_deref(), Some("memo@1.2.3"));
457
458 let back = Card::try_from(wire).expect("wire → card");
459 assert_eq!(back, card);
460 }
461
462 #[test]
464 fn card_wire_json_shape() {
465 let card = Card::try_from(CardWire {
466 kind: "note".to_string(),
467 quill: None,
468 ext: None,
469 seed: None,
470 payload_items: vec![PayloadItemWire::Field {
471 key: "x".to_string(),
472 value: json!(1),
473 fill: false,
474 nested_fills: Vec::new(),
475 }],
476 body: JsonValue::Null,
477 })
478 .unwrap();
479 let json = serde_json::to_value(CardWire::from(&card)).unwrap();
480 assert_eq!(json["kind"], json!("note"));
481 assert_eq!(json["payloadItems"][0]["type"], json!("field"));
482 assert_eq!(json["payloadItems"][0]["key"], json!("x"));
483 assert!(json.get("quill").is_none(), "absent quill is omitted");
484 }
485
486 #[test]
488 fn card_wire_rejects_bad_quill() {
489 let err = Card::try_from(CardWire {
490 kind: String::new(),
491 quill: Some("@nope".to_string()),
492 ext: None,
493 seed: None,
494 payload_items: Vec::new(),
495 body: JsonValue::Null,
496 })
497 .unwrap_err();
498 assert!(matches!(err, WireError::InvalidQuillReference { .. }));
499 }
500
501 #[test]
505 fn card_wire_accepts_any_kind() {
506 let card = Card::try_from(CardWire {
507 kind: "BadKind".to_string(),
508 quill: None,
509 ext: None,
510 seed: None,
511 payload_items: Vec::new(),
512 body: JsonValue::Null,
513 })
514 .expect("construction does not police the kind grammar");
515 assert_eq!(card.kind(), Some("BadKind"));
516 }
517}