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\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{}\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.selector_precision,
60 row.broad_scope,
61 row.review_after.unwrap_or("-"),
62 row.expires.unwrap_or("-"),
63 row.reason
64 ));
65 }
66 if rows.is_empty() {
67 out.push_str("(no allow entries matched filters)\n");
68 }
69 out.push_str(CLAIM_BOUNDARY_TEXT);
70 out.push('\n');
71 out
72}
73
74fn list_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
75 inventory
76 .files_scanned
77 .map(|files| format!("; files scanned: {files}"))
78 .unwrap_or_default()
79}
80
81fn empty_as_dash(value: &str) -> &str {
82 if value.trim().is_empty() { "-" } else { value }
83}
84
85fn render_list_row_json(row: &ListRow<'_>) -> String {
86 let mut out = String::new();
87 out.push_str(" {\n");
88 out.push_str(&format!(" \"id\": \"{}\",\n", json_escape(row.id)));
89 out.push_str(&format!(
90 " \"status\": \"{}\",\n",
91 json_escape(row.status)
92 ));
93 out.push_str(&format!(" \"matches\": {},\n", row.matches));
94 out.push_str(&format!(" \"kind\": \"{}\",\n", json_escape(row.kind)));
95 out.push_str(&format!(" \"family\": {},\n", option_json(row.family)));
96 out.push_str(&format!(
97 " \"owner\": \"{}\",\n",
98 json_escape(row.owner)
99 ));
100 out.push_str(&format!(
101 " \"classification\": \"{}\",\n",
102 json_escape(row.classification)
103 ));
104 out.push_str(&format!(
105 " \"scope\": \"{}\",\n",
106 json_escape(row.scope)
107 ));
108 out.push_str(&format!(
109 " \"source_package\": {},\n",
110 option_json(row.source_package)
111 ));
112 out.push_str(&format!(
113 " \"evidence_count\": {},\n",
114 row.evidence_count
115 ));
116 out.push_str(&format!(
117 " \"selector_precision\": {},\n",
118 row.selector_precision
119 ));
120 out.push_str(&format!(
121 " \"broad_scope\": {},\n",
122 bool_json(row.broad_scope)
123 ));
124 out.push_str(&format!(
125 " \"review_after\": {},\n",
126 option_json(row.review_after)
127 ));
128 out.push_str(&format!(
129 " \"expires\": {},\n",
130 option_json(row.expires)
131 ));
132 out.push_str(&format!(
133 " \"reason\": \"{}\"\n",
134 json_escape(row.reason)
135 ));
136 out.push_str(" }");
137 out
138}
139
140fn render_list_filters_json(filters: ListFilters<'_>, indent: &str) -> String {
141 let mut out = String::new();
142 out.push_str("{\n");
143 out.push_str(&format!(
144 "{indent} \"kind\": {},\n",
145 option_json(filters.kind)
146 ));
147 out.push_str(&format!(
148 "{indent} \"family\": {},\n",
149 option_json(filters.family)
150 ));
151 out.push_str(&format!(
152 "{indent} \"owner\": {},\n",
153 option_json(filters.owner)
154 ));
155 out.push_str(&format!(
156 "{indent} \"classification\": {},\n",
157 option_json(filters.classification)
158 ));
159 out.push_str(&format!(
160 "{indent} \"path\": {},\n",
161 option_json(filters.path)
162 ));
163 out.push_str(&format!(
164 "{indent} \"source_package\": {},\n",
165 option_json(filters.source_package)
166 ));
167 out.push_str(&format!(
168 "{indent} \"allow_id\": {},\n",
169 option_json(filters.allow_id)
170 ));
171 out.push_str(&format!(
172 "{indent} \"status\": {},\n",
173 option_json(filters.status)
174 ));
175 out.push_str(&format!(
176 "{indent} \"expired\": {},\n",
177 bool_json(filters.expired)
178 ));
179 out.push_str(&format!(
180 "{indent} \"review_due\": {},\n",
181 bool_json(filters.review_due)
182 ));
183 out.push_str(&format!(
184 "{indent} \"stale\": {},\n",
185 bool_json(filters.stale)
186 ));
187 out.push_str(&format!(
188 "{indent} \"baseline_debt\": {},\n",
189 bool_json(filters.baseline_debt)
190 ));
191 out.push_str(&format!(
192 "{indent} \"broad_scope\": {},\n",
193 bool_json(filters.broad_scope)
194 ));
195 out.push_str(&format!(
196 "{indent} \"missing_evidence\": {}\n",
197 bool_json(filters.missing_evidence)
198 ));
199 out.push_str(&format!("{indent}}}"));
200 out
201}