1use alloy_dyn_abi::DynSolValue;
4use eyre::Result;
5use foundry_common::{
6 fmt::{format_tokens, serialize_value_as_json},
7 sh_println, shell,
8};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, to_string};
11
12pub const JSON_SCHEMA_VERSION: u32 = 1;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub struct JsonEnvelope<T> {
22 pub schema_version: u32,
24 pub success: bool,
28 pub data: Option<T>,
30 pub errors: Vec<JsonMessage>,
32 pub warnings: Vec<JsonMessage>,
34}
35
36impl<T> JsonEnvelope<T> {
37 pub const fn success(data: T) -> Self {
39 Self {
40 schema_version: JSON_SCHEMA_VERSION,
41 success: true,
42 data: Some(data),
43 errors: Vec::new(),
44 warnings: Vec::new(),
45 }
46 }
47
48 pub const fn success_with_warnings(data: T, warnings: Vec<JsonMessage>) -> Self {
50 Self {
51 schema_version: JSON_SCHEMA_VERSION,
52 success: true,
53 data: Some(data),
54 errors: Vec::new(),
55 warnings,
56 }
57 }
58
59 pub const fn failure_with_data(data: T, errors: Vec<JsonMessage>) -> Self {
61 Self {
62 schema_version: JSON_SCHEMA_VERSION,
63 success: false,
64 data: Some(data),
65 errors,
66 warnings: Vec::new(),
67 }
68 }
69}
70
71#[derive(Debug)]
73pub struct JsonError {
74 pub data: Value,
76 pub errors: Vec<JsonMessage>,
78}
79
80impl JsonError {
81 pub fn new(data: impl Serialize, error: JsonMessage) -> serde_json::Result<Self> {
83 Ok(Self { data: serde_json::to_value(data)?, errors: vec![error] })
84 }
85}
86
87impl std::fmt::Display for JsonError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str(self.errors.first().map_or("JSON command failed", |error| &error.message))
90 }
91}
92
93impl std::error::Error for JsonError {}
94
95impl JsonEnvelope<()> {
96 pub fn error(error: JsonMessage) -> Self {
98 Self::failure(vec![error])
99 }
100
101 pub const fn failure(errors: Vec<JsonMessage>) -> Self {
103 Self {
104 schema_version: JSON_SCHEMA_VERSION,
105 success: false,
106 data: None,
107 errors,
108 warnings: Vec::new(),
109 }
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum JsonMessageLevel {
121 Error,
123 Warning,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
132pub struct JsonMessage {
133 pub level: JsonMessageLevel,
135 pub code: String,
137 pub message: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub details: Option<Value>,
142}
143
144impl JsonMessage {
145 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
147 Self {
148 level: JsonMessageLevel::Error,
149 code: code.into(),
150 message: message.into(),
151 details: None,
152 }
153 }
154
155 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
157 Self {
158 level: JsonMessageLevel::Warning,
159 code: code.into(),
160 message: message.into(),
161 details: None,
162 }
163 }
164
165 pub fn with_details(mut self, details: Value) -> Self {
167 self.details = Some(details);
168 self
169 }
170}
171
172pub fn print_json_object<T: Serialize>(value: T) -> Result<()> {
177 if foundry_common::shell::is_json() {
178 print_json_success(value)
179 } else {
180 sh_println!("{}", serde_json::to_string_pretty(&value)?)?;
181 Ok(())
182 }
183}
184
185pub fn print_json<T: Serialize>(value: &T) -> Result<()> {
190 let s = to_string(value)?;
191 let mut shell = foundry_common::shell::Shell::get();
192 let out = shell.out();
193 writeln!(out, "{s}")?;
194 out.flush()?;
195 Ok(())
196}
197
198pub fn print_json_success<T: Serialize>(data: T) -> Result<()> {
200 print_json(&JsonEnvelope::success(data))
201}
202
203pub fn print_json_success_with_warnings<T: Serialize>(
205 data: T,
206 warnings: Vec<JsonMessage>,
207) -> Result<()> {
208 print_json(&JsonEnvelope::success_with_warnings(data, warnings))
209}
210
211pub fn print_json_value_or_scalar(value: impl AsRef<str> + std::fmt::Display) -> Result<()> {
214 if shell::is_json() {
215 match serde_json::from_str::<Value>(value.as_ref()) {
216 Ok(value) => print_json_success(value),
217 Err(_) => print_json_success(value.as_ref()),
218 }
219 } else {
220 sh_println!("{value}")?;
221 Ok(())
222 }
223}
224
225pub fn print_scalar(value: impl Serialize + std::fmt::Display) -> Result<()> {
227 if shell::is_json() {
228 print_json_success(value)
229 } else {
230 sh_println!("{value}")?;
231 Ok(())
232 }
233}
234
235pub fn print_list<T: Serialize + std::fmt::Display>(items: &[T]) -> Result<()> {
238 if shell::is_json() {
239 print_json_success(items)
240 } else {
241 for item in items {
242 sh_println!("{item}")?;
243 }
244 Ok(())
245 }
246}
247
248pub fn print_tokens(tokens: &[DynSolValue]) -> Result<()> {
251 if shell::is_json() {
252 let values = tokens
253 .iter()
254 .cloned()
255 .map(|t| serialize_value_as_json(t, None, true))
256 .collect::<Result<Vec<Value>>>()?;
257 print_json_success(values)
258 } else {
259 format_tokens(tokens).try_for_each(|t| sh_println!("{t}"))
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use serde_json::{json, to_value};
267
268 #[derive(Serialize)]
269 struct BuildData {
270 contracts: usize,
271 }
272
273 #[test]
274 fn success_envelope_serializes_all_top_level_fields() {
275 let envelope = JsonEnvelope::success(BuildData { contracts: 2 });
276
277 let json = to_string(&envelope).unwrap();
278
279 assert_eq!(
280 json,
281 r#"{"schema_version":1,"success":true,"data":{"contracts":2},"errors":[],"warnings":[]}"#
282 );
283 }
284
285 #[test]
286 fn warning_details_are_structured() {
287 let warning = JsonMessage::warning("compiler.remappings", "auto-detected remappings")
288 .with_details(json!({ "count": 3 }));
289 let envelope =
290 JsonEnvelope::success_with_warnings(BuildData { contracts: 1 }, vec![warning]);
291
292 let value = to_value(&envelope).unwrap();
293
294 assert_eq!(value["success"], true);
295 assert_eq!(value["warnings"][0]["level"], "warning");
296 assert_eq!(value["warnings"][0]["code"], "compiler.remappings");
297 assert_eq!(value["warnings"][0]["details"]["count"], 3);
298 }
299
300 #[test]
301 fn failure_envelope_serializes_null_data_and_structured_errors() {
302 let error = JsonMessage::error("config.invalid", "invalid foundry.toml")
303 .with_details(json!({ "path": "foundry.toml" }));
304 let envelope = JsonEnvelope::error(error);
305
306 let value = to_value(&envelope).unwrap();
307
308 assert_eq!(value["schema_version"], JSON_SCHEMA_VERSION);
309 assert_eq!(value["success"], false);
310 assert!(value["data"].is_null());
311 assert_eq!(value["errors"][0]["level"], "error");
312 assert_eq!(value["errors"][0]["code"], "config.invalid");
313 assert_eq!(value["errors"][0]["details"]["path"], "foundry.toml");
314 assert_eq!(value["warnings"], json!([]));
315 }
316
317 #[test]
318 fn failure_with_data_envelope_preserves_data() {
319 let envelope = JsonEnvelope::failure_with_data(
320 json!({ "compatible": false }),
321 vec![JsonMessage::error("compatibility.failed", "compatibility check failed")],
322 );
323 let value = to_value(envelope).unwrap();
324
325 assert_eq!(value["success"], false);
326 assert_eq!(value["data"]["compatible"], false);
327 assert_eq!(value["errors"][0]["code"], "compatibility.failed");
328 }
329}