Skip to main content

boxology_contract/
typed.rs

1//! Strict typed conversion for transport-neutral contract values.
2//!
3//! Every value of each supported scalar and `String` round-trips. The only
4//! exception is a non-finite float, which fails during encoding.
5//! Presence wrappers may nest as Rust types; descriptor construction remains
6//! responsible for rejecting wrappers in illegal positions.
7//! Maps accept any source object order and re-encode keys in sorted order.
8
9use std::collections::BTreeMap;
10use std::error::Error;
11use std::fmt;
12
13use crate::{ContractValue, Field, SlotValue, ValueRef};
14
15/// One segment in a contract-value diagnostic path.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum PathSegment {
19    Field(String),
20    Index(usize),
21    MapKey(String),
22    Variant(String),
23}
24
25/// The category of a typed encoding failure.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum EncodeErrorKind {
29    NonFiniteF32,
30    NonFiniteF64,
31    UnsupportedPosition,
32}
33
34/// The category of a typed decoding failure.
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum DecodeErrorKind {
38    MissingRequired,
39    UnexpectedNull,
40    UnexpectedMissing,
41    KindMismatch,
42    OutOfRange,
43    UnexpectedPayload,
44    UnknownField(String),
45    UnknownVariant(String),
46    UnsupportedPosition,
47}
48
49macro_rules! error_type {
50    ($name:ident, $kind:ident) => {
51        /// A typed conversion failure with a schema path and payload-free category.
52        #[derive(Debug, Clone, PartialEq, Eq)]
53        pub struct $name {
54            path: Vec<PathSegment>,
55            kind: $kind,
56        }
57
58        impl $name {
59            pub fn new(kind: $kind) -> Self {
60                Self {
61                    path: Vec::new(),
62                    kind,
63                }
64            }
65
66            pub fn under(mut self, segment: PathSegment) -> Self {
67                self.path.insert(0, segment);
68                self
69            }
70
71            pub fn kind(&self) -> &$kind {
72                &self.kind
73            }
74
75            pub fn path(&self) -> &[PathSegment] {
76                &self.path
77            }
78        }
79
80        impl fmt::Display for $name {
81            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82                write!(formatter, "{:?} at {:?}", self.kind, self.path)
83            }
84        }
85
86        impl Error for $name {}
87    };
88}
89
90error_type!(EncodeError, EncodeErrorKind);
91error_type!(DecodeError, DecodeErrorKind);
92
93/// A Rust type with an exact transport-neutral contract representation.
94pub trait ContractType: Sized {
95    fn encode_value(&self) -> Result<ContractValue, EncodeError>;
96    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError>;
97
98    fn encode(&self) -> Result<SlotValue, EncodeError> {
99        Ok(SlotValue::Value(self.encode_value()?))
100    }
101
102    fn decode(slot: &SlotValue) -> Result<Self, DecodeError> {
103        match slot {
104            SlotValue::Missing => Err(DecodeError::new(DecodeErrorKind::MissingRequired)),
105            SlotValue::Null => Err(DecodeError::new(DecodeErrorKind::UnexpectedNull)),
106            SlotValue::Value(value) => Self::decode_value(value),
107        }
108    }
109
110    fn encode_field(&self) -> Result<Option<ContractValue>, EncodeError> {
111        Ok(Some(self.encode_value()?))
112    }
113
114    fn decode_field(field: Option<&ContractValue>) -> Result<Self, DecodeError> {
115        match field {
116            None => Err(DecodeError::new(DecodeErrorKind::MissingRequired)),
117            Some(value) => Self::decode_value(value),
118        }
119    }
120}
121
122fn mismatch<T>() -> Result<T, DecodeError> {
123    Err(DecodeError::new(DecodeErrorKind::KindMismatch))
124}
125
126macro_rules! exact_scalar {
127    ($type:ty, $constructor:ident, $variant:ident) => {
128        impl ContractType for $type {
129            fn encode_value(&self) -> Result<ContractValue, EncodeError> {
130                Ok(ContractValue::$constructor(*self))
131            }
132
133            fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
134                match value.view() {
135                    ValueRef::$variant(value) => Ok(value),
136                    _ => mismatch(),
137                }
138            }
139        }
140    };
141}
142
143exact_scalar!(bool, bool, Bool);
144exact_scalar!(i64, i64, I64);
145exact_scalar!(u64, u64, U64);
146
147macro_rules! narrow_integers {
148    ($constructor:ident, $variant:ident; $($type:ty),+ $(,)?) => {$(
149        impl ContractType for $type {
150            fn encode_value(&self) -> Result<ContractValue, EncodeError> {
151                Ok(ContractValue::$constructor((*self).into()))
152            }
153
154            fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
155                match value.view() {
156                    ValueRef::$variant(value) => value
157                        .try_into()
158                        .map_err(|_| DecodeError::new(DecodeErrorKind::OutOfRange)),
159                    _ => mismatch(),
160                }
161            }
162        }
163    )+};
164}
165
166narrow_integers!(i64, I64; i8, i16, i32);
167narrow_integers!(u64, U64; u8, u16, u32);
168
169macro_rules! floats {
170    ($type:ty, $constructor:ident, $variant:ident, $kind:ident) => {
171        impl ContractType for $type {
172            fn encode_value(&self) -> Result<ContractValue, EncodeError> {
173                ContractValue::$constructor(*self)
174                    .map_err(|_| EncodeError::new(EncodeErrorKind::$kind))
175            }
176
177            fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
178                match value.view() {
179                    ValueRef::$variant(value) => Ok(value),
180                    _ => mismatch(),
181                }
182            }
183        }
184    };
185}
186
187floats!(f32, f32, F32, NonFiniteF32);
188floats!(f64, f64, F64, NonFiniteF64);
189
190impl ContractType for String {
191    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
192        Ok(ContractValue::string(self))
193    }
194
195    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
196        match value.view() {
197            ValueRef::String(value) => Ok(value.into()),
198            _ => mismatch(),
199        }
200    }
201}
202
203impl<T: ContractType> ContractType for Option<T> {
204    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
205        match self {
206            None => Ok(ContractValue::null()),
207            Some(value) => value.encode_value(),
208        }
209    }
210
211    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
212        match value.view() {
213            ValueRef::Null => Ok(None),
214            _ => T::decode_value(value).map(Some),
215        }
216    }
217
218    fn encode(&self) -> Result<SlotValue, EncodeError> {
219        match self {
220            None => Ok(SlotValue::Null),
221            Some(value) => Ok(SlotValue::Value(value.encode_value()?)),
222        }
223    }
224
225    fn decode(slot: &SlotValue) -> Result<Self, DecodeError> {
226        match slot {
227            SlotValue::Missing => Err(DecodeError::new(DecodeErrorKind::UnexpectedMissing)),
228            SlotValue::Null => Ok(None),
229            SlotValue::Value(value) => T::decode_value(value).map(Some),
230        }
231    }
232
233    fn encode_field(&self) -> Result<Option<ContractValue>, EncodeError> {
234        match self {
235            None => Ok(None),
236            Some(value) => value.encode_value().map(Some),
237        }
238    }
239
240    fn decode_field(field: Option<&ContractValue>) -> Result<Self, DecodeError> {
241        match field {
242            None => Ok(None),
243            Some(value) => T::decode_value(value).map(Some),
244        }
245    }
246}
247
248impl<T: ContractType> ContractType for Field<T> {
249    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
250        Err(EncodeError::new(EncodeErrorKind::UnsupportedPosition))
251    }
252
253    fn decode_value(_value: &ContractValue) -> Result<Self, DecodeError> {
254        Err(DecodeError::new(DecodeErrorKind::UnsupportedPosition))
255    }
256
257    fn encode(&self) -> Result<SlotValue, EncodeError> {
258        match self {
259            Field::Missing => Ok(SlotValue::Missing),
260            Field::Null => Ok(SlotValue::Null),
261            Field::Value(value) => Ok(SlotValue::Value(value.encode_value()?)),
262        }
263    }
264
265    fn decode(slot: &SlotValue) -> Result<Self, DecodeError> {
266        match slot {
267            SlotValue::Missing => Ok(Field::Missing),
268            SlotValue::Null => Ok(Field::Null),
269            SlotValue::Value(value) => T::decode_value(value).map(Field::Value),
270        }
271    }
272
273    fn encode_field(&self) -> Result<Option<ContractValue>, EncodeError> {
274        match self {
275            Field::Missing => Ok(None),
276            Field::Null => Ok(Some(ContractValue::null())),
277            Field::Value(value) => value.encode_value().map(Some),
278        }
279    }
280
281    fn decode_field(field: Option<&ContractValue>) -> Result<Self, DecodeError> {
282        match field {
283            None => Ok(Field::Missing),
284            Some(value) if matches!(value.view(), ValueRef::Null) => Ok(Field::Null),
285            Some(value) => T::decode_value(value).map(Field::Value),
286        }
287    }
288}
289
290impl<T: ContractType> ContractType for Vec<T> {
291    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
292        let values = self
293            .iter()
294            .enumerate()
295            .map(|(index, value)| {
296                value
297                    .encode_value()
298                    .map_err(|error| error.under(PathSegment::Index(index)))
299            })
300            .collect::<Result<Vec<_>, _>>()?;
301        Ok(ContractValue::list(values))
302    }
303
304    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
305        match value.view() {
306            ValueRef::Null => Err(DecodeError::new(DecodeErrorKind::UnexpectedNull)),
307            ValueRef::List(values) => values
308                .iter()
309                .enumerate()
310                .map(|(index, value)| {
311                    T::decode_value(value).map_err(|error| error.under(PathSegment::Index(index)))
312                })
313                .collect(),
314            _ => mismatch(),
315        }
316    }
317}
318
319impl<T: ContractType> ContractType for BTreeMap<String, T> {
320    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
321        let entries = self
322            .iter()
323            .map(|(key, value)| {
324                value
325                    .encode_value()
326                    .map(|value| (key.clone(), value))
327                    .map_err(|error| error.under(PathSegment::MapKey(key.clone())))
328            })
329            .collect::<Result<Vec<_>, _>>()?;
330        ContractValue::object(entries).map_err(|_| unreachable!())
331    }
332
333    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
334        match value.view() {
335            ValueRef::Null => Err(DecodeError::new(DecodeErrorKind::UnexpectedNull)),
336            ValueRef::Object(object) => object
337                .entries()
338                .map(|(key, value)| {
339                    T::decode_value(value)
340                        .map(|value| (key.into(), value))
341                        .map_err(|error| error.under(PathSegment::MapKey(key.into())))
342                })
343                .collect(),
344            _ => mismatch(),
345        }
346    }
347}
348
349/// A value whose diagnostic representations must remain redacted.
350#[derive(Clone, PartialEq, Eq)]
351pub struct Secret<T>(T);
352
353impl<T> Secret<T> {
354    pub fn new(value: T) -> Self {
355        Self(value)
356    }
357
358    /// Explicitly reveals the wrapped value.
359    ///
360    /// Callers must treat the returned content as sensitive data.
361    pub fn reveal(&self) -> &T {
362        &self.0
363    }
364
365    /// Consumes the wrapper and explicitly reveals the wrapped value.
366    ///
367    /// Callers must treat the returned content as sensitive data.
368    pub fn into_revealed(self) -> T {
369        self.0
370    }
371}
372
373impl<T> fmt::Debug for Secret<T> {
374    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
375        formatter.write_str("Secret(<redacted>)")
376    }
377}
378
379impl<T> fmt::Display for Secret<T> {
380    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381        formatter.write_str("Secret(<redacted>)")
382    }
383}
384
385impl<T: ContractType> ContractType for Secret<T> {
386    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
387        self.0.encode_value().map(ContractValue::sensitive)
388    }
389
390    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
391        match value.view() {
392            ValueRef::Null => Err(DecodeError::new(DecodeErrorKind::UnexpectedNull)),
393            ValueRef::Sensitive(inner) => T::decode_value(inner).map(Self),
394            _ => mismatch(),
395        }
396    }
397}
398
399/// An ordinary, non-sensitive byte string.
400#[derive(Debug, Clone, PartialEq, Eq)]
401pub struct Blob(Vec<u8>);
402
403impl Blob {
404    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
405        Self(bytes.into())
406    }
407
408    pub fn as_bytes(&self) -> &[u8] {
409        &self.0
410    }
411
412    pub fn into_bytes(self) -> Vec<u8> {
413        self.0
414    }
415}
416
417impl ContractType for Blob {
418    fn encode_value(&self) -> Result<ContractValue, EncodeError> {
419        Ok(ContractValue::bytes(self.0.clone()))
420    }
421
422    fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
423        match value.view() {
424            ValueRef::Bytes(bytes) => Ok(Self(bytes.to_vec())),
425            _ => mismatch(),
426        }
427    }
428}
429
430/// A domain error with a stable variant tag crossing `ErasedCallError::Domain`.
431pub trait ContractError: ContractType {
432    fn error_tag(&self) -> &str;
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::conform::{Shape, conform_slot};
439    use crate::{OpaquePayload, OpaqueTree};
440
441    fn round_trip<T: ContractType + fmt::Debug + PartialEq>(value: T) {
442        assert_eq!(T::decode(&value.encode().unwrap()).unwrap(), value);
443    }
444
445    #[test]
446    fn every_supported_scalar_and_string_round_trips() {
447        round_trip(false);
448        round_trip(true);
449        round_trip(i8::MIN);
450        round_trip(i16::MAX);
451        round_trip(i32::MIN);
452        round_trip(i64::MAX);
453        round_trip(u8::MAX);
454        round_trip(u16::MIN);
455        round_trip(u32::MAX);
456        round_trip(u64::MAX);
457        round_trip(-0.0_f32);
458        round_trip(f64::MIN);
459        round_trip(String::from("owned UTF-8 α"));
460    }
461
462    fn out_of_range<T: ContractType + fmt::Debug>(value: ContractValue) {
463        assert_eq!(
464            T::decode_value(&value).unwrap_err().kind(),
465            &DecodeErrorKind::OutOfRange
466        );
467    }
468
469    #[test]
470    fn narrow_integer_boundaries_are_exact() {
471        macro_rules! signed {
472            ($($type:ty),+) => {$(
473                assert_eq!(<$type>::decode_value(&ContractValue::i64(<$type>::MIN.into())), Ok(<$type>::MIN));
474                assert_eq!(<$type>::decode_value(&ContractValue::i64(<$type>::MAX.into())), Ok(<$type>::MAX));
475                out_of_range::<$type>(ContractValue::i64(i64::from(<$type>::MIN) - 1));
476                out_of_range::<$type>(ContractValue::i64(i64::from(<$type>::MAX) + 1));
477            )+}; }
478        macro_rules! unsigned {
479            ($($type:ty),+) => {$(
480                assert_eq!(<$type>::decode_value(&ContractValue::u64(0)), Ok(0));
481                assert_eq!(<$type>::decode_value(&ContractValue::u64(<$type>::MAX.into())), Ok(<$type>::MAX));
482                out_of_range::<$type>(ContractValue::u64(u64::from(<$type>::MAX) + 1));
483            )+}; }
484        signed!(i8, i16, i32);
485        unsigned!(u8, u16, u32);
486    }
487
488    #[test]
489    fn non_finite_float_encoding_is_fallible() {
490        for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
491            assert_eq!(
492                value.encode_value().unwrap_err().kind(),
493                &EncodeErrorKind::NonFiniteF32
494            );
495        }
496        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
497            assert_eq!(
498                value.encode_value().unwrap_err().kind(),
499                &EncodeErrorKind::NonFiniteF64
500            );
501        }
502    }
503
504    fn rejects_other_kinds<T: ContractType + fmt::Debug>(values: &[ContractValue], own: usize) {
505        for (index, value) in values.iter().enumerate() {
506            if index != own {
507                assert_eq!(
508                    T::decode_value(value).unwrap_err().kind(),
509                    &DecodeErrorKind::KindMismatch
510                );
511            }
512        }
513    }
514
515    #[test]
516    fn scalars_reject_every_other_value_kind() {
517        let values = vec![
518            ContractValue::null(),
519            ContractValue::bool(true),
520            ContractValue::i64(0),
521            ContractValue::u64(0),
522            ContractValue::f32(0.0).unwrap(),
523            ContractValue::f64(0.0).unwrap(),
524            ContractValue::string("text"),
525            ContractValue::bytes([]),
526            ContractValue::list([]),
527            ContractValue::object([("x".into(), ContractValue::null())]).unwrap(),
528            ContractValue::enum_value("tag", SlotValue::Null),
529            ContractValue::opaque(OpaquePayload::new(OpaqueTree::String("raw".into()))),
530            ContractValue::sensitive(ContractValue::string("secret")),
531        ];
532        rejects_other_kinds::<bool>(&values, 1);
533        rejects_other_kinds::<i8>(&values, 2);
534        rejects_other_kinds::<i16>(&values, 2);
535        rejects_other_kinds::<i32>(&values, 2);
536        rejects_other_kinds::<i64>(&values, 2);
537        rejects_other_kinds::<u8>(&values, 3);
538        rejects_other_kinds::<u16>(&values, 3);
539        rejects_other_kinds::<u32>(&values, 3);
540        rejects_other_kinds::<u64>(&values, 3);
541        rejects_other_kinds::<f32>(&values, 4);
542        rejects_other_kinds::<f64>(&values, 5);
543        rejects_other_kinds::<String>(&values, 6);
544    }
545
546    #[test]
547    fn required_defaults_distinguish_missing_null_and_wrong_kind() {
548        assert_eq!(
549            bool::decode(&SlotValue::Missing).unwrap_err().kind(),
550            &DecodeErrorKind::MissingRequired
551        );
552        assert_eq!(
553            bool::decode(&SlotValue::Null).unwrap_err().kind(),
554            &DecodeErrorKind::UnexpectedNull
555        );
556        assert_eq!(
557            bool::decode(&SlotValue::Value(ContractValue::null()))
558                .unwrap_err()
559                .kind(),
560            &DecodeErrorKind::KindMismatch
561        );
562        assert_eq!(
563            bool::decode_field(None).unwrap_err().kind(),
564            &DecodeErrorKind::MissingRequired
565        );
566        let field = 7_u8.encode_field().unwrap();
567        assert_eq!(u8::decode_field(field.as_ref()), Ok(7));
568    }
569
570    #[test]
571    fn error_paths_prepend_and_diagnostics_expose_no_payloads() {
572        let decode = DecodeError::new(DecodeErrorKind::UnknownField("leaf".into()))
573            .under(PathSegment::Index(3))
574            .under(PathSegment::Field("root".into()));
575        assert_eq!(decode.kind(), &DecodeErrorKind::UnknownField("leaf".into()));
576        assert_eq!(
577            decode.path(),
578            &[PathSegment::Field("root".into()), PathSegment::Index(3)]
579        );
580        assert_eq!(
581            decode.to_string(),
582            "UnknownField(\"leaf\") at [Field(\"root\"), Index(3)]"
583        );
584
585        let encode = EncodeError::new(EncodeErrorKind::NonFiniteF64)
586            .under(PathSegment::MapKey("item".into()));
587        assert_eq!(encode.kind(), &EncodeErrorKind::NonFiniteF64);
588        assert_eq!(encode.path(), &[PathSegment::MapKey("item".into())]);
589
590        const SENTINEL: &str = "runtime-payload-must-not-escape";
591        for payload in [
592            ContractValue::sensitive(ContractValue::string(SENTINEL)),
593            ContractValue::opaque(OpaquePayload::new(OpaqueTree::String(SENTINEL.into()))),
594        ] {
595            let error = bool::decode_value(&payload).unwrap_err();
596            for diagnostic in [format!("{error:?}"), error.to_string()] {
597                assert!(!diagnostic.contains(SENTINEL));
598            }
599        }
600    }
601
602    #[test]
603    fn public_diagnostic_types_are_send_sync_and_static() {
604        fn assert_bounds<T: Send + Sync + 'static>() {}
605        assert_bounds::<PathSegment>();
606        assert_bounds::<EncodeErrorKind>();
607        assert_bounds::<EncodeError>();
608        assert_bounds::<DecodeErrorKind>();
609        assert_bounds::<DecodeError>();
610    }
611
612    fn decode_error<T: fmt::Debug>(result: Result<T, DecodeError>, kind: DecodeErrorKind) {
613        assert_eq!(result.unwrap_err().kind(), &kind);
614    }
615
616    #[test]
617    fn presence_grid_is_exact_at_slots_fields_and_value_positions() {
618        let null = ContractValue::null();
619        assert_eq!(Option::<u8>::decode(&SlotValue::Null), Ok(None));
620        assert_eq!(None::<u8>.encode().unwrap(), SlotValue::Null);
621        decode_error(
622            Option::<u8>::decode(&SlotValue::Missing),
623            DecodeErrorKind::UnexpectedMissing,
624        );
625        decode_error(
626            Option::<u8>::decode(&SlotValue::Value(null.clone())),
627            DecodeErrorKind::KindMismatch,
628        );
629        assert_eq!(Option::<u8>::decode_value(&null), Ok(None));
630        assert_eq!(None::<u8>.encode_value().unwrap(), null);
631        assert_eq!(Option::<u8>::decode_field(None), Ok(None));
632        assert_eq!(None::<u8>.encode_field().unwrap(), None);
633        let some = Some(7_u8);
634        assert_eq!(
635            some.encode().unwrap(),
636            SlotValue::Value(ContractValue::u64(7))
637        );
638        assert_eq!(Option::<u8>::decode(&some.encode().unwrap()), Ok(some));
639        let some_field = some.encode_field().unwrap();
640        assert_eq!(some_field, Some(ContractValue::u64(7)));
641        assert_eq!(Option::<u8>::decode_field(some_field.as_ref()), Ok(some));
642        decode_error(
643            Option::<u8>::decode_field(Some(&ContractValue::null())),
644            DecodeErrorKind::KindMismatch,
645        );
646
647        for (field, slot) in [
648            (Field::Missing, SlotValue::Missing),
649            (Field::Null, SlotValue::Null),
650            (Field::Value(7_u8), SlotValue::Value(ContractValue::u64(7))),
651        ] {
652            assert_eq!(field.encode().unwrap(), slot);
653            assert_eq!(Field::<u8>::decode(&slot), Ok(field));
654        }
655        assert_eq!(Field::<u8>::decode_field(None), Ok(Field::Missing));
656        assert_eq!(Field::<u8>::decode_field(Some(&null)), Ok(Field::Null));
657        assert_eq!(
658            Field::<u8>::Null.encode_field().unwrap(),
659            Some(null.clone())
660        );
661        assert_eq!(Field::<u8>::Missing.encode_field().unwrap(), None);
662        let value_field = Field::Value(7_u8).encode_field().unwrap();
663        assert_eq!(value_field, Some(ContractValue::u64(7)));
664        assert_eq!(
665            Field::<u8>::decode_field(value_field.as_ref()),
666            Ok(Field::Value(7))
667        );
668        decode_error(
669            Field::<u8>::decode(&SlotValue::Value(null.clone())),
670            DecodeErrorKind::KindMismatch,
671        );
672        decode_error(
673            Field::<u8>::decode_field(Some(&ContractValue::bool(true))),
674            DecodeErrorKind::KindMismatch,
675        );
676        for field in [Field::Missing, Field::Null, Field::Value(1_u8)] {
677            assert_eq!(
678                field.encode_value().unwrap_err().kind(),
679                &EncodeErrorKind::UnsupportedPosition
680            );
681        }
682        for value in [null, ContractValue::u64(1)] {
683            decode_error(
684                Field::<u8>::decode_value(&value),
685                DecodeErrorKind::UnsupportedPosition,
686            );
687        }
688    }
689
690    #[test]
691    fn nested_optional_lists_round_trip_and_paths_include_indices() {
692        let values = vec![Some(1_u8), None, Some(3)];
693        round_trip(values.clone());
694        let encoded = values.encode_value().unwrap();
695        let ValueRef::List(items) = encoded.view() else {
696            panic!()
697        };
698        assert!(matches!(items[1].view(), ValueRef::Null));
699
700        let encode = vec![0.0_f32, f32::NAN].encode_value().unwrap_err();
701        assert_eq!(encode.kind(), &EncodeErrorKind::NonFiniteF32);
702        assert_eq!(encode.path(), &[PathSegment::Index(1)]);
703        let bad = ContractValue::list([ContractValue::u64(1), ContractValue::u64(256)]);
704        let decode = Vec::<u8>::decode_value(&bad).unwrap_err();
705        assert_eq!(decode.kind(), &DecodeErrorKind::OutOfRange);
706        assert_eq!(decode.path(), &[PathSegment::Index(1)]);
707        decode_error(
708            Vec::<u8>::decode_value(&ContractValue::null()),
709            DecodeErrorKind::UnexpectedNull,
710        );
711        decode_error(
712            Vec::<u8>::decode_value(&ContractValue::bool(false)),
713            DecodeErrorKind::KindMismatch,
714        );
715    }
716
717    #[test]
718    fn maps_round_trip_sort_input_and_report_key_paths() {
719        let map = BTreeMap::from([("z".into(), 2_u8), ("a".into(), 1)]);
720        round_trip(map);
721        let input = ContractValue::object([
722            ("z".into(), ContractValue::u64(2)),
723            ("a".into(), ContractValue::u64(1)),
724        ])
725        .unwrap();
726        let decoded = BTreeMap::<String, u8>::decode_value(&input).unwrap();
727        let encoded = decoded.encode_value().unwrap();
728        let ValueRef::Object(object) = encoded.view() else {
729            panic!()
730        };
731        assert_eq!(
732            object.entries().map(|(key, _)| key).collect::<Vec<_>>(),
733            ["a", "z"]
734        );
735
736        let encode = BTreeMap::from([("secret".into(), f64::NAN)])
737            .encode_value()
738            .unwrap_err();
739        assert_eq!(encode.kind(), &EncodeErrorKind::NonFiniteF64);
740        assert_eq!(encode.path(), &[PathSegment::MapKey("secret".into())]);
741        let bad = ContractValue::object([("bad".into(), ContractValue::u64(256))]).unwrap();
742        let decode = BTreeMap::<String, u8>::decode_value(&bad).unwrap_err();
743        assert_eq!(decode.kind(), &DecodeErrorKind::OutOfRange);
744        assert_eq!(decode.path(), &[PathSegment::MapKey("bad".into())]);
745        decode_error(
746            BTreeMap::<String, u8>::decode_value(&ContractValue::null()),
747            DecodeErrorKind::UnexpectedNull,
748        );
749        decode_error(
750            BTreeMap::<String, u8>::decode_value(&ContractValue::bool(false)),
751            DecodeErrorKind::KindMismatch,
752        );
753    }
754
755    fn walker_agrees<T: ContractType + fmt::Debug>(shape: &Shape, slot: SlotValue) {
756        let typed = T::decode(&slot).unwrap().encode().unwrap();
757        for role in [
758            crate::DecodeRole::ProviderInput,
759            crate::DecodeRole::ConsumerOutput,
760        ] {
761            assert_eq!(conform_slot(shape, role, slot.clone()).unwrap(), typed);
762        }
763    }
764
765    #[test]
766    fn typed_presence_acceptance_agrees_with_the_walker() {
767        let optional = Shape::optional(Shape::i64()).unwrap();
768        walker_agrees::<Option<i64>>(&optional, SlotValue::Null);
769        walker_agrees::<Option<i64>>(&optional, SlotValue::Value(ContractValue::i64(4)));
770        let tri_state = Shape::tri_state(Shape::i64()).unwrap();
771        walker_agrees::<Field<i64>>(&tri_state, SlotValue::Missing);
772        walker_agrees::<Field<i64>>(&tri_state, SlotValue::Null);
773        walker_agrees::<Field<i64>>(&tri_state, SlotValue::Value(ContractValue::i64(4)));
774    }
775
776    #[test]
777    fn secrets_round_trip_in_every_supported_position() {
778        let secret = Secret::new(String::from("classified"));
779        let encoded = secret.encode_value().unwrap();
780        assert_eq!(Secret::<String>::decode_value(&encoded), Ok(secret.clone()));
781        round_trip(secret.clone());
782        round_trip(Some(secret.clone()));
783        round_trip(Field::Value(secret.clone()));
784        round_trip(vec![secret.clone(), Secret::new("second".into())]);
785        round_trip(BTreeMap::from([("key".into(), secret)]));
786    }
787
788    #[test]
789    fn secret_decode_is_strict_and_unwraps_exactly_one_layer() {
790        const SENTINEL: &str = "secret-visitor-sentinel";
791        let encoded = Secret::new(String::from(SENTINEL)).encode_value().unwrap();
792        let ValueRef::Sensitive(inner) = encoded.view() else {
793            panic!()
794        };
795        assert!(matches!(inner.view(), ValueRef::String(SENTINEL)));
796
797        decode_error(
798            Secret::<String>::decode_value(&ContractValue::null()),
799            DecodeErrorKind::UnexpectedNull,
800        );
801        for value in [
802            ContractValue::string(SENTINEL),
803            ContractValue::opaque(OpaquePayload::new(OpaqueTree::String(SENTINEL.into()))),
804        ] {
805            decode_error(
806                Secret::<String>::decode_value(&value),
807                DecodeErrorKind::KindMismatch,
808            );
809        }
810
811        let nested = Secret::new(Secret::new(String::from(SENTINEL)));
812        let nested_value = nested.encode_value().unwrap();
813        assert_eq!(
814            Secret::<Secret<String>>::decode_value(&nested_value),
815            Ok(nested)
816        );
817        decode_error(
818            Secret::<Secret<String>>::decode_value(&encoded),
819            DecodeErrorKind::KindMismatch,
820        );
821    }
822
823    #[test]
824    fn secret_diagnostics_never_reveal_payloads() {
825        const SENTINEL: &str = "never-print-secret-payload";
826        let secret = Secret::new(String::from(SENTINEL));
827        assert_eq!(format!("{secret:?}"), "Secret(<redacted>)");
828        assert_eq!(secret.to_string(), "Secret(<redacted>)");
829
830        let sensitive = secret.encode_value().unwrap();
831        let values = [
832            sensitive.clone(),
833            ContractValue::list([sensitive.clone()]),
834            ContractValue::object([("secret".into(), sensitive.clone())]).unwrap(),
835            ContractValue::enum_value("secret", SlotValue::Value(sensitive)),
836        ];
837        for value in values {
838            for diagnostic in [
839                format!("{value:?}"),
840                format!("{:?}", SlotValue::Value(value)),
841            ] {
842                assert!(!diagnostic.contains(SENTINEL));
843                assert!(diagnostic.contains("<redacted>"));
844            }
845        }
846
847        let sensitive = ContractValue::sensitive(ContractValue::string(SENTINEL));
848        let decode = Secret::<u8>::decode_value(&sensitive).unwrap_err();
849        for diagnostic in [format!("{decode:?}"), decode.to_string()] {
850            assert!(!diagnostic.contains(SENTINEL));
851        }
852        let encode = Secret::new(f32::NAN).encode_value().unwrap_err();
853        assert_eq!(encode.kind(), &EncodeErrorKind::NonFiniteF32);
854        for diagnostic in [format!("{encode:?}"), encode.to_string()] {
855            assert!(!diagnostic.contains("NaN"));
856        }
857    }
858
859    #[test]
860    fn blob_is_an_exact_owned_byte_value() {
861        let blob = Blob::new([0, 1, 255]);
862        assert_eq!(blob.as_bytes(), &[0, 1, 255]);
863        assert_eq!(blob.clone().into_bytes(), vec![0, 1, 255]);
864        round_trip(blob.clone());
865        let encoded = blob.encode_value().unwrap();
866        assert!(matches!(encoded.view(), ValueRef::Bytes([0, 1, 255])));
867        decode_error(
868            Blob::decode_value(&ContractValue::null()),
869            DecodeErrorKind::KindMismatch,
870        );
871        decode_error(
872            Blob::decode_value(&ContractValue::string("bytes")),
873            DecodeErrorKind::KindMismatch,
874        );
875        decode_error(
876            Blob::decode(&SlotValue::Null),
877            DecodeErrorKind::UnexpectedNull,
878        );
879    }
880
881    #[derive(Debug)]
882    struct TestDomainError;
883
884    impl ContractType for TestDomainError {
885        fn encode_value(&self) -> Result<ContractValue, EncodeError> {
886            Ok(ContractValue::string("test-domain"))
887        }
888
889        fn decode_value(value: &ContractValue) -> Result<Self, DecodeError> {
890            String::decode_value(value).map(|_| Self)
891        }
892    }
893
894    impl ContractError for TestDomainError {
895        fn error_tag(&self) -> &str {
896            "test-domain"
897        }
898    }
899
900    #[test]
901    fn new_public_types_have_expected_bounds_and_error_tag() {
902        fn assert_bounds<T: Send + Sync + 'static>() {}
903        fn assert_error<T: ContractError + Send + Sync + 'static>(error: &T) -> &str {
904            error.error_tag()
905        }
906        assert_bounds::<Blob>();
907        assert_bounds::<Secret<String>>();
908        assert_eq!(assert_error(&TestDomainError), "test-domain");
909    }
910}