1use crate::contracts::WORKLIST_ARTIFACT;
2use crate::json::{bool_json, json_string_array, option_json, push_json_fixed_artifact_preamble};
3use crate::worklist_summary::{
4 worklist_difficulty_count, worklist_kind_counts, worklist_risk_count,
5};
6use crate::{InventoryContext, WorklistFilters, WorklistItem};
7use allow_core::json_escape;
8
9pub fn render_worklist_json(
10 items: &[WorklistItem<'_>],
11 filters: WorklistFilters<'_>,
12 inventory: InventoryContext<'_>,
13) -> String {
14 let mut out = String::new();
15 out.push_str("{\n");
16 push_json_fixed_artifact_preamble(&mut out, WORKLIST_ARTIFACT, inventory);
17 out.push_str(" \"filters\": ");
18 out.push_str(&render_worklist_filters_json(filters, " "));
19 out.push_str(",\n");
20 out.push_str(" \"summary\": {\n");
21 out.push_str(&format!(" \"work_items\": {},\n", items.len()));
22 out.push_str(&format!(
23 " \"high\": {},\n",
24 worklist_risk_count(items, "high")
25 ));
26 out.push_str(&format!(
27 " \"medium\": {},\n",
28 worklist_risk_count(items, "medium")
29 ));
30 out.push_str(&format!(
31 " \"low\": {},\n",
32 worklist_risk_count(items, "low")
33 ));
34 out.push_str(&format!(
35 " \"small_difficulty\": {},\n",
36 worklist_difficulty_count(items, "small")
37 ));
38 out.push_str(&format!(
39 " \"medium_difficulty\": {}",
40 worklist_difficulty_count(items, "medium")
41 ));
42 let kind_counts = worklist_kind_counts(items);
43 if kind_counts.is_empty() {
44 out.push('\n');
45 } else {
46 out.push_str(",\n");
47 out.push_str(" \"item_kinds\": {\n");
48 for (index, (kind, count)) in kind_counts.iter().enumerate() {
49 if index > 0 {
50 out.push_str(",\n");
51 }
52 out.push_str(&format!(" \"{}\": {}", json_escape(kind), count));
53 }
54 out.push_str("\n }\n");
55 }
56 out.push_str(" },\n");
57 out.push_str(" \"work_items\": [\n");
58 for (index, item) in items.iter().enumerate() {
59 if index > 0 {
60 out.push_str(",\n");
61 }
62 out.push_str(&render_work_item_json(item));
63 }
64 out.push_str("\n ]\n");
65 out.push_str("}\n");
66 out
67}
68
69fn render_work_item_json(item: &WorklistItem<'_>) -> String {
70 let mut out = String::new();
71 out.push_str(" {\n");
72 out.push_str(&format!(" \"id\": \"{}\",\n", json_escape(item.id)));
73 out.push_str(&format!(
74 " \"kind\": \"{}\",\n",
75 json_escape(item.kind)
76 ));
77 out.push_str(&format!(
78 " \"exception_kind\": {},\n",
79 option_json(item.exception_kind)
80 ));
81 out.push_str(&format!(
82 " \"family\": {},\n",
83 option_json(item.family)
84 ));
85 out.push_str(&format!(" \"owner\": {},\n", option_json(item.owner)));
86 out.push_str(&format!(
87 " \"classification\": {},\n",
88 option_json(item.classification)
89 ));
90 out.push_str(&format!(
91 " \"reason\": {},\n",
92 option_json(item.reason)
93 ));
94 out.push_str(&format!(
95 " \"created\": {},\n",
96 option_json(item.created)
97 ));
98 out.push_str(&format!(
99 " \"review_after\": {},\n",
100 option_json(item.review_after)
101 ));
102 out.push_str(&format!(
103 " \"expires\": {},\n",
104 option_json(item.expires)
105 ));
106 out.push_str(&format!(
107 " \"evidence_count\": {},\n",
108 item.evidence_count
109 .map(|count| count.to_string())
110 .unwrap_or_else(|| "null".to_string())
111 ));
112 if let Some(selector_precision) = item.selector_precision {
113 out.push_str(&format!(
114 " \"selector_precision\": {selector_precision},\n"
115 ));
116 }
117 out.push_str(&format!(
118 " \"risk\": \"{}\",\n",
119 json_escape(item.risk)
120 ));
121 out.push_str(&format!(
122 " \"difficulty\": \"{}\",\n",
123 json_escape(item.difficulty)
124 ));
125 out.push_str(&format!(
126 " \"status\": \"{}\",\n",
127 json_escape(item.status)
128 ));
129 out.push_str(&format!(
130 " \"allow_id\": {},\n",
131 option_json(item.allow_id)
132 ));
133 out.push_str(&format!(
134 " \"finding_index\": {},\n",
135 item.finding_index
136 .map(|index| index.to_string())
137 .unwrap_or_else(|| "null".to_string())
138 ));
139 out.push_str(&format!(" \"path\": {},\n", option_json(item.path)));
140 if let Some(reference) = item.evidence_reference.as_ref() {
141 out.push_str(" \"evidence_reference\": ");
142 out.push_str(&render_worklist_evidence_reference_json(reference));
143 out.push_str(",\n");
144 }
145 out.push_str(&format!(
146 " \"source_package\": {},\n",
147 option_json(item.source_package)
148 ));
149 out.push_str(&format!(
150 " \"message\": \"{}\",\n",
151 json_escape(item.message)
152 ));
153 out.push_str(&format!(
154 " \"suggested_actions\": {},\n",
155 json_string_array(item.suggested_actions)
156 ));
157 out.push_str(&format!(
158 " \"proof_commands\": {}\n",
159 json_string_array(item.proof_commands)
160 ));
161 out.push_str(" }");
162 out
163}
164
165fn render_worklist_evidence_reference_json(reference: &crate::EvidenceReference<'_>) -> String {
166 format!(
167 "{{\n \"raw\": \"{}\",\n \"prefix\": {},\n \"target\": {},\n \"status\": \"{}\",\n \"category\": \"{}\",\n \"message\": \"{}\"\n }}",
168 json_escape(reference.raw),
169 option_json(reference.prefix),
170 option_json(reference.target),
171 json_escape(reference.status),
172 json_escape(reference.category),
173 json_escape(reference.message)
174 )
175}
176
177fn render_worklist_filters_json(filters: WorklistFilters<'_>, indent: &str) -> String {
178 let mut out = String::new();
179 out.push_str("{\n");
180 out.push_str(&format!(
181 "{indent} \"kind\": {},\n",
182 option_json(filters.kind)
183 ));
184 out.push_str(&format!(
185 "{indent} \"family\": {},\n",
186 option_json(filters.family)
187 ));
188 out.push_str(&format!(
189 "{indent} \"item_kind\": {},\n",
190 option_json(filters.item_kind)
191 ));
192 out.push_str(&format!(
193 "{indent} \"status\": {},\n",
194 option_json(filters.status)
195 ));
196 out.push_str(&format!(
197 "{indent} \"allow_id\": {},\n",
198 option_json(filters.allow_id)
199 ));
200 out.push_str(&format!(
201 "{indent} \"path\": {},\n",
202 option_json(filters.path)
203 ));
204 out.push_str(&format!(
205 "{indent} \"source_package\": {},\n",
206 option_json(filters.source_package)
207 ));
208 out.push_str(&format!(
209 "{indent} \"owner\": {},\n",
210 option_json(filters.owner)
211 ));
212 out.push_str(&format!(
213 "{indent} \"classification\": {},\n",
214 option_json(filters.classification)
215 ));
216 out.push_str(&format!(
217 "{indent} \"baseline_debt\": {},\n",
218 bool_json(filters.baseline_debt)
219 ));
220 out.push_str(&format!(
221 "{indent} \"broad_scope\": {},\n",
222 bool_json(filters.broad_scope)
223 ));
224 out.push_str(&format!(
225 "{indent} \"risk\": {},\n",
226 option_json(filters.risk)
227 ));
228 out.push_str(&format!(
229 "{indent} \"difficulty\": {},\n",
230 option_json(filters.difficulty)
231 ));
232 out.push_str(&format!(
233 "{indent} \"missing_evidence\": {}\n",
234 bool_json(filters.missing_evidence)
235 ));
236 out.push_str(&format!("{indent}}}"));
237 out
238}