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 => "field names must match [a-z_][a-z0-9_]*".to_string(),
303 FieldViolation::TooDeep => format!(
304 "nests deeper than the maximum of {} levels",
305 crate::document::limits::MAX_YAML_DEPTH
306 ),
307 },
308 })
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use serde_json::json;
315
316 #[test]
319 fn card_wire_round_trips_nested_fill() {
320 let mut addr = QuillValue::from_json(json!({"street": null, "city": "Anytown"}));
321 assert!(addr.set_fill_at(&[PathSegment::Key("street".to_string())]));
322 let payload = Payload::from_items(vec![PayloadItem::Field {
323 key: "addr".to_string(),
324 value: addr,
325 fill: false,
326 nested_comments: Vec::new(),
327 }]);
328 let card = Card::from_parts(payload, String::new());
329
330 let wire = CardWire::from(&card);
331 let as_json = serde_json::to_value(&wire).unwrap();
332 assert_eq!(
333 as_json["payloadItems"][0]["nestedFills"],
334 json!([["street"]]),
335 "nested fill path rides the wire as a JS array; JSON value stays fill-free"
336 );
337 assert_eq!(
338 as_json["payloadItems"][0]["value"],
339 json!({"street": null, "city": "Anytown"})
340 );
341
342 let back = Card::try_from(wire).expect("wire → card");
343 assert_eq!(back, card, "nested fill must survive Card → wire → Card");
344 }
345
346 #[test]
348 fn card_wire_round_trips_fields_and_comment() {
349 let mut payload = Payload::from_items(vec![
350 PayloadItem::comment("a note"),
351 PayloadItem::field("title", QuillValue::from_json(json!("Hi"))),
352 PayloadItem::Field {
353 key: "count".to_string(),
354 value: QuillValue::from_json(json!(3)),
355 fill: true,
356 nested_comments: Vec::new(),
357 },
358 ]);
359 payload.set_kind("note");
360 let card = Card::from_parts(payload, "body text".to_string());
361
362 let wire = CardWire::from(&card);
363 assert_eq!(wire.kind, "note");
364 assert_eq!(wire.payload_items.len(), 3);
365
366 let back = Card::try_from(wire).expect("wire → card");
367 assert_eq!(back, card, "Card → wire → Card must be identity");
368 }
369
370 #[test]
372 fn card_wire_round_trips_quill() {
373 let mut payload = Payload::from_index_map(Default::default());
374 payload.set_quill("memo@1.2.3".parse().unwrap());
375 payload.set_kind("main");
376 let card = Card::from_parts(payload, String::new());
377
378 let wire = CardWire::from(&card);
379 assert_eq!(wire.quill.as_deref(), Some("memo@1.2.3"));
380
381 let back = Card::try_from(wire).expect("wire → card");
382 assert_eq!(back, card);
383 }
384
385 #[test]
387 fn card_wire_json_shape() {
388 let card = Card::try_from(CardWire {
389 kind: "note".to_string(),
390 quill: None,
391 id: None,
392 ext: None,
393 seed: None,
394 payload_items: vec![PayloadItemWire::Field {
395 key: "x".to_string(),
396 value: json!(1),
397 fill: false,
398 nested_fills: Vec::new(),
399 }],
400 body: String::new(),
401 })
402 .unwrap();
403 let json = serde_json::to_value(CardWire::from(&card)).unwrap();
404 assert_eq!(json["kind"], json!("note"));
405 assert_eq!(json["payloadItems"][0]["type"], json!("field"));
406 assert_eq!(json["payloadItems"][0]["key"], json!("x"));
407 assert!(json.get("quill").is_none(), "absent quill is omitted");
408 }
409
410 #[test]
412 fn card_wire_rejects_bad_quill() {
413 let err = Card::try_from(CardWire {
414 kind: String::new(),
415 quill: Some("@nope".to_string()),
416 id: None,
417 ext: None,
418 seed: None,
419 payload_items: Vec::new(),
420 body: String::new(),
421 })
422 .unwrap_err();
423 assert!(matches!(err, WireError::InvalidQuillReference { .. }));
424 }
425}