quillmark-core 0.95.1

Core types and functionality for the Quillmark schema-driven document engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
//! Parsing and typed in-memory model for Quillmark card-yaml documents.
//!
//! A [`Document`] holds a root [`Card`] plus ordered composable cards; each
//! card carries a [`Payload`] — source-ordered items ([`PayloadItem`]:
//! `$quill`/`$kind`/`$id` metadata, user fields, and comments, in the order
//! they appear in the block's YAML content) — and a Markdown body.
//! [`Document::parse`] returns errors for malformed YAML, unclosed
//! fences, a missing root `$quill`, or unknown `$`-prefixed system keys.
//!
//! See [markdown-spec.md](https://github.com/borb-sh/quillmark/blob/main/prose/references/markdown-spec.md)
//! for the card-yaml format specification.

use serde::{Deserialize, Serialize};

use quillmark_content::import::{from_markdown as import_markdown, ImportError};
use quillmark_content::Content;

use crate::error::ParseError;
use crate::version::QuillReference;
use crate::Diagnostic;

/// The single markdown→content boundary for card bodies. Every construction path
/// that starts from an authored markdown string ([`Document::parse`],
/// wire/storage deserialization, seeding, blueprint) routes through it, so the
/// markdown parser is reached from exactly one helper. An empty string yields
/// the empty content without invoking the parser.
pub(crate) fn import_body(md: &str) -> Result<Content, ImportError> {
    if md.is_empty() {
        Ok(Content::empty())
    } else {
        import_markdown(md)
    }
}

/// Which encoding a `decode_richtext_value` failure came from, so a call site
/// can prefix its diagnostic per encoding without re-deriving the dispatch.
/// Surfaced publicly as the error of [`Card::field_richtext`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RichtextDecodeError {
    /// A JSON object that is not a valid canonical content.
    NotContent(String),
    /// A markdown string that failed to import.
    BadMarkdown(String),
}

impl RichtextDecodeError {
    /// The inner failure message, without an encoding-specific prefix.
    pub fn into_message(self) -> String {
        match self {
            RichtextDecodeError::NotContent(m) | RichtextDecodeError::BadMarkdown(m) => m,
        }
    }
}

/// Decode a JSON value in either accepted richtext encoding: a canonical content
/// **object** ([`from_canonical_value`](quillmark_content::serial::from_canonical_value))
/// or an authored markdown **string** (via [`import_body`], the single markdown
/// boundary). The one place the object-vs-string dispatch lives; a call site
/// handles the shapes that are neither — `null`, array, scalar — and maps the
/// error into its own type.
///
/// - `Some(Ok(rt))` — decoded.
/// - `Some(Err(e))` — an object that is not a content, or a string that failed
///   to import; `e` names the encoding so the caller can prefix its message.
/// - `None` — the value is neither an object nor a string.
pub(crate) fn decode_richtext_value(
    value: &serde_json::Value,
) -> Option<Result<Content, RichtextDecodeError>> {
    match value {
        serde_json::Value::Object(_) => Some(
            quillmark_content::serial::from_canonical_value(value)
                .map_err(|e| RichtextDecodeError::NotContent(e.to_string())),
        ),
        serde_json::Value::String(md) => {
            Some(import_body(md).map_err(|e| RichtextDecodeError::BadMarkdown(e.to_string())))
        }
        _ => None,
    }
}

/// Decode a JSON value for a `plaintext` field: a canonical content **object**
/// (revalidated) or a literal **string** imported verbatim
/// ([`from_plaintext`](quillmark_content::from_plaintext) — never markdown, so
/// `*hi*` stays four plain characters). The plaintext twin of
/// [`decode_richtext_value`]: the string branch is infallible (literal import
/// can't fail), so only the object branch yields `Err` (`String` message). A
/// call site handles the shapes that are neither — `null`, array, scalar. This
/// is the single plaintext object-vs-string dispatch, shared by the coercion
/// literal-import site and the validation shape check.
///
/// - `Some(Ok(rt))` — decoded.
/// - `Some(Err(msg))` — an object that is not a valid content.
/// - `None` — the value is neither an object nor a string.
pub(crate) fn decode_plaintext_value(
    value: &serde_json::Value,
) -> Option<Result<Content, String>> {
    match value {
        serde_json::Value::Object(_) => {
            Some(quillmark_content::serial::from_canonical_value(value).map_err(|e| e.to_string()))
        }
        serde_json::Value::String(s) => Some(Ok(quillmark_content::from_plaintext(s))),
        _ => None,
    }
}

