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