Skip to main content

icydb_core/value/
input.rs

1use crate::{
2    traits::EntityKey,
3    types::{
4        Account, Blob, Date, Decimal, Duration, Float32, Float64, Id, IntBig, NatBig, Principal,
5        Subaccount, Timestamp, Ulid, Unit,
6    },
7    value::Value,
8};
9use candid::CandidType;
10use serde::Deserialize;
11
12//
13// InputValue
14//
15// Public input-side value boundary used by literal-taking API surfaces.
16// This stays separate from runtime `Value` so public write/query inputs can
17// move off the internal execution representation incrementally.
18//
19
20#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
21pub enum InputValue {
22    Account(Account),
23    Blob(Vec<u8>),
24    Bool(bool),
25    Date(Date),
26    Decimal(Decimal),
27    Duration(Duration),
28    Enum(InputValueEnum),
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// InputValueEnum
52//
53// Input-side enum payload contract paired with `InputValue`.
54// Payload stays recursive through `InputValue` so explicit enum-valued public
55// inputs can cross the boundary without using runtime `Value` directly.
56//
57
58#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
59pub struct InputValueEnum {
60    variant: String,
61    path: Option<String>,
62    payload: Option<Box<InputValue>>,
63}
64
65impl InputValueEnum {
66    /// Build an enum input with an optional schema-visible type path.
67    #[must_use]
68    pub fn new(variant: &str, path: Option<&str>) -> Self {
69        Self {
70            variant: variant.to_string(),
71            path: path.map(ToString::to_string),
72            payload: None,
73        }
74    }
75
76    /// Build an enum input whose type is resolved from its expected contract.
77    #[must_use]
78    pub fn loose(variant: impl Into<String>) -> Self {
79        Self {
80            variant: variant.into(),
81            path: None,
82            payload: None,
83        }
84    }
85
86    /// Attach one unresolved payload value to this enum input.
87    #[must_use]
88    pub fn with_payload(mut self, payload: InputValue) -> Self {
89        self.payload = Some(Box::new(payload));
90        self
91    }
92
93    #[must_use]
94    pub const fn variant(&self) -> &str {
95        self.variant.as_str()
96    }
97
98    #[must_use]
99    pub fn path(&self) -> Option<&str> {
100        self.path.as_deref()
101    }
102
103    #[must_use]
104    pub fn payload(&self) -> Option<&InputValue> {
105        self.payload.as_deref()
106    }
107
108    pub(crate) fn into_parts(self) -> (String, Option<String>, Option<InputValue>) {
109        (
110            self.variant,
111            self.path,
112            self.payload.map(|payload| *payload),
113        )
114    }
115}
116
117impl InputValue {
118    /// Lower an input that cannot require accepted enum admission.
119    ///
120    /// Enum input, including nested enum input, stays unresolved and must use
121    /// the accepted catalog admission boundary instead.
122    pub(crate) fn try_into_runtime_non_enum(self) -> Option<Value> {
123        Some(match self {
124            Self::Account(value) => Value::Account(value),
125            Self::Blob(value) => Value::Blob(value),
126            Self::Bool(value) => Value::Bool(value),
127            Self::Date(value) => Value::Date(value),
128            Self::Decimal(value) => Value::Decimal(value),
129            Self::Duration(value) => Value::Duration(value),
130            Self::Enum(_) => return None,
131            Self::Float32(value) => Value::Float32(value),
132            Self::Float64(value) => Value::Float64(value),
133            Self::Int64(value) => Value::Int64(value),
134            Self::Int128(value) => Value::Int128(value),
135            Self::IntBig(value) => Value::IntBig(value),
136            Self::List(values) => Value::List(
137                values
138                    .into_iter()
139                    .map(Self::try_into_runtime_non_enum)
140                    .collect::<Option<Vec<_>>>()?,
141            ),
142            Self::Map(entries) => Value::Map(
143                entries
144                    .into_iter()
145                    .map(|(key, value)| {
146                        Some((
147                            key.try_into_runtime_non_enum()?,
148                            value.try_into_runtime_non_enum()?,
149                        ))
150                    })
151                    .collect::<Option<Vec<_>>>()?,
152            ),
153            Self::Null => Value::Null,
154            Self::Principal(value) => Value::Principal(value),
155            Self::Subaccount(value) => Value::Subaccount(value),
156            Self::Text(value) => Value::Text(value),
157            Self::Timestamp(value) => Value::Timestamp(value),
158            Self::Nat64(value) => Value::Nat64(value),
159            Self::Nat128(value) => Value::Nat128(value),
160            Self::NatBig(value) => Value::NatBig(value),
161            Self::Ulid(value) => Value::Ulid(value),
162            Self::Unit => Value::Unit,
163        })
164    }
165
166    /// Lift a runtime value without canonical enum IDs into authored input.
167    pub(crate) fn try_from_runtime_non_enum(value: &Value) -> Option<Self> {
168        Some(match value {
169            Value::Account(value) => Self::Account(*value),
170            Value::Blob(value) => Self::Blob(value.clone()),
171            Value::Bool(value) => Self::Bool(*value),
172            Value::Date(value) => Self::Date(*value),
173            Value::Decimal(value) => Self::Decimal(*value),
174            Value::Duration(value) => Self::Duration(*value),
175            Value::Enum(_) => return None,
176            Value::Float32(value) => Self::Float32(*value),
177            Value::Float64(value) => Self::Float64(*value),
178            Value::Int64(value) => Self::Int64(*value),
179            Value::Int128(value) => Self::Int128(*value),
180            Value::IntBig(value) => Self::IntBig(value.clone()),
181            Value::List(values) => Self::List(
182                values
183                    .iter()
184                    .map(Self::try_from_runtime_non_enum)
185                    .collect::<Option<Vec<_>>>()?,
186            ),
187            Value::Map(entries) => Self::Map(
188                entries
189                    .iter()
190                    .map(|(key, value)| {
191                        Some((
192                            Self::try_from_runtime_non_enum(key)?,
193                            Self::try_from_runtime_non_enum(value)?,
194                        ))
195                    })
196                    .collect::<Option<Vec<_>>>()?,
197            ),
198            Value::Null => Self::Null,
199            Value::Principal(value) => Self::Principal(*value),
200            Value::Subaccount(value) => Self::Subaccount(*value),
201            Value::Text(value) => Self::Text(value.clone()),
202            Value::Timestamp(value) => Self::Timestamp(*value),
203            Value::Nat64(value) => Self::Nat64(*value),
204            Value::Nat128(value) => Self::Nat128(*value),
205            Value::NatBig(value) => Self::NatBig(value.clone()),
206            Value::Ulid(value) => Self::Ulid(*value),
207            Value::Unit => Self::Unit,
208        })
209    }
210}
211
212#[cfg(test)]
213impl From<Value> for InputValue {
214    fn from(value: Value) -> Self {
215        Self::try_from_runtime_non_enum(&value)
216            .expect("test runtime-to-input conversion must not contain canonical enum IDs")
217    }
218}
219
220#[cfg(test)]
221impl From<&Value> for InputValue {
222    fn from(value: &Value) -> Self {
223        Self::try_from_runtime_non_enum(value)
224            .expect("test runtime-to-input conversion must not contain canonical enum IDs")
225    }
226}
227
228impl From<&str> for InputValue {
229    fn from(value: &str) -> Self {
230        Self::Text(value.to_string())
231    }
232}
233
234impl From<String> for InputValue {
235    fn from(value: String) -> Self {
236        Self::Text(value)
237    }
238}
239
240impl From<Vec<u8>> for InputValue {
241    fn from(value: Vec<u8>) -> Self {
242        Self::Blob(value)
243    }
244}
245
246impl From<Blob> for InputValue {
247    fn from(value: Blob) -> Self {
248        Self::Blob(value.to_vec())
249    }
250}
251
252impl From<bool> for InputValue {
253    fn from(value: bool) -> Self {
254        Self::Bool(value)
255    }
256}
257
258impl From<Account> for InputValue {
259    fn from(value: Account) -> Self {
260        Self::Account(value)
261    }
262}
263
264impl From<Date> for InputValue {
265    fn from(value: Date) -> Self {
266        Self::Date(value)
267    }
268}
269
270impl From<Decimal> for InputValue {
271    fn from(value: Decimal) -> Self {
272        Self::Decimal(value)
273    }
274}
275
276impl From<Duration> for InputValue {
277    fn from(value: Duration) -> Self {
278        Self::Duration(value)
279    }
280}
281
282impl From<Float32> for InputValue {
283    fn from(value: Float32) -> Self {
284        Self::Float32(value)
285    }
286}
287
288impl From<Float64> for InputValue {
289    fn from(value: Float64) -> Self {
290        Self::Float64(value)
291    }
292}
293
294impl From<IntBig> for InputValue {
295    fn from(value: IntBig) -> Self {
296        Self::IntBig(value)
297    }
298}
299
300impl From<i128> for InputValue {
301    fn from(value: i128) -> Self {
302        Self::Int128(value)
303    }
304}
305
306impl From<NatBig> for InputValue {
307    fn from(value: NatBig) -> Self {
308        Self::NatBig(value)
309    }
310}
311
312impl From<u128> for InputValue {
313    fn from(value: u128) -> Self {
314        Self::Nat128(value)
315    }
316}
317
318impl From<Principal> for InputValue {
319    fn from(value: Principal) -> Self {
320        Self::Principal(value)
321    }
322}
323
324impl From<Subaccount> for InputValue {
325    fn from(value: Subaccount) -> Self {
326        Self::Subaccount(value)
327    }
328}
329
330impl From<Timestamp> for InputValue {
331    fn from(value: Timestamp) -> Self {
332        Self::Timestamp(value)
333    }
334}
335
336impl From<Ulid> for InputValue {
337    fn from(value: Ulid) -> Self {
338        Self::Ulid(value)
339    }
340}
341
342impl From<()> for InputValue {
343    fn from((): ()) -> Self {
344        Self::Unit
345    }
346}
347
348impl From<Unit> for InputValue {
349    fn from(_value: Unit) -> Self {
350        Self::Unit
351    }
352}
353
354impl<T> From<Option<T>> for InputValue
355where
356    T: Into<Self>,
357{
358    fn from(value: Option<T>) -> Self {
359        match value {
360            Some(value) => value.into(),
361            None => Self::Null,
362        }
363    }
364}
365
366impl<T> From<Box<T>> for InputValue
367where
368    T: Into<Self>,
369{
370    fn from(value: Box<T>) -> Self {
371        (*value).into()
372    }
373}
374
375impl<E> From<Id<E>> for InputValue
376where
377    E: EntityKey,
378    E::Key: Into<Self>,
379{
380    fn from(value: Id<E>) -> Self {
381        value.into_key().into()
382    }
383}
384
385impl<E> From<&Id<E>> for InputValue
386where
387    E: EntityKey,
388    E::Key: Into<Self>,
389{
390    fn from(value: &Id<E>) -> Self {
391        value.key().into()
392    }
393}
394
395macro_rules! impl_input_value_int {
396    ($($ty:ty),* $(,)?) => {
397        $(
398            impl From<$ty> for InputValue {
399                fn from(value: $ty) -> Self {
400                    Self::Int64(i64::from(value))
401                }
402            }
403        )*
404    };
405}
406
407macro_rules! impl_input_value_nat {
408    ($($ty:ty),* $(,)?) => {
409        $(
410            impl From<$ty> for InputValue {
411                fn from(value: $ty) -> Self {
412                    Self::Nat64(u64::from(value))
413                }
414            }
415        )*
416    };
417}
418
419impl_input_value_int!(i8, i16, i32, i64);
420impl_input_value_nat!(u8, u16, u32, u64);
421
422///
423/// TESTS
424///
425
426#[cfg(test)]
427mod tests {
428    use crate::value::{InputValue, InputValueEnum, Value};
429
430    #[test]
431    fn runtime_to_input_value_keeps_recursive_collection_shape() {
432        let runtime = Value::List(vec![
433            Value::Nat64(7),
434            Value::Map(vec![(Value::Text("x".to_string()), Value::Bool(true))]),
435        ]);
436
437        assert_eq!(
438            InputValue::from(runtime),
439            InputValue::List(vec![
440                InputValue::Nat64(7),
441                InputValue::Map(vec![(
442                    InputValue::Text("x".to_string()),
443                    InputValue::Bool(true),
444                )]),
445            ]),
446        );
447    }
448
449    #[test]
450    fn unresolved_enum_input_cannot_lower_without_admission() {
451        let direct = InputValue::Enum(InputValueEnum::loose("Active"));
452        let nested = InputValue::List(vec![direct.clone()]);
453
454        assert_eq!(direct.try_into_runtime_non_enum(), None);
455        assert_eq!(nested.try_into_runtime_non_enum(), None);
456    }
457}