Repository navigation
Expand file tree
/
Copy pathmod.rs
More file actions
154 lines (137 loc) · 5.21 KB
/
Copy pathmod.rs
File metadata and controls
154 lines (137 loc) · 5.21 KB
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
//! Shared types for JSON-RPC / CLI controller surfaces.
//!
//! This module provides the foundational types and utilities for handling
//! RPC outcomes across different domain modules. It ensures a consistent
//! response format for both internal consumption and external presentation.
//!
//! Domain `rpc` modules should use [`RpcOutcome`] to wrap their results,
//! which facilitates consistent logging and error handling.
use serde::Serialize;
use serde_json::json;
mod structured_error;
pub use structured_error::{StructuredRpcError, STRUCTURED_RPC_ERROR_SENTINEL};
/// Successful RPC handler result: serialized JSON value plus optional log lines.
///
/// This type represents the result of a domain-specific RPC call, including
/// any log messages generated during execution.
#[derive(Debug)]
pub struct RpcOutcome<T> {
/// The actual data returned by the RPC call.
pub value: T,
/// A collection of log messages for auditing or debugging.
pub logs: Vec<String>,
}
impl<T> RpcOutcome<T> {
/// Creates a new `RpcOutcome` with a value and a list of logs.
pub fn new(value: T, logs: Vec<String>) -> Self {
Self { value, logs }
}
}
impl<T: Serialize> RpcOutcome<T> {
/// Creates a new `RpcOutcome` with a value and a single log message.
pub fn single_log(value: T, log: impl Into<String>) -> Self {
Self {
value,
logs: vec![log.into()],
}
}
/// Converts the outcome into a CLI-compatible JSON value.
///
/// The resulting JSON shape matches the core CLI expectations:
/// - If no logs are present, the value is returned directly.
/// - If logs are present, an object with `result` and `logs` keys is returned.
///
/// # Errors
///
/// Returns an error if serialization to JSON fails.
pub fn into_cli_compatible_json(self) -> Result<serde_json::Value, String> {
let RpcOutcome { value, logs } = self;
let value = serde_json::to_value(value).map_err(|e| e.to_string())?;
if logs.is_empty() {
Ok(value)
} else {
Ok(json!({ "result": value, "logs": logs }))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn new_preserves_value_and_logs() {
let outcome: RpcOutcome<i64> = RpcOutcome::new(7, vec!["a".into(), "b".into()]);
assert_eq!(outcome.value, 7);
assert_eq!(outcome.logs, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn single_log_stores_exactly_one_log() {
let outcome = RpcOutcome::single_log(json!({"ok": true}), "hello");
assert_eq!(outcome.logs.len(), 1);
assert_eq!(outcome.logs[0], "hello");
assert_eq!(outcome.value, json!({"ok": true}));
}
#[test]
fn single_log_accepts_string_and_str_via_into() {
let a = RpcOutcome::single_log(json!(1), "static str");
let b = RpcOutcome::single_log(json!(1), String::from("owned string"));
assert_eq!(a.logs[0], "static str");
assert_eq!(b.logs[0], "owned string");
}
#[test]
fn into_cli_compatible_json_no_logs_returns_bare_value() {
let outcome = RpcOutcome::<serde_json::Value>::new(json!({"x": 1}), vec![]);
let out = outcome.into_cli_compatible_json().unwrap();
assert_eq!(out, json!({"x": 1}));
assert!(out.get("logs").is_none());
}
#[test]
fn into_cli_compatible_json_with_logs_wraps_in_envelope() {
let outcome = RpcOutcome::single_log(json!(42), "did something");
let out = outcome.into_cli_compatible_json().unwrap();
assert_eq!(out["result"], json!(42));
assert_eq!(out["logs"], json!(["did something"]));
// And only those two keys exist.
assert_eq!(out.as_object().unwrap().len(), 2);
}
#[test]
fn into_cli_compatible_json_serializes_typed_value() {
#[derive(serde::Serialize)]
struct Payload<'a> {
name: &'a str,
count: u32,
}
let outcome = RpcOutcome::new(
Payload {
name: "atlas",
count: 3,
},
vec![],
);
let out = outcome.into_cli_compatible_json().unwrap();
assert_eq!(out, json!({"name": "atlas", "count": 3}));
}
#[test]
fn into_cli_compatible_json_treats_null_value_as_bare_when_no_logs() {
let outcome: RpcOutcome<Option<i32>> = RpcOutcome::new(None, vec![]);
let out = outcome.into_cli_compatible_json().unwrap();
assert!(out.is_null());
}
#[test]
fn into_cli_compatible_json_preserves_log_order() {
let outcome = RpcOutcome::new(
json!({"ok": true}),
vec!["first".into(), "second".into(), "third".into()],
);
let out = outcome.into_cli_compatible_json().unwrap();
assert_eq!(out["logs"], json!(["first", "second", "third"]));
}
#[test]
fn into_cli_compatible_json_empty_string_logs_still_envelope() {
// An empty log string is still a log — envelope shape must kick in.
let outcome = RpcOutcome::new(json!("x"), vec!["".into()]);
let out = outcome.into_cli_compatible_json().unwrap();
assert!(out.get("result").is_some());
assert_eq!(out["logs"], json!([""]));
}
}