Skip to main content

plexus_core/plexus/
errors.rs

1//! DEPRECATED: Stream-based guidance replaces error data structures
2//!
3//! This module is kept for historical reference only. Error guidance is now
4//! provided via `GuidanceErrorType` and `GuidanceSuggestion` in stream events.
5//!
6//! **Migration:** Use `PlexusStreamEvent::Guidance` instead of parsing error data.
7//! See: `docs/architecture/16680880693241553663_frontend-guidance-migration.md`
8//!
9//! ---
10//!
11//! ## Legacy Documentation
12
13#![allow(dead_code)]
14
15use jsonrpsee::types::ErrorObjectOwned;
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18
19/// Standard JSON-RPC 2.0 error codes
20pub mod codes {
21    /// Invalid JSON was received (parse error)
22    pub const PARSE_ERROR: i32 = -32700;
23    /// The JSON sent is not a valid Request object
24    pub const INVALID_REQUEST: i32 = -32600;
25    /// The method does not exist / is not available
26    pub const METHOD_NOT_FOUND: i32 = -32601;
27    /// Invalid method parameter(s)
28    pub const INVALID_PARAMS: i32 = -32602;
29    /// Internal JSON-RPC error
30    pub const INTERNAL_ERROR: i32 = -32603;
31}
32
33/// A suggested JSON-RPC request to try next
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TryRequest {
36    pub jsonrpc: String,
37    pub id: u64,
38    pub method: String,
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub params: Vec<Value>,
41}
42
43impl TryRequest {
44    /// Create a try request for plexus.schema (the discovery endpoint)
45    pub fn schema() -> Self {
46        Self {
47            jsonrpc: "2.0".to_string(),
48            id: 1,
49            method: "plexus.schema".to_string(),
50            params: vec![],
51        }
52    }
53
54    /// Create a try request for a specific method with example params
55    pub fn method(method: &str, params: Vec<Value>) -> Self {
56        Self {
57            jsonrpc: "2.0".to_string(),
58            id: 1,
59            method: method.to_string(),
60            params,
61        }
62    }
63}
64
65/// Error data with `try` field for guided discovery
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct GuidedErrorData {
68    /// Suggested request to try next
69    #[serde(rename = "try")]
70    pub try_request: TryRequest,
71
72    /// Additional context-specific fields (flattened into the data object)
73    #[serde(flatten)]
74    pub context: Value,
75}
76
77impl GuidedErrorData {
78    /// Create error data with just a try request
79    pub fn new(try_request: TryRequest) -> Self {
80        Self {
81            try_request,
82            context: json!({}),
83        }
84    }
85
86    /// Create error data with additional context
87    pub fn with_context(try_request: TryRequest, context: Value) -> Self {
88        Self {
89            try_request,
90            context,
91        }
92    }
93}
94
95/// Builder for creating guided JSON-RPC errors
96pub struct GuidedError;
97
98impl GuidedError {
99    /// Parse error - client sent non-JSON-RPC content
100    pub fn parse_error(message: &str) -> ErrorObjectOwned {
101        let data = GuidedErrorData::new(TryRequest::schema());
102        ErrorObjectOwned::owned(
103            codes::PARSE_ERROR,
104            format!("Parse error: {}. This server speaks JSON-RPC 2.0 over WebSocket", message),
105            Some(data),
106        )
107    }
108
109    /// Invalid request - malformed JSON-RPC request
110    pub fn invalid_request(message: &str) -> ErrorObjectOwned {
111        let data = GuidedErrorData::new(TryRequest::schema());
112        ErrorObjectOwned::owned(
113            codes::INVALID_REQUEST,
114            format!("Invalid request: {}", message),
115            Some(data),
116        )
117    }
118
119    /// Activation not found - the namespace doesn't exist
120    pub fn activation_not_found(activation: &str, available: Vec<String>) -> ErrorObjectOwned {
121        let data = GuidedErrorData::with_context(
122            TryRequest::schema(),
123            json!({
124                "activation": activation,
125                "available_activations": available,
126            }),
127        );
128        ErrorObjectOwned::owned(
129            codes::METHOD_NOT_FOUND,
130            format!("Activation '{}' not found", activation),
131            Some(data),
132        )
133    }
134
135    /// Method not found - the activation exists but not the method
136    pub fn method_not_found(
137        activation: &str,
138        method: &str,
139        available_methods: Vec<String>,
140        example_method: Option<(&str, Vec<Value>)>,
141    ) -> ErrorObjectOwned {
142        // If we have an example method, suggest trying it; otherwise suggest schema
143        let try_request = match example_method {
144            Some((method_name, params)) => TryRequest::method(method_name, params),
145            None => TryRequest::schema(),
146        };
147
148        let data = GuidedErrorData::with_context(
149            try_request,
150            json!({
151                "activation": activation,
152                "method": method,
153                "available_methods": available_methods,
154            }),
155        );
156        ErrorObjectOwned::owned(
157            codes::METHOD_NOT_FOUND,
158            format!("Method '{}' not found in activation '{}'", method, activation),
159            Some(data),
160        )
161    }
162
163    /// Invalid params - the method exists but params are wrong
164    pub fn invalid_params(
165        method: &str,
166        message: &str,
167        usage: Option<&str>,
168        example: Option<TryRequest>,
169    ) -> ErrorObjectOwned {
170        let try_request = example.unwrap_or_else(TryRequest::schema);
171
172        let mut context = json!({
173            "method": method,
174        });
175
176        if let Some(usage_str) = usage {
177            context["usage"] = json!(usage_str);
178        }
179
180        let data = GuidedErrorData::with_context(try_request, context);
181        ErrorObjectOwned::owned(
182            codes::INVALID_PARAMS,
183            format!("Invalid params for {}: {}", method, message),
184            Some(data),
185        )
186    }
187
188    /// Internal/execution error
189    pub fn internal_error(message: &str) -> ErrorObjectOwned {
190        let data = GuidedErrorData::new(TryRequest::schema());
191        ErrorObjectOwned::owned(
192            codes::INTERNAL_ERROR,
193            format!("Internal error: {}", message),
194            Some(data),
195        )
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_parse_error_includes_try() {
205        let error = GuidedError::parse_error("invalid JSON");
206        let data: GuidedErrorData = serde_json::from_str(
207            error.data().unwrap().get()
208        ).unwrap();
209
210        assert_eq!(data.try_request.method, "plexus.schema");
211    }
212
213    #[test]
214    fn test_activation_not_found_includes_available() {
215        let error = GuidedError::activation_not_found(
216            "foo",
217            vec!["arbor".into(), "bash".into(), "health".into()],
218        );
219        let data: GuidedErrorData = serde_json::from_str(
220            error.data().unwrap().get()
221        ).unwrap();
222
223        assert_eq!(data.try_request.method, "plexus.schema");
224        assert_eq!(data.context["available_activations"].as_array().unwrap().len(), 3);
225    }
226
227    #[test]
228    fn test_method_not_found_with_example() {
229        let error = GuidedError::method_not_found(
230            "bash",
231            "foo",
232            vec!["execute".into()],
233            Some(("bash_execute", vec![json!("echo hello")])),
234        );
235        let data: GuidedErrorData = serde_json::from_str(
236            error.data().unwrap().get()
237        ).unwrap();
238
239        // Should suggest the example method, not schema
240        assert_eq!(data.try_request.method, "bash_execute");
241        assert_eq!(data.try_request.params[0], json!("echo hello"));
242    }
243}