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 Self::from(&value)
86 }
87}
88
89impl From<&Value> for OutputValue {
90 fn from(value: &Value) -> Self {
91 match value {
92 Value::Account(value) => Self::Account(*value),
93 Value::Blob(value) => Self::Blob(value.clone()),
94 Value::Bool(value) => Self::Bool(*value),
95 Value::Date(value) => Self::Date(*value),
96 Value::Decimal(value) => Self::Decimal(*value),
97 Value::Duration(value) => Self::Duration(*value),
98 Value::Enum(value) => Self::Enum(OutputValueEnum::from(value)),
99 Value::Float32(value) => Self::Float32(*value),
100 Value::Float64(value) => Self::Float64(*value),
101 Value::Int64(value) => Self::Int64(*value),
102 Value::Int128(value) => Self::Int128(*value),
103 Value::IntBig(value) => Self::IntBig(value.clone()),
104 Value::List(items) => Self::List(items.iter().map(Self::from).collect()),
105 Value::Map(entries) => Self::Map(
106 entries
107 .iter()
108 .map(|(key, value)| (Self::from(key), Self::from(value)))
109 .collect(),
110 ),
111 Value::Null => Self::Null,
112 Value::Principal(value) => Self::Principal(*value),
113 Value::Subaccount(value) => Self::Subaccount(*value),
114 Value::Text(value) => Self::Text(value.clone()),
115 Value::Timestamp(value) => Self::Timestamp(*value),
116 Value::Nat64(value) => Self::Nat64(*value),
117 Value::Nat128(value) => Self::Nat128(*value),
118 Value::NatBig(value) => Self::NatBig(value.clone()),
119 Value::Ulid(value) => Self::Ulid(*value),
120 Value::Unit => Self::Unit,
121 }
122 }
123}
124
125impl From<ValueEnum> for OutputValueEnum {
126 fn from(value: ValueEnum) -> Self {
127 Self::from(&value)
128 }
129}
130
131impl From<&ValueEnum> for OutputValueEnum {
132 fn from(value: &ValueEnum) -> Self {
133 Self {
134 variant: value.variant().to_string(),
135 path: value.path().map(ToString::to_string),
136 payload: value
137 .payload()
138 .map(|payload| Box::new(OutputValue::from(payload))),
139 }
140 }
141}
142
143#[must_use]
145pub fn render_output_value_text(value: &OutputValue) -> String {
146 match value {
147 OutputValue::Account(v) => v.to_string(),
148 OutputValue::Blob(v) => render_blob_value(v),
149 OutputValue::Bool(v) => v.to_string(),
150 OutputValue::Date(v) => v.to_string(),
151 OutputValue::Decimal(v) => v.to_string(),
152 OutputValue::Duration(v) => render_duration_value(v.as_millis()),
153 OutputValue::Enum(v) => render_enum(v),
154 OutputValue::Float32(v) => v.to_string(),
155 OutputValue::Float64(v) => v.to_string(),
156 OutputValue::Int64(v) => v.to_string(),
157 OutputValue::Int128(v) => v.to_string(),
158 OutputValue::IntBig(v) => v.to_string(),
159 OutputValue::List(items) => render_list_value(items.as_slice()),
160 OutputValue::Map(entries) => render_map_value(entries.as_slice()),
161 OutputValue::Null => "null".to_string(),
162 OutputValue::Principal(v) => v.to_string(),
163 OutputValue::Subaccount(v) => v.to_string(),
164 OutputValue::Text(v) => v.clone(),
165 OutputValue::Timestamp(v) => v.as_millis().to_string(),
166 OutputValue::Nat64(v) => v.to_string(),
167 OutputValue::Nat128(v) => v.to_string(),
168 OutputValue::NatBig(v) => v.to_string(),
169 OutputValue::Ulid(v) => v.to_string(),
170 OutputValue::Unit => "()".to_string(),
171 }
172}
173
174fn render_blob_value(bytes: &[u8]) -> String {
175 let mut rendered = String::from("0x");
176 rendered.push_str(encode_hex_lower_output_value(bytes).as_str());
177
178 rendered
179}
180
181fn encode_hex_lower_output_value(bytes: &[u8]) -> String {
182 const HEX: &[u8; 16] = b"0123456789abcdef";
183
184 let mut rendered = String::with_capacity(bytes.len().saturating_mul(2));
185 for byte in bytes {
186 let byte = *byte;
187 rendered.push(char::from(HEX[usize::from(byte >> 4)]));
188 rendered.push(char::from(HEX[usize::from(byte & 0x0f)]));
189 }
190
191 rendered
192}
193
194fn render_duration_value(millis: u64) -> String {
195 let mut rendered = millis.to_string();
196 rendered.push_str("ms");
197
198 rendered
199}
200
201fn render_list_value(items: &[OutputValue]) -> String {
202 let mut rendered = String::from("[");
203
204 for (index, item) in items.iter().enumerate() {
205 if index != 0 {
206 rendered.push_str(", ");
207 }
208
209 rendered.push_str(render_output_value_text(item).as_str());
210 }
211
212 rendered.push(']');
213
214 rendered
215}
216
217fn render_map_value(entries: &[(OutputValue, OutputValue)]) -> String {
218 let mut rendered = String::from("{");
219
220 for (index, (key, value)) in entries.iter().enumerate() {
221 if index != 0 {
222 rendered.push_str(", ");
223 }
224
225 rendered.push_str(render_output_value_text(key).as_str());
226 rendered.push_str(": ");
227 rendered.push_str(render_output_value_text(value).as_str());
228 }
229
230 rendered.push('}');
231
232 rendered
233}
234
235fn render_enum(value: &OutputValueEnum) -> String {
236 let mut rendered = String::new();
237 if let Some(path) = value.path() {
238 rendered.push_str(path);
239 rendered.push_str("::");
240 }
241 rendered.push_str(value.variant());
242 if let Some(payload) = value.payload() {
243 rendered.push('(');
244 rendered.push_str(render_output_value_text(payload).as_str());
245 rendered.push(')');
246 }
247
248 rendered
249}
250
251#[cfg(test)]
256mod tests {
257 use crate::value::{OutputValue, OutputValueEnum, Value, ValueEnum};
258
259 #[test]
260 fn output_value_from_runtime_value_keeps_recursive_collection_shape() {
261 let runtime = Value::List(vec![
262 Value::Nat64(7),
263 Value::Map(vec![(Value::Text("x".to_string()), Value::Bool(true))]),
264 ]);
265
266 assert_eq!(
267 OutputValue::from(runtime),
268 OutputValue::List(vec![
269 OutputValue::Nat64(7),
270 OutputValue::Map(vec![(
271 OutputValue::Text("x".to_string()),
272 OutputValue::Bool(true),
273 )]),
274 ]),
275 );
276 }
277
278 #[test]
279 fn output_value_enum_from_runtime_enum_keeps_payload() {
280 let runtime =
281 ValueEnum::new("Example", Some("test::OutputEnum")).with_payload(Value::Nat64(9));
282
283 assert_eq!(
284 OutputValueEnum::from(runtime),
285 OutputValueEnum {
286 variant: "Example".to_string(),
287 path: Some("test::OutputEnum".to_string()),
288 payload: Some(Box::new(OutputValue::Nat64(9))),
289 },
290 );
291 }
292}