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;
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(tag = "type", rename_all = "lowercase")]
39pub enum PayloadItemWire {
40 Field {
42 key: String,
43 value: JsonValue,
44 #[serde(default)]
46 fill: bool,
47 #[serde(
52 default,
53 rename = "nestedFills",
54 alias = "nested_fills",
55 skip_serializing_if = "Vec::is_empty"
56 )]
57 nested_fills: Vec<Vec<PathStepWire>>,
58 },
59 Comment {
61 text: String,
62 #[serde(default)]
64 inline: bool,
65 },
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(untagged)]
73pub enum PathStepWire {
74 Index(usize),
75 Key(String),
76}
77
78impl From<&PathSegment> for PathStepWire {
79 fn from(seg: &PathSegment) -> Self {
80 match seg {
81 PathSegment::Key(k) => PathStepWire::Key(k.clone()),
82 PathSegment::Index(i) => PathStepWire::Index(*i),
83 }
84 }
85}
86
87impl From<&PathStepWire> for PathSegment {
88 fn from(seg: &PathStepWire) -> Self {
89 match seg {
90 PathStepWire::Key(k) => PathSegment::Key(k.clone()),
91 PathStepWire::Index(i) => PathSegment::Index(*i),
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct CardWire {
105 #[serde(default)]
108 pub kind: String,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub quill: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub id: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub ext: Option<JsonMap<String, JsonValue>>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub seed: Option<JsonMap<String, JsonValue>>,
123 #[serde(default, alias = "payload_items")]
125 pub payload_items: Vec<PayloadItemWire>,
126 #[serde(default)]
128 pub body: String,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum WireError {
134 InvalidQuillReference { value: String, reason: String },
136 InvalidField { key: String, reason: String },
140}
141
142impl std::fmt::Display for WireError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 WireError::InvalidQuillReference { value, reason } => {
146 write!(f, "invalid `quill` reference {value:?}: {reason}")
147 }
148 WireError::InvalidField { key, reason } => {
149 write!(f, "invalid field {key:?}: {reason}")
150 }
151 }
152 }
153}
154
155impl std::error::Error for WireError {}
156
157impl From<&Card> for CardWire {
158 fn from(card: &Card) -> Self {
159 let mut wire = CardWire {
160 kind: String::new(),
161 quill: None,
162 id: None,
163 ext: None,
164 seed: None,
165 payload_items: Vec::new(),
166 body: card.body().to_string(),
167 };
168 for item in card.payload().items() {
169 match item {
170 PayloadItem::Quill { reference } => wire.quill = Some(reference.to_string()),
171 PayloadItem::Kind { value } => wire.kind = value.clone(),
172 PayloadItem::Id { value } => wire.id = Some(value.clone()),
173 PayloadItem::Meta {
174 key: MetaKey::Ext,
175 value,
176 ..
177 } => wire.ext = Some(value.clone()),
178 PayloadItem::Meta {
179 key: MetaKey::Seed,
180 value,
181 ..
182 } => wire.seed = Some(value.clone()),
183 PayloadItem::Field {
184 key, value, fill, ..
185 } => {
186 let nested_fills = value
187 .nonroot_fill_paths()
188 .map(|p| p.iter().map(PathStepWire::from).collect())
189 .collect();
190 wire.payload_items.push(PayloadItemWire::Field {
191 key: key.clone(),
192 value: value.as_json().clone(),
193 fill: *fill,
194 nested_fills,
195 })
196 }
197 PayloadItem::Comment { text, inline } => {
198 wire.payload_items.push(PayloadItemWire::Comment {
199 text: text.clone(),
200 inline: *inline,
201 })
202 }
203 }
204 }
205 wire
206 }
207}
208
209impl TryFrom<CardWire> for Card {
210 type Error = WireError;
211
212 fn try_from(wire: CardWire) -> Result<Self, Self::Error> {
213 let items = wire
214 .payload_items
215 .into_iter()
216 .map(|item| match item {
217 PayloadItemWire::Field {
218 key,
219 value,
220 fill,
221 nested_fills,
222 } => {
223 validate_wire_field(&key, &value)?;
224 let mut qv = QuillValue::from_json(value);
225 for path in &nested_fills {
226 let segs: Vec<PathSegment> = path.iter().map(PathSegment::from).collect();
227 qv.set_fill_at(&segs);
228 }
229 Ok(PayloadItem::Field {
230 key,
231 value: qv,
232 fill,
233 nested_comments: Vec::new(),
234 })
235 }
236 PayloadItemWire::Comment { text, inline } => {
237 Ok(PayloadItem::Comment { text, inline })
238 }
239 })
240 .collect::<Result<Vec<_>, WireError>>()?;
241
242 let mut payload = Payload::from_items(items);
246 if let Some(value) = wire.quill {
247 let reference = QuillReference::from_str(&value)
248 .map_err(|reason| WireError::InvalidQuillReference { value, reason })?;
249 payload.set_quill(reference);
250 }
251 if !wire.kind.is_empty() {
252 payload.set_kind(wire.kind);
253 }
254 if let Some(id) = wire.id {
255 payload.set_id(id);
256 }
257 if let Some(ext) = wire.ext {
258 let as_value = JsonValue::Object(ext);
259 if crate::value::json_depth_exceeds(&as_value, crate::document::limits::MAX_YAML_DEPTH)
260 {
261 return Err(WireError::InvalidField {
262 key: "$ext".to_string(),
263 reason: format!(
264 "nests deeper than the maximum of {} levels",
265 crate::document::limits::MAX_YAML_DEPTH
266 ),
267 });
268 }
269 let JsonValue::Object(ext) = as_value else {
270 unreachable!("constructed as Object above")
271 };
272 payload.set_ext(ext);
273 }
274 if let Some(seed) = wire.seed {
275 let as_value = JsonValue::Object(seed);
276 if crate::value::json_depth_exceeds(&as_value, crate::document::limits::MAX_YAML_DEPTH)
277 {
278 return Err(WireError::InvalidField {
279 key: "$seed".to_string(),
280 reason: format!(
281 "nests deeper than the maximum of {} levels",
282 crate::document::limits::MAX_YAML_DEPTH
283 ),
284 });
285 }
286 let JsonValue::Object(seed) = as_value else {
287 unreachable!("constructed as Object above")
288 };
289 payload.set_seed(seed);
290 }
291 Ok(Card::from_parts(payload, wire.body))
292 }
293}
294
295fn validate_wire_field(key: &str, value: &JsonValue) -> Result<(), WireError> {
298 use super::edit::{validate_field, FieldViolation};
299 validate_field(key, value).map_err(|v| WireError::InvalidField {
300 key: key.to_string(),
301 reason: match v {
302 FieldViolation::InvalidName => {
303 "field names must match [A-Za-z_][A-Za-z0-9_]*".to_string()
304 }
305 FieldViolation::TooDeep => format!(
306 "nests deeper than the maximum of {} levels",
307 crate::document::limits::MAX_YAML_DEPTH
308 ),
309 },
310 })
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use serde_json::json;
317
318 #[test]
321 fn card_wire_round_trips_nested_fill() {
322 let mut addr = QuillValue::from_json(json!({"street": null, "city": "Anytown"}));
323 assert!(addr.set_fill_at(&[PathSegment::Key("street".to_string())]));
324 let payload = Payload::from_items(vec![PayloadItem::Field {
325 key: "addr".to_string(),
326 value: addr,
327 fill: false,
328 nested_comments: Vec::new(),
329 }]);
330 let card = Card::from_parts(payload, String::new());
331
332 let wire = CardWire::from(&card);
333 let as_json = serde_json::to_value(&wire).unwrap();
334 assert_eq!(
335 as_json["payloadItems"][0]["nestedFills"],
336 json!([["street"]]),
337 "nested fill path rides the wire as a JS array; JSON value stays fill-free"
338 );
339 assert_eq!(
340 as_json["payloadItems"][0]["value"],
341 json!({"street": null, "city": "Anytown"})
342 );
343
344 let back = Card::try_from(wire).expect("wire → card");
345 assert_eq!(back, card, "nested fill must survive Card → wire → Card");
346 }
347
348 #[test]
350 fn card_wire_round_trips_fields_and_comment() {
351 let mut payload = Payload::from_items(vec![
352 PayloadItem::comment("a note"),
353 PayloadItem::field("title", QuillValue::from_json(json!("Hi"))),
354 PayloadItem::Field {
355 key: "count".to_string(),
356 value: QuillValue::from_json(json!(3)),
357 fill: true,
358 nested_comments: Vec::new(),
359 },
360 ]);
361 payload.set_kind("note");
362 let card = Card::from_parts(payload, "body text".to_string());
363
364 let wire = CardWire::from(&card);
365 assert_eq!(wire.kind, "note");
366 assert_eq!(wire.payload_items.len(), 3);
367
368 let back = Card::try_from(wire).expect("wire → card");
369 assert_eq!(back, card, "Card → wire → Card must be identity");
370 }
371
372 #[test]
374 fn card_wire_round_trips_quill() {
375 let mut payload = Payload::from_index_map(Default::default());
376 payload.set_quill("memo@1.2.3".parse().unwrap());
377 payload.set_kind("main");
378 let card = Card::from_parts(payload, String::new());
379
380 let wire = CardWire::from(&card);
381 assert_eq!(wire.quill.as_deref(), Some("memo@1.2.3"));
382
383 let back = Card::try_from(wire).expect("wire → card");
384 assert_eq!(back, card);
385 }
386
387 #[test]
389 fn card_wire_json_shape() {
390 let card = Card::try_from(CardWire {
391 kind: "note".to_string(),
392 quill: None,
393 id: None,
394 ext: None,
395 seed: None,
396 payload_items: vec![PayloadItemWire::Field {
397 key: "x".to_string(),
398 value: json!(1),
399 fill: false,
400 nested_fills: Vec::new(),
401 }],
402 body: String::new(),
403 })
404 .unwrap();
405 let json = serde_json::to_value(CardWire::from(&card)).unwrap();
406 assert_eq!(json["kind"], json!("note"));
407 assert_eq!(json["payloadItems"][0]["type"], json!("field"));
408 assert_eq!(json["payloadItems"][0]["key"], json!("x"));
409 assert!(json.get("quill").is_none(), "absent quill is omitted");
410 }
411
412 #[test]
414 fn card_wire_rejects_bad_quill() {
415 let err = Card::try_from(CardWire {
416 kind: String::new(),
417 quill: Some("@nope".to_string()),
418 id: None,
419 ext: None,
420 seed: None,
421 payload_items: Vec::new(),
422 body: String::new(),
423 })
424 .unwrap_err();
425 assert!(matches!(err, WireError::InvalidQuillReference { .. }));
426 }
427}