sdforge 0.5.0-rc.2

Multi-protocol SDK framework with unified macro configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// Copyright (c) 2026 Kirky.X
// SPDX-License-Identifier: MIT
//! Service response and error types
//!
//! Provides unified response wrappers and error types for the framework.

use serde::{Deserialize, Serialize};

/// Unified response wrapper
///
/// A generic response type that can represent both successful responses
/// and errors. The generic parameter T represents the type of data
/// returned on success.
#[derive(Debug, Serialize, Deserialize)]
pub struct ServiceResponse<T = serde_json::Value> {
    /// Whether the request was successful
    pub(crate) success: bool,
    /// Response data
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) data: Option<T>,
    /// Error details
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) error: Option<ServiceError>,
    /// 成功响应的 HTTP/gRPC 状态码;None 表示由宏 `status` 参数或默认 200 决定。
    /// `#[serde(default)]` + `skip_serializing_if` 保证反序列化向后兼容、
    /// 序列化无该键时输出与现状逐字节一致(零破坏)。
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub(crate) status_code: Option<u16>,
    /// Response timestamp
    #[cfg(feature = "timestamp")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) timestamp: Option<i64>,
}

/// Service error representation
///
/// Represents an error that occurred during request processing.
/// Includes an error code, message, optional details, and HTTP status.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceError {
    /// Error code
    pub(crate) code: String,
    /// Error message
    pub(crate) message: String,
    /// Additional error details
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) details: Option<serde_json::Value>,
    /// HTTP status code
    pub(crate) http_status: u16,
}

mod response_impl;

#[cfg(test)]
mod tests {
    use super::*;

    /// Test ServiceResponse::success
    #[test]
    fn test_service_response_success() {
        let response = ServiceResponse::success("test data");
        assert!(response.is_success());
        assert_eq!(response.data(), Some(&"test data"));
        assert!(response.error_ref().is_none());
    }

    /// Test ServiceResponse::error
    #[test]
    fn test_service_response_error_response() {
        let error = ServiceError::new("TEST_ERROR", "Test error message", 400);
        let response = ServiceResponse::<String>::error(error);
        assert!(!response.is_success());
        assert!(response.data.is_none());
        assert!(response.error.is_some());
    }

    /// Test ServiceResponse with generic type
    #[test]
    fn test_service_response_generic() {
        #[derive(Debug, Serialize, Deserialize)]
        struct User {
            name: String,
            age: u32,
        }
        let user = User {
            name: "Alice".to_string(),
            age: 30,
        };
        let response = ServiceResponse::success(user);
        assert!(response.is_success());
        let data = response.data().unwrap();
        assert_eq!(data.name, "Alice");
    }

    /// Test ServiceError::new
    #[test]
    fn test_service_error_new() {
        let error = ServiceError::new("NOT_FOUND", "Resource not found", 404);
        assert_eq!(error.code(), "NOT_FOUND");
        assert_eq!(error.message(), "Resource not found");
        assert_eq!(error.http_status(), 404);
        assert!(error.details().is_none());
    }

    /// Test ServiceError::with_details
    #[test]
    fn test_service_error_with_details() {
        let details = serde_json::json!({
            "resource": "user",
            "id": "123"
        });
        let error =
            ServiceError::with_details("VALIDATION_ERROR", "Invalid input", details.clone(), 422);
        assert_eq!(error.code(), "VALIDATION_ERROR");
        assert_eq!(error.message(), "Invalid input");
        assert_eq!(error.http_status(), 422);
        assert_eq!(error.details(), Some(&details));
    }

    /// Test ServiceError accessors
    #[test]
    fn test_service_error_accessors() {
        let error =
            ServiceError::with_details("TEST", "message", serde_json::json!({"key": "value"}), 500);
        assert_eq!(error.code(), "TEST");
        assert_eq!(error.message(), "message");
        assert_eq!(error.http_status(), 500);
        let details = error.details().unwrap();
        assert_eq!(details["key"], "value");
    }

