Skip to main content

tokmd_ffi_envelope/
lib.rs

1//! JSON envelope parsing/extraction helpers for tokmd FFI bindings.
2//!
3//! This crate centralizes handling of the `{"ok": bool, "data": ..., "error": ...}`
4//! response envelope used by `tokmd_core::ffi::run_json`.
5
6#![forbid(unsafe_code)]
7
8use serde_json::Value;
9
10/// Errors produced while parsing or extracting a response envelope.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum EnvelopeExtractError {
13    /// Input could not be parsed as JSON.
14    JsonParse(String),
15    /// Extracted value could not be serialized back to JSON.
16    JsonSerialize(String),
17    /// Envelope is not a JSON object.
18    InvalidResponseFormat,
19    /// Upstream returned `{ "ok": false, "error": ... }`.
20    Upstream(String),
21}
22
23impl std::fmt::Display for EnvelopeExtractError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::JsonParse(err) => write!(f, "JSON parse error: {err}"),
27            Self::JsonSerialize(err) => write!(f, "JSON serialize error: {err}"),
28            Self::InvalidResponseFormat => write!(f, "Invalid response format"),
29            Self::Upstream(msg) => write!(f, "{msg}"),
30        }
31    }
32}
33
34impl std::error::Error for EnvelopeExtractError {}
35
36/// Parse a JSON envelope.
37///
38/// # Examples
39///
40/// ```
41/// use tokmd_ffi_envelope::parse_envelope;
42///
43/// let val = parse_envelope(r#"{"ok": true, "data": 42}"#).unwrap();
44/// assert_eq!(val["ok"], true);
45/// assert_eq!(val["data"], 42);
46///
47/// // Invalid JSON returns an error
48/// assert!(parse_envelope("{not json").is_err());
49/// ```
50pub fn parse_envelope(result_json: &str) -> Result<Value, EnvelopeExtractError> {
51    serde_json::from_str(result_json)
52        .map_err(|err| EnvelopeExtractError::JsonParse(err.to_string()))
53}
54
55/// Format an upstream error object into a stable message.
56///
57/// Expected shape: `{"code": "...", "message": "..."}`.
58/// Falls back to `"Unknown error"` when missing or invalid.
59///
60/// # Examples
61///
62/// ```
63/// use tokmd_ffi_envelope::format_error_message;
64/// use serde_json::json;
65///
66/// let err = json!({"code": "scan_failed", "message": "Path not found"});
67/// assert_eq!(format_error_message(Some(&err)), "[scan_failed] Path not found");
68///
69/// // Missing fields fall back to defaults
70/// assert_eq!(format_error_message(None), "Unknown error");
71/// ```
72pub fn format_error_message(error_obj: Option<&Value>) -> String {
73    let Some(error_obj) = error_obj else {
74        return "Unknown error".to_string();
75    };
76    let Some(error_obj) = error_obj.as_object() else {
77        return "Unknown error".to_string();
78    };
79
80    let code = error_obj
81        .get("code")
82        .and_then(Value::as_str)
83        .unwrap_or("unknown");
84    let message = error_obj
85        .get("message")
86        .and_then(Value::as_str)
87        .unwrap_or("Unknown error");
88    format!("[{code}] {message}")
89}
90
91/// Extract `data` from an already-parsed envelope.
92///
93/// Rules:
94/// - If `ok` is true and `data` exists, return `data`.
95/// - If `ok` is true and `data` is missing, return the full envelope unchanged.
96/// - Otherwise return an `Upstream` error with a normalized message.
97///
98/// # Examples
99///
100/// ```
101/// use tokmd_ffi_envelope::extract_data;
102/// use serde_json::json;
103///
104/// let envelope = json!({"ok": true, "data": {"count": 5}});
105/// let data = extract_data(envelope).unwrap();
106/// assert_eq!(data["count"], 5);
107///
108/// // An error envelope returns Err
109/// let fail = json!({"ok": false, "error": {"code": "e", "message": "boom"}});
110/// assert!(extract_data(fail).is_err());
111/// ```
112pub fn extract_data(envelope: Value) -> Result<Value, EnvelopeExtractError> {
113    let Some(obj) = envelope.as_object() else {
114        return Err(EnvelopeExtractError::InvalidResponseFormat);
115    };
116
117    let ok = obj.get("ok").and_then(Value::as_bool).unwrap_or(false);
118    if ok {
119        if let Some(data) = obj.get("data") {
120            return Ok(data.clone());
121        }
122        return Ok(envelope);
123    }
124
125    Err(EnvelopeExtractError::Upstream(format_error_message(
126        obj.get("error"),
127    )))
128}
129
130/// Parse and extract from a JSON envelope string.
131///
132/// # Examples
133///
134/// ```
135/// use tokmd_ffi_envelope::extract_data_from_json;
136///
137/// let json_str = r#"{"ok": true, "data": {"mode": "lang"}}"#;
138/// let data = extract_data_from_json(json_str).unwrap();
139/// assert_eq!(data["mode"], "lang");
140/// ```
141pub fn extract_data_from_json(result_json: &str) -> Result<Value, EnvelopeExtractError> {
142    let envelope = parse_envelope(result_json)?;
143    extract_data(envelope)
144}
145
146/// Parse and extract, returning a JSON-encoded data payload.
147///
148/// # Examples
149///
150/// ```
151/// use tokmd_ffi_envelope::extract_data_json;
152///
153/// let input = r#"{"ok": true, "data": {"v": 1}}"#;
154/// let json_out = extract_data_json(input).unwrap();
155/// let parsed: serde_json::Value = serde_json::from_str(&json_out).unwrap();
156/// assert_eq!(parsed["v"], 1);
157/// ```
158pub fn extract_data_json(result_json: &str) -> Result<String, EnvelopeExtractError> {
159    let data = extract_data_from_json(result_json)?;
160    serde_json::to_string(&data).map_err(|err| EnvelopeExtractError::JsonSerialize(err.to_string()))
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use serde_json::json;
167
168    #[test]
169    fn parse_envelope_invalid_json_errors() {
170        let err = parse_envelope("{").unwrap_err();
171        assert!(matches!(err, EnvelopeExtractError::JsonParse(_)));
172        assert!(err.to_string().contains("JSON parse error"));
173    }
174
175    #[test]
176    fn extract_data_success_returns_data() {
177        let envelope = json!({
178            "ok": true,
179            "data": { "mode": "version" }
180        });
181        let data = extract_data(envelope).unwrap();
182        assert_eq!(data["mode"], "version");
183    }
184
185    #[test]
186    fn extract_data_success_without_data_returns_envelope() {
187        let envelope = json!({
188            "ok": true,
189            "mode": "version"
190        });
191        let data = extract_data(envelope.clone()).unwrap();
192        assert_eq!(data, envelope);
193    }
194
195    #[test]
196    fn extract_data_error_formats_message() {
197        let envelope = json!({
198            "ok": false,
199            "error": { "code": "unknown_mode", "message": "Unknown mode: nope" }
200        });
201        let err = extract_data(envelope).unwrap_err();
202        assert_eq!(
203            err,
204            EnvelopeExtractError::Upstream("[unknown_mode] Unknown mode: nope".to_string())
205        );
206    }
207
208    #[test]
209    fn extract_data_non_object_is_invalid_format() {
210        let err = extract_data(json!(["not", "an", "envelope"])).unwrap_err();
211        assert_eq!(err, EnvelopeExtractError::InvalidResponseFormat);
212    }
213
214    #[test]
215    fn format_error_message_defaults_when_missing_fields() {
216        let missing = json!({});
217        assert_eq!(
218            format_error_message(Some(&missing)),
219            "[unknown] Unknown error"
220        );
221        assert_eq!(format_error_message(None), "Unknown error");
222        assert_eq!(format_error_message(Some(&json!("boom"))), "Unknown error");
223    }
224
225    #[test]
226    fn extract_data_json_serializes_payload() {
227        let envelope = json!({
228            "ok": true,
229            "data": { "a": 1, "b": true }
230        });
231        let encoded = extract_data_json(&envelope.to_string()).unwrap();
232        let parsed: Value = serde_json::from_str(&encoded).unwrap();
233        assert_eq!(parsed["a"], 1);
234        assert_eq!(parsed["b"], true);
235    }
236}