1use eyre::Result;
4use serde::{Deserialize, Serialize};
5use serde_json::{Value, to_string};
6
7pub const JSON_SCHEMA_VERSION: u32 = 1;
9
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct JsonEnvelope<T> {
17 pub schema_version: u32,
19 pub success: bool,
23 pub data: Option<T>,
25 pub errors: Vec<JsonMessage>,
27 pub warnings: Vec<JsonMessage>,
29}
30
31impl<T> JsonEnvelope<T> {
32 pub const fn success(data: T) -> Self {
34 Self {
35 schema_version: JSON_SCHEMA_VERSION,
36 success: true,
37 data: Some(data),
38 errors: Vec::new(),
39 warnings: Vec::new(),
40 }
41 }
42
43 pub const fn success_with_warnings(data: T, warnings: Vec<JsonMessage>) -> Self {
45 Self {
46 schema_version: JSON_SCHEMA_VERSION,
47 success: true,
48 data: Some(data),
49 errors: Vec::new(),
50 warnings,
51 }
52 }
53}
54
55impl JsonEnvelope<()> {
56 pub fn error(error: JsonMessage) -> Self {
58 Self::failure(vec![error])
59 }
60
61 pub const fn failure(errors: Vec<JsonMessage>) -> Self {
63 Self {
64 schema_version: JSON_SCHEMA_VERSION,
65 success: false,
66 data: None,
67 errors,
68 warnings: Vec::new(),
69 }
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum JsonMessageLevel {
81 Error,
83 Warning,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct JsonMessage {
93 pub level: JsonMessageLevel,
95 pub code: String,
97 pub message: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub details: Option<Value>,
102}
103
104impl JsonMessage {
105 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
107 Self {
108 level: JsonMessageLevel::Error,
109 code: code.into(),
110 message: message.into(),
111 details: None,
112 }
113 }
114
115 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
117 Self {
118 level: JsonMessageLevel::Warning,
119 code: code.into(),
120 message: message.into(),
121 details: None,
122 }
123 }
124
125 pub fn with_details(mut self, details: Value) -> Self {
127 self.details = Some(details);
128 self
129 }
130}
131
132pub fn print_json<T: Serialize>(value: &T) -> Result<()> {
137 sh_println!("{}", to_string(value)?)?;
138 Ok(())
139}
140
141pub fn print_json_success<T: Serialize>(data: T) -> Result<()> {
143 print_json(&JsonEnvelope::success(data))
144}
145
146pub fn print_json_success_with_warnings<T: Serialize>(
148 data: T,
149 warnings: Vec<JsonMessage>,
150) -> Result<()> {
151 print_json(&JsonEnvelope::success_with_warnings(data, warnings))
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use serde_json::{json, to_value};
158
159 #[derive(Serialize)]
160 struct BuildData {
161 contracts: usize,
162 }
163
164 #[test]
165 fn success_envelope_serializes_all_top_level_fields() {
166 let envelope = JsonEnvelope::success(BuildData { contracts: 2 });
167
168 let json = to_string(&envelope).unwrap();
169
170 assert_eq!(
171 json,
172 r#"{"schema_version":1,"success":true,"data":{"contracts":2},"errors":[],"warnings":[]}"#
173 );
174 }
175
176 #[test]
177 fn warning_details_are_structured() {
178 let warning = JsonMessage::warning("compiler.remappings", "auto-detected remappings")
179 .with_details(json!({ "count": 3 }));
180 let envelope =
181 JsonEnvelope::success_with_warnings(BuildData { contracts: 1 }, vec![warning]);
182
183 let value = to_value(&envelope).unwrap();
184
185 assert_eq!(value["success"], true);
186 assert_eq!(value["warnings"][0]["level"], "warning");
187 assert_eq!(value["warnings"][0]["code"], "compiler.remappings");
188 assert_eq!(value["warnings"][0]["details"]["count"], 3);
189 }
190
191 #[test]
192 fn failure_envelope_serializes_null_data_and_structured_errors() {
193 let error = JsonMessage::error("config.invalid", "invalid foundry.toml")
194 .with_details(json!({ "path": "foundry.toml" }));
195 let envelope = JsonEnvelope::error(error);
196
197 let value = to_value(&envelope).unwrap();
198
199 assert_eq!(value["schema_version"], JSON_SCHEMA_VERSION);
200 assert_eq!(value["success"], false);
201 assert!(value["data"].is_null());
202 assert_eq!(value["errors"][0]["level"], "error");
203 assert_eq!(value["errors"][0]["code"], "config.invalid");
204 assert_eq!(value["errors"][0]["details"]["path"], "foundry.toml");
205 assert_eq!(value["warnings"], json!([]));
206 }
207}