1use allow_core::{MatchOutcome, json_escape};
2
3use crate::{
4 ARTIFACT_STATUS_FAILED, ARTIFACT_STATUS_PASSED, ArtifactContract, CLAIM_BOUNDARY,
5 InventoryContext, SCANNER_LIMITATIONS,
6};
7
8pub(crate) fn push_json_artifact_header(
9 out: &mut String,
10 contract: ArtifactContract,
11 command: &str,
12) {
13 if let Some(fixed_command) = contract.fixed_command {
14 debug_assert_eq!(fixed_command, command);
15 }
16 let schema_version = contract.schema_version;
17 out.push_str(&format!(" \"schema_version\": {schema_version},\n"));
18 out.push_str(&format!(
19 " \"schema_id\": \"{}\",\n",
20 json_escape(contract.schema_id)
21 ));
22 out.push_str(" \"tool\": \"cargo-allow\",\n");
23 out.push_str(&format!(" \"command\": \"{}\",\n", json_escape(command)));
24}
25
26pub(crate) fn push_json_artifact_preamble(
27 out: &mut String,
28 contract: ArtifactContract,
29 command: &str,
30 inventory: InventoryContext<'_>,
31) {
32 push_json_artifact_header(out, contract, command);
33 push_json_artifact_source_context(out, inventory);
34}
35
36pub(crate) fn push_json_fixed_artifact_preamble(
37 out: &mut String,
38 contract: ArtifactContract,
39 inventory: InventoryContext<'_>,
40) {
41 let Some(command) = contract.fixed_command else {
42 std::panic::panic_any("fixed artifact preamble requires a fixed-command artifact contract");
43 };
44 push_json_artifact_preamble(out, contract, command, inventory);
45}
46
47pub(crate) fn push_json_artifact_source_context(out: &mut String, inventory: InventoryContext<'_>) {
48 out.push_str(&format!(
49 " \"claim_boundary\": {},\n",
50 render_claim_boundary_json()
51 ));
52 out.push_str(&format!(
53 " \"scanner_limitations\": {},\n",
54 render_scanner_limitations_json()
55 ));
56 out.push_str(" \"inventory\": ");
57 out.push_str(&render_inventory_json(inventory, " "));
58 out.push_str(",\n");
59}
60
61pub(crate) fn push_json_status_fields(out: &mut String, failed: bool) {
62 out.push_str(&format!(
63 " \"status\": \"{}\",\n",
64 if failed {
65 ARTIFACT_STATUS_FAILED
66 } else {
67 ARTIFACT_STATUS_PASSED
68 }
69 ));
70 out.push_str(&format!(" \"failed\": {},\n", bool_json(failed)));
71}
72
73pub(crate) fn push_json_source_context_properties(
74 out: &mut String,
75 inventory: InventoryContext<'_>,
76 indent: &str,
77) {
78 out.push_str(&format!("{indent}\"inventory\": "));
79 out.push_str(&render_inventory_json(inventory, indent));
80 out.push_str(",\n");
81 out.push_str(&format!(
82 "{indent}\"claim_boundary\": {},\n",
83 render_claim_boundary_json()
84 ));
85 out.push_str(&format!(
86 "{indent}\"scanner_limitations\": {}\n",
87 render_scanner_limitations_json()
88 ));
89}
90
91pub(crate) fn option_json(value: Option<&str>) -> String {
92 value
93 .map(|v| format!("\"{}\"", json_escape(v)))
94 .unwrap_or_else(|| "null".to_string())
95}
96
97pub(crate) fn bool_json(value: bool) -> &'static str {
98 if value { "true" } else { "false" }
99}
100
101pub(crate) fn option_u32_json(value: Option<u32>) -> String {
102 value
103 .map(|value| value.to_string())
104 .unwrap_or_else(|| "null".to_string())
105}
106
107pub(crate) fn option_usize_json(value: Option<usize>) -> String {
108 value
109 .map(|value| value.to_string())
110 .unwrap_or_else(|| "null".to_string())
111}
112
113pub(crate) fn render_match_outcome_json(outcome: &MatchOutcome, indent: &str) -> String {
114 let fields = MatchOutcomeJsonFields::new(outcome);
115 format!(
116 "{indent} {{\n{indent} \"status\": \"{}\",\n{indent} \"allow_id\": {},\n{indent} \"finding_index\": {},\n{indent} \"score\": {},\n{indent} \"message\": \"{}\"\n{indent} }}",
117 fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
118 )
119}
120
121pub(crate) fn render_match_outcome_json_compact(outcome: &MatchOutcome) -> String {
122 let fields = MatchOutcomeJsonFields::new(outcome);
123 format!(
124 "{{\"status\": \"{}\", \"allow_id\": {}, \"finding_index\": {}, \"score\": {}, \"message\": \"{}\"}}",
125 fields.status, fields.allow_id, fields.finding_index, fields.score, fields.message
126 )
127}
128
129struct MatchOutcomeJsonFields {
130 status: &'static str,
131 allow_id: String,
132 finding_index: String,
133 score: u32,
134 message: String,
135}
136
137impl MatchOutcomeJsonFields {
138 fn new(outcome: &MatchOutcome) -> Self {
139 Self {
140 status: outcome.status.as_str(),
141 allow_id: option_json(outcome.allow_id.as_deref()),
142 finding_index: option_usize_json(outcome.finding_index),
143 score: outcome.score,
144 message: json_escape(&outcome.message),
145 }
146 }
147}
148
149pub(crate) fn json_string_array<T: AsRef<str>>(values: &[T]) -> String {
150 format!(
151 "[{}]",
152 values
153 .iter()
154 .map(|value| format!("\"{}\"", json_escape(value.as_ref())))
155 .collect::<Vec<_>>()
156 .join(", ")
157 )
158}
159
160pub fn render_claim_boundary_json() -> String {
161 json_string_array(CLAIM_BOUNDARY)
162}
163
164pub fn render_scanner_limitations_json() -> String {
165 json_string_array(SCANNER_LIMITATIONS)
166}
167
168pub fn render_inventory_json(context: InventoryContext<'_>, indent: &str) -> String {
169 let mut out = String::new();
170 out.push_str("{\n");
171 out.push_str(&format!(
172 "{indent} \"scope\": \"{}\",\n",
173 json_escape(context.scope)
174 ));
175 out.push_str(&format!(
176 "{indent} \"scanner\": \"{}\",\n",
177 json_escape(context.scanner)
178 ));
179 out.push_str(&format!(
180 "{indent} \"source\": \"{}\"",
181 json_escape(context.source)
182 ));
183 if let Some(root) = context.root {
184 out.push_str(",\n");
185 out.push_str(&format!("{indent} \"root\": \"{}\"", json_escape(root)));
186 }
187 if let Some(files) = context.files_scanned {
188 out.push_str(",\n");
189 out.push_str(&format!("{indent} \"files_scanned\": {files}"));
190 }
191 out.push('\n');
192 out.push_str(&format!("{indent}}}"));
193 out
194}