1use std::str::FromStr;
26
27use serde::{Deserialize, Serialize};
28use serde_json::{Map as JsonMap, Value as JsonValue};
29
30use super::payload::{Payload, PayloadItem};
31use super::Card;
32use crate::value::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 },
48 Comment {
50 text: String,
51 #[serde(default)]
53 inline: bool,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct CardWire {
66 #[serde(default)]
69 pub kind: String,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub quill: Option<String>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub id: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub ext: Option<JsonMap<String, JsonValue>>,
80 #[serde(default, alias = "payload_items")]
82 pub payload_items: Vec<PayloadItemWire>,
83 #[serde(default)]
85 pub body: String,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum WireError {
91 InvalidQuillReference { value: String, reason: String },
93 InvalidField { key: String, reason: String },
97}
98
99impl std::fmt::Display for WireError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 WireError::InvalidQuillReference { value, reason } => {
103 write!(f, "invalid `quill` reference {value:?}: {reason}")
104 }
105 WireError::InvalidField { key, reason } => {
106 write!(f, "invalid field {key:?}: {reason}")
107 }
108 }
109 }
110}
111
112impl std::error::Error for WireError {}
113
114impl From<&Card> for CardWire {
115 fn from(card: &Card) -> Self {
116 let mut wire = CardWire {
117 kind: String::new(),
118 quill: None,
119 id: None,
120 ext: None,
121 payload_items: Vec::new(),
122 body: card.body().to_string(),
123 };
124 for item in card.payload().items() {
125 match item {
126 PayloadItem::Quill { reference } => wire.quill = Some(reference.to_string()),
127 PayloadItem::Kind { value } => wire.kind = value.clone(),
128 PayloadItem::Id { value } => wire.id = Some(value.clone()),
129 PayloadItem::Ext { value, .. } => wire.ext = Some(value.clone()),
130 PayloadItem::Field {
131 key, value, fill, ..
132 } => wire.payload_items.push(PayloadItemWire::Field {
133 key: key.clone(),
134 value: value.as_json().clone(),
135 fill: *fill,
136 }),
137 PayloadItem::Comment { text, inline } => {
138 wire.payload_items.push(PayloadItemWire::Comment {
139 text: text.clone(),
140 inline: *inline,
141 })
142 }
143 }
144 }
145 wire
146 }
147}
148
149impl TryFrom<CardWire> for Card {
150 type Error = WireError;
151
152 fn try_from(wire: CardWire) -> Result<Self, Self::Error> {
153 let items = wire
154 .payload_items
155 .into_iter()
156 .map(|item| match item {
157 PayloadItemWire::Field { key, value, fill } => {
158 validate_wire_field(&key, &value)?;
159 Ok(PayloadItem::Field {
160 key,
161 value: QuillValue::from_json(value),
162 fill,
163 nested_comments: Vec::new(),
164 })
165 }
166 PayloadItemWire::Comment { text, inline } => {
167 Ok(PayloadItem::Comment { text, inline })
168 }
169 })
170 .collect::<Result<Vec<_>, WireError>>()?;
171
172 let mut payload = Payload::from_items(items);
176 if let Some(value) = wire.quill {
177 let reference = QuillReference::from_str(&value)
178 .map_err(|reason| WireError::InvalidQuillReference { value, reason })?;
179 payload.set_quill(reference);
180 }
181 if !wire.kind.is_empty() {
182 payload.set_kind(wire.kind);
183 }
184 if let Some(id) = wire.id {
185 payload.set_id(id);
186 }
187 if let Some(ext) = wire.ext {
188 let as_value = JsonValue::Object(ext);
189 if crate::value::json_depth_exceeds(&as_value, crate::document::limits::MAX_YAML_DEPTH)
190 {
191 return Err(WireError::InvalidField {
192 key: "$ext".to_string(),
193 reason: format!(
194 "nests deeper than the maximum of {} levels",
195 crate::document::limits::MAX_YAML_DEPTH
196 ),
197 });
198 }
199 let JsonValue::Object(ext) = as_value else {
200 unreachable!("constructed as Object above")
201 };
202 payload.set_ext(ext);
203 }
204 Ok(Card::from_parts(payload, wire.body))
205 }
206}
207
208fn validate_wire_field(key: &str, value: &JsonValue) -> Result<(), WireError> {
211 use super::edit::{validate_field, FieldViolation};
212 validate_field(key, value).map_err(|v| WireError::InvalidField {
213 key: key.to_string(),
214 reason: match v {
215 FieldViolation::InvalidName => "field names must match [a-z_][a-z0-9_]*".to_string(),
216 FieldViolation::TooDeep => format!(
217 "nests deeper than the maximum of {} levels",
218 crate::document::limits::MAX_YAML_DEPTH
219 ),
220 },
221 })
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use serde_json::json;
228
229 #[test]
231 fn card_wire_round_trips_fields_and_comment() {
232 let mut payload = Payload::from_items(vec![
233 PayloadItem::comment("a note"),
234 PayloadItem::field("title", QuillValue::from_json(json!("Hi"))),
235 PayloadItem::Field {
236 key: "count".to_string(),
237 value: QuillValue::from_json(json!(3)),
238 fill: true,
239 nested_comments: Vec::new(),
240 },
241 ]);
242 payload.set_kind("note");
243 let card = Card::from_parts(payload, "body text".to_string());
244
245 let wire = CardWire::from(&card);
246 assert_eq!(wire.kind, "note");
247 assert_eq!(wire.payload_items.len(), 3);
248
249 let back = Card::try_from(wire).expect("wire → card");
250 assert_eq!(back, card, "Card → wire → Card must be identity");
251 }
252
253 #[test]
255 fn card_wire_round_trips_quill() {
256 let mut payload = Payload::from_index_map(Default::default());
257 payload.set_quill("memo@1.2.3".parse().unwrap());
258 payload.set_kind("main");
259 let card = Card::from_parts(payload, String::new());
260
261 let wire = CardWire::from(&card);
262 assert_eq!(wire.quill.as_deref(), Some("memo@1.2.3"));
263
264 let back = Card::try_from(wire).expect("wire → card");
265 assert_eq!(back, card);
266 }
267
268 #[test]
270 fn card_wire_json_shape() {
271 let card = Card::try_from(CardWire {
272 kind: "note".to_string(),
273 quill: None,
274 id: None,
275 ext: None,
276 payload_items: vec![PayloadItemWire::Field {
277 key: "x".to_string(),
278 value: json!(1),
279 fill: false,
280 }],
281 body: String::new(),
282 })
283 .unwrap();
284 let json = serde_json::to_value(CardWire::from(&card)).unwrap();
285 assert_eq!(json["kind"], json!("note"));
286 assert_eq!(json["payloadItems"][0]["type"], json!("field"));
287 assert_eq!(json["payloadItems"][0]["key"], json!("x"));
288 assert!(json.get("quill").is_none(), "absent quill is omitted");
289 }
290
291 #[test]
293 fn card_wire_rejects_bad_quill() {
294 let err = Card::try_from(CardWire {
295 kind: String::new(),
296 quill: Some("@nope".to_string()),
297 id: None,
298 ext: None,
299 payload_items: Vec::new(),
300 body: String::new(),
301 })
302 .unwrap_err();
303 assert!(matches!(err, WireError::InvalidQuillReference { .. }));
304 }
305}