1use crate::contracts::LIST_ARTIFACT;
2use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
3use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, ListFilters, ListRow};
4use allow_core::json_escape;
5
6pub fn render_list_json(
7 rows: &[ListRow<'_>],
8 filters: ListFilters<'_>,
9 inventory: InventoryContext<'_>,
10) -> String {
11 let mut out = String::new();
12 out.push_str("{\n");
13 push_json_fixed_artifact_preamble(&mut out, LIST_ARTIFACT, inventory);
14 out.push_str(" \"filters\": ");
15 out.push_str(&render_list_filters_json(filters, " "));
16 out.push_str(",\n");
17 out.push_str(&format!(
18 " \"summary\": {{\n \"allow_entries\": {}\n }},\n",
19 rows.len()
20 ));
21 out.push_str(" \"allow_entries\": [\n");
22 for (index, row) in rows.iter().enumerate() {
23 if index > 0 {
24 out.push_str(",\n");
25 }
26 out.push_str(&render_list_row_json(row));
27 }
28 out.push_str("\n ]\n");
29 out.push_str("}\n");
30 out
31}
32
33pub fn render_list_human(rows: &[ListRow<'_>], inventory: InventoryContext<'_>) -> String {
34 let mut out = String::new();
35 out.push_str(&format!(
36 "inventory: {}/{} via {}{}\n",
37 inventory.scope,
38 inventory.scanner,
39 inventory.source,
40 list_inventory_files_suffix(inventory)
41 ));
42 if let Some(root) = inventory.root {
43 out.push_str(&format!("source_tree_root: {root}\n"));
44 }
45 out.push_str("id\tstatus\tmatches\tkind\tfamily\towner\tclassification\tscope\tsource_package\tevidence_count\tbroken_evidence_references\tweak_evidence_references\tselector_precision\tbroad_scope\treview_after\texpires\treason\n");
46 for row in rows {
47 out.push_str(&format!(
48 "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
49 row.id,
50 row.status,
51 row.matches,
52 row.kind,
53 row.family.unwrap_or("-"),
54 empty_as_dash(row.owner),
55 empty_as_dash(row.classification),
56 row.scope,
57 row.source_package.unwrap_or("-"),
58 row.evidence_count,
59 row.broken_evidence_references,
60 row.weak_evidence_references,
61 row.selector_precision,
62 row.broad_scope,
63 row.review_after.unwrap_or("-"),
64 row.expires.unwrap_or("-"),
65 row.reason
66 ));
67 }
68 if rows.is_empty() {
69 out.push_str("(no allow entries matched filters)\n");
70 }
71 out.push_str(CLAIM_BOUNDARY_TEXT);
72 out.push('\n');
73 out
74}
75
76fn list_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
77 inventory
78 .files_scanned
79 .map(|files| format!("; files scanned: {files}"))
80 .unwrap_or_default()
81}
82
83fn empty_as_dash(value: &str) -> &str {
84 if value.trim().is_empty() { "-" } else { value }
85}
86
87fn render_list_row_json(row: &ListRow<'_>) -> String {
88 let mut out = String::new();
89 out.push_str(" {\n");
90 out.push_str(&format!(" \"id\": \"{}\",\n", json_escape(row.id)));
91 out.push_str(&format!(
92 " \"status\": \"{}\",\n",
93 json_escape(row.status)
94 ));
95 out.push_str(&format!(" \"matches\": {},\n", row.matches));
96 out.push_str(&format!(" \"kind\": \"{}\",\n", json_escape(row.kind)));
97 out.push_str(&format!(" \"family\": {},\n", option_json(row.family)));
98 out.push_str(&format!(
99 " \"owner\": \"{}\",\n",
100 json_escape(row.owner)
101 ));
102 out.push_str(&format!(
103 " \"classification\": \"{}\",\n",
104 json_escape(row.classification)
105 ));
106 out.push_str(&format!(
107 " \"scope\": \"{}\",\n",
108 json_escape(row.scope)
109 ));
110 out.push_str(&format!(
111 " \"source_package\": {},\n",
112 option_json(row.source_package)
113 ));
114 out.push_str(&format!(
115 " \"evidence_count\": {},\n",
116 row.evidence_count
117 ));
118 if row.broken_evidence_references > 0 {
119 out.push_str(&format!(
120 " \"broken_evidence_references\": {},\n",
121 row.broken_evidence_references
122 ));
123 }
124 if row.weak_evidence_references > 0 {
125 out.push_str(&format!(
126 " \"weak_evidence_references\": {},\n",
127 row.weak_evidence_references
128 ));
129 }
130 out.push_str(&format!(
131 " \"selector_precision\": {},\n",
132 row.selector_precision
133 ));
134 out.push_str(&format!(
135 " \"broad_scope\": {},\n",
136 bool_json(row.broad_scope)
137 ));
138 out.push_str(&format!(
139 " \"review_after\": {},\n",
140 option_json(row.review_after)
141 ));
142 out.push_str(&format!(
143 " \"expires\": {},\n",
144 option_json(row.expires)
145 ));
146 out.push_str(&format!(
147 " \"reason\": \"{}\"\n",
148 json_escape(row.reason)
149 ));
150 out.push_str(" }");
151 out
152}
153
154fn render_list_filters_json(filters: ListFilters<'_>, indent: &str) -> String {
155 let mut out = String::new();
156 out.push_str("{\n");
157 out.push_str(&format!(
158 "{indent} \"kind\": {},\n",
159 option_json(filters.kind)
160 ));
161 out.push_str(&format!(
162 "{indent} \"family\": {},\n",
163 option_json(filters.family)
164 ));
165 out.push_str(&format!(
166 "{indent} \"owner\": {},\n",
167 option_json(filters.owner)
168 ));
169 out.push_str(&format!(
170 "{indent} \"classification\": {},\n",
171 option_json(filters.classification)
172 ));
173 out.push_str(&format!(
174 "{indent} \"path\": {},\n",
175 option_json(filters.path)
176 ));
177 out.push_str(&format!(
178 "{indent} \"source_package\": {},\n",
179 option_json(filters.source_package)
180 ));
181 out.push_str(&format!(
182 "{indent} \"allow_id\": {},\n",
183 option_json(filters.allow_id)
184 ));
185 out.push_str(&format!(
186 "{indent} \"status\": {},\n",
187 option_json(filters.status)
188 ));
189 out.push_str(&format!(
190 "{indent} \"expired\": {},\n",
191 bool_json(filters.expired)
192 ));
193 out.push_str(&format!(
194 "{indent} \"review_due\": {},\n",
195 bool_json(filters.review_due)
196 ));
197 out.push_str(&format!(
198 "{indent} \"stale\": {},\n",
199 bool_json(filters.stale)
200 ));
201 out.push_str(&format!(
202 "{indent} \"baseline_debt\": {},\n",
203 bool_json(filters.baseline_debt)
204 ));
205 out.push_str(&format!(
206 "{indent} \"broad_scope\": {},\n",
207 bool_json(filters.broad_scope)
208 ));
209 out.push_str(&format!(
210 "{indent} \"missing_evidence\": {},\n",
211 bool_json(filters.missing_evidence)
212 ));
213 out.push_str(&format!(
214 "{indent} \"broken_evidence\": {},\n",
215 bool_json(filters.broken_evidence)
216 ));
217 out.push_str(&format!(
218 "{indent} \"weak_evidence\": {}\n",
219 bool_json(filters.weak_evidence)
220 ));
221 out.push_str(&format!("{indent}}}"));
222 out
223}