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