pub mod assemble;
pub mod dto;
pub mod edit;
pub mod emit;
pub mod fences;
pub mod limits;
pub mod meta;
pub mod payload;
pub mod prescan;
pub mod wire;
pub(crate) mod yaml_hints;

pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_93_0};
pub use edit::EditError;
pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
pub use payload::{MetaKey, Payload, PayloadItem};
pub use wire::{CardWire, PayloadItemWire, WireError};

/// Authoring-format rules for the `~~~` card-yaml markdown surface.
///
/// Surfaced verbatim to LLM/MCP consumers (and to CLI / Python bindings via
/// the same text) so error parity holds — every consumer reads the same
/// rules. This is the single source of truth; bindings should call into it
/// rather than re-stating the rules in their own glue.
pub const FORMAT_RULES: &str = "Document format rules:
\u{2022} Block opener and closer are EXACTLY `~~~` (three tildes, no info string). The `~~~card-yaml` opener is also accepted as a non-canonical alias.
\u{2022} A blank line must precede every `~~~` block opener (unless it is line 1), and the opener must be at column zero (no leading spaces). An indented `~~~` is an ordinary code block, not a card.
\u{2022} The first block is the root and MUST contain `$quill: <name>@<version>`. Its `$kind` is `main` by position \u{2014} an explicit `$kind: main` is accepted but not required. Additional blocks declare composable cards via `$kind: <card_kind>`.
\u{2022} Reserved `$`-keys: `$quill`, `$kind`, `$id`, `$ext`, `$seed`. User fields use lowercase snake_case.
\u{2022} Prose body is the text after a block's closing `~~~`, up to the next opener or EOF. To include a literal fenced code block in prose, use a backtick fence (```); any column-zero `~~~` block is parsed as card metadata.
\u{2022} A field that already shows a concrete value carries a default and is shippable as-is \u{2014} keep the line, override the value, or delete it to fall back to the default. A blank or null value (`field:`, `field: null`, `field: ~`) is treated the same as omitting the field: it falls back to the default, or to the type-empty zero value.
\u{2022} `field: !must_fill <value>` marks a placeholder awaiting your input \u{2014} replace it with a real value and drop the `!must_fill` tag before shipping. A bare `field: !must_fill` is an empty placeholder. A leftover marker never blocks rendering, but it is reported as a warning until you replace it.
\u{2022} Numbers and booleans MUST be unquoted (`year: 2025`, `pinned: true`); quoting turns them into strings and fails validation.
\u{2022} Plain-scalar values cannot start with `*` or `&` (YAML alias/anchor markers) and cannot contain `: ` (colon-space). For markdown emphasis, embedded colons, or other special prefixes, quote the value: `field: '**bold**'` or `field: \"Name: subtitle\"`. Multi-line values use `|-`, not multi-line quoted scalars.";

/// Authoring-ergonomics header that introduces a blueprint to an LLM/MCP
/// consumer. The `{quill}` placeholder is substituted with the quill name.
/// Designed to be shown above [`FORMAT_RULES`], which covers field-level
/// semantics like the `!must_fill` marker — keep the wording tight here so the
/// two strings do not duplicate guidance.
const BLUEPRINT_INSTRUCTION_TEMPLATE: &str =
    "Fill in the `{quill}` blueprint below: replace each `!must_fill` placeholder with a real \
value and edit the body prose. Submit the filled markdown as `content` to `create_document`.";

/// Render the blueprint-instruction header with `quill_name` substituted in.
/// Single source of truth for the prose so every binding shows identical text.
pub fn blueprint_instruction(quill_name: &str) -> String {
    BLUEPRINT_INSTRUCTION_TEMPLATE.replace("{quill}", quill_name)
}

#[cfg(test)]
mod tests;

/// The record of one parse: the [`Document`] and any non-fatal warnings.
/// Returned by [`Document::parse`], the single parse entry. Warnings live here
/// and only here — `Document` is the value (equality, the storage DTO, and
/// mutators all exclude warnings); `Parsed` is the parse *event*. A caller that
/// wants only the document writes `Document::parse(md)?.document`.
#[derive(Debug)]
#[must_use = "carries parse warnings; read `.document`/`.warnings` or bind it"]
pub struct Parsed {
    pub document: Document,
    pub warnings: Vec<Diagnostic>,
}

/// A single card-yaml block (root or composable). `body` is the content
/// ([`Content`]) form of the prose after the closing fence — the empty content
/// when none follows; check `card.body().is_blank()`. Markdown is a projection:
/// [`Card::body_markdown`] re-emits it.
#[derive(Debug, Clone, PartialEq)]
pub struct Card {
    payload: Payload,
    body: Content,
}

