1use serde::Serialize;
4
5use crate::exit_code::ExitCode;
6
7const VERSIONED_OBJECTS_FAMILY: &str = "versioned_objects";
8const LOCKS_FAMILY: &str = "locks";
9const BUCKET_OPERATIONS_FAMILY: &str = "bucket_operations";
10
11#[derive(Debug, Serialize)]
12pub struct V3SuccessEnvelope<T> {
13 schema_version: u8,
14 #[serde(rename = "type")]
15 family: &'static str,
16 status: &'static str,
17 data: T,
18}
19
20impl<T> V3SuccessEnvelope<T> {
21 pub fn versioned_objects(data: T) -> Self {
22 Self {
23 schema_version: 3,
24 family: VERSIONED_OBJECTS_FAMILY,
25 status: "success",
26 data,
27 }
28 }
29
30 pub fn locks(data: T) -> Self {
31 Self {
32 schema_version: 3,
33 family: LOCKS_FAMILY,
34 status: "success",
35 data,
36 }
37 }
38
39 pub fn bucket_operations(data: T) -> Self {
40 Self {
41 schema_version: 3,
42 family: BUCKET_OPERATIONS_FAMILY,
43 status: "success",
44 data,
45 }
46 }
47}
48
49#[derive(Debug, Serialize)]
50pub struct V3ErrorEnvelope {
51 schema_version: u8,
52 #[serde(rename = "type")]
53 family: &'static str,
54 status: &'static str,
55 error: V3ErrorDetail,
56}
57
58impl V3ErrorEnvelope {
59 pub fn versioned_objects(
60 code: ExitCode,
61 message: impl Into<String>,
62 capability: Option<&str>,
63 ) -> Self {
64 Self {
65 schema_version: 3,
66 family: VERSIONED_OBJECTS_FAMILY,
67 status: "error",
68 error: V3ErrorDetail::from_exit_code(code, message.into(), capability),
69 }
70 }
71
72 pub fn locks(code: ExitCode, message: impl Into<String>, capability: Option<&str>) -> Self {
73 Self {
74 schema_version: 3,
75 family: LOCKS_FAMILY,
76 status: "error",
77 error: V3ErrorDetail::from_exit_code(code, message.into(), capability),
78 }
79 }
80
81 pub fn bucket_operations(
82 code: ExitCode,
83 message: impl Into<String>,
84 capability: Option<&str>,
85 ) -> Self {
86 Self {
87 schema_version: 3,
88 family: BUCKET_OPERATIONS_FAMILY,
89 status: "error",
90 error: V3ErrorDetail::from_exit_code(code, message.into(), capability),
91 }
92 }
93}
94
95#[derive(Debug, Serialize)]
96pub struct V3PartialErrorEnvelope<T> {
97 schema_version: u8,
98 #[serde(rename = "type")]
99 family: &'static str,
100 status: &'static str,
101 error: V3ErrorDetail,
102 data: T,
103}
104
105impl<T> V3PartialErrorEnvelope<T> {
106 pub fn versioned_objects(
107 code: ExitCode,
108 message: impl Into<String>,
109 capability: Option<&str>,
110 data: T,
111 ) -> Self {
112 Self {
113 schema_version: 3,
114 family: VERSIONED_OBJECTS_FAMILY,
115 status: "error",
116 error: V3ErrorDetail::from_exit_code(code, message.into(), capability),
117 data,
118 }
119 }
120
121 pub fn bucket_operations(
122 code: ExitCode,
123 message: impl Into<String>,
124 capability: Option<&str>,
125 data: T,
126 ) -> Self {
127 Self {
128 schema_version: 3,
129 family: BUCKET_OPERATIONS_FAMILY,
130 status: "error",
131 error: V3ErrorDetail::from_exit_code(code, message.into(), capability),
132 data,
133 }
134 }
135}
136
137#[derive(Debug, Serialize)]
138#[serde(untagged)]
139enum V3ErrorDetail {
140 Standard(V3StandardError),
141 Unsupported(V3UnsupportedError),
142}
143
144impl V3ErrorDetail {
145 fn from_exit_code(code: ExitCode, message: String, capability: Option<&str>) -> Self {
146 let (error_type, retryable, suggestion) = match code {
147 ExitCode::UnsupportedFeature => {
148 return Self::Unsupported(V3UnsupportedError {
149 error_type: "unsupported_feature",
150 message,
151 retryable: false,
152 capability: capability.unwrap_or("versioned_objects").to_string(),
153 server: None,
154 suggestion: Some(
155 "Verify that the target RustFS version supports this operation."
156 .to_string(),
157 ),
158 });
159 }
160 ExitCode::Success => ("general_error", false, None),
161 ExitCode::GeneralError => ("general_error", false, None),
162 ExitCode::UsageError => (
163 "usage_error",
164 false,
165 Some("Review the command arguments and retry.".to_string()),
166 ),
167 ExitCode::NetworkError => (
168 "network_error",
169 true,
170 Some("Verify the endpoint and network connectivity, then retry.".to_string()),
171 ),
172 ExitCode::AuthError => (
173 "auth_error",
174 false,
175 Some("Verify the alias credentials and permissions, then retry.".to_string()),
176 ),
177 ExitCode::NotFound => (
178 "not_found",
179 false,
180 Some("Check the bucket, object key, and version ID, then retry.".to_string()),
181 ),
182 ExitCode::Conflict => (
183 "conflict",
184 false,
185 Some("Review the version state or retention policy, then retry.".to_string()),
186 ),
187 ExitCode::Interrupted => (
188 "interrupted",
189 true,
190 Some("Retry if the operation still needs to complete.".to_string()),
191 ),
192 };
193
194 Self::Standard(V3StandardError {
195 error_type,
196 message,
197 retryable,
198 suggestion,
199 })
200 }
201}
202
203#[derive(Debug, Serialize)]
204struct V3StandardError {
205 #[serde(rename = "type")]
206 error_type: &'static str,
207 message: String,
208 retryable: bool,
209 suggestion: Option<String>,
210}
211
212#[derive(Debug, Serialize)]
213struct V3UnsupportedError {
214 #[serde(rename = "type")]
215 error_type: &'static str,
216 message: String,
217 retryable: bool,
218 capability: String,
219 server: Option<String>,
220 suggestion: Option<String>,
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn versioned_success_uses_the_v3_envelope() {
229 let value = serde_json::to_value(V3SuccessEnvelope::versioned_objects(
230 serde_json::json!({ "operation": "copy" }),
231 ))
232 .expect("serialize v3 success envelope");
233
234 assert_eq!(value["schema_version"], 3);
235 assert_eq!(value["type"], "versioned_objects");
236 assert_eq!(value["status"], "success");
237 }
238
239 #[test]
240 fn versioned_errors_use_stable_error_kinds() {
241 let value = serde_json::to_value(V3ErrorEnvelope::versioned_objects(
242 ExitCode::AuthError,
243 "Access denied",
244 None,
245 ))
246 .expect("serialize v3 error envelope");
247
248 assert_eq!(value["status"], "error");
249 assert_eq!(value["error"]["type"], "auth_error");
250 assert_eq!(value["error"]["retryable"], false);
251 }
252
253 #[test]
254 fn lock_success_and_errors_use_the_locks_family() {
255 let success = serde_json::to_value(V3SuccessEnvelope::locks(
256 serde_json::json!({ "operation": "retention_info" }),
257 ))
258 .expect("serialize lock success envelope");
259 let error = serde_json::to_value(V3ErrorEnvelope::locks(
260 ExitCode::Conflict,
261 "Compliance retention cannot be shortened",
262 Some("object_retention"),
263 ))
264 .expect("serialize lock error envelope");
265
266 assert_eq!(success["type"], "locks");
267 assert_eq!(error["type"], "locks");
268 assert_eq!(error["error"]["type"], "conflict");
269 }
270
271 #[test]
272 fn bucket_operation_envelopes_preserve_partial_stage_data() {
273 let success = serde_json::to_value(V3SuccessEnvelope::bucket_operations(
274 serde_json::json!({ "operation": "create" }),
275 ))
276 .expect("serialize bucket operation success");
277 let partial = serde_json::to_value(V3PartialErrorEnvelope::bucket_operations(
278 ExitCode::Conflict,
279 "Versioning verification failed",
280 Some("bucket_versioning"),
281 serde_json::json!({ "failed_stage": "verify_versioning" }),
282 ))
283 .expect("serialize bucket operation partial failure");
284
285 assert_eq!(success["type"], "bucket_operations");
286 assert_eq!(partial["type"], "bucket_operations");
287 assert_eq!(partial["data"]["failed_stage"], "verify_versioning");
288 }
289}