    /// Test ServiceResponse serialization
    #[test]
    fn test_service_response_serialization() {
        let response = ServiceResponse::success("data");
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("\"success\":true"));
        assert!(json.contains("\"data\":\"data\""));
    }

    /// Test ServiceError serialization
    #[test]
    fn test_service_error_serialization() {
        let error = ServiceError::new("ERROR_CODE", "Error message", 500);
        let json = serde_json::to_string(&error).unwrap();
        assert!(json.contains("\"code\":\"ERROR_CODE\""));
        assert!(json.contains("\"message\":\"Error message\""));
        assert!(json.contains("\"http_status\":500"));
    }

    /// Test ServiceResponse with None timestamp (without timestamp feature)
    #[test]
    fn test_service_response_no_timestamp() {
        let response = ServiceResponse::success("data");
        // When timestamp feature is disabled, the field is not available
        // We just verify the response is created successfully
        assert!(response.is_success());
        assert!(response.data().is_some());
    }

    /// Test ServiceResponse deserialization
    #[test]
    fn test_service_response_deserialization() {
        let json = r#"{"success":true,"data":"test"}"#;
        let response: ServiceResponse<String> = serde_json::from_str(json).unwrap();
        assert!(response.is_success());
        assert_eq!(response.data(), Some(&"test".to_string()));
    }

    /// Test ServiceError deserialization
    #[test]
    fn test_service_error_deserialization() {
        let json = r#"{"code":"ERR","message":"msg","http_status":400}"#;
        let error: ServiceError = serde_json::from_str(json).unwrap();
        assert_eq!(error.code(), "ERR");
        assert_eq!(error.message(), "msg");
        assert_eq!(error.http_status(), 400);
    }

    /// Test ServiceResponse error path
    #[test]
    fn test_service_response_error_details() {
        let error = ServiceError::with_details(
            "CODE",
            "message",
            serde_json::json!({"field": "value"}),
            400,
        );
        let response = ServiceResponse::<String>::error(error);
        assert!(!response.is_success());
        assert!(response.data.is_none());
        let err = response.error_ref().unwrap();
        assert_eq!(err.code(), "CODE");
    }

    /// Test ServiceError::new produces an error with no details (None).
    #[test]
    fn test_service_error_new_has_no_details() {
        let error = ServiceError::new("NOT_FOUND", "missing", 404);
        assert!(error.details().is_none());
        assert_eq!(error.details(), None);
    }

    /// Test ServiceError::with_details with null JSON value.
    #[test]
    fn test_service_error_with_null_details() {
        let error = ServiceError::with_details("ERR", "msg", serde_json::Value::Null, 500);
        assert_eq!(error.details(), Some(&serde_json::Value::Null));
    }

    /// Test ServiceResponse success then error_ref returns None.
    #[test]
    fn test_service_response_success_has_no_error() {
        let response = ServiceResponse::success("data");
        assert!(response.error_ref().is_none());
    }

    /// Test ServiceResponse error then data() returns None.
    #[test]
    fn test_service_response_error_has_no_data() {
        let error = ServiceError::new("ERR", "msg", 500);
        let response = ServiceResponse::<String>::error(error);
        assert!(response.data().is_none());
    }

    /// Test ServiceError http_status() returns the configured status code.
    #[test]
    fn test_service_error_http_status_various_codes() {
        for status in [200u16, 400, 401, 403, 404, 422, 429, 500, 503] {
            let error = ServiceError::new("CODE", "msg", status);
            assert_eq!(error.http_status(), status);
        }
    }

    /// Test ServiceResponse with a complex generic type (Vec) serializes
    /// correctly and the data field contains the array.
    #[test]
    fn test_service_response_with_vec_serialization() {
        let response = ServiceResponse::success(vec![1, 2, 3]);
        let json = serde_json::to_string(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["success"], true);
        assert_eq!(parsed["data"], serde_json::json!([1, 2, 3]));
    }

    /// Test ServiceError serialization omits details when None (via
    /// skip_serializing_if).
    #[test]
    fn test_service_error_serialization_omits_details_when_none() {
        let error = ServiceError::new("CODE", "msg", 400);
        let json = serde_json::to_string(&error).unwrap();
        assert!(
            !json.contains("details"),
            "details should be omitted when None: {}",
            json
        );
    }

    /// Test ServiceError serialization includes details when Some.
    #[test]
    fn test_service_error_serialization_includes_details_when_some() {
        let error =
            ServiceError::with_details("CODE", "msg", serde_json::json!({"key": "value"}), 400);
        let json = serde_json::to_string(&error).unwrap();
        assert!(
            json.contains("details"),
            "details should be included when Some: {}",
            json
        );
    }

    /// Test ServiceResponse::is_success returns false for error responses.
    #[test]
    fn test_service_response_is_success_false_for_error() {
        let error = ServiceError::new("ERR", "msg", 500);
        let response = ServiceResponse::<String>::error(error);
        assert!(!response.is_success());
    }

    /// Test ServiceError Debug formatting contains the code and message.
    #[test]
    fn test_service_error_debug_format() {
        let error = ServiceError::new("DEBUG_CODE", "debug message", 418);
        let debug = format!("{:?}", error);
        assert!(debug.contains("DEBUG_CODE"));
        assert!(debug.contains("debug message"));
        assert!(debug.contains("418"));
    }

    // ============================================================================
    // forge-success-status-code: status_code field + constructors
    //
    // R-core-response-001: 字段与零破坏序列化
    // R-core-response-002: success_with_status 动态构造器
    // R-core-response-003: with_status_code_opt 合并语义(字段优先)
    // R-core-response-004: status_code 访问器
    // ============================================================================

    /// R-core-response-001: success("x") 序列化结果不含 status_code 键。
    #[test]
    fn test_status_code_field_absent_on_success() {
        let response = ServiceResponse::success("x");
        let json = serde_json::to_string(&response).unwrap();
        assert!(
            !json.contains("status_code"),
            "status_code should be omitted when None (zero-breaking): {}",
            json
        );
    }

    /// R-core-response-001: 反序列化历史 JSON(无 status_code 键)成功且字段为 None。
    #[test]
    fn test_status_code_field_backward_compatible_deserialization() {
        let json = r#"{"success":true,"data":"x"}"#;
        let response: ServiceResponse<String> = serde_json::from_str(json).unwrap();
        assert!(response.is_success());
        assert_eq!(response.data(), Some(&"x".to_string()));
        assert_eq!(response.status_code(), None);
    }

    /// R-core-response-002: success_with_status 构造 success=true 且 status_code=Some(code)。
    #[test]
    fn test_success_with_status_sets_field() {
        let response = ServiceResponse::success_with_status("x", 201);
        assert!(response.is_success());
        assert_eq!(response.status_code(), Some(201));
        assert_eq!(response.data(), Some(&"x"));
    }

    /// R-core-response-002: code 取合法边界 100、999 时正常构造。
    #[test]
    fn test_success_with_status_boundary_codes() {
        for code in [100u16, 999] {
            let response = ServiceResponse::success_with_status("x", code);
            assert_eq!(response.status_code(), Some(code));
        }
    }

    /// R-core-response-003: with_status_code_opt 在字段 None 时填入。
    #[test]
    fn test_with_status_code_opt_fills_when_none() {
        let response = ServiceResponse::success("x").with_status_code_opt(Some(201));
        assert_eq!(response.status_code(), Some(201));
    }

    /// R-core-response-003: 字段优先 — 已有值时不被 with_status_code_opt 覆盖。
    #[test]
    fn test_with_status_code_opt_does_not_overwrite_existing() {
        let response =
            ServiceResponse::success_with_status("x", 200).with_status_code_opt(Some(201));
        assert_eq!(
            response.status_code(),
            Some(200),
            "field-set status_code must take precedence over macro fallback"
        );
    }

    /// R-core-response-003: with_status_code_opt(None) 不改字段(None 不改)。
    #[test]
    fn test_with_status_code_opt_none_is_noop() {
        let response = ServiceResponse::success("x").with_status_code_opt(None);
        assert_eq!(response.status_code(), None);
    }

    /// R-core-response-004: success("x").status_code() == None。
    #[test]
    fn test_status_code_accessor_none_on_success() {
        assert_eq!(ServiceResponse::success("x").status_code(), None);
    }

    /// R-core-response-004: success_with_status("x", 201).status_code() == Some(201)。
    #[test]
    fn test_status_code_accessor_some_on_success_with_status() {
        assert_eq!(
            ServiceResponse::success_with_status("x", 201).status_code(),
            Some(201)
        );
    }

    /// 错误响应的 status_code 也应为 None(错误侧走 ServiceError.http_status)。
    #[test]
    fn test_status_code_none_on_error_response() {
        let err = ServiceError::new("ERR", "msg", 500);
        let response = ServiceResponse::<String>::error(err);
        assert_eq!(response.status_code(), None);
    }

    /// success_with_status 序列化包含 status_code 键(与 success 的零破坏行为对比)。
    #[test]
    fn test_success_with_status_serializes_status_code() {
        let response = ServiceResponse::success_with_status("x", 201);
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("\"status_code\":201"), "json: {}", json);
    }

    /// 反序列化含 status_code 的 JSON 时字段正确还原。
    #[test]
    fn test_status_code_deserialization_roundtrip() {
        let json = r#"{"success":true,"data":"x","status_code":201}"#;
        let response: ServiceResponse<String> = serde_json::from_str(json).unwrap();
        assert_eq!(response.status_code(), Some(201));
    }
}