impl Card {
    /// Create a `Card` from its parts without validation. `body` is the content
    /// form; to build from an authored markdown string, import it first via the
    /// crate-internal `import_body` boundary. For user-facing construction of
    /// composable cards use [`Card::new`].
    pub fn from_parts(payload: Payload, body: Content) -> Self {
        Self { payload, body }
    }

    pub fn quill(&self) -> Option<&QuillReference> {
        self.payload.quill()
    }

    pub fn kind(&self) -> Option<&str> {
        self.payload.kind()
    }

    pub fn id(&self) -> Option<&str> {
        self.payload.id()
    }

    /// Opaque `$ext` map for out-of-band extension data (UI editor state,
    /// agent annotations, …). Carried through Markdown and storage DTO
    /// round-trips; never emitted into the plate JSON consumed by
    /// backends.
    pub fn ext(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
        self.payload.ext()
    }

    pub fn payload(&self) -> &Payload {
        &self.payload
    }

    pub fn payload_mut(&mut self) -> &mut Payload {
        &mut self.payload
    }

    /// The card body as a [`Content`] content — the canonical content model.
    /// For the markdown projection use [`Card::body_markdown`].
    pub fn body(&self) -> &Content {
        &self.body
    }

    /// The card body rendered back to its markdown projection. This is a
    /// derived view (`export ∘ body`), not stored state; a `Document` round-trip
    /// therefore canonicalizes the body (e.g. `__b__` → `**b**`).
    pub fn body_markdown(&self) -> String {
        quillmark_content::export::to_markdown(&self.body)
    }

    pub(crate) fn overwrite_body(&mut self, body: Content) {
        self.body = body;
    }

    pub(crate) fn body_mut(&mut self) -> &mut Content {
        &mut self.body
    }

    /// Read a richtext-valued user field back as a [`Content`] content — the
    /// field-level twin of [`Card::body`]. Decodes the stored value through the
    /// same object-or-markdown dispatch the writer
    /// ([`commit_field`](Card::commit_field)) commits, so a field
    /// stored as a canonical content reads back losslessly (identity marks
    /// intact) and a still-authored markdown string imports.
    ///
    /// - `None` — the field is absent.
    /// - `Some(Ok(rt))` — decoded content.
    /// - `Some(Err(_))` — the field is present but neither a content object nor
    ///   an importable markdown string (e.g. a bare number a `store_field` wrote).
    ///
    /// A `Document` carries no schema, so this cannot itself tell a richtext
    /// field from a plain string field; the caller names a field it knows is
    /// richtext, exactly as it does when writing.
    pub fn field_richtext(&self, name: &str) -> Option<Result<Content, RichtextDecodeError>> {
        let value = self.payload.get(name)?.as_json();
        Some(match crate::document::decode_richtext_value(value) {
            Some(result) => result,
            None => match value {
                serde_json::Value::Null => Ok(Content::empty()),
                _ => Err(RichtextDecodeError::NotContent(
                    "expected a richtext content object or a markdown string".to_string(),
                )),
            },
        })
    }

    /// The markdown projection of a richtext-valued field (`export ∘ decode`) —
    /// the field-level twin of [`Card::body_markdown`], and the projection an
    /// emit or a markdown save writes for a content-valued field. The projection
    /// twin of [`field_richtext`](Card::field_richtext), carrying its `Ok`/`Err`
    /// decode outcome:
    ///
    /// - `None` — the field is absent.
    /// - `Some(Ok(md))` — the projected markdown.
    /// - `Some(Err(_))` — the field is present but does not decode as richtext
    ///   (a scalar/array/object a `store_field` wrote, or a non-content object).
    ///
    /// Absence returns `None`; a present non-richtext value returns `Some(Err)`,
    /// so the projection surfaces the type mismatch instead of blanking on it.
    pub fn field_markdown(&self, name: &str) -> Option<Result<String, RichtextDecodeError>> {
        Some(self.field_richtext(name)?.map(|rt| quillmark_content::export::to_markdown(&rt)))
    }

