Skip to main content

ff_script/
result.rs

1//! Parse Valkey FCALL return values into structured results.
2//!
3//! Lua return convention (RFC-010 ยง4.9):
4//!   Success: {1, "OK", ...values}       or {1, "ALREADY_SATISFIED", ...}
5//!   Failure: {0, "ERROR_NAME", ...context}
6//!
7//! The FCALL result comes back as a ferriskey `Value::Array`.
8
9use crate::error::ScriptError;
10use ferriskey::Value;
11
12/// Parsed FCALL result from a FlowFabric Lua function.
13#[derive(Debug)]
14pub struct FcallResult {
15    /// 1 = success, 0 = failure.
16    pub success: bool,
17    /// Status string: "OK", "ALREADY_SATISFIED", "DUPLICATE", or error code.
18    pub status: String,
19    /// Remaining fields after status code and status string.
20    pub fields: Vec<Value>,
21}
22
23impl FcallResult {
24    /// Parse a raw `Value` returned by `FCALL` into a structured result.
25    pub fn parse(raw: &Value) -> Result<Self, ScriptError> {
26        let items = match raw {
27            Value::Array(arr) => arr,
28            // ff_version returns a bare string, not an array.
29            // Individual callers handle that; this parser is for the
30            // standard {status_code, status_string, ...} convention.
31            _ => {
32                return Err(ScriptError::Parse(format!(
33                    "expected Array, got {:?}",
34                    value_type_name(raw)
35                )));
36            }
37        };
38
39        if items.is_empty() {
40            return Err(ScriptError::Parse("empty FCALL result array".into()));
41        }
42
43        // Element [0]: status code (Int 1 or 0)
44        let status_code = match items.first() {
45            Some(Ok(Value::Int(n))) => *n,
46            other => {
47                return Err(ScriptError::Parse(format!(
48                    "expected Int at index 0, got {:?}",
49                    other
50                )));
51            }
52        };
53
54        // Element [1]: status string
55        let status = if items.len() > 1 {
56            value_to_string(items[1].as_ref().ok())
57        } else {
58            String::new()
59        };
60
61        // Remaining elements
62        let fields: Vec<Value> = items
63            .iter()
64            .skip(2)
65            .filter_map(|r| r.as_ref().ok().cloned())
66            .collect();
67
68        Ok(FcallResult {
69            success: status_code == 1,
70            status,
71            fields,
72        })
73    }
74
75    /// If this is a failure result, convert the status string to a ScriptError.
76    /// Returns Ok(self) if success.
77    ///
78    /// For variants carrying a detail payload (e.g. `CapabilityMismatch`,
79    /// `InvalidCapabilities`, `InvalidInput`) the first Lua-return field
80    /// is folded into the variant so callers can log the specifics of WHY
81    /// the call failed (which caps were missing, which bound was violated)
82    /// instead of an empty String placeholder.
83    pub fn into_success(self) -> Result<Self, ScriptError> {
84        if self.success {
85            Ok(self)
86        } else {
87            let detail = value_to_string(self.fields.first());
88            let err = ScriptError::from_code_with_detail(&self.status, &detail)
89                .unwrap_or_else(|| {
90                    ScriptError::Parse(format!("unknown error code: {}", self.status))
91                });
92            Err(err)
93        }
94    }
95
96    /// Get a field as a string, or empty string if missing.
97    pub fn field_str(&self, index: usize) -> String {
98        self.fields
99            .get(index)
100            .map(|v| value_to_string(Some(v)))
101            .unwrap_or_default()
102    }
103}
104
105/// Trait for converting a raw FCALL `Value` into a typed result.
106///
107/// Each contract Result type (e.g. `CreateExecutionResult`, `CompleteExecutionResult`)
108/// implements this trait to parse the Lua return into the appropriate Rust enum variant.
109pub trait FromFcallResult: Sized {
110    fn from_fcall_result(raw: &Value) -> Result<Self, ScriptError>;
111}
112
113/// Extract a string from a Value.
114fn value_to_string(v: Option<&Value>) -> String {
115    match v {
116        Some(Value::BulkString(b)) => String::from_utf8_lossy(b).into_owned(),
117        Some(Value::SimpleString(s)) => s.clone(),
118        Some(Value::Int(n)) => n.to_string(),
119        Some(Value::Okay) => "OK".into(),
120        _ => String::new(),
121    }
122}
123
124/// Type name for error messages.
125fn value_type_name(v: &Value) -> &'static str {
126    match v {
127        Value::Nil => "Nil",
128        Value::Int(_) => "Int",
129        Value::BulkString(_) => "BulkString",
130        Value::Array(_) => "Array",
131        Value::SimpleString(_) => "SimpleString",
132        Value::Okay => "Okay",
133        Value::Map(_) => "Map",
134        Value::Set(_) => "Set",
135        Value::Double(_) => "Double",
136        Value::Boolean(_) => "Boolean",
137        Value::BigNumber(_) => "BigNumber",
138        Value::VerbatimString { .. } => "VerbatimString",
139        Value::Attribute { .. } => "Attribute",
140        Value::Push { .. } => "Push",
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn ok_value(fields: Vec<Value>) -> Value {
149        let mut arr: Vec<Result<Value, ferriskey::Error>> = vec![
150            Ok(Value::Int(1)),
151            Ok(Value::BulkString("OK".into())),
152        ];
153        for f in fields {
154            arr.push(Ok(f));
155        }
156        Value::Array(arr)
157    }
158
159    fn err_value(code: &str) -> Value {
160        Value::Array(vec![
161            Ok(Value::Int(0)),
162            Ok(Value::BulkString(Vec::from(code.as_bytes()).into())),
163        ])
164    }
165
166    #[test]
167    fn parse_success() {
168        let raw = ok_value(vec![Value::BulkString("hello".into())]);
169        let result = FcallResult::parse(&raw).unwrap();
170        assert!(result.success);
171        assert_eq!(result.status, "OK");
172        assert_eq!(result.fields.len(), 1);
173        assert_eq!(result.field_str(0), "hello");
174    }
175
176    #[test]
177    fn parse_error() {
178        let raw = err_value("stale_lease");
179        let result = FcallResult::parse(&raw).unwrap();
180        assert!(!result.success);
181        assert_eq!(result.status, "stale_lease");
182    }
183
184    #[test]
185    fn into_success_ok() {
186        let raw = ok_value(vec![]);
187        let result = FcallResult::parse(&raw).unwrap().into_success();
188        assert!(result.is_ok());
189    }
190
191    #[test]
192    fn into_success_err() {
193        let raw = err_value("lease_expired");
194        let result = FcallResult::parse(&raw).unwrap().into_success();
195        assert!(result.is_err());
196        assert!(matches!(result.unwrap_err(), ScriptError::LeaseExpired));
197    }
198
199    #[test]
200    fn parse_non_array_fails() {
201        let raw = Value::SimpleString("hello".into());
202        let result = FcallResult::parse(&raw);
203        assert!(result.is_err());
204    }
205}