1use indexmap::IndexMap;
19use serde::Serialize;
20
21use super::compose::resolve_card_sourced;
22use super::{CardSchema, Quill, QuillConfig};
23use crate::{Card, Document, QuillValue};
24
25#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
28#[serde(rename_all = "lowercase")]
29pub enum FieldSource {
30 Authored,
32 Default,
34 Zero,
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize)]
45pub struct ResolvedField {
46 pub name: String,
47 pub value: QuillValue,
48 pub source: FieldSource,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize)]
54pub struct ResolvedMain {
55 pub fields: Vec<ResolvedField>,
56 pub body: Option<ResolvedField>,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize)]
63pub struct ResolvedCard {
64 pub kind: Option<String>,
65 pub index: usize,
66 pub fields: Vec<ResolvedField>,
67 pub body: Option<ResolvedField>,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize)]
72pub struct Resolved {
73 pub main: ResolvedMain,
74 pub cards: Vec<ResolvedCard>,
75}
76
77impl Quill {
78 pub fn resolve(&self, doc: &Document) -> Resolved {
88 let config = self.config();
89 let (fields, body) = resolve_card_fields(&config.main, doc.main());
90 let main = ResolvedMain { fields, body };
91 let cards = doc
92 .cards()
93 .iter()
94 .enumerate()
95 .map(|(index, card)| card_states(config, card, index))
96 .collect();
97 Resolved { main, cards }
98 }
99}
100
101fn resolve_card_fields(schema: &CardSchema, card: &Card) -> (Vec<ResolvedField>, Option<ResolvedField>) {
114 let sourced: IndexMap<String, (QuillValue, FieldSource)> = resolve_card_sourced(schema, card);
115 let mut fields = Vec::new();
116
117 for (name, _field_schema) in &schema.fields {
120 let (value, source) = sourced
121 .get(name)
122 .cloned()
123 .expect("resolve_card_sourced emits every declared field");
124 fields.push(ResolvedField {
125 name: name.clone(),
126 value,
127 source,
128 });
129 }
130
131 for (name, (value, source)) in &sourced {
135 if !schema.fields.contains_key(name) {
136 fields.push(ResolvedField {
137 name: name.clone(),
138 value: value.clone(),
139 source: *source,
140 });
141 }
142 }
143
144 let body = schema.body_enabled().then(|| body_state(card));
145 (fields, body)
146}
147
148fn card_states(config: &QuillConfig, card: &Card, index: usize) -> ResolvedCard {
153 let kind = card.kind().map(String::from);
156 match card.kind().and_then(|k| config.card_kind(k)) {
157 Some(schema) => {
158 let (fields, body) = resolve_card_fields(schema, card);
159 ResolvedCard {
160 kind,
161 index,
162 fields,
163 body,
164 }
165 }
166 None => {
167 let fields = card
170 .payload()
171 .to_index_map()
172 .into_iter()
173 .map(|(name, value)| ResolvedField {
174 name,
175 value,
176 source: FieldSource::Authored,
177 })
178 .collect();
179 ResolvedCard {
180 kind,
181 index,
182 fields,
183 body: None,
184 }
185 }
186 }
187}
188
189fn body_state(card: &Card) -> ResolvedField {
194 let value = QuillValue::from_json(quillmark_content::serial::to_canonical_value(card.body()));
195 let source = if card.body().is_blank() {
196 FieldSource::Zero
197 } else {
198 FieldSource::Authored
199 };
200 ResolvedField {
201 name: "body".to_string(),
202 value,
203 source,
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::quill::quill_from_yaml;
211 use crate::{Card, Document, Payload};
212
213 fn parse(md: &str) -> Document {
214 Document::parse(md).expect("document should parse").document
215 }
216
217 fn row<'a>(fields: &'a [ResolvedField], name: &str) -> &'a ResolvedField {
219 fields
220 .iter()
221 .find(|f| f.name == name)
222 .unwrap_or_else(|| panic!("no row `{name}`"))
223 }
224
225 fn has_row(fields: &[ResolvedField], name: &str) -> bool {
226 fields.iter().any(|f| f.name == name)
227 }
228
229 const QUILL: &str = r#"
230quill:
231 name: fs_test
232 version: "1.0"
233 backend: typst
234 description: Field-state tests
235main:
236 body:
237 example: "Example body prose."
238 fields:
239 title:
240 type: string
241 status:
242 type: string
243 default: draft
244 notes:
245 type: string
246 intro:
247 type: richtext
248 default: "**hi**"
249 recipients:
250 type: array
251 items:
252 type: object
253 properties:
254 name: { type: string }
255card_kinds:
256 note:
257 fields:
258 author:
259 type: string
260 example: A. Author
261 tag:
262 type: string
263"#;
264
265 #[test]
268 fn scalar_sources_authored_default_zero() {
269 let quill = quill_from_yaml(QUILL);
270 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: Hello\n~~~\n");
272 let states = quill.resolve(&doc);
273 let f = &states.main.fields;
274
275 assert_eq!(row(f, "title").source, FieldSource::Authored);
276 assert_eq!(row(f, "title").value.as_json(), &serde_json::json!("Hello"));
277
278 assert_eq!(row(f, "status").source, FieldSource::Default);
279 assert_eq!(row(f, "status").value.as_json(), &serde_json::json!("draft"));
280
281 assert_eq!(row(f, "notes").source, FieldSource::Zero);
282 assert_eq!(row(f, "notes").value.as_json(), &serde_json::json!(""));
283 }
284
285 #[test]
286 fn richtext_default_reports_default_and_matches_plate() {
287 let quill = quill_from_yaml(QUILL);
288 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n");
290 let states = quill.resolve(&doc);
291 let intro = row(&states.main.fields, "intro");
292
293 assert_eq!(intro.source, FieldSource::Default);
294 let plate = quill.compile_data(&doc).expect("compile");
296 assert_eq!(intro.value.as_json(), &plate["intro"]);
297 assert!(intro.value.as_json().is_object());
299 }
300
301 #[test]
302 fn present_null_is_absent_takes_default_rung() {
303 let quill = quill_from_yaml(QUILL);
304 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\nstatus:\n~~~\n");
306 let states = quill.resolve(&doc);
307 let status = row(&states.main.fields, "status");
308 assert_eq!(status.source, FieldSource::Default);
309 assert_eq!(status.value.as_json(), &serde_json::json!("draft"));
310 }
311
312 #[test]
320 fn every_row_is_byte_for_byte_with_compile_data() {
321 let quill = quill_from_yaml(QUILL);
322 let md = "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\n\
323 title: Hello\nintro: \"**bold**\"\nrecipients:\n - name: Alice\n~~~\n\n\
324 Body prose here.\n\n\
325 ~~~card-yaml\n$kind: note\nauthor: Zed\n~~~\nNote body.\n";
326 let doc = parse(md);
327 let states = quill.resolve(&doc);
328 let plate = quill.compile_data(&doc).expect("compile");
329
330 for name in ["title", "status", "notes", "intro", "recipients"] {
332 assert_eq!(
333 row(&states.main.fields, name).value.as_json(),
334 &plate[name],
335 "main row `{name}` must be byte-for-byte with the plate"
336 );
337 }
338 assert_eq!(
339 states.main.body.as_ref().unwrap().value.as_json(),
340 &plate["$body"]
341 );
342
343 let plate_card = &plate["$cards"][0];
345 let card = &states.cards[0];
346 for name in ["author", "tag"] {
347 assert_eq!(
348 row(&card.fields, name).value.as_json(),
349 &plate_card[name],
350 "card row `{name}` must be byte-for-byte with the plate"
351 );
352 }
353 assert_eq!(
354 card.body.as_ref().unwrap().value.as_json(),
355 &plate_card["$body"]
356 );
357 }
358
359 #[test]
360 fn non_nfc_key_on_a_constructed_payload_rows_under_its_nfc_spelling() {
361 let quill = quill_from_yaml(QUILL);
367 let mut map = IndexMap::new();
368 map.insert(
370 "cafe\u{301}".to_string(),
371 QuillValue::from_json(serde_json::json!("hot")),
372 );
373 let mut payload = Payload::from_index_map(map);
374 payload.set_quill("fs_test@1.0".parse().unwrap());
375 payload.set_kind("main");
376 let main = Card::from_parts(payload, quillmark_content::Content::empty());
377 let doc = Document::from_main_and_cards(main, Vec::new());
378 let states = quill.resolve(&doc);
379
380 assert!(!has_row(&states.main.fields, "cafe\u{301}"));
381 let r = row(&states.main.fields, "caf\u{e9}");
382 assert_eq!(r.source, FieldSource::Authored);
383 assert_eq!(r.value.as_json(), &serde_json::json!("hot"));
384 }
385
386 #[test]
389 fn body_row_authored_vs_blank_source() {
390 let quill = quill_from_yaml(QUILL);
391
392 let authored =
393 parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n\nHello body.\n");
394 assert_eq!(
395 quill.resolve(&authored).main.body.unwrap().source,
396 FieldSource::Authored
397 );
398
399 let blank = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n");
400 let states = quill.resolve(&blank);
401 let body = states.main.body.as_ref().unwrap();
402 assert_eq!(body.source, FieldSource::Zero);
403 assert!(body.value.as_json().is_object(), "blank body is empty content");
404 }
405
406 const BODY_DISABLED_QUILL: &str = r#"
407quill:
408 name: bd_test
409 version: "1.0"
410 backend: typst
411 description: Body-disabled test
412main:
413 fields:
414 title:
415 type: string
416card_kinds:
417 stamp:
418 body:
419 enabled: false
420 fields:
421 label:
422 type: string
423"#;
424
425 #[test]
426 fn body_disabled_kind_omits_body_row() {
427 let quill = quill_from_yaml(BODY_DISABLED_QUILL);
428 let doc = parse(
429 "~~~card-yaml\n$quill: bd_test@1.0\n$kind: main\ntitle: T\n~~~\n\n\
430 ~~~card-yaml\n$kind: stamp\nlabel: L\n~~~\nStray prose.\n",
431 );
432 let states = quill.resolve(&doc);
433 let card = &states.cards[0];
434 assert!(card.body.is_none(), "a body-disabled kind has no body row");
435 assert!(has_row(&card.fields, "label"), "declared rows still present");
436 }
437
438 #[test]
441 fn unknown_kind_card_shape() {
442 let quill = quill_from_yaml(QUILL);
443 let doc = parse(
444 "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n\n\
445 ~~~card-yaml\n$kind: mystery\nfoo: bar\n~~~\nUnread body.\n",
446 );
447 let states = quill.resolve(&doc);
448 let card = &states.cards[0];
449
450 assert_eq!(card.kind.as_deref(), Some("mystery"));
451 assert_eq!(card.index, 0);
452 assert_eq!(row(&card.fields, "foo").source, FieldSource::Authored);
454 assert_eq!(row(&card.fields, "foo").value.as_json(), &serde_json::json!("bar"));
455 assert!(card.body.is_none());
456 }
457
458 #[test]
461 fn undeclared_authored_field_row_is_authored() {
462 let quill = quill_from_yaml(QUILL);
463 let doc = parse(
464 "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\nextra: whatever\n~~~\n",
465 );
466 let states = quill.resolve(&doc);
467 let r = row(&states.main.fields, "extra");
468 assert_eq!(r.source, FieldSource::Authored);
469 assert_eq!(r.value.as_json(), &serde_json::json!("whatever"));
470 }
471
472 #[test]
475 fn field_state_is_name_value_and_source_only() {
476 let state = ResolvedField {
477 name: "x".to_string(),
478 value: QuillValue::from_json(serde_json::json!("v")),
479 source: FieldSource::Authored,
480 };
481 let json = serde_json::to_value(&state).unwrap();
482 let obj = json.as_object().unwrap();
483 assert_eq!(obj.len(), 3, "only name + value + source on the wire: {json}");
484 assert!(
485 obj.contains_key("name") && obj.contains_key("value") && obj.contains_key("source")
486 );
487 }
488}