    /// The plaintext projection of a content-valued field (`to_plaintext ∘
    /// decode`) — the literal-codec twin of [`field_markdown`](Card::field_markdown),
    /// for a `plaintext`-typed field: marks are never interpreted, so the text is
    /// verbatim both ways. Carries [`field_richtext`](Card::field_richtext)'s
    /// `Ok`/`Err` decode outcome:
    ///
    /// - `None` — the field is absent.
    /// - `Some(Ok(text))` — the projected literal text.
    /// - `Some(Err(_))` — the field is present but does not decode as content.
    pub fn field_plaintext(&self, name: &str) -> Option<Result<String, RichtextDecodeError>> {
        Some(self.field_richtext(name)?.map(|rt| quillmark_content::export::to_plaintext(&rt)))
    }
}

/// A parsed, per-kind **seed overlay**: the sparse fields (and optional body)
/// a newly-added card of a given kind starts with. Built from a `$seed[<kind>]`
/// entry of the main card's [`Card::seed`] map via [`SeedOverlay::from_json`],
/// and layered over the quill's schema-example seed by
/// [`crate::Quill::seed_card`] (overlay › example › absent). The reserved inner
/// key `$body` carries the body override; every other user field becomes an
/// entry, while any other `$`-prefixed key is reserved and dropped.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SeedOverlay {
    /// Field-value overrides, keyed by field name.
    pub fields: indexmap::IndexMap<String, crate::value::QuillValue>,
    /// Body override, when the overlay declares a `$body` string.
    pub body: Option<String>,
}

impl SeedOverlay {
    /// Parse an overlay from a `$seed[<kind>]` JSON value, or `None` when it is
    /// not a mapping. Use this to turn the raw overlay object a consumer reads
    /// from the main card's `$seed` map ([`Card::seed`]) into a typed overlay to
    /// hand to [`crate::Quill::seed_card`] — e.g.
    /// `doc.main().seed().and_then(|m| m.get(kind)).and_then(SeedOverlay::from_json)`.
    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
        value.as_object().map(Self::from_json_map)
    }

    /// Build an overlay from a single `$seed[<kind>]` JSON map: the reserved
    /// `$body` string becomes [`body`](Self::body); every other user-field entry
    /// becomes a field. A non-string `$body` is ignored (no body override). Any
    /// other `$`-prefixed key is reserved and dropped — never stored as a user
    /// field — since an overlay only ever carries user fields plus `$body`.
    fn from_json_map(map: &serde_json::Map<String, serde_json::Value>) -> Self {
        let mut fields = indexmap::IndexMap::new();
        let mut body = None;
        for (key, value) in map {
            if key == "$body" {
                if let Some(s) = value.as_str() {
                    body = Some(s.to_string());
                }
            } else if key.starts_with('$') {
                // Reserved key other than `$body`: not a user field. Drop it
                // rather than smuggle a `$`-key into the field set.
                continue;
            } else {
                fields.insert(
                    key.clone(),
                    crate::value::QuillValue::from_json(value.clone()),
                );
            }
        }
        SeedOverlay { fields, body }
    }
}

/// A fully-parsed Quillmark document. Serde routes through [`StoredDocument`];
/// for the plate wire shape see [`Document::to_plate_json`].
///
/// Parse-time warnings are *not* document state — they ride out-of-band on
/// [`Parsed`] from [`Document::parse`], the single owner. Equality and the
/// storage DTO therefore cover only structural content (`main` and `cards`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(into = "StoredDocument", try_from = "StoredDocument")]
pub struct Document {
    main: Card,
    cards: Vec<Card>,
}

impl Document {
    /// Create a blank document: a main card carrying only `$quill`, an empty
    /// body, and no composable cards. The programmatic blank canvas — every
    /// schema field is absent and resolves at render time (`default`, else
    /// type-empty zero), so nothing the caller did not set reaches the
    /// output. For an example-filled starter shaped like the blueprint, use
    /// `Quill::seed_document`.
    pub fn new(quill: QuillReference) -> Self {
        let mut payload = Payload::new();
        payload.set_quill(quill);
        // Parsed main cards always carry `$kind: main` (the parser normalizes
        // it in); match that shape so a blank document round-trips equal.
        payload.set_kind("main");
        Self {
            main: Card::from_parts(payload, Content::empty()),
            cards: Vec::new(),
        }
    }

    /// Create a `Document` from a pre-built main card and composable cards.
    /// `main` must carry `$quill`; composable cards must not.
    pub fn from_main_and_cards(main: Card, cards: Vec<Card>) -> Self {
        debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
        debug_assert!(
            cards.iter().all(|c| c.quill().is_none()),
            "composable cards must not carry `$quill`"
        );
        debug_assert!(
            cards.iter().all(|c| c.seed().is_none()),
            "composable cards must not carry `$seed`"
        );
        Self { main, cards }
    }

