cfgd_core/output/
component.rs1use serde::Serialize;
2
3use super::Role;
4
5#[derive(Debug, Clone, Serialize)]
8#[serde(tag = "type", rename_all = "snake_case")]
9pub enum Component {
10 Heading {
11 text: String,
12 },
13 KvBlock {
14 pairs: Vec<KvPair>,
15 },
16 Bullet {
17 text: String,
18 },
19 Status {
20 role: Role,
21 subject: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 detail: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 duration_ms: Option<u128>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 target: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
33 label: Option<StatusLabel>,
34 },
35 Hint {
36 text: String,
37 },
38 Note {
39 text: String,
40 },
41 Table {
42 headers: Vec<String>,
43 rows: Vec<Vec<String>>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 row_roles: Vec<Vec<Option<Role>>>,
49 },
50 Section {
51 name: String,
52 keep_when_empty: bool,
54 empty_state: Option<String>,
56 children: Vec<Component>,
57 },
58}
59
60#[derive(Debug, Clone, Serialize)]
61pub struct StatusLabel {
62 pub role: Role,
63 pub text: String,
64}
65
66#[derive(Debug, Clone, Serialize)]
67pub struct KvPair {
68 pub key: String,
69 pub value: String,
70}
71
72impl KvPair {
73 pub fn new(k: impl Into<String>, v: impl Into<String>) -> Self {
74 Self {
75 key: k.into(),
76 value: v.into(),
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn heading_serializes_with_type_tag() {
87 let c = Component::Heading {
88 text: "Status".into(),
89 };
90 let json = serde_json::to_value(&c).unwrap();
91 assert_eq!(json["type"], "heading");
92 assert_eq!(json["text"], "Status");
93 }
94
95 #[test]
96 fn status_omits_optional_fields_when_unset() {
97 let c = Component::Status {
98 role: Role::Ok,
99 subject: "ok".into(),
100 detail: None,
101 duration_ms: None,
102 target: None,
103 label: None,
104 };
105 let json = serde_json::to_value(&c).unwrap();
106 assert!(json.get("detail").is_none());
107 assert!(json.get("duration_ms").is_none());
108 assert!(json.get("target").is_none());
109 assert!(json.get("label").is_none());
110 assert_eq!(json["role"], "ok");
111 }
112
113 #[test]
114 fn status_label_serializes_with_role_and_text() {
115 let c = Component::Status {
116 role: Role::Ok,
117 subject: "ok".into(),
118 detail: None,
119 duration_ms: None,
120 target: None,
121 label: Some(StatusLabel {
122 role: Role::Secondary,
123 text: "[team-config]".into(),
124 }),
125 };
126 let json = serde_json::to_value(&c).unwrap();
127 let label = json.get("label").expect("label must serialize when set");
128 assert_eq!(label["role"], "secondary");
129 assert_eq!(label["text"], "[team-config]");
130 }
131
132 #[test]
133 fn section_keep_when_empty_distinguishes_variants() {
134 let plain = Component::Section {
135 name: "X".into(),
136 keep_when_empty: true,
137 empty_state: None,
138 children: vec![],
139 };
140 let collapse = Component::Section {
141 name: "X".into(),
142 keep_when_empty: false,
143 empty_state: None,
144 children: vec![],
145 };
146 let p = serde_json::to_value(&plain).unwrap();
147 let c = serde_json::to_value(&collapse).unwrap();
148 assert_eq!(p["keep_when_empty"], true);
149 assert_eq!(c["keep_when_empty"], false);
150 }
151}