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#[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#[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#[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#[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}