    /// Parse card-yaml Markdown into a [`Parsed`] — the [`Document`] plus any
    /// non-fatal warnings. The single parse entry; a caller that wants only the
    /// document writes `Document::parse(md)?.document`. Errors on malformed
    /// YAML, a missing root `$quill`, an over-size input, and the other
    /// [`ParseError`] variants.
    #[doc(alias = "from_markdown")]
    pub fn parse(markdown: &str) -> Result<Parsed, ParseError> {
        assemble::decompose_with_warnings(markdown)
            .map(|(document, warnings)| Parsed { document, warnings })
    }

    pub fn main(&self) -> &Card {
        &self.main
    }

    pub fn main_mut(&mut self) -> &mut Card {
        &mut self.main
    }

    /// The `$quill` reference from the root block. Always present on parsed documents.
    pub fn quill_reference(&self) -> QuillReference {
        self.main
            .quill()
            .cloned()
            .expect("root block's $quill is validated at parse time")
    }

    pub fn cards(&self) -> &[Card] {
        &self.cards
    }

    pub fn cards_mut(&mut self) -> &mut [Card] {
        &mut self.cards
    }

    /// A single composable card by index — the immutable twin of
    /// [`card_mut`](Document::card_mut), so reading one card's payload does not
    /// require materializing every card via [`cards`](Document::cards). `None`
    /// when out of range.
    pub fn card(&self, index: usize) -> Option<&Card> {
        self.cards.get(index)
    }

    /// The first composable card whose `$id` equals `id`, with its index —
    /// resolving the canonical durable address ([PROGRAMMATIC.md]) without a
    /// hand-rolled scan over [`cards`](Document::cards). `$id` is non-unique by
    /// design, so this returns the first match; `None` when no card carries it.
    ///
    /// [PROGRAMMATIC.md]: https://github.com/borb-sh/quillmark/blob/main/prose/canon/PROGRAMMATIC.md
    pub fn find_card(&self, id: &str) -> Option<(usize, &Card)> {
        self.cards
            .iter()
            .enumerate()
            .find(|(_, card)| card.id() == Some(id))
    }

    pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
        &mut self.cards
    }

    /// Serialize to the JSON wire shape consumed by backend plates. This is
    /// the **only** place in `quillmark-core` that produces this shape:
    ///
    /// ```json
    /// {
    ///   "$quill": "<ref>",
    ///   "$body": { "text": "…", "lines": [...], "marks": [...], "islands": [...] },
    ///   "$cards": [{ "$kind": "<tag>", "$body": <content>, "<field>": <value>, ... }],
    ///   "<field>": <value>, ...
    /// }
    /// ```
    ///
    /// `$body` (global and per-card) is canonical Content-JSON — the content as
    /// a nested object, not a markdown string. Richtext payload fields likewise
    /// cross as content objects (committed at coercion time).
    ///
    /// `$`-prefixed keys carry document-level metadata (quill ref, body
    /// text, card list, card kind). User payload fields stay flat at the
    /// root — they cannot collide with `$` keys because user field names are
    /// never `$`-prefixed (they match `[A-Za-z_][A-Za-z0-9_]*`).
    pub fn to_plate_json(&self) -> serde_json::Value {
        let mut map = serde_json::Map::new();

        map.insert(
            "$quill".to_string(),
            serde_json::Value::String(self.quill_reference().to_string()),
        );

        // The seam carries the body as canonical Content-JSON (Option A): a
        // nested content object, byte-identical to `to_canonical_json`, never a lossy
        // markdown string. Backends lower the content (typst → markup + source
        // map; pdfform → `.text`); the markdown projection is `body_markdown`.
        map.insert(
            "$body".to_string(),
            quillmark_content::serial::to_canonical_value(self.main.body()),
        );

        let cards_array: Vec<serde_json::Value> = self
            .cards
            .iter()
            .map(|card| {
                let mut card_map = serde_json::Map::new();
                card_map.insert(
                    "$kind".to_string(),
                    serde_json::Value::String(card.kind().unwrap_or("").to_string()),
                );
                card_map.insert(
                    "$body".to_string(),
                    quillmark_content::serial::to_canonical_value(card.body()),
                );
                for (key, value) in card.payload.iter() {
                    card_map.insert(key.clone(), value.as_json().clone());
                }
                serde_json::Value::Object(card_map)
            })
            .collect();

        map.insert("$cards".to_string(), serde_json::Value::Array(cards_array));

        for (key, value) in self.main.payload.iter() {
            map.insert(key.clone(), value.as_json().clone());
        }

        serde_json::Value::Object(map)
    }
}