Skip to main content

boxology_contract/
descriptor.rs

1//! Owned, transport-neutral contract type descriptors.
2//!
3//! Unknown-variant opaque capture belongs to conformance, not to the declared
4//! variant payload vocabulary. Type names and type-level deprecation remain
5//! schema-owned rather than metadata on `TypeDescriptor`.
6
7use std::error::Error;
8use std::fmt;
9
10use crate::conform::{ConformanceError, Shape, VariantShape, conform_slot};
11use crate::{DecodeRole, SlotValue};
12
13/// An owned contract type descriptor with an invariant-preserving private representation.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TypeDescriptor(Repr);
16
17macro_rules! descriptor_model {
18    ($($constructor:ident => $variant:ident : $doc:literal),+ $(,)?) => {
19        #[derive(Debug, Clone, PartialEq, Eq)]
20        enum Repr {
21            $($variant,)+
22            Secret(Box<TypeDescriptor>),
23            Optional(Box<TypeDescriptor>),
24            TriState(Box<TypeDescriptor>),
25            List(Box<TypeDescriptor>),
26            Map(Box<TypeDescriptor>),
27            Struct(Vec<FieldDescriptor>),
28            Enum(Vec<VariantDescriptor>),
29        }
30
31        impl TypeDescriptor {
32            $(
33                #[doc = concat!("Constructs the ", $doc, " descriptor.")]
34                pub fn $constructor() -> Self { Self(Repr::$variant) }
35            )+
36
37            /// Borrows this descriptor through its complete read-only view.
38            pub fn view(&self) -> DescriptorRef<'_> {
39                match &self.0 {
40                    $(Repr::$variant => DescriptorRef::$variant,)+
41                    Repr::Secret(inner) => DescriptorRef::Secret(inner),
42                    Repr::Optional(inner) => DescriptorRef::Optional(inner),
43                    Repr::TriState(inner) => DescriptorRef::TriState(inner),
44                    Repr::List(inner) => DescriptorRef::List(inner),
45                    Repr::Map(inner) => DescriptorRef::Map(inner),
46                    Repr::Struct(fields) => DescriptorRef::Struct(fields),
47                    Repr::Enum(variants) => DescriptorRef::Enum(variants),
48                }
49            }
50        }
51
52        /// A complete borrowed view of a type descriptor.
53        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54        pub enum DescriptorRef<'a> {
55            $(#[doc = concat!("The ", $doc, " descriptor.")] $variant,)+
56            /// Sensitive inner descriptor.
57            Secret(&'a TypeDescriptor),
58            /// Nullable inner descriptor.
59            Optional(&'a TypeDescriptor),
60            /// Missing-or-null inner descriptor.
61            TriState(&'a TypeDescriptor),
62            /// List element descriptor.
63            List(&'a TypeDescriptor),
64            /// String-keyed map value descriptor.
65            Map(&'a TypeDescriptor),
66            /// Ordered struct fields.
67            Struct(&'a [FieldDescriptor]),
68            /// Ordered enum variants.
69            Enum(&'a [VariantDescriptor]),
70        }
71    };
72}
73
74descriptor_model!(
75    bool => Bool: "boolean", i8 => I8: "signed 8-bit integer",
76    i16 => I16: "signed 16-bit integer", i32 => I32: "signed 32-bit integer",
77    i64 => I64: "signed 64-bit integer", u8 => U8: "unsigned 8-bit integer",
78    u16 => U16: "unsigned 16-bit integer", u32 => U32: "unsigned 32-bit integer",
79    u64 => U64: "unsigned 64-bit integer", f32 => F32: "32-bit float",
80    f64 => F64: "64-bit float", string => String: "UTF-8 string",
81    blob => Blob: "owned byte string",
82);
83
84macro_rules! unary_descriptors {
85    ($($name:ident => $variant:ident, $check:ident, $error:ident, $doc:literal);+ $(;)?) => {$(
86        #[doc = $doc]
87        pub fn $name(inner: Self) -> Result<Self, DescriptorError> {
88            if $check(&inner.0) { Err(DescriptorError::$error) }
89            else { Ok(Self(Repr::$variant(Box::new(inner)))) }
90        }
91    )+};
92}
93
94impl TypeDescriptor {
95    unary_descriptors!(
96        secret => Secret, is_tri_state, TriStateSecretInner, "Constructs a sensitive value descriptor.";
97        optional => Optional, is_presence, NestedPresence, "Constructs an optional descriptor.";
98        tri_state => TriState, is_presence, NestedPresence, "Constructs a tri-state descriptor.";
99        list => List, is_tri_state, TriStateListElement, "Constructs a list descriptor.";
100        map => Map, is_tri_state, TriStateMapValue, "Constructs a string-keyed map descriptor.";
101    );
102
103    /// Constructs an ordered struct descriptor with unique field names.
104    pub fn structure(
105        fields: impl IntoIterator<Item = FieldDescriptor>,
106    ) -> Result<Self, DescriptorError> {
107        unique(
108            fields,
109            FieldDescriptor::name,
110            DescriptorError::DuplicateField,
111        )
112        .map(|fields| Self(Repr::Struct(fields)))
113    }
114
115    /// Constructs an ordered enum descriptor with unique variant tags.
116    pub fn enumeration(
117        variants: impl IntoIterator<Item = VariantDescriptor>,
118    ) -> Result<Self, DescriptorError> {
119        let variants = unique(
120            variants,
121            VariantDescriptor::tag,
122            DescriptorError::DuplicateVariant,
123        )?;
124        if variants.iter().any(|variant| {
125            matches!(variant.payload(), VariantPayload::Value(inner) if matches!(inner.0, Repr::TriState(_)))
126        }) {
127            Err(DescriptorError::TriStateEnumPayload)
128        } else {
129            Ok(Self(Repr::Enum(variants)))
130        }
131    }
132
133    /// Conforms a call slot to this descriptor for the selected decoding role.
134    pub fn conform(
135        &self,
136        role: DecodeRole,
137        slot: SlotValue,
138    ) -> Result<SlotValue, ConformanceError> {
139        conform_slot(&self.to_shape(), role, slot)
140    }
141
142    fn to_shape(&self) -> Shape {
143        // Public descriptor construction enforces the same or stricter
144        // recursive invariants than each fallible private shape constructor.
145        match &self.0 {
146            Repr::Bool => Shape::bool(),
147            Repr::I8 | Repr::I16 | Repr::I32 | Repr::I64 => Shape::i64(),
148            Repr::U8 | Repr::U16 | Repr::U32 | Repr::U64 => Shape::u64(),
149            Repr::F32 => Shape::f32(),
150            Repr::F64 => Shape::f64(),
151            Repr::String => Shape::string(),
152            Repr::Blob => Shape::bytes(),
153            Repr::Secret(inner) => Shape::sensitive(inner.to_shape())
154                .expect("validated secret descriptor lowers to a sensitive shape"),
155            Repr::Optional(inner) => Shape::optional(inner.to_shape())
156                .expect("validated optional descriptor lowers to an optional shape"),
157            Repr::TriState(inner) => Shape::tri_state(inner.to_shape())
158                .expect("validated tri-state descriptor lowers to a tri-state shape"),
159            Repr::List(inner) => Shape::list(inner.to_shape())
160                .expect("validated list descriptor lowers to a list shape"),
161            Repr::Map(inner) => Shape::map(inner.to_shape())
162                .expect("validated map descriptor lowers to a map shape"),
163            Repr::Struct(fields) => Shape::structure(
164                fields
165                    .iter()
166                    .map(|field| (field.name().into(), field.descriptor().to_shape())),
167            )
168            .expect("validated struct descriptor lowers to a struct shape"),
169            Repr::Enum(variants) => Shape::enumeration(variants.iter().map(|variant| {
170                let payload = match variant.payload() {
171                    VariantPayload::Unit => VariantShape::Unit,
172                    VariantPayload::Value(inner) => VariantShape::Value(inner.to_shape()),
173                };
174                (variant.tag().into(), payload)
175            }))
176            .expect("validated enum descriptor lowers to an enum shape"),
177        }
178    }
179}
180
181fn is_presence(repr: &Repr) -> bool {
182    matches!(repr, Repr::Optional(_) | Repr::TriState(_))
183}
184
185fn is_tri_state(repr: &Repr) -> bool {
186    matches!(repr, Repr::TriState(_))
187}
188
189/// Deprecation metadata attached to a declared contract element.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct Deprecation {
192    note: Option<String>,
193}
194
195impl Deprecation {
196    /// Constructs deprecation metadata with an optional note.
197    pub fn new(note: Option<String>) -> Self {
198        Self { note }
199    }
200
201    /// Returns the optional note.
202    pub fn note(&self) -> Option<&str> {
203        self.note.as_deref()
204    }
205}
206
207/// A declared enum variant payload.
208#[derive(Debug, Clone, PartialEq, Eq)]
209pub enum VariantPayload {
210    /// No payload.
211    Unit,
212    /// One typed payload.
213    Value(TypeDescriptor),
214}
215
216macro_rules! named_descriptor {
217    ($type:ident, $key:ident, $value:ident : $value_type:ty, $noun:literal) => {
218        #[doc = concat!("One named, ordered ", $noun, ".")]
219        #[derive(Debug, Clone, PartialEq, Eq)]
220        pub struct $type {
221            $key: String,
222            $value: $value_type,
223            deprecation: Option<Deprecation>,
224        }
225
226        impl $type {
227            #[doc = concat!("Constructs a ", $noun, " without imposing a name grammar.")]
228            pub fn new(
229                $key: impl Into<String>,
230                $value: $value_type,
231                deprecation: Option<Deprecation>,
232            ) -> Self {
233                Self {
234                    $key: $key.into(),
235                    $value,
236                    deprecation,
237                }
238            }
239
240            #[doc = concat!("Returns the schema-owned ", $noun, " name.")]
241            pub fn $key(&self) -> &str {
242                &self.$key
243            }
244
245            #[doc = concat!("Returns the ", $noun, " value.")]
246            pub fn $value(&self) -> &$value_type {
247                &self.$value
248            }
249
250            #[doc = concat!("Returns the ", $noun, " deprecation metadata.")]
251            pub fn deprecation(&self) -> Option<&Deprecation> {
252                self.deprecation.as_ref()
253            }
254        }
255    };
256}
257
258named_descriptor!(FieldDescriptor, name, descriptor: TypeDescriptor, "struct field");
259named_descriptor!(VariantDescriptor, tag, payload: VariantPayload, "enum variant");
260
261/// A failure to construct a legal type descriptor.
262#[derive(Debug, Clone, PartialEq, Eq)]
263#[non_exhaustive]
264pub enum DescriptorError {
265    /// Presence wrappers were nested directly.
266    NestedPresence,
267    /// A list element was tri-state.
268    TriStateListElement,
269    /// A map value was tri-state.
270    TriStateMapValue,
271    /// An enum payload was tri-state.
272    TriStateEnumPayload,
273    /// A secret inner value was tri-state.
274    TriStateSecretInner,
275    /// A struct repeated this field name.
276    DuplicateField(String),
277    /// An enum repeated this variant tag.
278    DuplicateVariant(String),
279}
280
281impl fmt::Display for DescriptorError {
282    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self {
284            Self::NestedPresence => formatter.write_str("presence wrappers cannot be nested"),
285            Self::TriStateListElement => formatter.write_str("list elements cannot be tri-state"),
286            Self::TriStateMapValue => formatter.write_str("map values cannot be tri-state"),
287            Self::TriStateEnumPayload => formatter.write_str("enum payloads cannot be tri-state"),
288            Self::TriStateSecretInner => formatter.write_str("secret values cannot be tri-state"),
289            Self::DuplicateField(name) => write!(formatter, "duplicate struct field: {name:?}"),
290            Self::DuplicateVariant(tag) => write!(formatter, "duplicate enum variant: {tag:?}"),
291        }
292    }
293}
294
295impl Error for DescriptorError {}
296
297fn unique<T>(
298    items: impl IntoIterator<Item = T>,
299    name: impl Fn(&T) -> &str,
300    duplicate: impl Fn(String) -> DescriptorError,
301) -> Result<Vec<T>, DescriptorError> {
302    let mut result = Vec::new();
303    for item in items {
304        if result.iter().any(|known| name(known) == name(&item)) {
305            return Err(duplicate(name(&item).into()));
306        }
307        result.push(item);
308    }
309    Ok(result)
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::{
316        Blob, ConformanceErrorKind, ContractType, ContractValue, DecodeErrorKind, PathSegment,
317        ValueRef,
318    };
319
320    fn field(name: &str, descriptor: TypeDescriptor) -> FieldDescriptor {
321        FieldDescriptor::new(name, descriptor, None)
322    }
323
324    fn variant(tag: &str, payload: VariantPayload) -> VariantDescriptor {
325        VariantDescriptor::new(tag, payload, None)
326    }
327
328    macro_rules! rejects {
329        ($($value:expr => $error:expr),+ $(,)?) => {$(
330            assert_eq!($value.unwrap_err(), $error);
331        )+};
332    }
333
334    fn view_path(descriptor: &TypeDescriptor) -> String {
335        let (name, inner) = match descriptor.view() {
336            DescriptorRef::List(inner) => ("list", Some(inner)),
337            DescriptorRef::Map(inner) => ("map", Some(inner)),
338            DescriptorRef::Optional(inner) => ("optional", Some(inner)),
339            DescriptorRef::Secret(inner) => ("secret", Some(inner)),
340            DescriptorRef::Blob => ("blob", None),
341            _ => panic!(),
342        };
343        inner.map_or_else(
344            || name.into(),
345            |inner| format!("{name}/{}", view_path(inner)),
346        )
347    }
348
349    #[test]
350    fn recursive_public_view_exposes_owned_children() {
351        let descriptor = TypeDescriptor::list(
352            TypeDescriptor::map(
353                TypeDescriptor::optional(TypeDescriptor::secret(TypeDescriptor::blob()).unwrap())
354                    .unwrap(),
355            )
356            .unwrap(),
357        )
358        .unwrap();
359        assert_eq!(view_path(&descriptor), "list/map/optional/secret/blob");
360    }
361
362    #[test]
363    fn every_illegal_position_returns_its_exact_error() {
364        let optional = || TypeDescriptor::optional(TypeDescriptor::bool()).unwrap();
365        let tri_state = || TypeDescriptor::tri_state(TypeDescriptor::bool()).unwrap();
366        rejects!(
367            TypeDescriptor::optional(optional()) => DescriptorError::NestedPresence,
368            TypeDescriptor::optional(tri_state()) => DescriptorError::NestedPresence,
369            TypeDescriptor::tri_state(optional()) => DescriptorError::NestedPresence,
370            TypeDescriptor::tri_state(tri_state()) => DescriptorError::NestedPresence,
371            TypeDescriptor::list(tri_state()) => DescriptorError::TriStateListElement,
372            TypeDescriptor::map(tri_state()) => DescriptorError::TriStateMapValue,
373            TypeDescriptor::secret(tri_state()) => DescriptorError::TriStateSecretInner,
374            TypeDescriptor::enumeration([variant("bad", VariantPayload::Value(tri_state()))])
375                => DescriptorError::TriStateEnumPayload,
376        );
377    }
378
379    #[test]
380    fn duplicate_names_return_exact_errors() {
381        rejects!(
382            TypeDescriptor::structure([
383                field("x", TypeDescriptor::i8()), field("x", TypeDescriptor::i16())
384            ]) => DescriptorError::DuplicateField("x".into()),
385            TypeDescriptor::enumeration([
386                variant("x", VariantPayload::Unit), variant("x", VariantPayload::Unit)
387            ]) => DescriptorError::DuplicateVariant("x".into()),
388        );
389        assert_eq!(
390            DescriptorError::DuplicateField("field".into()).to_string(),
391            "duplicate struct field: \"field\""
392        );
393        assert_eq!(
394            DescriptorError::DuplicateVariant("variant".into()).to_string(),
395            "duplicate enum variant: \"variant\""
396        );
397    }
398
399    #[test]
400    fn legal_edges_metadata_and_public_bounds_are_preserved() {
401        macro_rules! views { ($($name:ident => $variant:ident),+) => {$(
402            assert!(matches!(TypeDescriptor::$name().view(), DescriptorRef::$variant));
403        )+} }
404        views!(
405            bool => Bool, i8 => I8, i16 => I16, i32 => I32, i64 => I64,
406            u8 => U8, u16 => U16, u32 => U32, u64 => U64,
407            f32 => F32, f64 => F64, string => String, blob => Blob
408        );
409        let optional = TypeDescriptor::optional(TypeDescriptor::string()).unwrap();
410        assert!(TypeDescriptor::secret(optional).is_ok());
411        let secret = TypeDescriptor::secret(TypeDescriptor::string()).unwrap();
412        assert!(TypeDescriptor::optional(secret.clone()).is_ok());
413        assert!(TypeDescriptor::secret(secret).is_ok());
414        assert!(TypeDescriptor::structure([]).is_ok());
415        assert!(TypeDescriptor::enumeration([]).is_ok());
416        assert!(
417            TypeDescriptor::structure([field(
418                "any name!",
419                TypeDescriptor::tri_state(TypeDescriptor::bool()).unwrap(),
420            )])
421            .is_ok()
422        );
423        let deprecation = Deprecation::new(Some("use next".into()));
424        let field =
425            FieldDescriptor::new("field", TypeDescriptor::bool(), Some(deprecation.clone()));
426        let variant = VariantDescriptor::new("variant", VariantPayload::Unit, Some(deprecation));
427        assert_eq!(field.name(), "field");
428        assert_eq!(field.deprecation().unwrap().note(), Some("use next"));
429        assert_eq!(variant.tag(), "variant");
430        assert_eq!(variant.deprecation().unwrap().note(), Some("use next"));
431        assert!(matches!(field.descriptor().view(), DescriptorRef::Bool));
432        assert!(matches!(variant.payload(), VariantPayload::Unit));
433
434        fn bounds<T: Send + Sync + 'static>() {}
435        bounds::<TypeDescriptor>();
436        bounds::<Deprecation>();
437        bounds::<FieldDescriptor>();
438        bounds::<VariantDescriptor>();
439        bounds::<VariantPayload>();
440        bounds::<DescriptorError>();
441        assert_eq!(
442            DescriptorError::NestedPresence.to_string(),
443            "presence wrappers cannot be nested"
444        );
445    }
446
447    fn slot(value: ContractValue) -> SlotValue {
448        SlotValue::Value(value)
449    }
450
451    #[test]
452    fn lowering_preserves_carriers_narrow_widths_and_blob_bytes() {
453        let role = DecodeRole::ProviderInput;
454        let signed = slot(ContractValue::i64(128));
455        for descriptor in [
456            TypeDescriptor::i8(),
457            TypeDescriptor::i16(),
458            TypeDescriptor::i32(),
459            TypeDescriptor::i64(),
460        ] {
461            assert_eq!(descriptor.conform(role, signed.clone()), Ok(signed.clone()));
462        }
463        assert_eq!(
464            i8::decode(&signed).unwrap_err().kind(),
465            &DecodeErrorKind::OutOfRange
466        );
467        let unsigned = slot(ContractValue::u64(u64::MAX));
468        for descriptor in [
469            TypeDescriptor::u8(),
470            TypeDescriptor::u16(),
471            TypeDescriptor::u32(),
472            TypeDescriptor::u64(),
473        ] {
474            assert_eq!(
475                descriptor.conform(role, unsigned.clone()),
476                Ok(unsigned.clone())
477            );
478        }
479        for (descriptor, input) in [
480            (TypeDescriptor::bool(), slot(ContractValue::bool(true))),
481            (
482                TypeDescriptor::f32(),
483                slot(ContractValue::f32(1.5).unwrap()),
484            ),
485            (
486                TypeDescriptor::f64(),
487                slot(ContractValue::f64(2.5).unwrap()),
488            ),
489            (
490                TypeDescriptor::string(),
491                slot(ContractValue::string("text")),
492            ),
493        ] {
494            assert_eq!(descriptor.conform(role, input.clone()), Ok(input));
495        }
496        assert_eq!(
497            TypeDescriptor::f32()
498                .conform(role, slot(ContractValue::f64(1.5).unwrap()))
499                .unwrap_err()
500                .kind(),
501            &ConformanceErrorKind::KindMismatch
502        );
503        let bytes = slot(ContractValue::bytes([0, 1, 255]));
504        assert_eq!(
505            TypeDescriptor::blob().conform(role, bytes.clone()),
506            Ok(bytes.clone())
507        );
508        assert_eq!(Blob::decode(&bytes).unwrap().as_bytes(), &[0, 1, 255]);
509    }
510
511    #[test]
512    fn secret_lowering_is_exact_nullable_nested_and_redacted() {
513        const SENTINEL: &str = "descriptor-secret-sentinel";
514        let role = DecodeRole::ProviderInput;
515        let secret = TypeDescriptor::secret(TypeDescriptor::string()).unwrap();
516        let input = slot(ContractValue::sensitive(ContractValue::string(SENTINEL)));
517        let output = secret.conform(role, input.clone()).unwrap();
518        assert_eq!(output, input);
519        let SlotValue::Value(output) = output else {
520            panic!()
521        };
522        let ValueRef::Sensitive(inner) = output.view() else {
523            panic!()
524        };
525        assert!(matches!(inner.view(), ValueRef::String(SENTINEL)));
526
527        let bare = slot(ContractValue::string(SENTINEL));
528        assert_eq!(
529            secret.conform(role, bare).unwrap_err().kind(),
530            &ConformanceErrorKind::KindMismatch
531        );
532        assert_eq!(
533            TypeDescriptor::string()
534                .conform(role, input.clone())
535                .unwrap_err()
536                .kind(),
537            &ConformanceErrorKind::KindMismatch
538        );
539
540        let nullable = TypeDescriptor::optional(TypeDescriptor::string()).unwrap();
541        let transitive =
542            TypeDescriptor::optional(TypeDescriptor::secret(nullable).unwrap()).unwrap();
543        let sensitive_null = slot(ContractValue::sensitive(ContractValue::null()));
544        assert_eq!(
545            transitive.conform(role, sensitive_null.clone()),
546            Ok(sensitive_null)
547        );
548        let nested = TypeDescriptor::secret(secret).unwrap();
549        let nested_input = slot(ContractValue::sensitive(ContractValue::sensitive(
550            ContractValue::string(SENTINEL),
551        )));
552        assert_eq!(nested.conform(role, nested_input.clone()), Ok(nested_input));
553
554        let error = TypeDescriptor::secret(TypeDescriptor::bool())
555            .unwrap()
556            .conform(role, input)
557            .unwrap_err();
558        assert_eq!(error.kind(), &ConformanceErrorKind::KindMismatch);
559        assert!(!format!("{error:?} {error}").contains(SENTINEL));
560    }
561
562    #[test]
563    fn aggregate_lowering_and_public_errors_are_structural() {
564        let payload = TypeDescriptor::map(
565            TypeDescriptor::list(TypeDescriptor::secret(TypeDescriptor::blob()).unwrap()).unwrap(),
566        )
567        .unwrap();
568        let event = TypeDescriptor::enumeration([
569            variant("idle", VariantPayload::Unit),
570            variant("data", VariantPayload::Value(payload)),
571        ])
572        .unwrap();
573        let descriptor = TypeDescriptor::structure([
574            FieldDescriptor::new(
575                "event",
576                event,
577                Some(Deprecation::new(Some("legacy".into()))),
578            ),
579            field(
580                "state",
581                TypeDescriptor::tri_state(TypeDescriptor::bool()).unwrap(),
582            ),
583        ])
584        .unwrap();
585        let map = ContractValue::object([(
586            "key".into(),
587            ContractValue::list([ContractValue::sensitive(ContractValue::bytes([7]))]),
588        )])
589        .unwrap();
590        let input = slot(
591            ContractValue::object([(
592                "event".into(),
593                ContractValue::enum_value("data", SlotValue::Value(map)),
594            )])
595            .unwrap(),
596        );
597        assert_eq!(
598            descriptor.conform(DecodeRole::ProviderInput, input.clone()),
599            Ok(input)
600        );
601
602        let error = TypeDescriptor::list(TypeDescriptor::bool())
603            .unwrap()
604            .conform(
605                DecodeRole::ProviderInput,
606                slot(ContractValue::list([ContractValue::string("wrong")])),
607            )
608            .unwrap_err();
609        assert_eq!(error.kind(), &ConformanceErrorKind::KindMismatch);
610        assert_eq!(error.path(), &[PathSegment::Index(0)]);
611
612        fn error_bounds<T: std::error::Error + Send + Sync + 'static>() {}
613        fn bounds<T: Send + Sync + 'static>() {}
614        error_bounds::<ConformanceError>();
615        bounds::<ConformanceErrorKind>();
616    }
617
618    fn object(entries: impl IntoIterator<Item = (&'static str, ContractValue)>) -> ContractValue {
619        ContractValue::object(
620            entries
621                .into_iter()
622                .map(|(name, value)| (name.into(), value)),
623        )
624        .unwrap()
625    }
626
627    fn agrees_accepts(descriptor: TypeDescriptor, input: SlotValue) {
628        for role in [DecodeRole::ProviderInput, DecodeRole::ConsumerOutput] {
629            let private = conform_slot(&descriptor.to_shape(), role, input.clone());
630            let public = descriptor.conform(role, input.clone());
631            assert_eq!(public, private);
632            assert_eq!(public, Ok(input.clone()));
633        }
634    }
635
636    fn agrees_rejects(
637        descriptor: TypeDescriptor,
638        input: SlotValue,
639        kind: ConformanceErrorKind,
640        path: &[PathSegment],
641    ) {
642        for role in [DecodeRole::ProviderInput, DecodeRole::ConsumerOutput] {
643            let private = conform_slot(&descriptor.to_shape(), role, input.clone());
644            let public = descriptor.conform(role, input.clone());
645            assert_eq!(public, private);
646            let error = public.unwrap_err();
647            assert_eq!(error.kind(), &kind);
648            assert_eq!(error.path(), path);
649        }
650    }
651
652    fn structure_field(descriptor: TypeDescriptor) -> TypeDescriptor {
653        TypeDescriptor::structure([field("x", descriptor)]).unwrap()
654    }
655
656    #[test]
657    fn public_and_private_presence_grids_agree_in_both_roles() {
658        let integer = || ContractValue::i64(7);
659        let value = || slot(integer());
660        let optional = || TypeDescriptor::optional(TypeDescriptor::i64()).unwrap();
661        let tri_state = || TypeDescriptor::tri_state(TypeDescriptor::i64()).unwrap();
662
663        agrees_rejects(
664            TypeDescriptor::i64(),
665            SlotValue::Missing,
666            ConformanceErrorKind::MissingRequired,
667            &[],
668        );
669        agrees_rejects(
670            TypeDescriptor::i64(),
671            SlotValue::Null,
672            ConformanceErrorKind::UnexpectedNull,
673            &[],
674        );
675        agrees_accepts(TypeDescriptor::i64(), value());
676        agrees_rejects(
677            optional(),
678            SlotValue::Missing,
679            ConformanceErrorKind::UnexpectedMissing,
680            &[],
681        );
682        agrees_accepts(optional(), SlotValue::Null);
683        agrees_accepts(optional(), value());
684        agrees_accepts(tri_state(), SlotValue::Missing);
685        agrees_accepts(tri_state(), SlotValue::Null);
686        agrees_accepts(tri_state(), value());
687
688        let field_path = [PathSegment::Field("x".into())];
689        agrees_rejects(
690            structure_field(TypeDescriptor::i64()),
691            slot(object([])),
692            ConformanceErrorKind::MissingRequired,
693            &field_path,
694        );
695        agrees_rejects(
696            structure_field(TypeDescriptor::i64()),
697            slot(object([("x", ContractValue::null())])),
698            ConformanceErrorKind::UnexpectedNull,
699            &field_path,
700        );
701        agrees_accepts(
702            structure_field(TypeDescriptor::i64()),
703            slot(object([("x", integer())])),
704        );
705        agrees_accepts(structure_field(optional()), slot(object([])));
706        agrees_rejects(
707            structure_field(optional()),
708            slot(object([("x", ContractValue::null())])),
709            ConformanceErrorKind::UnexpectedNull,
710            &field_path,
711        );
712        agrees_accepts(
713            structure_field(optional()),
714            slot(object([("x", integer())])),
715        );
716        agrees_accepts(structure_field(tri_state()), slot(object([])));
717        agrees_accepts(
718            structure_field(tri_state()),
719            slot(object([("x", ContractValue::null())])),
720        );
721        agrees_accepts(
722            structure_field(tri_state()),
723            slot(object([("x", integer())])),
724        );
725
726        let index_path = [PathSegment::Index(0)];
727        agrees_accepts(
728            TypeDescriptor::list(TypeDescriptor::i64()).unwrap(),
729            slot(ContractValue::list([integer()])),
730        );
731        agrees_rejects(
732            TypeDescriptor::list(TypeDescriptor::i64()).unwrap(),
733            slot(ContractValue::list([ContractValue::null()])),
734            ConformanceErrorKind::UnexpectedNull,
735            &index_path,
736        );
737        agrees_accepts(
738            TypeDescriptor::list(optional()).unwrap(),
739            slot(ContractValue::list([ContractValue::null(), integer()])),
740        );
741        let key_path = [PathSegment::MapKey("x".into())];
742        agrees_rejects(
743            TypeDescriptor::map(TypeDescriptor::i64()).unwrap(),
744            slot(object([("x", ContractValue::null())])),
745            ConformanceErrorKind::UnexpectedNull,
746            &key_path,
747        );
748        agrees_accepts(
749            TypeDescriptor::map(optional()).unwrap(),
750            slot(object([("x", ContractValue::null())])),
751        );
752        agrees_accepts(
753            structure_field(TypeDescriptor::list(optional()).unwrap()),
754            slot(object([(
755                "x",
756                ContractValue::list([ContractValue::null(), integer()]),
757            )])),
758        );
759
760        let enum_slot = |payload| slot(ContractValue::enum_value("event", payload));
761        let variant_path = [PathSegment::Variant("event".into())];
762        let unit =
763            || TypeDescriptor::enumeration([variant("event", VariantPayload::Unit)]).unwrap();
764        agrees_accepts(unit(), enum_slot(SlotValue::Null));
765        agrees_rejects(
766            unit(),
767            enum_slot(SlotValue::Missing),
768            ConformanceErrorKind::UnexpectedMissing,
769            &variant_path,
770        );
771        agrees_rejects(
772            unit(),
773            enum_slot(value()),
774            ConformanceErrorKind::UnexpectedPayload,
775            &variant_path,
776        );
777        let required = || {
778            TypeDescriptor::enumeration([variant(
779                "event",
780                VariantPayload::Value(TypeDescriptor::i64()),
781            )])
782            .unwrap()
783        };
784        agrees_rejects(
785            required(),
786            enum_slot(SlotValue::Missing),
787            ConformanceErrorKind::MissingRequired,
788            &variant_path,
789        );
790        agrees_rejects(
791            required(),
792            enum_slot(SlotValue::Null),
793            ConformanceErrorKind::UnexpectedNull,
794            &variant_path,
795        );
796        agrees_accepts(required(), enum_slot(value()));
797        let optional_variant = || {
798            TypeDescriptor::enumeration([variant("event", VariantPayload::Value(optional()))])
799                .unwrap()
800        };
801        agrees_rejects(
802            optional_variant(),
803            enum_slot(SlotValue::Missing),
804            ConformanceErrorKind::UnexpectedMissing,
805            &variant_path,
806        );
807        agrees_accepts(optional_variant(), enum_slot(SlotValue::Null));
808        agrees_accepts(optional_variant(), enum_slot(value()));
809    }
810
811    #[test]
812    fn public_struct_conformance_is_strict_or_ordered_and_tolerant() {
813        const SENTINEL: &str = "public-unknown-field-sentinel";
814        let descriptor = TypeDescriptor::structure([
815            field("a", TypeDescriptor::i64()),
816            field("b", TypeDescriptor::i64()),
817        ])
818        .unwrap();
819        let input = slot(object([
820            ("b", ContractValue::i64(2)),
821            ("extra", ContractValue::string(SENTINEL)),
822            ("a", ContractValue::i64(1)),
823        ]));
824        let error = descriptor
825            .conform(DecodeRole::ProviderInput, input.clone())
826            .unwrap_err();
827        assert_eq!(
828            error.kind(),
829            &ConformanceErrorKind::UnknownField("extra".into())
830        );
831        assert_eq!(error.path(), &[PathSegment::Field("extra".into())]);
832        assert!(!format!("{error:?} {error}").contains(SENTINEL));
833        let output = descriptor
834            .conform(DecodeRole::ConsumerOutput, input)
835            .unwrap();
836        assert_eq!(
837            output,
838            slot(object([
839                ("b", ContractValue::i64(2)),
840                ("a", ContractValue::i64(1)),
841            ]))
842        );
843    }
844
845    #[test]
846    fn aggregate_views_expose_ordered_children_and_every_wrapper() {
847        let deprecated = Deprecation::new(Some("legacy".into()));
848        let enumeration = TypeDescriptor::enumeration([
849            VariantDescriptor::new("unit", VariantPayload::Unit, None),
850            VariantDescriptor::new(
851                "value",
852                VariantPayload::Value(TypeDescriptor::list(TypeDescriptor::i64()).unwrap()),
853                Some(deprecated.clone()),
854            ),
855        ])
856        .unwrap();
857        let descriptor = TypeDescriptor::structure([
858            FieldDescriptor::new(
859                "state",
860                TypeDescriptor::tri_state(TypeDescriptor::bool()).unwrap(),
861                Some(deprecated),
862            ),
863            field("event", enumeration),
864        ])
865        .unwrap();
866        let DescriptorRef::Struct(fields) = descriptor.view() else {
867            panic!()
868        };
869        assert_eq!(
870            fields.iter().map(FieldDescriptor::name).collect::<Vec<_>>(),
871            ["state", "event"]
872        );
873        assert!(matches!(
874            fields[0].descriptor().view(),
875            DescriptorRef::TriState(_)
876        ));
877        assert_eq!(fields[0].deprecation().unwrap().note(), Some("legacy"));
878        let DescriptorRef::Enum(variants) = fields[1].descriptor().view() else {
879            panic!()
880        };
881        assert_eq!(
882            variants
883                .iter()
884                .map(VariantDescriptor::tag)
885                .collect::<Vec<_>>(),
886            ["unit", "value"]
887        );
888        assert!(matches!(variants[0].payload(), VariantPayload::Unit));
889        let VariantPayload::Value(value) = variants[1].payload() else {
890            panic!()
891        };
892        assert!(matches!(value.view(), DescriptorRef::List(_)));
893        assert_eq!(variants[1].deprecation().unwrap().note(), Some("legacy"));
894    }
895
896    #[test]
897    fn descriptor_structural_equality_covers_every_semantic_dimension() {
898        let a = || field("a", TypeDescriptor::i64());
899        let b = || field("b", TypeDescriptor::string());
900        let c = || field("c", TypeDescriptor::string());
901        let ordered = || TypeDescriptor::structure([a(), b()]).unwrap();
902        assert_ne!(TypeDescriptor::bool(), TypeDescriptor::string());
903        assert_ne!(TypeDescriptor::i8(), TypeDescriptor::i16());
904        assert_ne!(
905            TypeDescriptor::list(TypeDescriptor::i8()).unwrap(),
906            TypeDescriptor::list(TypeDescriptor::i16()).unwrap()
907        );
908        assert_ne!(ordered(), TypeDescriptor::structure([b(), a()]).unwrap());
909        assert_ne!(
910            TypeDescriptor::structure([field("a", TypeDescriptor::i64())]).unwrap(),
911            TypeDescriptor::structure([field("renamed", TypeDescriptor::i64())]).unwrap()
912        );
913        let deprecated = |note: &str| Some(Deprecation::new(Some(note.into())));
914        assert_ne!(
915            TypeDescriptor::structure([FieldDescriptor::new(
916                "a",
917                TypeDescriptor::i64(),
918                deprecated("one"),
919            )])
920            .unwrap(),
921            TypeDescriptor::structure([FieldDescriptor::new(
922                "a",
923                TypeDescriptor::i64(),
924                deprecated("two"),
925            )])
926            .unwrap()
927        );
928        let enumeration = |tag: &str, payload, deprecation| {
929            TypeDescriptor::enumeration([VariantDescriptor::new(tag, payload, deprecation)])
930                .unwrap()
931        };
932        assert_ne!(
933            enumeration("a", VariantPayload::Unit, None),
934            enumeration("b", VariantPayload::Unit, None)
935        );
936        assert_ne!(
937            enumeration("a", VariantPayload::Unit, None),
938            enumeration("a", VariantPayload::Value(TypeDescriptor::bool()), None)
939        );
940        assert_ne!(
941            enumeration("a", VariantPayload::Unit, deprecated("one")),
942            enumeration("a", VariantPayload::Unit, deprecated("two"))
943        );
944        let enum_pair = |first, second| {
945            TypeDescriptor::enumeration([
946                variant(first, VariantPayload::Unit),
947                variant(second, VariantPayload::Unit),
948            ])
949            .unwrap()
950        };
951        assert_ne!(enum_pair("a", "b"), enum_pair("b", "a"));
952        let nested = TypeDescriptor::optional(
953            TypeDescriptor::secret(TypeDescriptor::optional(ordered()).unwrap()).unwrap(),
954        )
955        .unwrap();
956        assert_eq!(nested, nested.clone());
957        assert_ne!(
958            nested,
959            TypeDescriptor::optional(
960                TypeDescriptor::secret(
961                    TypeDescriptor::optional(TypeDescriptor::structure([a(), c()]).unwrap())
962                        .unwrap()
963                )
964                .unwrap()
965            )
966            .unwrap()
967        );
968    }
969}