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 id: Option<String>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub ext: Option<JsonMap<String, JsonValue>>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub seed: Option<JsonMap<String, JsonValue>>,
127 #[serde(default, alias = "payload_items")]
129 pub payload_items: Vec<PayloadItemWire>,
130 #[serde(default)]
140 pub body: JsonValue,
141}
142
143impl CardWire {
144 pub fn new(kind: String, body: JsonValue) -> Self {
148 Self {
149 kind,
150 quill: None,
151 id: None,
152 ext: None,
153 seed: None,
154 payload_items: Vec::new(),
155 body,
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162#[non_exhaustive]
163pub enum WireError {
164 InvalidQuillReference { value: String, reason: String },
166 InvalidField { key: String, reason: String },
170}
171
172impl std::fmt::Display for WireError {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 WireError::InvalidQuillReference { value, reason } => {
176 write!(f, "invalid `quill` reference {value:?}: {reason}")
177 }
178 WireError::InvalidField { key, reason } => {
179 write!(f, "invalid field {key:?}: {reason}")
180 }
181 }
182 }
183}
184
185impl std::error::Error for WireError {}
186
187impl From<&Card> for CardWire {
188 fn from(card: &Card) -> Self {
189 let mut wire = CardWire {
190 kind: String::new(),
191 quill: None,
192 id: None,
193 ext: None,
194 seed: None,
195 payload_items: Vec::new(),
196 body: quillmark_content::serial::to_canonical_value(card.body()),
197 };
198 for item in card.payload().items() {
199 match item {
200 PayloadItem::Quill { reference } => wire.quill = Some(reference.to_string()),
201 PayloadItem::Kind { value } => wire.kind = value.clone(),
202 PayloadItem::Id { value } => wire.id = Some(value.clone()),
203 PayloadItem::Meta {
204 key: MetaKey::Ext,
205 value,
206 ..
207 } => wire.ext = Some(value.clone()),
208 PayloadItem::Meta {
209 key: MetaKey::Seed,
210 value,
211 ..
212 } => wire.seed = Some(value.clone()),
213 PayloadItem::Field {
214 key, value, fill, ..
215 } => {
216 let nested_fills = value
217 .nonroot_fill_paths()
218 .map(|p| p.iter().map(PathStepWire::from).collect())
219 .collect();
220 wire.payload_items.push(PayloadItemWire::Field {
221 key: key.clone(),
222 value: value.as_json().clone(),
223 fill: *fill,
224 nested_fills,
225 })
226 }
227 PayloadItem::Comment { text, inline } => {
228 wire.payload_items.push(PayloadItemWire::Comment {
229 text: text.clone(),
230 inline: *inline,
231 })
232 }
233 }
234 }
235 wire
236 }
237}
238
239impl TryFrom<CardWire> for Card {
240 type Error = WireError;
241
242 fn try_from(wire: CardWire) -> Result<Self, Self::Error> {
243 let items = wire
244 .payload_items
245 .into_iter()
246 .map(|item| match item {
247 PayloadItemWire::Field {
248 key,
249 value,
250 fill,
251 nested_fills,
252 } => {
253 validate_wire_field(&key, &value)?;
254 let mut qv = QuillValue::from_json(value);
255 for path in &nested_fills {
256 let segs: Vec<PathSegment> = path.iter().map(PathSegment::from).collect();
257 qv.set_fill_at(&segs);
258 }
259 Ok(PayloadItem::Field {
260 key,
261 value: qv,
262 fill,
263 nested_comments: Vec::new(),
264 })
265 }
266 PayloadItemWire::Comment { text, inline } => {
267 Ok(PayloadItem::Comment { text, inline })
268 }
269 })
270 .collect::<Result<Vec<_>, WireError>>()?;
271
272 let mut payload = Payload::from_items(items);
276 if let Some(value) = wire.quill {
277 let reference = QuillReference::from_str(&value)
278 .map_err(|reason| WireError::InvalidQuillReference { value, reason })?;
279 payload.set_quill(reference);
280 }
281 if !wire.kind.is_empty() {
293 payload.set_kind(wire.kind);
294 }
295 if let Some(id) = wire.id {
296 payload.set_id(id);
297 }
298 let too_deep = |key: &str| {
299 let key = key.to_string();
300 move |max| WireError::InvalidField {
301 key,
302 reason: format!("nests deeper than the maximum of {} levels", max),
303 }
304 };
305 if let Some(ext) = wire.ext {
306 payload.set_ext(crate::value::depth_check_meta_map(ext, too_deep("$ext"))?);
307 }
308 if let Some(seed) = wire.seed {
309 payload.set_seed(crate::value::depth_check_meta_map(seed, too_deep("$seed"))?);
310 }
311 let body = body_from_wire(&wire.body)?;
312 Ok(Card::from_parts(payload, body))
313 }
314}
315
316fn body_from_wire(body: &JsonValue) -> Result<Content, WireError> {
322 let invalid = |reason: String| WireError::InvalidField {
323 key: "$body".to_string(),
324 reason,
325 };
326 match super::decode_richtext_value(body) {
327 Some(result) => result.map_err(|e| invalid(e.into_message())),
328 None => match body {
331 JsonValue::Null => Ok(Content::empty()),
332 other => Err(invalid(format!(
333 "expected a richtext content object or a markdown string, got {}",
334 match other {
335 JsonValue::Bool(_) => "a boolean",
336 JsonValue::Number(_) => "a number",
337 JsonValue::Array(_) => "an array",
338 _ => "an unsupported value",
339 }
340 ))),
341 },
342 }
343}
344
345fn validate_wire_field(key: &str, value: &JsonValue) -> Result<(), WireError> {
348 use super::edit::{validate_field, FieldViolation};
349 validate_field(key, value).map_err(|v| WireError::InvalidField {
350 key: key.to_string(),
351 reason: match v {
352 FieldViolation::InvalidName => {
353 "field names must match [A-Za-z_][A-Za-z0-9_]*".to_string()
354 }
355 FieldViolation::TooDeep => format!(
356 "nests deeper than the maximum of {} levels",
357 crate::document::limits::MAX_YAML_DEPTH
358 ),
359 },
360 })
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use serde_json::json;
367
368 #[test]
371 fn card_wire_round_trips_nested_fill() {
372 let mut addr = QuillValue::from_json(json!({"street": null, "city": "Anytown"}));
373 assert!(addr.set_fill_at(&[PathSegment::Key("street".to_string())]));
374 let payload = Payload::from_items(vec![PayloadItem::Field {
375 key: "addr".to_string(),
376 value: addr,
377 fill: false,
378 nested_comments: Vec::new(),
379 }]);
380 let card = Card::from_parts(payload, quillmark_content::Content::empty());
381
382 let wire = CardWire::from(&card);
383 let as_json = serde_json::to_value(&wire).unwrap();
384 assert_eq!(
385 as_json["payloadItems"][0]["nestedFills"],
386 json!([["street"]]),
387 "nested fill path rides the wire as a JS array; JSON value stays fill-free"
388 );
389 assert_eq!(
390 as_json["payloadItems"][0]["value"],
391 json!({"street": null, "city": "Anytown"})
392 );
393
394 let back = Card::try_from(wire).expect("wire → card");
395 assert_eq!(back, card, "nested fill must survive Card → wire → Card");
396 }
397
398 #[test]
404 fn card_wire_round_trips_content_field_losslessly() {
405 use quillmark_content::model::{Mark, MarkKind};
406
407 let mut card = Card::new("note").unwrap();
408 let mut content = quillmark_content::import::from_markdown("underlined intro").unwrap();
409 content.marks.push(Mark {
410 start: 0,
411 end: 10,
412 kind: MarkKind::Underline,
413 });
414 content.normalize();
415 let json = quillmark_content::serial::to_canonical_value(&content);
416 let schema = crate::quill::FieldSchema::new(
417 "intro".to_string(),
418 crate::quill::FieldType::RichText { inline: false },
419 None,
420 );
421 card.commit_field("intro", crate::QuillValue::from_json(json), &schema)
422 .unwrap();
423
424 let wire = CardWire::from(&card);
425 let as_json = serde_json::to_value(&wire).unwrap();
427 assert!(as_json["payloadItems"][0]["value"].is_object());
428
429 let back = Card::try_from(wire).expect("wire → card");
430 assert_eq!(back, card, "content field must survive Card → wire → Card");
431 let read = back.field_richtext("intro").unwrap().unwrap();
433 assert!(read.marks.iter().any(|m| matches!(m.kind, MarkKind::Underline)));
434 }
435
436 #[test]
438 fn card_wire_round_trips_fields_and_comment() {
439 let mut payload = Payload::from_items(vec![
440 PayloadItem::comment("a note"),
441 PayloadItem::field("title", QuillValue::from_json(json!("Hi"))),
442 PayloadItem::Field {
443 key: "count".to_string(),
444 value: QuillValue::from_json(json!(3)),
445 fill: true,
446 nested_comments: Vec::new(),
447 },
448 ]);
449 payload.set_kind("note");
450 let card = Card::from_parts(payload, crate::document::import_body("body text").unwrap());
451
452 let wire = CardWire::from(&card);
453 assert_eq!(wire.kind, "note");
454 assert_eq!(wire.payload_items.len(), 3);
455
456 let back = Card::try_from(wire).expect("wire → card");
457 assert_eq!(back, card, "Card → wire → Card must be identity");
458 }
459
460 #[test]
462 fn card_wire_round_trips_quill() {
463 let mut payload = Payload::from_index_map(Default::default());
464 payload.set_quill("memo@1.2.3".parse().unwrap());
465 payload.set_kind("main");
466 let card = Card::from_parts(payload, quillmark_content::Content::empty());
467
468 let wire = CardWire::from(&card);
469 assert_eq!(wire.quill.as_deref(), Some("memo@1.2.3"));
470
471 let back = Card::try_from(wire).expect("wire → card");
472 assert_eq!(back, card);
473 }
474
475 #[test]
477 fn card_wire_json_shape() {
478 let card = Card::try_from(CardWire {
479 kind: "note".to_string(),
480 quill: None,
481 id: None,
482 ext: None,
483 seed: None,
484 payload_items: vec![PayloadItemWire::Field {
485 key: "x".to_string(),
486 value: json!(1),
487 fill: false,
488 nested_fills: Vec::new(),
489 }],
490 body: JsonValue::Null,
491 })
492 .unwrap();
493 let json = serde_json::to_value(CardWire::from(&card)).unwrap();
494 assert_eq!(json["kind"], json!("note"));
495 assert_eq!(json["payloadItems"][0]["type"], json!("field"));
496 assert_eq!(json["payloadItems"][0]["key"], json!("x"));
497 assert!(json.get("quill").is_none(), "absent quill is omitted");
498 }
499
500 #[test]
502 fn card_wire_rejects_bad_quill() {
503 let err = Card::try_from(CardWire {
504 kind: String::new(),
505 quill: Some("@nope".to_string()),
506 id: None,
507 ext: None,
508 seed: None,
509 payload_items: Vec::new(),
510 body: JsonValue::Null,
511 })
512 .unwrap_err();
513 assert!(matches!(err, WireError::InvalidQuillReference { .. }));
514 }
515
516 #[test]
520 fn card_wire_accepts_any_kind() {
521 let card = Card::try_from(CardWire {
522 kind: "BadKind".to_string(),
523 quill: None,
524 id: None,
525 ext: None,
526 seed: None,
527 payload_items: Vec::new(),
528 body: JsonValue::Null,
529 })
530 .expect("construction does not police the kind grammar");
531 assert_eq!(card.kind(), Some("BadKind"));
532 }
533}