Skip to main content

icydb_core/value/
output.rs

1use crate::types::{
2    Account, Date, Decimal, Duration, Float32, Float64, IntBig, NatBig, Principal, Subaccount,
3    Timestamp, Ulid,
4};
5use candid::CandidType;
6use serde::Deserialize;
7
8#[cfg(test)]
9use crate::value::Value;
10
11//
12// OutputValue
13//
14// Public output-side value boundary used by API and wire surfaces.
15// This stays separate from runtime `Value` so public result payloads can move
16// off the internal execution representation without forcing a storage or
17// planner rewrite in the same slice.
18//
19
20#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
21pub enum OutputValue {
22    Account(Account),
23    Blob(Vec<u8>),
24    Bool(bool),
25    Date(Date),
26    Decimal(Decimal),
27    Duration(Duration),
28    Enum(OutputValueEnum),
29    Float32(Float32),
30    Float64(Float64),
31    #[serde(rename = "Int")]
32    Int64(i64),
33    Int128(i128),
34    IntBig(IntBig),
35    List(Vec<Self>),
36    Map(Vec<(Self, Self)>),
37    Null,
38    Principal(Principal),
39    Subaccount(Subaccount),
40    Text(String),
41    Timestamp(Timestamp),
42    #[serde(rename = "Nat")]
43    Nat64(u64),
44    Nat128(u128),
45    NatBig(NatBig),
46    Ulid(Ulid),
47    Unit,
48}
49
50//
51// OutputValueEnum
52//
53// Output-side enum payload contract paired with `OutputValue`.
54// Payload stays recursive through `OutputValue` so public boundary conversion
55// remains total for data-carrying enum values already representable by
56// runtime `Value`.
57//
58
59#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
60pub struct OutputValueEnum {
61    variant: String,
62    path: Option<String>,
63    payload: Option<Box<OutputValue>>,
64}
65
66impl OutputValueEnum {
67    /// Build output enum metadata resolved from one accepted catalog revision.
68    #[must_use]
69    pub(crate) fn from_catalog_parts(
70        variant: &str,
71        path: &str,
72        payload: Option<OutputValue>,
73    ) -> Self {
74        Self {
75            variant: variant.to_string(),
76            path: Some(path.to_string()),
77            payload: payload.map(Box::new),
78        }
79    }
80
81    #[must_use]
82    pub const fn variant(&self) -> &str {
83        self.variant.as_str()
84    }
85
86    #[must_use]
87    pub fn path(&self) -> Option<&str> {
88        self.path.as_deref()
89    }
90
91    #[must_use]
92    pub fn payload(&self) -> Option<&OutputValue> {
93        self.payload.as_deref()
94    }
95}
96
97#[cfg(test)]
98impl From<Value> for OutputValue {
99    fn from(value: Value) -> Self {
100        output_value_from_non_enum_test_value(&value)
101    }
102}
103
104#[cfg(test)]
105fn output_value_from_non_enum_test_value(value: &Value) -> OutputValue {
106    match value {
107        Value::Account(value) => OutputValue::Account(*value),
108        Value::Blob(value) => OutputValue::Blob(value.clone()),
109        Value::Bool(value) => OutputValue::Bool(*value),
110        Value::Date(value) => OutputValue::Date(*value),
111        Value::Decimal(value) => OutputValue::Decimal(*value),
112        Value::Duration(value) => OutputValue::Duration(*value),
113        Value::Enum(_) => panic!("test output conversion requires accepted enum catalog"),
114        Value::Float32(value) => OutputValue::Float32(*value),
115        Value::Float64(value) => OutputValue::Float64(*value),
116        Value::Int64(value) => OutputValue::Int64(*value),
117        Value::Int128(value) => OutputValue::Int128(*value),
118        Value::IntBig(value) => OutputValue::IntBig(value.clone()),
119        Value::List(values) => OutputValue::List(
120            values
121                .iter()
122                .map(output_value_from_non_enum_test_value)
123                .collect(),
124        ),
125        Value::Map(entries) => OutputValue::Map(
126            entries
127                .iter()
128                .map(|(key, value)| {
129                    (
130                        output_value_from_non_enum_test_value(key),
131                        output_value_from_non_enum_test_value(value),
132                    )
133                })
134                .collect(),
135        ),
136        Value::Null => OutputValue::Null,
137        Value::Principal(value) => OutputValue::Principal(*value),
138        Value::Subaccount(value) => OutputValue::Subaccount(*value),
139        Value::Text(value) => OutputValue::Text(value.clone()),
140        Value::Timestamp(value) => OutputValue::Timestamp(*value),
141        Value::Nat64(value) => OutputValue::Nat64(*value),
142        Value::Nat128(value) => OutputValue::Nat128(*value),
143        Value::NatBig(value) => OutputValue::NatBig(value.clone()),
144        Value::Ulid(value) => OutputValue::Ulid(*value),
145        Value::Unit => OutputValue::Unit,
146    }
147}
148
149/// Render one output value into a stable text form for row projection payloads.
150#[must_use]
151pub fn render_output_value_text(value: &OutputValue) -> String {
152    match value {
153        OutputValue::Account(v) => v.to_string(),
154        OutputValue::Blob(v) => render_blob_value(v),
155        OutputValue::Bool(v) => v.to_string(),
156        OutputValue::Date(v) => v.to_string(),
157        OutputValue::Decimal(v) => v.to_string(),
158        OutputValue::Duration(v) => render_duration_value(v.as_millis()),
159        OutputValue::Enum(v) => render_enum(v),
160        OutputValue::Float32(v) => v.to_string(),
161        OutputValue::Float64(v) => v.to_string(),
162        OutputValue::Int64(v) => v.to_string(),
163        OutputValue::Int128(v) => v.to_string(),
164        OutputValue::IntBig(v) => v.to_string(),
165        OutputValue::List(items) => render_list_value(items.as_slice()),
166        OutputValue::Map(entries) => render_map_value(entries.as_slice()),
167        OutputValue::Null => "null".to_string(),
168        OutputValue::Principal(v) => v.to_string(),
169        OutputValue::Subaccount(v) => v.to_string(),
170        OutputValue::Text(v) => v.clone(),
171        OutputValue::Timestamp(v) => v.as_millis().to_string(),
172        OutputValue::Nat64(v) => v.to_string(),
173        OutputValue::Nat128(v) => v.to_string(),
174        OutputValue::NatBig(v) => v.to_string(),
175        OutputValue::Ulid(v) => v.to_string(),
176        OutputValue::Unit => "()".to_string(),
177    }
178}
179
180fn render_blob_value(bytes: &[u8]) -> String {
181    let mut rendered = String::from("0x");
182    rendered.push_str(encode_hex_lower_output_value(bytes).as_str());
183
184    rendered
185}
186
187fn encode_hex_lower_output_value(bytes: &[u8]) -> String {
188    const HEX: &[u8; 16] = b"0123456789abcdef";
189
190    let mut rendered = String::with_capacity(bytes.len().saturating_mul(2));
191    for byte in bytes {
192        let byte = *byte;
193        rendered.push(char::from(HEX[usize::from(byte >> 4)]));
194        rendered.push(char::from(HEX[usize::from(byte & 0x0f)]));
195    }
196
197    rendered
198}
199
200fn render_duration_value(millis: u64) -> String {
201    let mut rendered = millis.to_string();
202    rendered.push_str("ms");
203
204    rendered
205}
206
207fn render_list_value(items: &[OutputValue]) -> String {
208    let mut rendered = String::from("[");
209
210    for (index, item) in items.iter().enumerate() {
211        if index != 0 {
212            rendered.push_str(", ");
213        }
214
215        rendered.push_str(render_output_value_text(item).as_str());
216    }
217
218    rendered.push(']');
219
220    rendered
221}
222
223fn render_map_value(entries: &[(OutputValue, OutputValue)]) -> String {
224    let mut rendered = String::from("{");
225
226    for (index, (key, value)) in entries.iter().enumerate() {
227        if index != 0 {
228            rendered.push_str(", ");
229        }
230
231        rendered.push_str(render_output_value_text(key).as_str());
232        rendered.push_str(": ");
233        rendered.push_str(render_output_value_text(value).as_str());
234    }
235
236    rendered.push('}');
237
238    rendered
239}
240
241fn render_enum(value: &OutputValueEnum) -> String {
242    let mut rendered = String::new();
243    if let Some(path) = value.path() {
244        rendered.push_str(path);
245        rendered.push_str("::");
246    }
247    rendered.push_str(value.variant());
248    if let Some(payload) = value.payload() {
249        rendered.push('(');
250        rendered.push_str(render_output_value_text(payload).as_str());
251        rendered.push(')');
252    }
253
254    rendered
255}
256
257///
258/// TESTS
259///
260
261#[cfg(test)]
262mod tests {
263    use crate::value::{OutputValue, Value};
264
265    #[test]
266    fn output_value_from_runtime_value_keeps_recursive_collection_shape() {
267        let runtime = Value::List(vec![
268            Value::Nat64(7),
269            Value::Map(vec![(Value::Text("x".to_string()), Value::Bool(true))]),
270        ]);
271
272        assert_eq!(
273            OutputValue::from(runtime),
274            OutputValue::List(vec![
275                OutputValue::Nat64(7),
276                OutputValue::Map(vec![(
277                    OutputValue::Text("x".to_string()),
278                    OutputValue::Bool(true),
279                )]),
280            ]),
281        );
282    }
283}