Skip to main content

omni_dev/atlassian/
adf.rs

1//! Atlassian Document Format (ADF) type definitions.
2//!
3//! Provides serde-compatible structs for the ADF JSON structure used by
4//! JIRA Cloud REST API v3 and Confluence Cloud REST API v2.
5
6use serde::{Deserialize, Serialize};
7
8/// The root ADF document node.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct AdfDocument {
11    /// ADF version (always 1).
12    pub version: u32,
13
14    /// Node type (always "doc").
15    #[serde(rename = "type")]
16    pub doc_type: String,
17
18    /// Top-level block content nodes.
19    pub content: Vec<AdfNode>,
20}
21
22impl AdfDocument {
23    /// Creates a new empty ADF document.
24    #[must_use]
25    pub fn new() -> Self {
26        Self {
27            version: 1,
28            doc_type: "doc".to_string(),
29            content: Vec::new(),
30        }
31    }
32
33    /// Parses ADF JSON, treating a top-level `null` as an empty document.
34    ///
35    /// The Jira REST API returns `description: null` for issues with no
36    /// description. This helper accepts that as equivalent to the canonical
37    /// empty ADF document so that `jira read --format adf` output can be piped
38    /// into `convert from-adf` without a pre-filter.
39    pub fn from_json_str(input: &str) -> anyhow::Result<Self> {
40        use anyhow::Context;
41
42        let value: serde_json::Value =
43            serde_json::from_str(input).context("Failed to parse ADF JSON input")?;
44        if value.is_null() {
45            return Ok(Self::default());
46        }
47        serde_json::from_value(value).context("Failed to parse ADF JSON input")
48    }
49}
50
51impl Default for AdfDocument {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57/// A node in the ADF tree.
58///
59/// Represents both block nodes (paragraph, heading, codeBlock, etc.)
60/// and inline nodes (text, hardBreak, mention, etc.).
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct AdfNode {
63    /// The node type identifier (e.g., "paragraph", "text", "heading").
64    #[serde(rename = "type")]
65    pub node_type: String,
66
67    /// Node-specific attributes (e.g., heading level, code language).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub attrs: Option<serde_json::Value>,
70
71    /// Child content nodes.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub content: Option<Vec<Self>>,
74
75    /// Text content (only present on text nodes).
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub text: Option<String>,
78
79    /// Inline marks applied to this node (bold, italic, link, etc.).
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub marks: Option<Vec<AdfMark>>,
82
83    /// Top-level local ID (used by expand, nestedExpand, and other node types).
84    #[serde(rename = "localId", skip_serializing_if = "Option::is_none")]
85    pub local_id: Option<String>,
86
87    /// Top-level parameters (used by expand nodes with macroMetadata, etc.).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub parameters: Option<serde_json::Value>,
90}
91
92impl AdfNode {
93    /// Creates a text node with the given content.
94    #[must_use]
95    pub fn text(content: &str) -> Self {
96        Self {
97            node_type: "text".to_string(),
98            attrs: None,
99            content: None,
100            text: Some(content.to_string()),
101            marks: None,
102            local_id: None,
103            parameters: None,
104        }
105    }
106
107    /// Creates a text node with marks applied.
108    #[must_use]
109    pub fn text_with_marks(content: &str, marks: Vec<AdfMark>) -> Self {
110        Self {
111            node_type: "text".to_string(),
112            attrs: None,
113            content: None,
114            text: Some(content.to_string()),
115            marks: if marks.is_empty() { None } else { Some(marks) },
116            local_id: None,
117            parameters: None,
118        }
119    }
120
121    /// Creates a paragraph node with the given inline content.
122    #[must_use]
123    pub fn paragraph(content: Vec<Self>) -> Self {
124        Self {
125            node_type: "paragraph".to_string(),
126            attrs: None,
127            content: if content.is_empty() {
128                None
129            } else {
130                Some(content)
131            },
132            text: None,
133            marks: None,
134            local_id: None,
135            parameters: None,
136        }
137    }
138
139    /// Creates a heading node.
140    #[must_use]
141    pub fn heading(level: u8, content: Vec<Self>) -> Self {
142        Self {
143            node_type: "heading".to_string(),
144            attrs: Some(serde_json::json!({"level": level})),
145            content: if content.is_empty() {
146                None
147            } else {
148                Some(content)
149            },
150            text: None,
151            marks: None,
152            local_id: None,
153            parameters: None,
154        }
155    }
156
157    /// Creates a code block node.
158    #[must_use]
159    pub fn code_block(language: Option<&str>, text: &str) -> Self {
160        Self {
161            node_type: "codeBlock".to_string(),
162            attrs: language.map(|lang| serde_json::json!({"language": lang})),
163            content: Some(vec![Self::text(text)]),
164            text: None,
165            marks: None,
166            local_id: None,
167            parameters: None,
168        }
169    }
170
171    /// Creates a blockquote node.
172    #[must_use]
173    pub fn blockquote(content: Vec<Self>) -> Self {
174        Self {
175            node_type: "blockquote".to_string(),
176            attrs: None,
177            content: Some(content),
178            text: None,
179            marks: None,
180            local_id: None,
181            parameters: None,
182        }
183    }
184
185    /// Creates a horizontal rule node.
186    #[must_use]
187    pub fn rule() -> Self {
188        Self {
189            node_type: "rule".to_string(),
190            attrs: None,
191            content: None,
192            text: None,
193            marks: None,
194            local_id: None,
195            parameters: None,
196        }
197    }
198
199    /// Creates a bullet list node.
200    #[must_use]
201    pub fn bullet_list(items: Vec<Self>) -> Self {
202        Self {
203            node_type: "bulletList".to_string(),
204            attrs: None,
205            content: Some(items),
206            text: None,
207            marks: None,
208            local_id: None,
209            parameters: None,
210        }
211    }
212
213    /// Creates an ordered list node.
214    #[must_use]
215    pub fn ordered_list(items: Vec<Self>, start: Option<u32>) -> Self {
216        Self {
217            node_type: "orderedList".to_string(),
218            attrs: start.map(|s| serde_json::json!({"order": s})),
219            content: Some(items),
220            text: None,
221            marks: None,
222            local_id: None,
223            parameters: None,
224        }
225    }
226
227    /// Creates a list item node.
228    #[must_use]
229    pub fn list_item(content: Vec<Self>) -> Self {
230        Self {
231            node_type: "listItem".to_string(),
232            attrs: None,
233            content: Some(content),
234            text: None,
235            marks: None,
236            local_id: None,
237            parameters: None,
238        }
239    }
240
241    /// Creates a hard break node.
242    #[must_use]
243    pub fn hard_break() -> Self {
244        Self {
245            node_type: "hardBreak".to_string(),
246            attrs: None,
247            content: None,
248            text: None,
249            marks: None,
250            local_id: None,
251            parameters: None,
252        }
253    }
254
255    /// Creates a table node.
256    #[must_use]
257    pub fn table(rows: Vec<Self>) -> Self {
258        Self {
259            node_type: "table".to_string(),
260            attrs: None,
261            content: Some(rows),
262            text: None,
263            marks: None,
264            local_id: None,
265            parameters: None,
266        }
267    }
268
269    /// Creates a table node with attributes (layout, `isNumberColumnEnabled`).
270    #[must_use]
271    pub fn table_with_attrs(rows: Vec<Self>, attrs: serde_json::Value) -> Self {
272        Self {
273            node_type: "table".to_string(),
274            attrs: Some(attrs),
275            content: Some(rows),
276            text: None,
277            marks: None,
278            local_id: None,
279            parameters: None,
280        }
281    }
282
283    /// Creates a table row node.
284    #[must_use]
285    pub fn table_row(cells: Vec<Self>) -> Self {
286        Self {
287            node_type: "tableRow".to_string(),
288            attrs: None,
289            content: Some(cells),
290            text: None,
291            marks: None,
292            local_id: None,
293            parameters: None,
294        }
295    }
296
297    /// Creates a table header cell node.
298    #[must_use]
299    pub fn table_header(content: Vec<Self>) -> Self {
300        Self {
301            node_type: "tableHeader".to_string(),
302            attrs: None,
303            content: Some(content),
304            text: None,
305            marks: None,
306            local_id: None,
307            parameters: None,
308        }
309    }
310
311    /// Creates a table header cell node with attributes (colspan, rowspan, background, colwidth).
312    #[must_use]
313    pub fn table_header_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
314        Self {
315            node_type: "tableHeader".to_string(),
316            attrs: Some(attrs),
317            content: Some(content),
318            text: None,
319            marks: None,
320            local_id: None,
321            parameters: None,
322        }
323    }
324
325    /// Creates a table cell node.
326    #[must_use]
327    pub fn table_cell(content: Vec<Self>) -> Self {
328        Self {
329            node_type: "tableCell".to_string(),
330            attrs: None,
331            content: Some(content),
332            text: None,
333            marks: None,
334            local_id: None,
335            parameters: None,
336        }
337    }
338
339    /// Creates a table cell node with attributes (colspan, rowspan, background, colwidth).
340    #[must_use]
341    pub fn table_cell_with_attrs(content: Vec<Self>, attrs: serde_json::Value) -> Self {
342        Self {
343            node_type: "tableCell".to_string(),
344            attrs: Some(attrs),
345            content: Some(content),
346            text: None,
347            marks: None,
348            local_id: None,
349            parameters: None,
350        }
351    }
352
353    /// Creates a table header cell node with attributes and marks.
354    #[must_use]
355    pub fn table_header_with_attrs_and_marks(
356        content: Vec<Self>,
357        attrs: Option<serde_json::Value>,
358        marks: Vec<AdfMark>,
359    ) -> Self {
360        Self {
361            node_type: "tableHeader".to_string(),
362            attrs,
363            content: Some(content),
364            text: None,
365            marks: if marks.is_empty() { None } else { Some(marks) },
366            local_id: None,
367            parameters: None,
368        }
369    }
370
371    /// Creates a table cell node with attributes and marks.
372    #[must_use]
373    pub fn table_cell_with_attrs_and_marks(
374        content: Vec<Self>,
375        attrs: Option<serde_json::Value>,
376        marks: Vec<AdfMark>,
377    ) -> Self {
378        Self {
379            node_type: "tableCell".to_string(),
380            attrs,
381            content: Some(content),
382            text: None,
383            marks: if marks.is_empty() { None } else { Some(marks) },
384            local_id: None,
385            parameters: None,
386        }
387    }
388
389    /// Creates a caption node (used inside tables).
390    #[must_use]
391    pub fn caption(content: Vec<Self>) -> Self {
392        Self {
393            node_type: "caption".to_string(),
394            attrs: None,
395            content: Some(content),
396            text: None,
397            marks: None,
398            local_id: None,
399            parameters: None,
400        }
401    }
402
403    /// Creates an inline card node for a smart link (URL as both text and href).
404    #[must_use]
405    pub fn inline_card(url: &str) -> Self {
406        Self {
407            node_type: "inlineCard".to_string(),
408            attrs: Some(serde_json::json!({"url": url})),
409            content: None,
410            text: None,
411            marks: None,
412            local_id: None,
413            parameters: None,
414        }
415    }
416
417    /// Creates a `mediaInline` node with the given attributes.
418    #[must_use]
419    pub fn media_inline(attrs: serde_json::Value) -> Self {
420        Self {
421            node_type: "mediaInline".to_string(),
422            attrs: Some(attrs),
423            content: None,
424            text: None,
425            marks: None,
426            local_id: None,
427            parameters: None,
428        }
429    }
430
431    /// Creates a media single node wrapping an external image.
432    #[must_use]
433    pub fn media_single(url: &str, alt: Option<&str>) -> Self {
434        let mut media_attrs = serde_json::json!({
435            "type": "external",
436            "url": url,
437        });
438        if let Some(alt_text) = alt {
439            media_attrs["alt"] = serde_json::Value::String(alt_text.to_string());
440        }
441        Self {
442            node_type: "mediaSingle".to_string(),
443            attrs: Some(serde_json::json!({"layout": "center"})),
444            content: Some(vec![Self {
445                node_type: "media".to_string(),
446                attrs: Some(media_attrs),
447                content: None,
448                text: None,
449                marks: None,
450                local_id: None,
451                parameters: None,
452            }]),
453            text: None,
454            marks: None,
455            local_id: None,
456            parameters: None,
457        }
458    }
459
460    // ── Task lists ─────────────────────────────────────────────────
461
462    /// Creates a task list node.
463    #[must_use]
464    pub fn task_list(items: Vec<Self>) -> Self {
465        Self {
466            node_type: "taskList".to_string(),
467            attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
468            content: Some(items),
469            text: None,
470            marks: None,
471            local_id: None,
472            parameters: None,
473        }
474    }
475
476    /// Creates a task item node with state `"TODO"` or `"DONE"`.
477    #[must_use]
478    pub fn task_item(state: &str, content: Vec<Self>) -> Self {
479        Self {
480            node_type: "taskItem".to_string(),
481            attrs: Some(serde_json::json!({
482                "localId": uuid_placeholder(),
483                "state": state,
484            })),
485            content: if content.is_empty() {
486                None
487            } else {
488                Some(content)
489            },
490            text: None,
491            marks: None,
492            local_id: None,
493            parameters: None,
494        }
495    }
496
497    // ── Inline nodes ───────────────────────────────────────────────
498
499    /// Creates an emoji node.
500    #[must_use]
501    pub fn emoji(short_name: &str) -> Self {
502        Self {
503            node_type: "emoji".to_string(),
504            attrs: Some(serde_json::json!({"shortName": short_name})),
505            content: None,
506            text: None,
507            marks: None,
508            local_id: None,
509            parameters: None,
510        }
511    }
512
513    /// Creates a status badge node.
514    #[must_use]
515    pub fn status(text: &str, color: &str) -> Self {
516        Self {
517            node_type: "status".to_string(),
518            attrs: Some(serde_json::json!({
519                "text": text,
520                "color": color,
521                "localId": uuid_placeholder(),
522            })),
523            content: None,
524            text: None,
525            marks: None,
526            local_id: None,
527            parameters: None,
528        }
529    }
530
531    /// Creates a date node from an ISO 8601 date string.
532    #[must_use]
533    pub fn date(timestamp: &str) -> Self {
534        Self {
535            node_type: "date".to_string(),
536            attrs: Some(serde_json::json!({"timestamp": timestamp})),
537            content: None,
538            text: None,
539            marks: None,
540            local_id: None,
541            parameters: None,
542        }
543    }
544
545    /// Creates a placeholder node.
546    #[must_use]
547    pub fn placeholder(text: &str) -> Self {
548        Self {
549            node_type: "placeholder".to_string(),
550            attrs: Some(serde_json::json!({"text": text})),
551            content: None,
552            text: None,
553            marks: None,
554            local_id: None,
555            parameters: None,
556        }
557    }
558
559    /// Creates a mention node.
560    #[must_use]
561    pub fn mention(id: &str, display_text: &str) -> Self {
562        Self {
563            node_type: "mention".to_string(),
564            attrs: Some(serde_json::json!({
565                "id": id,
566                "text": display_text,
567            })),
568            content: None,
569            text: None,
570            marks: None,
571            local_id: None,
572            parameters: None,
573        }
574    }
575
576    // ── Block cards and embeds ─────────────────────────────────────
577
578    /// Creates a block card node (smart link displayed as a block).
579    #[must_use]
580    pub fn block_card(url: &str) -> Self {
581        Self {
582            node_type: "blockCard".to_string(),
583            attrs: Some(serde_json::json!({"url": url})),
584            content: None,
585            text: None,
586            marks: None,
587            local_id: None,
588            parameters: None,
589        }
590    }
591
592    /// Creates an embed card node.
593    #[must_use]
594    pub fn embed_card(
595        url: &str,
596        layout: Option<&str>,
597        original_height: Option<f64>,
598        width: Option<f64>,
599    ) -> Self {
600        let mut attrs = serde_json::json!({"url": url});
601        if let Some(l) = layout {
602            attrs["layout"] = serde_json::Value::String(l.to_string());
603        }
604        if let Some(h) = original_height {
605            attrs["originalHeight"] = serde_json::json!(h);
606        }
607        if let Some(w) = width {
608            attrs["width"] = serde_json::json!(w);
609        }
610        Self {
611            node_type: "embedCard".to_string(),
612            attrs: Some(attrs),
613            content: None,
614            text: None,
615            marks: None,
616            local_id: None,
617            parameters: None,
618        }
619    }
620
621    // ── Panels and expand ──────────────────────────────────────────
622
623    /// Creates a panel node.
624    #[must_use]
625    pub fn panel(panel_type: &str, content: Vec<Self>) -> Self {
626        Self {
627            node_type: "panel".to_string(),
628            attrs: Some(serde_json::json!({"panelType": panel_type})),
629            content: Some(content),
630            text: None,
631            marks: None,
632            local_id: None,
633            parameters: None,
634        }
635    }
636
637    /// Creates an expand (collapsible) node.
638    #[must_use]
639    pub fn expand(title: Option<&str>, content: Vec<Self>) -> Self {
640        let attrs = title.map(|t| serde_json::json!({"title": t}));
641        Self {
642            node_type: "expand".to_string(),
643            attrs,
644            content: Some(content),
645            text: None,
646            marks: None,
647            local_id: None,
648            parameters: None,
649        }
650    }
651
652    /// Creates a nested expand node.
653    #[must_use]
654    pub fn nested_expand(title: Option<&str>, content: Vec<Self>) -> Self {
655        let attrs = title.map(|t| serde_json::json!({"title": t}));
656        Self {
657            node_type: "nestedExpand".to_string(),
658            attrs,
659            content: Some(content),
660            text: None,
661            marks: None,
662            local_id: None,
663            parameters: None,
664        }
665    }
666
667    // ── Layout ─────────────────────────────────────────────────────
668
669    /// Creates a layout section node.
670    #[must_use]
671    pub fn layout_section(columns: Vec<Self>) -> Self {
672        Self {
673            node_type: "layoutSection".to_string(),
674            attrs: None,
675            content: Some(columns),
676            text: None,
677            marks: None,
678            local_id: None,
679            parameters: None,
680        }
681    }
682
683    /// Creates a layout column node.
684    ///
685    /// `width` accepts any value convertible into a JSON number (integer or
686    /// float). The original numeric type is preserved on the `width` attribute
687    /// so that round-tripping doesn't coerce integer widths to floats.
688    #[must_use]
689    pub fn layout_column<V: Into<serde_json::Value>>(width: V, content: Vec<Self>) -> Self {
690        Self {
691            node_type: "layoutColumn".to_string(),
692            attrs: Some(serde_json::json!({"width": width.into()})),
693            content: Some(content),
694            text: None,
695            marks: None,
696            local_id: None,
697            parameters: None,
698        }
699    }
700
701    // ── Decision lists ─────────────────────────────────────────────
702
703    /// Creates a decision list node.
704    #[must_use]
705    pub fn decision_list(items: Vec<Self>) -> Self {
706        Self {
707            node_type: "decisionList".to_string(),
708            attrs: Some(serde_json::json!({"localId": uuid_placeholder()})),
709            content: Some(items),
710            text: None,
711            marks: None,
712            local_id: None,
713            parameters: None,
714        }
715    }
716
717    /// Creates a decision item node.
718    #[must_use]
719    pub fn decision_item(state: &str, content: Vec<Self>) -> Self {
720        Self {
721            node_type: "decisionItem".to_string(),
722            attrs: Some(serde_json::json!({
723                "localId": uuid_placeholder(),
724                "state": state,
725            })),
726            content: Some(content),
727            text: None,
728            marks: None,
729            local_id: None,
730            parameters: None,
731        }
732    }
733
734    // ── Extensions ─────────────────────────────────────────────────
735
736    /// Creates a void (block) extension node.
737    #[must_use]
738    pub fn extension(
739        extension_type: &str,
740        extension_key: &str,
741        params: Option<serde_json::Value>,
742    ) -> Self {
743        let mut attrs = serde_json::json!({
744            "extensionType": extension_type,
745            "extensionKey": extension_key,
746        });
747        if let Some(p) = params {
748            attrs["parameters"] = p;
749        }
750        Self {
751            node_type: "extension".to_string(),
752            attrs: Some(attrs),
753            content: None,
754            text: None,
755            marks: None,
756            local_id: None,
757            parameters: None,
758        }
759    }
760
761    /// Creates a bodied extension node (extension with block content).
762    #[must_use]
763    pub fn bodied_extension(extension_type: &str, extension_key: &str, content: Vec<Self>) -> Self {
764        Self {
765            node_type: "bodiedExtension".to_string(),
766            attrs: Some(serde_json::json!({
767                "extensionType": extension_type,
768                "extensionKey": extension_key,
769            })),
770            content: Some(content),
771            text: None,
772            marks: None,
773            local_id: None,
774            parameters: None,
775        }
776    }
777
778    /// Creates an inline extension node.
779    #[must_use]
780    pub fn inline_extension(
781        extension_type: &str,
782        extension_key: &str,
783        fallback_text: Option<&str>,
784    ) -> Self {
785        Self {
786            node_type: "inlineExtension".to_string(),
787            attrs: Some(serde_json::json!({
788                "extensionType": extension_type,
789                "extensionKey": extension_key,
790            })),
791            content: None,
792            text: fallback_text.map(String::from),
793            marks: None,
794            local_id: None,
795            parameters: None,
796        }
797    }
798}
799
800/// Returns the default placeholder for nodes that require a `localId`.
801/// Empty string is used because Confluence itself emits `localId: ""`
802/// for auto-generated nodes; both `""` and the nil UUID
803/// `"00000000-0000-0000-0000-000000000000"` are treated as
804/// non-significant by the rendering layer.
805fn uuid_placeholder() -> String {
806    String::new()
807}
808
809/// An inline mark applied to a text node.
810#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
811pub struct AdfMark {
812    /// The mark type (e.g., "strong", "em", "code", "link", "strike").
813    #[serde(rename = "type")]
814    pub mark_type: String,
815
816    /// Mark-specific attributes (e.g., href for links).
817    #[serde(skip_serializing_if = "Option::is_none")]
818    pub attrs: Option<serde_json::Value>,
819}
820
821impl AdfMark {
822    /// Creates a strong (bold) mark.
823    #[must_use]
824    pub fn strong() -> Self {
825        Self {
826            mark_type: "strong".to_string(),
827            attrs: None,
828        }
829    }
830
831    /// Creates an emphasis (italic) mark.
832    #[must_use]
833    pub fn em() -> Self {
834        Self {
835            mark_type: "em".to_string(),
836            attrs: None,
837        }
838    }
839
840    /// Creates an inline code mark.
841    #[must_use]
842    pub fn code() -> Self {
843        Self {
844            mark_type: "code".to_string(),
845            attrs: None,
846        }
847    }
848
849    /// Creates a strikethrough mark.
850    #[must_use]
851    pub fn strike() -> Self {
852        Self {
853            mark_type: "strike".to_string(),
854            attrs: None,
855        }
856    }
857
858    /// Creates a link mark with the given URL.
859    #[must_use]
860    pub fn link(href: &str) -> Self {
861        Self {
862            mark_type: "link".to_string(),
863            attrs: Some(serde_json::json!({"href": href})),
864        }
865    }
866
867    /// Creates an underline mark.
868    #[must_use]
869    pub fn underline() -> Self {
870        Self {
871            mark_type: "underline".to_string(),
872            attrs: None,
873        }
874    }
875
876    /// Creates an annotation mark (inline comment highlight).
877    #[must_use]
878    pub fn annotation(id: &str, annotation_type: &str) -> Self {
879        Self {
880            mark_type: "annotation".to_string(),
881            attrs: Some(serde_json::json!({"id": id, "annotationType": annotation_type})),
882        }
883    }
884
885    /// Creates a text color mark.
886    #[must_use]
887    pub fn text_color(color: &str) -> Self {
888        Self {
889            mark_type: "textColor".to_string(),
890            attrs: Some(serde_json::json!({"color": color})),
891        }
892    }
893
894    /// Creates a background color mark.
895    #[must_use]
896    pub fn background_color(color: &str) -> Self {
897        Self {
898            mark_type: "backgroundColor".to_string(),
899            attrs: Some(serde_json::json!({"color": color})),
900        }
901    }
902
903    /// Creates a subscript or superscript mark.
904    #[must_use]
905    pub fn subsup(kind: &str) -> Self {
906        Self {
907            mark_type: "subsup".to_string(),
908            attrs: Some(serde_json::json!({"type": kind})),
909        }
910    }
911
912    /// Creates an alignment mark for block nodes.
913    #[must_use]
914    pub fn alignment(align: &str) -> Self {
915        Self {
916            mark_type: "alignment".to_string(),
917            attrs: Some(serde_json::json!({"align": align})),
918        }
919    }
920
921    /// Creates an indentation mark for block nodes.
922    #[must_use]
923    pub fn indentation(level: u32) -> Self {
924        Self {
925            mark_type: "indentation".to_string(),
926            attrs: Some(serde_json::json!({"level": level})),
927        }
928    }
929
930    /// Creates a breakout mark for block nodes.
931    #[must_use]
932    pub fn breakout(mode: &str, width: Option<u32>) -> Self {
933        let mut attrs = serde_json::json!({"mode": mode});
934        if let Some(w) = width {
935            attrs["width"] = serde_json::json!(w);
936        }
937        Self {
938            mark_type: "breakout".to_string(),
939            attrs: Some(attrs),
940        }
941    }
942
943    /// Creates a border mark for table cells/headers.
944    #[must_use]
945    pub fn border(color: &str, size: u32) -> Self {
946        Self {
947            mark_type: "border".to_string(),
948            attrs: Some(serde_json::json!({"color": color, "size": size})),
949        }
950    }
951}
952
953#[cfg(test)]
954#[allow(clippy::unwrap_used, clippy::expect_used)]
955mod tests {
956    use super::*;
957
958    #[test]
959    fn empty_document_serialization() {
960        let doc = AdfDocument::new();
961        let json = serde_json::to_string(&doc).unwrap();
962        assert!(json.contains(r#""version":1"#));
963        assert!(json.contains(r#""type":"doc""#));
964    }
965
966    #[test]
967    fn document_with_paragraph() {
968        let doc = AdfDocument {
969            version: 1,
970            doc_type: "doc".to_string(),
971            content: vec![AdfNode::paragraph(vec![AdfNode::text("Hello world")])],
972        };
973        let json = serde_json::to_value(&doc).unwrap();
974        let content = json["content"][0].clone();
975        assert_eq!(content["type"], "paragraph");
976        assert_eq!(content["content"][0]["text"], "Hello world");
977    }
978
979    #[test]
980    fn text_with_marks() {
981        let node = AdfNode::text_with_marks("bold text", vec![AdfMark::strong()]);
982        let json = serde_json::to_value(&node).unwrap();
983        assert_eq!(json["marks"][0]["type"], "strong");
984    }
985
986    #[test]
987    fn heading_with_level() {
988        let node = AdfNode::heading(2, vec![AdfNode::text("Title")]);
989        let json = serde_json::to_value(&node).unwrap();
990        assert_eq!(json["attrs"]["level"], 2);
991        assert_eq!(json["content"][0]["text"], "Title");
992    }
993
994    #[test]
995    fn code_block_with_language() {
996        let node = AdfNode::code_block(Some("rust"), "fn main() {}");
997        let json = serde_json::to_value(&node).unwrap();
998        assert_eq!(json["attrs"]["language"], "rust");
999        assert_eq!(json["content"][0]["text"], "fn main() {}");
1000    }
1001
1002    #[test]
1003    fn link_mark_attributes() {
1004        let mark = AdfMark::link("https://example.com");
1005        let json = serde_json::to_value(&mark).unwrap();
1006        assert_eq!(json["attrs"]["href"], "https://example.com");
1007    }
1008
1009    #[test]
1010    fn real_jira_adf_deserialization() {
1011        let adf_json = r#"{
1012            "version": 1,
1013            "type": "doc",
1014            "content": [
1015                {
1016                    "type": "paragraph",
1017                    "content": [
1018                        {"type": "text", "text": "Hello "},
1019                        {"type": "text", "text": "world", "marks": [{"type": "strong"}]}
1020                    ]
1021                },
1022                {
1023                    "type": "heading",
1024                    "attrs": {"level": 2},
1025                    "content": [
1026                        {"type": "text", "text": "Section"}
1027                    ]
1028                }
1029            ]
1030        }"#;
1031
1032        let doc: AdfDocument = serde_json::from_str(adf_json).unwrap();
1033        assert_eq!(doc.version, 1);
1034        assert_eq!(doc.content.len(), 2);
1035        assert_eq!(doc.content[0].node_type, "paragraph");
1036        assert_eq!(doc.content[1].node_type, "heading");
1037    }
1038
1039    #[test]
1040    fn round_trip_serialization() {
1041        let doc = AdfDocument {
1042            version: 1,
1043            doc_type: "doc".to_string(),
1044            content: vec![
1045                AdfNode::heading(1, vec![AdfNode::text("Title")]),
1046                AdfNode::paragraph(vec![
1047                    AdfNode::text("Normal "),
1048                    AdfNode::text_with_marks("bold", vec![AdfMark::strong()]),
1049                    AdfNode::text(" text"),
1050                ]),
1051                AdfNode::code_block(Some("rust"), "let x = 1;"),
1052                AdfNode::rule(),
1053            ],
1054        };
1055
1056        let json = serde_json::to_string(&doc).unwrap();
1057        let restored: AdfDocument = serde_json::from_str(&json).unwrap();
1058        assert_eq!(doc, restored);
1059    }
1060
1061    #[test]
1062    fn skip_none_fields_in_serialization() {
1063        let node = AdfNode::text("hello");
1064        let json = serde_json::to_value(&node).unwrap();
1065        assert!(json.get("attrs").is_none());
1066        assert!(json.get("content").is_none());
1067        assert!(json.get("marks").is_none());
1068    }
1069
1070    #[test]
1071    fn default_document() {
1072        let doc = AdfDocument::default();
1073        assert_eq!(doc.version, 1);
1074        assert_eq!(doc.doc_type, "doc");
1075        assert!(doc.content.is_empty());
1076    }
1077
1078    #[test]
1079    fn empty_paragraph_no_content() {
1080        let node = AdfNode::paragraph(vec![]);
1081        assert!(node.content.is_none());
1082    }
1083
1084    #[test]
1085    fn empty_heading_no_content() {
1086        let node = AdfNode::heading(1, vec![]);
1087        assert!(node.content.is_none());
1088    }
1089
1090    #[test]
1091    fn text_with_empty_marks_is_none() {
1092        let node = AdfNode::text_with_marks("test", vec![]);
1093        assert!(node.marks.is_none());
1094    }
1095
1096    #[test]
1097    fn code_block_no_language() {
1098        let node = AdfNode::code_block(None, "code");
1099        assert!(node.attrs.is_none());
1100        assert_eq!(
1101            node.content.as_ref().unwrap()[0].text.as_deref(),
1102            Some("code")
1103        );
1104    }
1105
1106    #[test]
1107    fn ordered_list_with_start() {
1108        let node = AdfNode::ordered_list(vec![], Some(5));
1109        let attrs = node.attrs.as_ref().unwrap();
1110        assert_eq!(attrs["order"], 5);
1111    }
1112
1113    #[test]
1114    fn ordered_list_no_start() {
1115        let node = AdfNode::ordered_list(vec![], None);
1116        assert!(node.attrs.is_none());
1117    }
1118
1119    #[test]
1120    fn media_single_with_alt() {
1121        let node = AdfNode::media_single("https://img.url", Some("Alt text"));
1122        let media = &node.content.as_ref().unwrap()[0];
1123        let attrs = media.attrs.as_ref().unwrap();
1124        assert_eq!(attrs["url"], "https://img.url");
1125        assert_eq!(attrs["alt"], "Alt text");
1126    }
1127
1128    #[test]
1129    fn media_single_no_alt() {
1130        let node = AdfNode::media_single("https://img.url", None);
1131        let media = &node.content.as_ref().unwrap()[0];
1132        let attrs = media.attrs.as_ref().unwrap();
1133        assert_eq!(attrs["url"], "https://img.url");
1134        assert!(attrs.get("alt").is_none());
1135    }
1136
1137    #[test]
1138    fn mark_constructors() {
1139        assert_eq!(AdfMark::em().mark_type, "em");
1140        assert_eq!(AdfMark::code().mark_type, "code");
1141        assert_eq!(AdfMark::strike().mark_type, "strike");
1142    }
1143
1144    #[test]
1145    fn table_structure() {
1146        let table = AdfNode::table(vec![AdfNode::table_row(vec![
1147            AdfNode::table_header(vec![AdfNode::paragraph(vec![AdfNode::text("H")])]),
1148            AdfNode::table_cell(vec![AdfNode::paragraph(vec![AdfNode::text("C")])]),
1149        ])]);
1150        assert_eq!(table.node_type, "table");
1151        let row = &table.content.as_ref().unwrap()[0];
1152        assert_eq!(row.node_type, "tableRow");
1153        let cells = row.content.as_ref().unwrap();
1154        assert_eq!(cells[0].node_type, "tableHeader");
1155        assert_eq!(cells[1].node_type, "tableCell");
1156    }
1157
1158    #[test]
1159    fn blockquote_structure() {
1160        let bq = AdfNode::blockquote(vec![AdfNode::paragraph(vec![AdfNode::text("quoted")])]);
1161        assert_eq!(bq.node_type, "blockquote");
1162        assert_eq!(bq.content.as_ref().unwrap()[0].node_type, "paragraph");
1163    }
1164
1165    #[test]
1166    fn hard_break_structure() {
1167        let br = AdfNode::hard_break();
1168        assert_eq!(br.node_type, "hardBreak");
1169        assert!(br.content.is_none());
1170        assert!(br.text.is_none());
1171    }
1172
1173    #[test]
1174    fn rule_structure() {
1175        let rule = AdfNode::rule();
1176        assert_eq!(rule.node_type, "rule");
1177        assert!(rule.content.is_none());
1178    }
1179
1180    // ── Additional node constructors ────────────────────────────────
1181
1182    #[test]
1183    fn bullet_list_structure() {
1184        let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("item")])]);
1185        let list = AdfNode::bullet_list(vec![item]);
1186        assert_eq!(list.node_type, "bulletList");
1187        assert_eq!(list.content.as_ref().unwrap().len(), 1);
1188    }
1189
1190    #[test]
1191    fn list_item_structure() {
1192        let item = AdfNode::list_item(vec![AdfNode::paragraph(vec![AdfNode::text("text")])]);
1193        assert_eq!(item.node_type, "listItem");
1194    }
1195
1196    #[test]
1197    fn inline_card_structure() {
1198        let card = AdfNode::inline_card("https://example.com");
1199        assert_eq!(card.node_type, "inlineCard");
1200        assert_eq!(card.attrs.as_ref().unwrap()["url"], "https://example.com");
1201    }
1202
1203    #[test]
1204    fn task_list_structure() {
1205        let item = AdfNode::task_item("TODO", vec![AdfNode::text("do this")]);
1206        let list = AdfNode::task_list(vec![item]);
1207        assert_eq!(list.node_type, "taskList");
1208        assert!(list.attrs.as_ref().unwrap()["localId"].is_string());
1209    }
1210
1211    #[test]
1212    fn task_item_states() {
1213        let todo = AdfNode::task_item("TODO", vec![]);
1214        assert_eq!(todo.attrs.as_ref().unwrap()["state"], "TODO");
1215
1216        let done = AdfNode::task_item("DONE", vec![]);
1217        assert_eq!(done.attrs.as_ref().unwrap()["state"], "DONE");
1218    }
1219
1220    #[test]
1221    fn emoji_node() {
1222        let node = AdfNode::emoji(":thumbsup:");
1223        assert_eq!(node.node_type, "emoji");
1224        assert_eq!(node.attrs.as_ref().unwrap()["shortName"], ":thumbsup:");
1225    }
1226
1227    #[test]
1228    fn status_node() {
1229        let node = AdfNode::status("In Progress", "blue");
1230        assert_eq!(node.node_type, "status");
1231        assert_eq!(node.attrs.as_ref().unwrap()["text"], "In Progress");
1232        assert_eq!(node.attrs.as_ref().unwrap()["color"], "blue");
1233    }
1234
1235    #[test]
1236    fn date_node() {
1237        let node = AdfNode::date("1680307200000");
1238        assert_eq!(node.node_type, "date");
1239        assert_eq!(node.attrs.as_ref().unwrap()["timestamp"], "1680307200000");
1240    }
1241
1242    #[test]
1243    fn mention_node() {
1244        let node = AdfNode::mention("user-123", "Alice");
1245        assert_eq!(node.node_type, "mention");
1246        assert_eq!(node.attrs.as_ref().unwrap()["id"], "user-123");
1247        assert_eq!(node.attrs.as_ref().unwrap()["text"], "Alice");
1248    }
1249
1250    #[test]
1251    fn block_card_structure() {
1252        let card = AdfNode::block_card("https://example.com/page");
1253        assert_eq!(card.node_type, "blockCard");
1254        assert_eq!(
1255            card.attrs.as_ref().unwrap()["url"],
1256            "https://example.com/page"
1257        );
1258    }
1259
1260    #[test]
1261    fn embed_card_with_all_options() {
1262        let card = AdfNode::embed_card(
1263            "https://example.com",
1264            Some("wide"),
1265            Some(732.0),
1266            Some(100.0),
1267        );
1268        let attrs = card.attrs.as_ref().unwrap();
1269        assert_eq!(attrs["url"], "https://example.com");
1270        assert_eq!(attrs["layout"], "wide");
1271        assert_eq!(attrs["originalHeight"], 732.0);
1272        assert_eq!(attrs["width"], 100.0);
1273    }
1274
1275    #[test]
1276    fn embed_card_minimal() {
1277        let card = AdfNode::embed_card("https://example.com", None, None, None);
1278        let attrs = card.attrs.as_ref().unwrap();
1279        assert_eq!(attrs["url"], "https://example.com");
1280        assert!(attrs.get("layout").is_none());
1281        assert!(attrs.get("originalHeight").is_none());
1282        assert!(attrs.get("width").is_none());
1283    }
1284
1285    #[test]
1286    fn panel_structure() {
1287        let panel = AdfNode::panel(
1288            "info",
1289            vec![AdfNode::paragraph(vec![AdfNode::text("note")])],
1290        );
1291        assert_eq!(panel.node_type, "panel");
1292        assert_eq!(panel.attrs.as_ref().unwrap()["panelType"], "info");
1293    }
1294
1295    #[test]
1296    fn expand_with_title() {
1297        let node = AdfNode::expand(Some("Details"), vec![AdfNode::paragraph(vec![])]);
1298        assert_eq!(node.node_type, "expand");
1299        assert_eq!(node.attrs.as_ref().unwrap()["title"], "Details");
1300    }
1301
1302    #[test]
1303    fn expand_without_title() {
1304        let node = AdfNode::expand(None, vec![AdfNode::paragraph(vec![])]);
1305        assert_eq!(node.node_type, "expand");
1306        assert!(node.attrs.is_none());
1307    }
1308
1309    #[test]
1310    fn nested_expand_structure() {
1311        let node = AdfNode::nested_expand(Some("Inner"), vec![]);
1312        assert_eq!(node.node_type, "nestedExpand");
1313        assert_eq!(node.attrs.as_ref().unwrap()["title"], "Inner");
1314    }
1315
1316    #[test]
1317    fn layout_section_and_column() {
1318        let col = AdfNode::layout_column(50.0, vec![AdfNode::paragraph(vec![])]);
1319        assert_eq!(col.node_type, "layoutColumn");
1320        assert_eq!(col.attrs.as_ref().unwrap()["width"], 50.0);
1321
1322        let section = AdfNode::layout_section(vec![col]);
1323        assert_eq!(section.node_type, "layoutSection");
1324    }
1325
1326    #[test]
1327    fn decision_list_and_item() {
1328        let item = AdfNode::decision_item("DECIDED", vec![AdfNode::text("yes")]);
1329        assert_eq!(item.attrs.as_ref().unwrap()["state"], "DECIDED");
1330
1331        let list = AdfNode::decision_list(vec![item]);
1332        assert_eq!(list.node_type, "decisionList");
1333    }
1334
1335    #[test]
1336    fn extension_with_params() {
1337        let node = AdfNode::extension(
1338            "com.atlassian.jira",
1339            "issue-list",
1340            Some(serde_json::json!({"jql": "project = PROJ"})),
1341        );
1342        assert_eq!(node.node_type, "extension");
1343        let attrs = node.attrs.as_ref().unwrap();
1344        assert_eq!(attrs["extensionType"], "com.atlassian.jira");
1345        assert_eq!(attrs["parameters"]["jql"], "project = PROJ");
1346    }
1347
1348    #[test]
1349    fn extension_without_params() {
1350        let node = AdfNode::extension("com.atlassian.jira", "issue-list", None);
1351        let attrs = node.attrs.as_ref().unwrap();
1352        assert!(attrs.get("parameters").is_none());
1353    }
1354
1355    #[test]
1356    fn bodied_extension_structure() {
1357        let node = AdfNode::bodied_extension(
1358            "com.atlassian.jira",
1359            "issue-list",
1360            vec![AdfNode::paragraph(vec![AdfNode::text("body")])],
1361        );
1362        assert_eq!(node.node_type, "bodiedExtension");
1363        assert!(node.content.is_some());
1364    }
1365
1366    #[test]
1367    fn inline_extension_structure() {
1368        let node = AdfNode::inline_extension("com.test", "inline-key", Some("fallback"));
1369        assert_eq!(node.node_type, "inlineExtension");
1370        assert_eq!(node.text.as_deref(), Some("fallback"));
1371    }
1372
1373    #[test]
1374    fn inline_extension_no_fallback() {
1375        let node = AdfNode::inline_extension("com.test", "inline-key", None);
1376        assert!(node.text.is_none());
1377    }
1378
1379    #[test]
1380    fn table_with_attrs_structure() {
1381        let row = AdfNode::table_row(vec![]);
1382        let table = AdfNode::table_with_attrs(
1383            vec![row],
1384            serde_json::json!({"isNumberColumnEnabled": true, "layout": "default"}),
1385        );
1386        assert_eq!(table.node_type, "table");
1387        assert_eq!(table.attrs.as_ref().unwrap()["isNumberColumnEnabled"], true);
1388    }
1389
1390    #[test]
1391    fn table_header_with_attrs_structure() {
1392        let header = AdfNode::table_header_with_attrs(
1393            vec![AdfNode::paragraph(vec![AdfNode::text("H")])],
1394            serde_json::json!({"colspan": 2, "background": "#deebff"}),
1395        );
1396        assert_eq!(header.node_type, "tableHeader");
1397        assert_eq!(header.attrs.as_ref().unwrap()["colspan"], 2);
1398    }
1399
1400    #[test]
1401    fn table_cell_with_attrs_structure() {
1402        let cell = AdfNode::table_cell_with_attrs(
1403            vec![AdfNode::paragraph(vec![AdfNode::text("C")])],
1404            serde_json::json!({"rowspan": 3}),
1405        );
1406        assert_eq!(cell.node_type, "tableCell");
1407        assert_eq!(cell.attrs.as_ref().unwrap()["rowspan"], 3);
1408    }
1409
1410    // ── Additional mark constructors ────────────────────────────────
1411
1412    #[test]
1413    fn underline_mark() {
1414        let mark = AdfMark::underline();
1415        assert_eq!(mark.mark_type, "underline");
1416        assert!(mark.attrs.is_none());
1417    }
1418
1419    #[test]
1420    fn text_color_mark() {
1421        let mark = AdfMark::text_color("#ff0000");
1422        assert_eq!(mark.mark_type, "textColor");
1423        assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#ff0000");
1424    }
1425
1426    #[test]
1427    fn background_color_mark() {
1428        let mark = AdfMark::background_color("#00ff00");
1429        assert_eq!(mark.mark_type, "backgroundColor");
1430        assert_eq!(mark.attrs.as_ref().unwrap()["color"], "#00ff00");
1431    }
1432
1433    #[test]
1434    fn subsup_mark() {
1435        let mark = AdfMark::subsup("sub");
1436        assert_eq!(mark.mark_type, "subsup");
1437        assert_eq!(mark.attrs.as_ref().unwrap()["type"], "sub");
1438    }
1439
1440    #[test]
1441    fn alignment_mark() {
1442        let mark = AdfMark::alignment("center");
1443        assert_eq!(mark.mark_type, "alignment");
1444        assert_eq!(mark.attrs.as_ref().unwrap()["align"], "center");
1445    }
1446
1447    #[test]
1448    fn indentation_mark() {
1449        let mark = AdfMark::indentation(2);
1450        assert_eq!(mark.mark_type, "indentation");
1451        assert_eq!(mark.attrs.as_ref().unwrap()["level"], 2);
1452    }
1453
1454    #[test]
1455    fn breakout_mark() {
1456        let mark = AdfMark::breakout("wide", None);
1457        assert_eq!(mark.mark_type, "breakout");
1458        assert_eq!(mark.attrs.as_ref().unwrap()["mode"], "wide");
1459        assert!(mark.attrs.as_ref().unwrap().get("width").is_none());
1460    }
1461
1462    #[test]
1463    fn breakout_mark_with_width() {
1464        let mark = AdfMark::breakout("wide", Some(1200));
1465        assert_eq!(mark.mark_type, "breakout");
1466        assert_eq!(mark.attrs.as_ref().unwrap()["mode"], "wide");
1467        assert_eq!(mark.attrs.as_ref().unwrap()["width"], 1200);
1468    }
1469
1470    #[test]
1471    fn border_mark() {
1472        let mark = AdfMark::border("#ff000033", 2);
1473        assert_eq!(mark.mark_type, "border");
1474        let attrs = mark.attrs.as_ref().unwrap();
1475        assert_eq!(attrs["color"], "#ff000033");
1476        assert_eq!(attrs["size"], 2);
1477    }
1478
1479    #[test]
1480    fn table_cell_with_attrs_and_marks_builder() {
1481        let cell = AdfNode::table_cell_with_attrs_and_marks(
1482            vec![AdfNode::paragraph(vec![])],
1483            Some(serde_json::json!({"background": "#e6fcff"})),
1484            vec![AdfMark::border("#ff0000", 1)],
1485        );
1486        assert_eq!(cell.node_type, "tableCell");
1487        assert!(cell.marks.is_some());
1488        assert_eq!(cell.marks.as_ref().unwrap()[0].mark_type, "border");
1489    }
1490
1491    #[test]
1492    fn table_header_with_attrs_and_marks_builder() {
1493        let cell = AdfNode::table_header_with_attrs_and_marks(
1494            vec![AdfNode::paragraph(vec![])],
1495            None,
1496            vec![AdfMark::border("#0000ff", 3)],
1497        );
1498        assert_eq!(cell.node_type, "tableHeader");
1499        assert!(cell.marks.is_some());
1500        assert_eq!(cell.marks.as_ref().unwrap()[0].mark_type, "border");
1501    }
1502
1503    #[test]
1504    fn table_cell_with_empty_marks_has_none() {
1505        let cell = AdfNode::table_cell_with_attrs_and_marks(
1506            vec![AdfNode::paragraph(vec![])],
1507            None,
1508            vec![],
1509        );
1510        assert!(cell.marks.is_none(), "empty marks vec should become None");
1511    }
1512
1513    #[test]
1514    fn border_mark_serde_roundtrip() {
1515        let mark = AdfMark::border("#ff000033", 2);
1516        let json = serde_json::to_string(&mark).unwrap();
1517        let deserialized: AdfMark = serde_json::from_str(&json).unwrap();
1518        assert_eq!(deserialized.mark_type, "border");
1519        assert_eq!(deserialized.attrs.as_ref().unwrap()["color"], "#ff000033");
1520        assert_eq!(deserialized.attrs.as_ref().unwrap()["size"], 2);
1521    }
1522
1523    #[test]
1524    fn from_json_str_null_yields_empty_document() {
1525        let doc = AdfDocument::from_json_str("null").unwrap();
1526        assert_eq!(doc, AdfDocument::default());
1527    }
1528
1529    #[test]
1530    fn from_json_str_whitespace_null_yields_empty_document() {
1531        let doc = AdfDocument::from_json_str("  null\n").unwrap();
1532        assert_eq!(doc, AdfDocument::default());
1533    }
1534
1535    #[test]
1536    fn from_json_str_empty_doc_roundtrips() {
1537        let doc = AdfDocument::from_json_str(r#"{"version":1,"type":"doc","content":[]}"#).unwrap();
1538        assert_eq!(doc, AdfDocument::default());
1539    }
1540
1541    #[test]
1542    fn from_json_str_populated_doc() {
1543        let input = r#"{"version":1,"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hi"}]}]}"#;
1544        let doc = AdfDocument::from_json_str(input).unwrap();
1545        assert_eq!(doc.content.len(), 1);
1546        assert_eq!(doc.content[0].node_type, "paragraph");
1547    }
1548
1549    #[test]
1550    fn from_json_str_invalid_json_errors() {
1551        assert!(AdfDocument::from_json_str("not json").is_err());
1552    }
1553
1554    #[test]
1555    fn from_json_str_wrong_shape_errors() {
1556        assert!(AdfDocument::from_json_str(r#"{"foo":"bar"}"#).is_err());
1557    }
1558}