Skip to main content

icydb_core/value/
output.rs

1use crate::{
2    types::{
3        Account, Date, Decimal, Duration, Float32, Float64, IntBig, NatBig, Principal, Subaccount,
4        Timestamp, Ulid,
5    },
6    value::{Value, ValueEnum},
7};
8use candid::CandidType;
9use serde::Deserialize;
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    #[must_use]
68    pub const fn variant(&self) -> &str {
69        self.variant.as_str()
70    }
71
72    #[must_use]
73    pub fn path(&self) -> Option<&str> {
74        self.path.as_deref()
75    }
76
77    #[must_use]
78    pub fn payload(&self) -> Option<&OutputValue> {
79        self.payload.as_deref()
80    }
81}
82
83impl From<Value> for OutputValue {
84    fn from(value: Value) -> Self {
85        match value {
86            Value::Account(value) => Self::Account(value),
87            Value::Blob(value) => Self::Blob(value),
88            Value::Bool(value) => Self::Bool(value),
89            Value::Date(value) => Self::Date(value),
90            Value::Decimal(value) => Self::Decimal(value),
91            Value::Duration(value) => Self::Duration(value),
92            Value::Enum(value) => Self::Enum(OutputValueEnum::from(value)),
93            Value::Float32(value) => Self::Float32(value),
94            Value::Float64(value) => Self::Float64(value),
95            Value::Int64(value) => Self::Int64(value),
96            Value::Int128(value) => Self::Int128(value),
97            Value::IntBig(value) => Self::IntBig(value),
98            Value::List(items) => Self::List(items.into_iter().map(Self::from).collect()),
99            Value::Map(entries) => Self::Map(
100                entries
101                    .into_iter()
102                    .map(|(key, value)| (Self::from(key), Self::from(value)))
103                    .collect(),
104            ),
105            Value::Null => Self::Null,
106            Value::Principal(value) => Self::Principal(value),
107            Value::Subaccount(value) => Self::Subaccount(value),
108            Value::Text(value) => Self::Text(value),
109            Value::Timestamp(value) => Self::Timestamp(value),
110            Value::Nat64(value) => Self::Nat64(value),
111            Value::Nat128(value) => Self::Nat128(value),
112            Value::NatBig(value) => Self::NatBig(value),
113            Value::Ulid(value) => Self::Ulid(value),
114            Value::Unit => Self::Unit,
115        }
116    }
117}
118
119impl From<&Value> for OutputValue {
120    fn from(value: &Value) -> Self {
121        match value {
122            Value::Account(value) => Self::Account(*value),
123            Value::Blob(value) => Self::Blob(value.clone()),
124            Value::Bool(value) => Self::Bool(*value),
125            Value::Date(value) => Self::Date(*value),
126            Value::Decimal(value) => Self::Decimal(*value),
127            Value::Duration(value) => Self::Duration(*value),
128            Value::Enum(value) => Self::Enum(OutputValueEnum::from(value)),
129            Value::Float32(value) => Self::Float32(*value),
130            Value::Float64(value) => Self::Float64(*value),
131            Value::Int64(value) => Self::Int64(*value),
132            Value::Int128(value) => Self::Int128(*value),
133            Value::IntBig(value) => Self::IntBig(value.clone()),
134            Value::List(items) => Self::List(items.iter().map(Self::from).collect()),
135            Value::Map(entries) => Self::Map(
136                entries
137                    .iter()
138                    .map(|(key, value)| (Self::from(key), Self::from(value)))
139                    .collect(),
140            ),
141            Value::Null => Self::Null,
142            Value::Principal(value) => Self::Principal(*value),
143            Value::Subaccount(value) => Self::Subaccount(*value),
144            Value::Text(value) => Self::Text(value.clone()),
145            Value::Timestamp(value) => Self::Timestamp(*value),
146            Value::Nat64(value) => Self::Nat64(*value),
147            Value::Nat128(value) => Self::Nat128(*value),
148            Value::NatBig(value) => Self::NatBig(value.clone()),
149            Value::Ulid(value) => Self::Ulid(*value),
150            Value::Unit => Self::Unit,
151        }
152    }
153}
154
155impl From<ValueEnum> for OutputValueEnum {
156    fn from(value: ValueEnum) -> Self {
157        let ValueEnum {
158            variant,
159            path,
160            payload,
161        } = value;
162
163        Self {
164            variant,
165            path,
166            payload: payload.map(|payload| Box::new(OutputValue::from(*payload))),
167        }
168    }
169}
170
171impl From<&ValueEnum> for OutputValueEnum {
172    fn from(value: &ValueEnum) -> Self {
173        Self {
174            variant: value.variant().to_string(),
175            path: value.path().map(ToString::to_string),
176            payload: value
177                .payload()
178                .map(|payload| Box::new(OutputValue::from(payload))),
179        }
180    }
181}
182
183/// Render one output value into a stable text form for row projection payloads.
184#[must_use]
185pub fn render_output_value_text(value: &OutputValue) -> String {
186    match value {
187        OutputValue::Account(v) => v.to_string(),
188        OutputValue::Blob(v) => render_blob_value(v),
189        OutputValue::Bool(v) => v.to_string(),
190        OutputValue::Date(v) => v.to_string(),
191        OutputValue::Decimal(v) => v.to_string(),
192        OutputValue::Duration(v) => render_duration_value(v.as_millis()),
193        OutputValue::Enum(v) => render_enum(v),
194        OutputValue::Float32(v) => v.to_string(),
195        OutputValue::Float64(v) => v.to_string(),
196        OutputValue::Int64(v) => v.to_string(),
197        OutputValue::Int128(v) => v.to_string(),
198        OutputValue::IntBig(v) => v.to_string(),
199        OutputValue::List(items) => render_list_value(items.as_slice()),
200        OutputValue::Map(entries) => render_map_value(entries.as_slice()),
201        OutputValue::Null => "null".to_string(),
202        OutputValue::Principal(v) => v.to_string(),
203        OutputValue::Subaccount(v) => v.to_string(),
204        OutputValue::Text(v) => v.clone(),
205        OutputValue::Timestamp(v) => v.as_millis().to_string(),
206        OutputValue::Nat64(v) => v.to_string(),
207        OutputValue::Nat128(v) => v.to_string(),
208        OutputValue::NatBig(v) => v.to_string(),
209        OutputValue::Ulid(v) => v.to_string(),
210        OutputValue::Unit => "()".to_string(),
211    }
212}
213
214fn render_blob_value(bytes: &[u8]) -> String {
215    let mut rendered = String::from("0x");
216    rendered.push_str(encode_hex_lower_output_value(bytes).as_str());
217
218    rendered
219}
220
221fn encode_hex_lower_output_value(bytes: &[u8]) -> String {
222    const HEX: &[u8; 16] = b"0123456789abcdef";
223
224    let mut rendered = String::with_capacity(bytes.len().saturating_mul(2));
225    for byte in bytes {
226        let byte = *byte;
227        rendered.push(char::from(HEX[usize::from(byte >> 4)]));
228        rendered.push(char::from(HEX[usize::from(byte & 0x0f)]));
229    }
230
231    rendered
232}
233
234fn render_duration_value(millis: u64) -> String {
235    let mut rendered = millis.to_string();
236    rendered.push_str("ms");
237
238    rendered
239}
240
241fn render_list_value(items: &[OutputValue]) -> String {
242    let mut rendered = String::from("[");
243
244    for (index, item) in items.iter().enumerate() {
245        if index != 0 {
246            rendered.push_str(", ");
247        }
248
249        rendered.push_str(render_output_value_text(item).as_str());
250    }
251
252    rendered.push(']');
253
254    rendered
255}
256
257fn render_map_value(entries: &[(OutputValue, OutputValue)]) -> String {
258    let mut rendered = String::from("{");
259
260    for (index, (key, value)) in entries.iter().enumerate() {
261        if index != 0 {
262            rendered.push_str(", ");
263        }
264
265        rendered.push_str(render_output_value_text(key).as_str());
266        rendered.push_str(": ");
267        rendered.push_str(render_output_value_text(value).as_str());
268    }
269
270    rendered.push('}');
271
272    rendered
273}
274
275fn render_enum(value: &OutputValueEnum) -> String {
276    let mut rendered = String::new();
277    if let Some(path) = value.path() {
278        rendered.push_str(path);
279        rendered.push_str("::");
280    }
281    rendered.push_str(value.variant());
282    if let Some(payload) = value.payload() {
283        rendered.push('(');
284        rendered.push_str(render_output_value_text(payload).as_str());
285        rendered.push(')');
286    }
287
288    rendered
289}
290
291///
292/// TESTS
293///
294
295#[cfg(test)]
296mod tests {
297    use crate::value::{OutputValue, OutputValueEnum, Value, ValueEnum};
298
299    #[test]
300    fn output_value_from_runtime_value_keeps_recursive_collection_shape() {
301        let runtime = Value::List(vec![
302            Value::Nat64(7),
303            Value::Map(vec![(Value::Text("x".to_string()), Value::Bool(true))]),
304        ]);
305
306        assert_eq!(
307            OutputValue::from(runtime),
308            OutputValue::List(vec![
309                OutputValue::Nat64(7),
310                OutputValue::Map(vec![(
311                    OutputValue::Text("x".to_string()),
312                    OutputValue::Bool(true),
313                )]),
314            ]),
315        );
316    }
317
318    #[test]
319    fn output_value_from_owned_blob_moves_payload_without_clone() {
320        let bytes = vec![0x10, 0x20, 0x30];
321        let original_ptr = bytes.as_ptr();
322
323        let OutputValue::Blob(output) = OutputValue::from(Value::Blob(bytes)) else {
324            panic!("owned blob conversion should preserve the blob variant");
325        };
326
327        assert_eq!(
328            output.as_ptr(),
329            original_ptr,
330            "owned output conversion should move blob bytes instead of cloning them",
331        );
332    }
333
334    #[test]
335    fn output_value_enum_from_runtime_enum_keeps_payload() {
336        let runtime =
337            ValueEnum::new("Example", Some("test::OutputEnum")).with_payload(Value::Nat64(9));
338
339        assert_eq!(
340            OutputValueEnum::from(runtime),
341            OutputValueEnum {
342                variant: "Example".to_string(),
343                path: Some("test::OutputEnum".to_string()),
344                payload: Some(Box::new(OutputValue::Nat64(9))),
345            },
346        );
347    }
348}