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")]
29#[non_exhaustive]
30pub enum FieldSource {
31 Authored,
33 Default,
35 Zero,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize)]
46#[non_exhaustive]
47pub struct ResolvedField {
48 pub name: String,
49 pub value: QuillValue,
50 pub source: FieldSource,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize)]
56#[non_exhaustive]
57pub struct ResolvedMain {
58 pub fields: Vec<ResolvedField>,
59 pub body: Option<ResolvedField>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize)]
66#[non_exhaustive]
67pub struct ResolvedCard {
68 pub kind: Option<String>,
69 pub index: usize,
70 pub fields: Vec<ResolvedField>,
71 pub body: Option<ResolvedField>,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize)]
76#[non_exhaustive]
77pub struct Resolved {
78 pub main: ResolvedMain,
79 pub cards: Vec<ResolvedCard>,
80}
81
82impl Quill {
83 pub fn resolve(&self, doc: &Document) -> Resolved {
93 let config = self.config();
94 let (fields, body) = resolve_card_fields(&config.main, doc.main());
95 let main = ResolvedMain { fields, body };
96 let cards = doc
97 .cards()
98 .iter()
99 .enumerate()
100 .map(|(index, card)| card_states(config, card, index))
101 .collect();
102 Resolved { main, cards }
103 }
104}
105
106fn resolve_card_fields(schema: &CardSchema, card: &Card) -> (Vec<ResolvedField>, Option<ResolvedField>) {
119 let sourced: IndexMap<String, (QuillValue, FieldSource)> = resolve_card_sourced(schema, card);
120 let mut fields = Vec::new();
121
122 for (name, _field_schema) in &schema.fields {
125 let (value, source) = sourced
126 .get(name)
127 .cloned()
128 .expect("resolve_card_sourced emits every declared field");
129 fields.push(ResolvedField {
130 name: name.clone(),
131 value,
132 source,
133 });
134 }
135
136 for (name, (value, source)) in &sourced {
140 if !schema.fields.contains_key(name) {
141 fields.push(ResolvedField {
142 name: name.clone(),
143 value: value.clone(),
144 source: *source,
145 });
146 }
147 }
148
149 let body = schema.body_enabled().then(|| body_state(card));
150 (fields, body)
151}
152
153fn card_states(config: &QuillConfig, card: &Card, index: usize) -> ResolvedCard {
158 let kind = card.kind().map(String::from);
161 match card.kind().and_then(|k| config.card_kind(k)) {
162 Some(schema) => {
163 let (fields, body) = resolve_card_fields(schema, card);
164 ResolvedCard {
165 kind,
166 index,
167 fields,
168 body,
169 }
170 }
171 None => {
172 let fields = card
175 .payload()
176 .to_index_map()
177 .into_iter()
178 .map(|(name, value)| ResolvedField {
179 name,
180 value,
181 source: FieldSource::Authored,
182 })
183 .collect();
184 ResolvedCard {
185 kind,
186 index,
187 fields,
188 body: None,
189 }
190 }
191 }
192}
193
194fn body_state(card: &Card) -> ResolvedField {
199 let value = QuillValue::from_json(quillmark_content::serial::to_canonical_value(card.body()));
200 let source = if card.body().is_blank() {
201 FieldSource::Zero
202 } else {
203 FieldSource::Authored
204 };
205 ResolvedField {
206 name: "body".to_string(),
207 value,
208 source,
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::quill::quill_from_yaml;
216 use crate::{Card, Document, Payload};
217
218 fn parse(md: &str) -> Document {
219 Document::parse(md).expect("document should parse").document
220 }
221
222 fn row<'a>(fields: &'a [ResolvedField], name: &str) -> &'a ResolvedField {
224 fields
225 .iter()
226 .find(|f| f.name == name)
227 .unwrap_or_else(|| panic!("no row `{name}`"))
228 }
229
230 fn has_row(fields: &[ResolvedField], name: &str) -> bool {
231 fields.iter().any(|f| f.name == name)
232 }
233
234 const QUILL: &str = r#"
235quill:
236 name: fs_test
237 version: "1.0"
238 backend: typst
239 description: Field-state tests
240main:
241 body:
242 example: "Example body prose."
243 fields:
244 title:
245 type: string
246 status:
247 type: string
248 default: draft
249 notes:
250 type: string
251 intro:
252 type: richtext
253 default: "**hi**"
254 recipients:
255 type: array
256 items:
257 type: object
258 properties:
259 name: { type: string }
260card_kinds:
261 note:
262 fields:
263 author:
264 type: string
265 example: A. Author
266 tag:
267 type: string
268"#;
269
270 #[test]
273 fn scalar_sources_authored_default_zero() {
274 let quill = quill_from_yaml(QUILL);
275 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: Hello\n~~~\n");
277 let states = quill.resolve(&doc);
278 let f = &states.main.fields;
279
280 assert_eq!(row(f, "title").source, FieldSource::Authored);
281 assert_eq!(row(f, "title").value.as_json(), &serde_json::json!("Hello"));
282
283 assert_eq!(row(f, "status").source, FieldSource::Default);
284 assert_eq!(row(f, "status").value.as_json(), &serde_json::json!("draft"));
285
286 assert_eq!(row(f, "notes").source, FieldSource::Zero);
287 assert_eq!(row(f, "notes").value.as_json(), &serde_json::json!(""));
288 }
289
290 #[test]
291 fn richtext_default_reports_default_and_matches_plate() {
292 let quill = quill_from_yaml(QUILL);
293 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n");
295 let states = quill.resolve(&doc);
296 let intro = row(&states.main.fields, "intro");
297
298 assert_eq!(intro.source, FieldSource::Default);
299 let plate = quill.compile_data(&doc).expect("compile");
301 assert_eq!(intro.value.as_json(), &plate["intro"]);
302 assert!(intro.value.as_json().is_object());
304 }
305
306 #[test]
307 fn present_null_is_absent_takes_default_rung() {
308 let quill = quill_from_yaml(QUILL);
309 let doc = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\nstatus:\n~~~\n");
311 let states = quill.resolve(&doc);
312 let status = row(&states.main.fields, "status");
313 assert_eq!(status.source, FieldSource::Default);
314 assert_eq!(status.value.as_json(), &serde_json::json!("draft"));
315 }
316
317 #[test]
325 fn every_row_is_byte_for_byte_with_compile_data() {
326 let quill = quill_from_yaml(QUILL);
327 let md = "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\n\
328 title: Hello\nintro: \"**bold**\"\nrecipients:\n - name: Alice\n~~~\n\n\
329 Body prose here.\n\n\
330 ~~~card-yaml\n$kind: note\nauthor: Zed\n~~~\nNote body.\n";
331 let doc = parse(md);
332 let states = quill.resolve(&doc);
333 let plate = quill.compile_data(&doc).expect("compile");
334
335 for name in ["title", "status", "notes", "intro", "recipients"] {
337 assert_eq!(
338 row(&states.main.fields, name).value.as_json(),
339 &plate[name],
340 "main row `{name}` must be byte-for-byte with the plate"
341 );
342 }
343 assert_eq!(
344 states.main.body.as_ref().unwrap().value.as_json(),
345 &plate["$body"]
346 );
347
348 let plate_card = &plate["$cards"][0];
350 let card = &states.cards[0];
351 for name in ["author", "tag"] {
352 assert_eq!(
353 row(&card.fields, name).value.as_json(),
354 &plate_card[name],
355 "card row `{name}` must be byte-for-byte with the plate"
356 );
357 }
358 assert_eq!(
359 card.body.as_ref().unwrap().value.as_json(),
360 &plate_card["$body"]
361 );
362 }
363
364 #[test]
365 fn non_nfc_key_on_a_constructed_payload_rows_under_its_nfc_spelling() {
366 let quill = quill_from_yaml(QUILL);
372 let mut map = IndexMap::new();
373 map.insert(
375 "cafe\u{301}".to_string(),
376 QuillValue::from_json(serde_json::json!("hot")),
377 );
378 let mut payload = Payload::from_index_map(map);
379 payload.set_quill("fs_test@1.0".parse().unwrap());
380 payload.set_kind("main");
381 let main = Card::from_parts(payload, quillmark_content::Content::empty());
382 let doc = Document::from_main_and_cards(main, Vec::new());
383 let states = quill.resolve(&doc);
384
385 assert!(!has_row(&states.main.fields, "cafe\u{301}"));
386 let r = row(&states.main.fields, "caf\u{e9}");
387 assert_eq!(r.source, FieldSource::Authored);
388 assert_eq!(r.value.as_json(), &serde_json::json!("hot"));
389 }
390
391 #[test]
394 fn body_row_authored_vs_blank_source() {
395 let quill = quill_from_yaml(QUILL);
396
397 let authored =
398 parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n\nHello body.\n");
399 assert_eq!(
400 quill.resolve(&authored).main.body.unwrap().source,
401 FieldSource::Authored
402 );
403
404 let blank = parse("~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n");
405 let states = quill.resolve(&blank);
406 let body = states.main.body.as_ref().unwrap();
407 assert_eq!(body.source, FieldSource::Zero);
408 assert!(body.value.as_json().is_object(), "blank body is empty content");
409 }
410
411 const BODY_DISABLED_QUILL: &str = r#"
412quill:
413 name: bd_test
414 version: "1.0"
415 backend: typst
416 description: Body-disabled test
417main:
418 fields:
419 title:
420 type: string
421card_kinds:
422 stamp:
423 body:
424 enabled: false
425 fields:
426 label:
427 type: string
428"#;
429
430 #[test]
431 fn body_disabled_kind_omits_body_row() {
432 let quill = quill_from_yaml(BODY_DISABLED_QUILL);
433 let doc = parse(
434 "~~~card-yaml\n$quill: bd_test@1.0\n$kind: main\ntitle: T\n~~~\n\n\
435 ~~~card-yaml\n$kind: stamp\nlabel: L\n~~~\nStray prose.\n",
436 );
437 let states = quill.resolve(&doc);
438 let card = &states.cards[0];
439 assert!(card.body.is_none(), "a body-disabled kind has no body row");
440 assert!(has_row(&card.fields, "label"), "declared rows still present");
441 }
442
443 #[test]
446 fn unknown_kind_card_shape() {
447 let quill = quill_from_yaml(QUILL);
448 let doc = parse(
449 "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\n~~~\n\n\
450 ~~~card-yaml\n$kind: mystery\nfoo: bar\n~~~\nUnread body.\n",
451 );
452 let states = quill.resolve(&doc);
453 let card = &states.cards[0];
454
455 assert_eq!(card.kind.as_deref(), Some("mystery"));
456 assert_eq!(card.index, 0);
457 assert_eq!(row(&card.fields, "foo").source, FieldSource::Authored);
459 assert_eq!(row(&card.fields, "foo").value.as_json(), &serde_json::json!("bar"));
460 assert!(card.body.is_none());
461 }
462
463 #[test]
466 fn undeclared_authored_field_row_is_authored() {
467 let quill = quill_from_yaml(QUILL);
468 let doc = parse(
469 "~~~card-yaml\n$quill: fs_test@1.0\n$kind: main\ntitle: T\nextra: whatever\n~~~\n",
470 );
471 let states = quill.resolve(&doc);
472 let r = row(&states.main.fields, "extra");
473 assert_eq!(r.source, FieldSource::Authored);
474 assert_eq!(r.value.as_json(), &serde_json::json!("whatever"));
475 }
476
477 #[test]
480 fn field_state_is_name_value_and_source_only() {
481 let state = ResolvedField {
482 name: "x".to_string(),
483 value: QuillValue::from_json(serde_json::json!("v")),
484 source: FieldSource::Authored,
485 };
486 let json = serde_json::to_value(&state).unwrap();
487 let obj = json.as_object().unwrap();
488 assert_eq!(obj.len(), 3, "only name + value + source on the wire: {json}");
489 assert!(
490 obj.contains_key("name") && obj.contains_key("value") && obj.contains_key("source")
491 );
492 }
493}