Skip to main content

rialo_types/
output.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Output Module
5//!
6//! Defines the output format for oracle results.
7//!
8//! This module provides the [`OracleOutput`] enum which represents all possible
9//! outcomes from an oracle execution. The output is designed to be:
10//! - Self-describing with status tags
11//! - Easily serializable to JSON
12//! - Type-safe with exhaustive matching
13//!
14//! ## Output Types
15//!
16//! - `Success` - The oracle executed successfully and returned data
17//! - `OracleError` - The oracle encountered an error during execution
18//! - `UnserializableResponse` - The oracle produced output that couldn't be serialized
19//!
20//! ## Serialization Format
21//!
22//! All outputs serialize to a consistent JSON structure:
23//! ```json
24//! {
25//!   "status": "success|oracle-error|unserializable-response",
26//!   "contents": <actual data or error details>
27//! }
28//! ```
29
30use std::{fmt, fmt::Debug};
31
32use borsh::{BorshDeserialize, BorshSerialize};
33use chrono::SecondsFormat;
34use rialo_cli_representable::Representable;
35use serde::{Deserialize, Serialize};
36
37use crate::OracleError;
38
39/// Result for filtering operations.
40#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
41pub enum FilterResult {
42    Success(Vec<u8>),
43    Error(String),
44}
45
46impl FilterResult {
47    /// Returns true if the filter result is a success.
48    pub fn is_success(&self) -> bool {
49        matches!(self, FilterResult::Success(_))
50    }
51
52    /// Returns true if the filter result is an error.
53    pub fn is_error(&self) -> bool {
54        matches!(self, FilterResult::Error(_))
55    }
56
57    /// Returns the contained bytes if the filter result is a success, otherwise returns `None`.
58    pub fn as_success(&self) -> Option<&Vec<u8>> {
59        if let FilterResult::Success(ref bytes) = self {
60            Some(bytes)
61        } else {
62            None
63        }
64    }
65
66    /// Returns the contained error message if the filter result is an error, otherwise returns `None`.
67    pub fn as_error(&self) -> Option<&String> {
68        if let FilterResult::Error(ref err) = self {
69            Some(err)
70        } else {
71            None
72        }
73    }
74}
75
76impl fmt::Display for FilterResult {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            FilterResult::Success(bytes) => write!(f, "Success: {:?}", bytes),
80            FilterResult::Error(err) => write!(f, "Error: {}", err),
81        }
82    }
83}
84
85impl From<Result<Vec<u8>, String>> for FilterResult {
86    fn from(result: Result<Vec<u8>, String>) -> Self {
87        match result {
88            Ok(bytes) => FilterResult::Success(bytes),
89            Err(err) => FilterResult::Error(err),
90        }
91    }
92}
93
94/// Represents the data returned by an oracle.
95/// This is a vector of bytes that can contain any data returned by the oracle.
96#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
97pub enum OracleData {
98    /// The raw bytes returned by the oracle.
99    Raw(Vec<u8>),
100    /// The filtered bytes returned by the oracle.
101    Filtered(Vec<FilterResult>),
102}
103
104impl OracleData {
105    /// Returns true if the oracle data is raw.
106    pub fn is_raw(&self) -> bool {
107        matches!(self, OracleData::Raw(_))
108    }
109
110    /// Returns true if the oracle data is filtered.
111    pub fn is_filtered(&self) -> bool {
112        matches!(self, OracleData::Filtered(_))
113    }
114
115    /// Returns the contained raw bytes if the oracle data is raw, otherwise returns `None`.
116    pub fn as_raw(&self) -> Option<&Vec<u8>> {
117        if let OracleData::Raw(ref data) = self {
118            Some(data)
119        } else {
120            None
121        }
122    }
123
124    /// Returns the contained filtered results if the oracle data is filtered, otherwise returns `None`.
125    pub fn as_filtered(&self) -> Option<&Vec<FilterResult>> {
126        if let OracleData::Filtered(ref results) = self {
127            Some(results)
128        } else {
129            None
130        }
131    }
132
133    /// Returns the length of the contained data in bytes.
134    pub fn len(&self) -> usize {
135        match self {
136            OracleData::Raw(data) => data.len(),
137            OracleData::Filtered(results) => results
138                .iter()
139                .map(|r| match r {
140                    FilterResult::Success(bytes) => bytes.len(),
141                    FilterResult::Error(err) => err.len(),
142                })
143                .sum(),
144        }
145    }
146
147    /// Returns true if the contained data is empty.
148    pub fn is_empty(&self) -> bool {
149        self.len() == 0
150    }
151}
152
153/// The response received from an oracle.
154///
155/// This data structure is used to represent the successful output of an oracle request.
156#[derive(
157    Clone,
158    Debug,
159    Eq,
160    PartialEq,
161    Serialize,
162    Deserialize,
163    BorshSerialize,
164    BorshDeserialize,
165    Representable,
166)]
167#[representable(human_readable = "oracle_response_human_readable")]
168pub struct OracleResponse {
169    /// The raw response data from the oracle, encoded as bytes.
170    pub response: OracleData,
171    /// The timestamp when the oracle response was generated.
172    pub timestamp: String,
173}
174
175fn oracle_response_human_readable(response: &OracleResponse) -> String {
176    let mut out = String::new();
177    out.push_str(&format!(
178        "Response: {:?}, Timestamp (from TEE): {}",
179        response.response, response.timestamp
180    ));
181    out
182}
183
184impl OracleResponse {
185    /// Creates a new `OracleResponse` with the given response data and timestamp.
186    pub fn new(response: OracleData) -> Self {
187        Self {
188            response,
189            // Always uses 'Z' suffix and 6 decimal places (microseconds)
190            // Produces exactly 30 bytes: "2025-08-24T10:59:40.123456Z"
191            timestamp: chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true),
192        }
193    }
194
195    /// Convert to successful response data. In case of `OracleData::Raw` returns a vector of one
196    /// element containing the raw bytes. In case of `OracleData::Filtered` returns a vector of all
197    /// successful filtered bytes.
198    ///
199    /// If there are no successful responses, returns an empty vector.
200    pub fn into_successful_responses(self) -> Vec<Vec<u8>> {
201        match self.response {
202            OracleData::Raw(data) => vec![data],
203            OracleData::Filtered(results) => results
204                .into_iter()
205                .filter_map(|r| {
206                    if let FilterResult::Success(bytes) = r {
207                        Some(bytes)
208                    } else {
209                        None
210                    }
211                })
212                .collect(),
213        }
214    }
215}
216
217/// The output from an oracle.
218///
219/// This enum represents the possible outcomes of an oracle execution.
220#[derive(
221    Clone,
222    Debug,
223    Eq,
224    PartialEq,
225    Serialize,
226    Deserialize,
227    BorshSerialize,
228    BorshDeserialize,
229    Representable,
230)]
231#[serde(tag = "status", content = "contents")]
232#[serde(rename_all = "kebab-case")]
233#[non_exhaustive]
234#[representable(human_readable = "oracle_output_human_readable")]
235pub enum OracleOutput {
236    /// The response of a successfully handled oracle request.
237    Success(OracleResponse),
238    /// The details of an error reported by an oracle.
239    OracleError(OracleError),
240    /// The oracle output an unserializable response.
241    UnserializableResponse(String),
242}
243
244fn oracle_output_human_readable(output: &OracleOutput) -> String {
245    match output {
246        OracleOutput::Success(resp) => {
247            let len = resp.response.len();
248            format!("Success: {} bytes @ {}", len, resp.timestamp)
249        }
250        OracleOutput::OracleError(err) => format!("OracleError: {}", err),
251        OracleOutput::UnserializableResponse(msg) => {
252            format!("UnserializableResponse: {}", msg)
253        }
254    }
255}
256
257impl OracleOutput {
258    /// Returns true if the output is a success.
259    pub fn is_success(&self) -> bool {
260        matches!(self, OracleOutput::Success(_))
261    }
262
263    /// Returns true if the output is an oracle error.
264    pub fn is_oracle_error(&self) -> bool {
265        matches!(self, OracleOutput::OracleError(_))
266    }
267
268    /// Returns true if the output is an unserializable response.
269    pub fn is_unserializable_response(&self) -> bool {
270        matches!(self, OracleOutput::UnserializableResponse(_))
271    }
272
273    /// Returns the contained `OracleResponse` if the output is a success, otherwise returns `None`.
274    pub fn as_success(&self) -> Option<&OracleResponse> {
275        if let OracleOutput::Success(ref response) = self {
276            Some(response)
277        } else {
278            None
279        }
280    }
281
282    /// Returns the contained `OracleError` if the output is an oracle error, otherwise returns `None`.
283    pub fn as_oracle_error(&self) -> Option<&OracleError> {
284        if let OracleOutput::OracleError(ref error) = self {
285            Some(error)
286        } else {
287            None
288        }
289    }
290
291    /// Converts to successful response data if the output is a success, and we have successful `OracleData`.
292    /// Otherwise, returns `None`.
293    pub fn into_success_payload(self) -> Option<Vec<Vec<u8>>> {
294        match self {
295            OracleOutput::Success(response) => Some(response.into_successful_responses()),
296            _ => None,
297        }
298    }
299}
300
301impl From<Result<OracleResponse, OracleError>> for OracleOutput {
302    fn from(result: Result<OracleResponse, OracleError>) -> Self {
303        match result {
304            Ok(response) => OracleOutput::Success(response),
305            Err(error) => OracleOutput::OracleError(error),
306        }
307    }
308}
309
310impl From<OracleError> for OracleOutput {
311    fn from(error: OracleError) -> Self {
312        OracleOutput::OracleError(error)
313    }
314}
315
316#[cfg(test)]
317mod test {
318    use serde_json::json;
319
320    use super::*;
321
322    #[test]
323    fn test_oracle_result_success_serialization() {
324        let oracle_data = OracleResponse::new(OracleData::Raw(
325            serde_json::to_vec(&json!({"data": 42, "message": "test"})).unwrap(),
326        ));
327        let result = OracleOutput::Success(oracle_data.clone());
328        let json = serde_json::to_value(&result).unwrap();
329
330        assert_eq!(
331            json,
332            json!({
333                "status": "success",
334                "contents": oracle_data
335            })
336        );
337    }
338
339    #[test]
340    fn test_oracle_result_error_serialization() {
341        let error = OracleError::HttpStatusError {
342            status: 404,
343            reason: String::from("HTTP Status Code 404 Not Found"),
344        };
345
346        let result = OracleOutput::OracleError(error.clone());
347        let json = serde_json::to_value(&result).unwrap();
348
349        assert_eq!(
350            json,
351            json!({
352                "status": "oracle-error",
353                "contents": error
354            })
355        );
356    }
357
358    #[test]
359    fn test_oracle_result_unserializable_serialization() {
360        let result = OracleOutput::UnserializableResponse("Failed to parse response".to_string());
361        let json = serde_json::to_value(&result).unwrap();
362
363        assert_eq!(
364            json,
365            json!({
366                "status": "unserializable-response",
367                "contents": "Failed to parse response"
368            })
369        );
370    }
371
372    #[test]
373    fn test_oracle_result_deserialization() {
374        // Test deserializing success
375        let expected = OracleResponse::new(OracleData::Raw(
376            serde_json::to_vec(&json!({"data": 123 })).unwrap(),
377        ));
378        let expected_clone = expected.clone();
379        let json = json!({
380            "status": "success",
381            "contents": expected_clone
382        });
383        let result: OracleOutput = serde_json::from_value(json).unwrap();
384        assert_eq!(result, OracleOutput::Success(expected_clone));
385
386        // Test deserializing error
387        let error = OracleError::HttpStatusError {
388            status: 500,
389            reason: "Internal Server Error".to_string(),
390        };
391        let json = json!({
392            "status": "oracle-error",
393            "contents": error.clone()
394        });
395        let result: OracleOutput = serde_json::from_value(json).unwrap();
396        assert_eq!(result, OracleOutput::OracleError(error));
397
398        // Test deserializing unserializable response
399        let json = json!({
400            "status": "unserializable-response",
401            "contents": "Parse error"
402        });
403        let result: OracleOutput = serde_json::from_value(json).unwrap();
404        assert_eq!(
405            result,
406            OracleOutput::UnserializableResponse("Parse error".to_string())
407        );
408    }
409
410    #[test]
411    fn test_oracle_result_roundtrip() {
412        let oracle_response = OracleResponse::new(OracleData::Raw(
413            serde_json::to_vec(&json!({"key": "value", "number": 42})).unwrap(),
414        ));
415        let error = OracleError::HttpStatusError {
416            status: 403,
417            reason: "Forbidden".to_string(),
418        };
419        let test_cases = vec![
420            OracleOutput::Success(oracle_response),
421            OracleOutput::OracleError(error),
422            OracleOutput::UnserializableResponse("Invalid JSON".to_string()),
423        ];
424
425        for original in test_cases {
426            let serialized = serde_json::to_string(&original).unwrap();
427            let deserialized: OracleOutput = serde_json::from_str(&serialized).unwrap();
428            assert_eq!(original, deserialized);
429        }
430    }
431
432    #[test]
433    fn test_oracle_result_complex_nested_values() {
434        let complex_value = json!({
435            "nested": {
436                "array": [1, 2, 3],
437                "object": {"key": "value"},
438                "null": null,
439                "bool": true
440            }
441        });
442        let oracle_response =
443            OracleResponse::new(OracleData::Raw(serde_json::to_vec(&complex_value).unwrap()));
444
445        let result = OracleOutput::Success(oracle_response.clone());
446        let json = serde_json::to_value(&result).unwrap();
447
448        assert_eq!(json["status"], "success");
449        assert_eq!(json["contents"], json!(oracle_response));
450    }
451
452    #[test]
453    fn test_oracle_result_empty_values() {
454        // Test with empty object
455        let oracle_response =
456            OracleResponse::new(OracleData::Raw(serde_json::to_vec(&json!({})).unwrap()));
457        let result = OracleOutput::Success(oracle_response.clone());
458        let json = serde_json::to_value(&result).unwrap();
459        assert_eq!(
460            json,
461            json!({
462                "status": "success",
463                "contents": oracle_response
464            })
465        );
466
467        // Test with empty string
468        let result = OracleOutput::UnserializableResponse("".to_string());
469        let json = serde_json::to_value(&result).unwrap();
470        assert_eq!(
471            json,
472            json!({
473                "status": "unserializable-response",
474                "contents": ""
475            })
476        );
477    }
478}