Skip to main content

boxology_contract/
conform.rs

1use std::error::Error;
2use std::fmt;
3
4use crate::{
5    ContractValue, DecodeRole, ObjectRef, OpaquePayload, PathSegment, SlotValue, ValueRef,
6};
7
8#[derive(Clone)]
9pub(crate) struct Shape(Repr);
10
11#[derive(Clone)]
12enum Repr {
13    Bool,
14    I64,
15    U64,
16    F32,
17    F64,
18    String,
19    Bytes,
20    Sensitive(Box<Shape>),
21    List(Box<Shape>),
22    Map(Box<Shape>),
23    Struct(Vec<(String, Shape)>),
24    Enum(Vec<(String, VariantShape)>),
25    Optional(Box<Shape>),
26    TriState(Box<Shape>),
27}
28
29#[derive(Clone)]
30pub(crate) enum VariantShape {
31    Unit,
32    Value(Shape),
33}
34
35macro_rules! primitives {
36    ($($name:ident => $variant:ident),+ $(,)?) => {$(
37        pub(crate) fn $name() -> Self { Self(Repr::$variant) }
38    )+};
39}
40
41impl Shape {
42    primitives!(
43        bool => Bool, i64 => I64, u64 => U64, f32 => F32,
44        f64 => F64, string => String, bytes => Bytes,
45    );
46
47    pub(crate) fn list(element: Shape) -> Result<Self, ShapeError> {
48        if matches!(element.0, Repr::TriState(_)) {
49            Err(ShapeError::TriStateListElement)
50        } else {
51            Ok(Self(Repr::List(Box::new(element))))
52        }
53    }
54
55    pub(crate) fn map(value: Shape) -> Result<Self, ShapeError> {
56        if matches!(value.0, Repr::TriState(_)) {
57            Err(ShapeError::TriStateMapValue)
58        } else {
59            Ok(Self(Repr::Map(Box::new(value))))
60        }
61    }
62
63    pub(crate) fn sensitive(inner: Shape) -> Result<Self, ShapeError> {
64        if matches!(inner.0, Repr::TriState(_)) {
65            Err(ShapeError::TriStateSecretInner)
66        } else {
67            Ok(Self(Repr::Sensitive(Box::new(inner))))
68        }
69    }
70
71    pub(crate) fn structure(
72        fields: impl IntoIterator<Item = (String, Shape)>,
73    ) -> Result<Self, ShapeError> {
74        let mut result = Vec::new();
75        for (name, shape) in fields {
76            if result
77                .iter()
78                .any(|(known, _): &(String, Shape)| known == &name)
79            {
80                return Err(ShapeError::DuplicateField(name));
81            }
82            result.push((name, shape));
83        }
84        Ok(Self(Repr::Struct(result)))
85    }
86
87    pub(crate) fn enumeration(
88        variants: impl IntoIterator<Item = (String, VariantShape)>,
89    ) -> Result<Self, ShapeError> {
90        let mut result = Vec::new();
91        for (tag, variant) in variants {
92            if result
93                .iter()
94                .any(|(known, _): &(String, VariantShape)| known == &tag)
95            {
96                return Err(ShapeError::DuplicateVariant(tag));
97            }
98            if matches!(&variant, VariantShape::Value(Shape(Repr::TriState(_)))) {
99                return Err(ShapeError::TriStateEnumPayload);
100            }
101            result.push((tag, variant));
102        }
103        Ok(Self(Repr::Enum(result)))
104    }
105
106    pub(crate) fn optional(inner: Shape) -> Result<Self, ShapeError> {
107        Self::wrapper(inner, false)
108    }
109
110    pub(crate) fn tri_state(inner: Shape) -> Result<Self, ShapeError> {
111        Self::wrapper(inner, true)
112    }
113
114    fn wrapper(inner: Shape, tri_state: bool) -> Result<Self, ShapeError> {
115        if matches!(inner.0, Repr::Optional(_) | Repr::TriState(_)) {
116            return Err(ShapeError::NestedPresence);
117        }
118        Ok(if tri_state {
119            Self(Repr::TriState(Box::new(inner)))
120        } else {
121            Self(Repr::Optional(Box::new(inner)))
122        })
123    }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub(crate) enum ShapeError {
128    NestedPresence,
129    TriStateListElement,
130    TriStateMapValue,
131    TriStateEnumPayload,
132    TriStateSecretInner,
133    DuplicateField(String),
134    DuplicateVariant(String),
135}
136
137/// The payload-free category of a conformance failure.
138#[derive(Debug, Clone, PartialEq, Eq)]
139#[non_exhaustive]
140pub enum ConformanceErrorKind {
141    /// A required value was absent.
142    MissingRequired,
143    /// Null was not accepted at this position.
144    UnexpectedNull,
145    /// Missing was not accepted at this position.
146    UnexpectedMissing,
147    /// A unit variant carried a payload.
148    UnexpectedPayload,
149    /// The value kind did not match its descriptor.
150    KindMismatch,
151    /// A strict struct contained an unknown field.
152    UnknownField(String),
153    /// A strict enum contained an unknown variant.
154    UnknownVariant(String),
155}
156
157/// A conformance failure with its descriptor path and payload-free category.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ConformanceError {
160    path: Vec<PathSegment>,
161    kind: ConformanceErrorKind,
162}
163
164impl ConformanceError {
165    /// Returns the failure category.
166    pub fn kind(&self) -> &ConformanceErrorKind {
167        &self.kind
168    }
169
170    /// Returns the path from the call slot to the failure.
171    pub fn path(&self) -> &[PathSegment] {
172        &self.path
173    }
174}
175
176impl fmt::Display for ConformanceError {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(formatter, "{:?} at {:?}", self.kind, self.path)
179    }
180}
181
182impl Error for ConformanceError {}
183
184pub(crate) fn conform_slot(
185    shape: &Shape,
186    role: DecodeRole,
187    slot: SlotValue,
188) -> Result<SlotValue, ConformanceError> {
189    conform_payload(shape, role, &slot, &mut Vec::new())
190}
191
192fn conform_payload(
193    shape: &Shape,
194    role: DecodeRole,
195    slot: &SlotValue,
196    path: &mut Vec<PathSegment>,
197) -> Result<SlotValue, ConformanceError> {
198    match slot {
199        SlotValue::Missing => match shape.0 {
200            Repr::TriState(_) => Ok(SlotValue::Missing),
201            Repr::Optional(_) => fail(path, ConformanceErrorKind::UnexpectedMissing),
202            _ => fail(path, ConformanceErrorKind::MissingRequired),
203        },
204        SlotValue::Null => match shape.0 {
205            Repr::Optional(_) | Repr::TriState(_) => Ok(SlotValue::Null),
206            _ => fail(path, ConformanceErrorKind::UnexpectedNull),
207        },
208        SlotValue::Value(value) => {
209            let inner = match &shape.0 {
210                Repr::Optional(inner) | Repr::TriState(inner) => inner.as_ref(),
211                _ => shape,
212            };
213            conform_value(inner, role, value, path, Position::Element).map(SlotValue::Value)
214        }
215    }
216}
217
218#[derive(Clone, Copy, PartialEq, Eq)]
219enum Position {
220    Field,
221    Element,
222}
223
224fn conform_value(
225    shape: &Shape,
226    role: DecodeRole,
227    value: &ContractValue,
228    path: &mut Vec<PathSegment>,
229    position: Position,
230) -> Result<ContractValue, ConformanceError> {
231    let (shape, nullable) = match &shape.0 {
232        Repr::Optional(inner) => (inner.as_ref(), position == Position::Element),
233        Repr::TriState(inner) => (inner.as_ref(), position == Position::Field),
234        _ => (shape, false),
235    };
236    match value.view() {
237        ValueRef::Null if nullable => return Ok(ContractValue::null()),
238        ValueRef::Null => return fail(path, ConformanceErrorKind::UnexpectedNull),
239        _ => {}
240    }
241    match (&shape.0, value.view()) {
242        (Repr::Bool, ValueRef::Bool(value)) => Ok(ContractValue::bool(value)),
243        (Repr::I64, ValueRef::I64(value)) => Ok(ContractValue::i64(value)),
244        (Repr::U64, ValueRef::U64(value)) => Ok(ContractValue::u64(value)),
245        (Repr::F32, ValueRef::F32(value)) => Ok(ContractValue::f32(value).unwrap()),
246        (Repr::F64, ValueRef::F64(value)) => Ok(ContractValue::f64(value).unwrap()),
247        (Repr::String, ValueRef::String(value)) => Ok(ContractValue::string(value)),
248        (Repr::Bytes, ValueRef::Bytes(value)) => Ok(ContractValue::bytes(value)),
249        (Repr::Sensitive(inner), ValueRef::Sensitive(value)) => {
250            conform_value(inner, role, value, path, Position::Element).map(ContractValue::sensitive)
251        }
252        (Repr::List(element), ValueRef::List(values)) => {
253            let values = values
254                .iter()
255                .enumerate()
256                .map(|(index, value)| {
257                    descend(path, PathSegment::Index(index), |path| {
258                        conform_value(element, role, value, path, Position::Element)
259                    })
260                })
261                .collect::<Result<Vec<_>, _>>()?;
262            Ok(ContractValue::list(values))
263        }
264        (Repr::Map(element), ValueRef::Object(object)) => conform_map(element, role, object, path),
265        (Repr::Struct(fields), ValueRef::Object(object)) => {
266            conform_struct(fields, role, object, path)
267        }
268        (Repr::Enum(variants), ValueRef::Enum { tag, payload }) => {
269            conform_enum(variants, role, tag, payload, path)
270        }
271        _ => fail(path, ConformanceErrorKind::KindMismatch),
272    }
273}
274
275fn conform_enum(
276    variants: &[(String, VariantShape)],
277    role: DecodeRole,
278    tag: &str,
279    payload: &SlotValue,
280    path: &mut Vec<PathSegment>,
281) -> Result<ContractValue, ConformanceError> {
282    descend(path, PathSegment::Variant(tag.into()), |path| {
283        let Some((_, variant)) = variants.iter().find(|(known, _)| known == tag) else {
284            return match role {
285                DecodeRole::ProviderInput => {
286                    fail(path, ConformanceErrorKind::UnknownVariant(tag.into()))
287                }
288                DecodeRole::ConsumerOutput => Ok(ContractValue::enum_value(
289                    tag,
290                    SlotValue::Value(ContractValue::opaque(OpaquePayload::capture(payload))),
291                )),
292            };
293        };
294
295        let payload = match variant {
296            VariantShape::Unit => match payload {
297                SlotValue::Null => Ok(SlotValue::Null),
298                SlotValue::Missing => fail(path, ConformanceErrorKind::UnexpectedMissing),
299                SlotValue::Value(_) => fail(path, ConformanceErrorKind::UnexpectedPayload),
300            },
301            VariantShape::Value(shape) => conform_payload(shape, role, payload, path),
302        }?;
303        Ok(ContractValue::enum_value(tag, payload))
304    })
305}
306
307fn conform_map(
308    element: &Shape,
309    role: DecodeRole,
310    object: ObjectRef<'_>,
311    path: &mut Vec<PathSegment>,
312) -> Result<ContractValue, ConformanceError> {
313    let mut output = Vec::new();
314    for (key, value) in object.entries() {
315        output.push(descend(path, PathSegment::MapKey(key.into()), |path| {
316            conform_value(element, role, value, path, Position::Element)
317                .map(|value| (key.into(), value))
318        })?);
319    }
320    ContractValue::object(output).map_err(|_| unreachable!())
321}
322
323fn conform_struct(
324    fields: &[(String, Shape)],
325    role: DecodeRole,
326    object: ObjectRef<'_>,
327    path: &mut Vec<PathSegment>,
328) -> Result<ContractValue, ConformanceError> {
329    let mut output = Vec::new();
330    for (name, value) in object.entries() {
331        let Some((_, shape)) = fields.iter().find(|(field, _)| field == name) else {
332            if role == DecodeRole::ConsumerOutput {
333                continue;
334            }
335            return descend(path, PathSegment::Field(name.into()), |path| {
336                fail(path, ConformanceErrorKind::UnknownField(name.into()))
337            });
338        };
339        output.push(descend(path, PathSegment::Field(name.into()), |path| {
340            conform_value(shape, role, value, path, Position::Field)
341                .map(|value| (name.into(), value))
342        })?);
343    }
344    for (name, shape) in fields {
345        if object.get(name).is_none() && !matches!(shape.0, Repr::Optional(_) | Repr::TriState(_)) {
346            return descend(path, PathSegment::Field(name.clone()), |path| {
347                fail(path, ConformanceErrorKind::MissingRequired)
348            });
349        }
350    }
351    ContractValue::object(output).map_err(|_| unreachable!())
352}
353
354fn descend<T>(
355    path: &mut Vec<PathSegment>,
356    segment: PathSegment,
357    operation: impl FnOnce(&mut Vec<PathSegment>) -> Result<T, ConformanceError>,
358) -> Result<T, ConformanceError> {
359    path.push(segment);
360    let result = operation(path);
361    path.pop();
362    result
363}
364
365fn fail<T>(path: &[PathSegment], kind: ConformanceErrorKind) -> Result<T, ConformanceError> {
366    Err(ConformanceError {
367        path: path.into(),
368        kind,
369    })
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::{OpaquePayload, OpaqueTree};
376
377    const ROLES: [DecodeRole; 2] = [DecodeRole::ProviderInput, DecodeRole::ConsumerOutput];
378
379    fn slot(value: ContractValue) -> SlotValue {
380        SlotValue::Value(value)
381    }
382
383    fn object(entries: Vec<(&str, ContractValue)>) -> ContractValue {
384        ContractValue::object(entries.into_iter().map(|(key, value)| (key.into(), value))).unwrap()
385    }
386
387    fn field(shape: Shape) -> Shape {
388        Shape::structure([("x".into(), shape)]).unwrap()
389    }
390
391    fn enumeration(tag: &str, variant: VariantShape) -> Shape {
392        Shape::enumeration([(tag.into(), variant)]).unwrap()
393    }
394
395    fn enum_slot(tag: &str, payload: SlotValue) -> SlotValue {
396        slot(ContractValue::enum_value(tag, payload))
397    }
398
399    fn unknown_capture(output: &SlotValue) -> (&str, &OpaqueTree) {
400        let SlotValue::Value(value) = output else {
401            panic!()
402        };
403        let ValueRef::Enum { tag, payload } = value.view() else {
404            panic!()
405        };
406        let SlotValue::Value(value) = payload else {
407            panic!()
408        };
409        let ValueRef::Opaque(payload) = value.view() else {
410            panic!()
411        };
412        (tag, payload.reveal())
413    }
414
415    fn assert_accepts(shape: Shape, input: SlotValue) {
416        for role in ROLES {
417            let output = conform_slot(&shape, role, input.clone()).unwrap();
418            assert_eq!(output, input);
419            assert_eq!(conform_slot(&shape, role, input.clone()).unwrap(), output);
420            assert_eq!(conform_slot(&shape, role, output.clone()).unwrap(), output);
421        }
422    }
423
424    fn assert_rejects(
425        shape: Shape,
426        input: SlotValue,
427        kind: ConformanceErrorKind,
428        path: Vec<PathSegment>,
429    ) {
430        for role in ROLES {
431            assert_eq!(
432                conform_slot(&shape, role, input.clone()).unwrap_err(),
433                ConformanceError {
434                    path: path.clone(),
435                    kind: kind.clone(),
436                }
437            );
438        }
439    }
440
441    #[test]
442    fn legal_primitive_and_composite_shapes_construct() {
443        let fields = [
444            ("bool".into(), Shape::bool()),
445            ("i64".into(), Shape::i64()),
446            ("u64".into(), Shape::u64()),
447            ("f32".into(), Shape::f32()),
448            ("f64".into(), Shape::f64()),
449            ("string".into(), Shape::string()),
450            ("bytes".into(), Shape::bytes()),
451            (
452                "optional-list".into(),
453                Shape::optional(Shape::list(Shape::i64()).unwrap()).unwrap(),
454            ),
455            ("tri-state".into(), Shape::tri_state(Shape::i64()).unwrap()),
456        ];
457        let shape = Shape::structure(fields).unwrap();
458        let Repr::Struct(fields) = shape.0 else {
459            panic!()
460        };
461        assert_eq!(fields.len(), 9);
462        let Repr::Optional(optional) = &fields[7].1.0 else {
463            panic!()
464        };
465        let Repr::List(element) = &optional.0 else {
466            panic!()
467        };
468        assert!(matches!(element.0, Repr::I64));
469        let Repr::TriState(tri_state) = &fields[8].1.0 else {
470            panic!()
471        };
472        assert!(matches!(tri_state.0, Repr::I64));
473    }
474
475    #[test]
476    fn every_direct_wrapper_on_wrapper_is_rejected() {
477        for outer_tri_state in [false, true] {
478            for inner_tri_state in [false, true] {
479                let inner = if inner_tri_state {
480                    Shape::tri_state(Shape::i64())
481                } else {
482                    Shape::optional(Shape::i64())
483                }
484                .unwrap();
485                let result = if outer_tri_state {
486                    Shape::tri_state(inner)
487                } else {
488                    Shape::optional(inner)
489                };
490                assert!(matches!(result, Err(ShapeError::NestedPresence)));
491            }
492        }
493    }
494
495    #[test]
496    fn tri_state_list_elements_and_duplicate_struct_fields_are_rejected() {
497        let tri_state = Shape::tri_state(Shape::i64()).unwrap();
498        assert!(matches!(
499            Shape::list(tri_state),
500            Err(ShapeError::TriStateListElement)
501        ));
502
503        let duplicate = Shape::structure([
504            ("same".into(), Shape::i64()),
505            ("same".into(), Shape::bool()),
506        ]);
507        assert!(matches!(
508            duplicate,
509            Err(ShapeError::DuplicateField(name)) if name == "same"
510        ));
511    }
512
513    #[test]
514    fn map_construction_accepts_optional_and_rejects_tri_state_values() {
515        let optional = Shape::optional(Shape::i64()).unwrap();
516        let map = Shape::map(optional).unwrap();
517        assert!(matches!(
518            map.0,
519            Repr::Map(value) if matches!(value.0, Repr::Optional(_))
520        ));
521
522        let tri_state = Shape::tri_state(Shape::i64()).unwrap();
523        assert!(matches!(
524            Shape::map(tri_state),
525            Err(ShapeError::TriStateMapValue)
526        ));
527    }
528
529    #[test]
530    fn enum_construction_distinguishes_variants_and_rejects_invalid_shapes() {
531        let optional = Shape::optional(Shape::string()).unwrap();
532        let shape = Shape::enumeration([
533            ("unit".into(), VariantShape::Unit),
534            ("value".into(), VariantShape::Value(optional)),
535        ])
536        .unwrap();
537        let Repr::Enum(variants) = shape.0 else {
538            panic!()
539        };
540        assert!(matches!(variants[0].1, VariantShape::Unit));
541        assert!(matches!(
542            variants[1].1,
543            VariantShape::Value(Shape(Repr::Optional(_)))
544        ));
545
546        let duplicate = Shape::enumeration([
547            ("same".into(), VariantShape::Unit),
548            ("same".into(), VariantShape::Value(Shape::i64())),
549        ]);
550        assert!(matches!(
551            duplicate,
552            Err(ShapeError::DuplicateVariant(tag)) if tag == "same"
553        ));
554
555        let tri_state = Shape::tri_state(Shape::i64()).unwrap();
556        assert!(matches!(
557            Shape::enumeration([("invalid".into(), VariantShape::Value(tri_state))]),
558            Err(ShapeError::TriStateEnumPayload)
559        ));
560    }
561
562    #[test]
563    fn known_unit_variants_require_the_canonical_null_payload() {
564        let shape = enumeration("ready", VariantShape::Unit);
565        assert_accepts(shape.clone(), enum_slot("ready", SlotValue::Null));
566        assert_rejects(
567            shape.clone(),
568            enum_slot("ready", SlotValue::Missing),
569            ConformanceErrorKind::UnexpectedMissing,
570            vec![PathSegment::Variant("ready".into())],
571        );
572        assert_rejects(
573            shape,
574            enum_slot("ready", slot(ContractValue::string("payload"))),
575            ConformanceErrorKind::UnexpectedPayload,
576            vec![PathSegment::Variant("ready".into())],
577        );
578    }
579
580    #[test]
581    fn known_value_variants_follow_top_slot_presence_and_kind_rules() {
582        let path = || vec![PathSegment::Variant("count".into())];
583        let scalar = || enumeration("count", VariantShape::Value(Shape::i64()));
584        assert_rejects(
585            scalar(),
586            enum_slot("count", SlotValue::Missing),
587            ConformanceErrorKind::MissingRequired,
588            path(),
589        );
590        assert_rejects(
591            scalar(),
592            enum_slot("count", SlotValue::Null),
593            ConformanceErrorKind::UnexpectedNull,
594            path(),
595        );
596        assert_accepts(scalar(), enum_slot("count", slot(ContractValue::i64(7))));
597        assert_rejects(
598            scalar(),
599            enum_slot("count", slot(ContractValue::string("seven"))),
600            ConformanceErrorKind::KindMismatch,
601            path(),
602        );
603
604        let optional = || {
605            enumeration(
606                "count",
607                VariantShape::Value(Shape::optional(Shape::i64()).unwrap()),
608            )
609        };
610        assert_rejects(
611            optional(),
612            enum_slot("count", SlotValue::Missing),
613            ConformanceErrorKind::UnexpectedMissing,
614            path(),
615        );
616        assert_accepts(optional(), enum_slot("count", SlotValue::Null));
617        assert_accepts(optional(), enum_slot("count", slot(ContractValue::i64(7))));
618        assert_rejects(
619            optional(),
620            enum_slot("count", slot(ContractValue::bool(true))),
621            ConformanceErrorKind::KindMismatch,
622            path(),
623        );
624    }
625
626    #[test]
627    fn complete_presence_grid_is_enforced_in_both_roles() {
628        let integer = || ContractValue::i64(7);
629
630        assert_rejects(
631            Shape::i64(),
632            SlotValue::Missing,
633            ConformanceErrorKind::MissingRequired,
634            vec![],
635        );
636        assert_rejects(
637            Shape::i64(),
638            SlotValue::Null,
639            ConformanceErrorKind::UnexpectedNull,
640            vec![],
641        );
642        assert_accepts(Shape::i64(), slot(integer()));
643
644        assert_rejects(
645            Shape::optional(Shape::i64()).unwrap(),
646            SlotValue::Missing,
647            ConformanceErrorKind::UnexpectedMissing,
648            vec![],
649        );
650        assert_accepts(Shape::optional(Shape::i64()).unwrap(), SlotValue::Null);
651        assert_accepts(Shape::optional(Shape::i64()).unwrap(), slot(integer()));
652
653        assert_accepts(Shape::tri_state(Shape::i64()).unwrap(), SlotValue::Missing);
654        assert_accepts(Shape::tri_state(Shape::i64()).unwrap(), SlotValue::Null);
655        assert_accepts(Shape::tri_state(Shape::i64()).unwrap(), slot(integer()));
656
657        let field_path = || vec![PathSegment::Field("x".into())];
658        assert_rejects(
659            field(Shape::i64()),
660            slot(object(vec![])),
661            ConformanceErrorKind::MissingRequired,
662            field_path(),
663        );
664        assert_rejects(
665            field(Shape::i64()),
666            slot(object(vec![("x", ContractValue::null())])),
667            ConformanceErrorKind::UnexpectedNull,
668            field_path(),
669        );
670        assert_accepts(field(Shape::i64()), slot(object(vec![("x", integer())])));
671
672        let optional = || Shape::optional(Shape::i64()).unwrap();
673        assert_accepts(field(optional()), slot(object(vec![])));
674        assert_rejects(
675            field(optional()),
676            slot(object(vec![("x", ContractValue::null())])),
677            ConformanceErrorKind::UnexpectedNull,
678            field_path(),
679        );
680        assert_accepts(field(optional()), slot(object(vec![("x", integer())])));
681
682        let tri_state = || Shape::tri_state(Shape::i64()).unwrap();
683        assert_accepts(field(tri_state()), slot(object(vec![])));
684        assert_accepts(
685            field(tri_state()),
686            slot(object(vec![("x", ContractValue::null())])),
687        );
688        assert_accepts(field(tri_state()), slot(object(vec![("x", integer())])));
689
690        assert_accepts(
691            Shape::list(Shape::i64()).unwrap(),
692            slot(ContractValue::list([integer()])),
693        );
694        assert_rejects(
695            Shape::list(Shape::i64()).unwrap(),
696            slot(ContractValue::list([ContractValue::null()])),
697            ConformanceErrorKind::UnexpectedNull,
698            vec![PathSegment::Index(0)],
699        );
700        assert_accepts(
701            Shape::list(optional()).unwrap(),
702            slot(ContractValue::list([ContractValue::null(), integer()])),
703        );
704
705        let nested = field(Shape::list(optional()).unwrap());
706        assert_accepts(
707            nested,
708            slot(object(vec![(
709                "x",
710                ContractValue::list([ContractValue::null(), integer()]),
711            )])),
712        );
713    }
714
715    #[test]
716    fn scalar_kinds_match_exactly_without_coercion() {
717        let scalars = [
718            (Shape::bool(), ContractValue::bool(true)),
719            (Shape::i64(), ContractValue::i64(1)),
720            (Shape::u64(), ContractValue::u64(1)),
721            (Shape::f32(), ContractValue::f32(1.0).unwrap()),
722            (Shape::f64(), ContractValue::f64(1.0).unwrap()),
723            (Shape::string(), ContractValue::string("one")),
724            (Shape::bytes(), ContractValue::bytes([1])),
725        ];
726        for (expected, (shape, value)) in scalars.iter().enumerate() {
727            assert_accepts(shape.clone(), slot(value.clone()));
728            for (actual, (_, value)) in scalars.iter().enumerate() {
729                if actual != expected {
730                    assert_rejects(
731                        shape.clone(),
732                        slot(value.clone()),
733                        ConformanceErrorKind::KindMismatch,
734                        vec![],
735                    );
736                }
737            }
738        }
739    }
740
741    #[test]
742    fn strict_unknown_fields_reject_and_tolerant_fields_drop_in_input_order() {
743        const SENTINEL: &str = "unknown-runtime-value";
744        let shape =
745            Shape::structure([("a".into(), Shape::i64()), ("b".into(), Shape::i64())]).unwrap();
746        let input = slot(object(vec![
747            ("b", ContractValue::i64(2)),
748            ("extra", ContractValue::string(SENTINEL)),
749            ("a", ContractValue::i64(1)),
750        ]));
751        let error = conform_slot(&shape, DecodeRole::ProviderInput, input.clone()).unwrap_err();
752        assert_eq!(
753            error,
754            ConformanceError {
755                path: vec![PathSegment::Field("extra".into())],
756                kind: ConformanceErrorKind::UnknownField("extra".into()),
757            }
758        );
759        assert!(!format!("{error:?} {error}").contains(SENTINEL));
760
761        let expected = slot(object(vec![
762            ("b", ContractValue::i64(2)),
763            ("a", ContractValue::i64(1)),
764        ]));
765        let output = conform_slot(&shape, DecodeRole::ConsumerOutput, input.clone()).unwrap();
766        assert_eq!(output, expected);
767        assert_eq!(
768            conform_slot(&shape, DecodeRole::ConsumerOutput, input).unwrap(),
769            output
770        );
771        assert_eq!(
772            conform_slot(&shape, DecodeRole::ConsumerOutput, output.clone()).unwrap(),
773            output
774        );
775    }
776
777    #[test]
778    fn rejected_runtime_values_never_appear_in_diagnostics() {
779        const SENTINEL: &str = "rejected-runtime-value";
780        let inputs = [
781            (Shape::bool(), slot(ContractValue::string(SENTINEL)), vec![]),
782            (
783                Shape::list(Shape::bool()).unwrap(),
784                slot(ContractValue::list([ContractValue::string(SENTINEL)])),
785                vec![PathSegment::Index(0)],
786            ),
787            (
788                field(Shape::bool()),
789                slot(object(vec![("x", ContractValue::string(SENTINEL))])),
790                vec![PathSegment::Field("x".into())],
791            ),
792            (
793                Shape::bool(),
794                slot(ContractValue::sensitive(ContractValue::string(SENTINEL))),
795                vec![],
796            ),
797            (
798                Shape::bool(),
799                slot(ContractValue::opaque(OpaquePayload::new(
800                    OpaqueTree::String(SENTINEL.into()),
801                ))),
802                vec![],
803            ),
804        ];
805        for (shape, input, path) in inputs {
806            for role in ROLES {
807                let error = conform_slot(&shape, role, input.clone()).unwrap_err();
808                assert_eq!(error.kind, ConformanceErrorKind::KindMismatch);
809                assert_eq!(error.path, path);
810                assert!(!format!("{error:?} {error}").contains(SENTINEL));
811            }
812        }
813    }
814
815    #[test]
816    fn map_values_preserve_arbitrary_keys_order_and_presence_in_both_roles() {
817        let input = slot(object(vec![
818            ("second/key", ContractValue::i64(2)),
819            ("", ContractValue::i64(1)),
820            ("not-a-schema-field", ContractValue::i64(3)),
821        ]));
822        assert_accepts(Shape::map(Shape::i64()).unwrap(), input);
823
824        assert_rejects(
825            Shape::map(Shape::i64()).unwrap(),
826            slot(object(vec![("null-key", ContractValue::null())])),
827            ConformanceErrorKind::UnexpectedNull,
828            vec![PathSegment::MapKey("null-key".into())],
829        );
830        assert_accepts(
831            Shape::map(Shape::optional(Shape::i64()).unwrap()).unwrap(),
832            slot(object(vec![
833                ("null", ContractValue::null()),
834                ("value", ContractValue::i64(4)),
835            ])),
836        );
837        assert_rejects(
838            Shape::map(Shape::i64()).unwrap(),
839            slot(ContractValue::list([])),
840            ConformanceErrorKind::KindMismatch,
841            vec![],
842        );
843    }
844
845    #[test]
846    fn struct_values_inside_maps_remain_strict_or_tolerant_by_role() {
847        const SENTINEL: &str = "nested-map-runtime-value";
848        let entry =
849            Shape::structure([("a".into(), Shape::i64()), ("b".into(), Shape::i64())]).unwrap();
850        let shape = Shape::map(entry).unwrap();
851        let input = slot(object(vec![
852            (
853                "customer/α",
854                object(vec![
855                    ("b", ContractValue::i64(2)),
856                    ("extra", ContractValue::string(SENTINEL)),
857                    ("a", ContractValue::i64(1)),
858                ]),
859            ),
860            (
861                "second",
862                object(vec![
863                    ("a", ContractValue::i64(3)),
864                    ("b", ContractValue::i64(4)),
865                ]),
866            ),
867        ]));
868
869        let error = conform_slot(&shape, DecodeRole::ProviderInput, input.clone()).unwrap_err();
870        assert_eq!(
871            error,
872            ConformanceError {
873                path: vec![
874                    PathSegment::MapKey("customer/α".into()),
875                    PathSegment::Field("extra".into()),
876                ],
877                kind: ConformanceErrorKind::UnknownField("extra".into()),
878            }
879        );
880        assert!(!format!("{error:?} {error}").contains(SENTINEL));
881
882        let expected = slot(object(vec![
883            (
884                "customer/α",
885                object(vec![
886                    ("b", ContractValue::i64(2)),
887                    ("a", ContractValue::i64(1)),
888                ]),
889            ),
890            (
891                "second",
892                object(vec![
893                    ("a", ContractValue::i64(3)),
894                    ("b", ContractValue::i64(4)),
895                ]),
896            ),
897        ]));
898        let output = conform_slot(&shape, DecodeRole::ConsumerOutput, input.clone()).unwrap();
899        assert_eq!(output, expected);
900        assert_eq!(
901            conform_slot(&shape, DecodeRole::ConsumerOutput, input).unwrap(),
902            output
903        );
904        assert_eq!(
905            conform_slot(&shape, DecodeRole::ConsumerOutput, output.clone()).unwrap(),
906            output
907        );
908    }
909
910    #[test]
911    fn known_enum_payloads_recurse_through_structs_lists_and_maps() {
912        const SENTINEL: &str = "nested-enum-runtime-value";
913        let entry = Shape::structure([("required".into(), Shape::i64())]).unwrap();
914        let payload = Shape::structure([(
915            "items".into(),
916            Shape::list(Shape::map(entry).unwrap()).unwrap(),
917        )])
918        .unwrap();
919        let shape = enumeration("batch", VariantShape::Value(payload));
920        let input = enum_slot(
921            "batch",
922            slot(object(vec![(
923                "items",
924                ContractValue::list([object(vec![(
925                    "entry",
926                    object(vec![
927                        ("extra", ContractValue::string(SENTINEL)),
928                        ("required", ContractValue::i64(1)),
929                    ]),
930                )])]),
931            )])),
932        );
933
934        let error = conform_slot(&shape, DecodeRole::ProviderInput, input.clone()).unwrap_err();
935        assert_eq!(
936            error,
937            ConformanceError {
938                path: vec![
939                    PathSegment::Variant("batch".into()),
940                    PathSegment::Field("items".into()),
941                    PathSegment::Index(0),
942                    PathSegment::MapKey("entry".into()),
943                    PathSegment::Field("extra".into()),
944                ],
945                kind: ConformanceErrorKind::UnknownField("extra".into()),
946            }
947        );
948        assert!(!format!("{error:?} {error}").contains(SENTINEL));
949
950        let expected = enum_slot(
951            "batch",
952            slot(object(vec![(
953                "items",
954                ContractValue::list([object(vec![(
955                    "entry",
956                    object(vec![("required", ContractValue::i64(1))]),
957                )])]),
958            )])),
959        );
960        let output = conform_slot(&shape, DecodeRole::ConsumerOutput, input.clone()).unwrap();
961        assert_eq!(output, expected);
962        assert_eq!(
963            conform_slot(&shape, DecodeRole::ConsumerOutput, input).unwrap(),
964            output
965        );
966        assert_eq!(
967            conform_slot(&shape, DecodeRole::ConsumerOutput, output.clone()).unwrap(),
968            output
969        );
970        assert_accepts(shape, expected);
971    }
972
973    #[test]
974    fn unknown_variants_are_strict_or_captured_opaquely_by_role() {
975        const SENTINEL: &str = "unknown-enum-runtime-value";
976        let duplicate_tree = OpaqueTree::Object(vec![
977            ("same".into(), OpaqueTree::String(SENTINEL.into())),
978            ("same".into(), OpaqueTree::Bool(true)),
979        ]);
980        let cases = vec![
981            (SlotValue::Missing, OpaqueTree::Null),
982            (SlotValue::Null, OpaqueTree::Null),
983            (
984                slot(ContractValue::bytes([0xfb])),
985                OpaqueTree::Object(vec![("base64".into(), OpaqueTree::String("+w==".into()))]),
986            ),
987            (
988                slot(ContractValue::list([object(vec![(
989                    "secret",
990                    ContractValue::string(SENTINEL),
991                )])])),
992                OpaqueTree::List(vec![OpaqueTree::Object(vec![(
993                    "secret".into(),
994                    OpaqueTree::String(SENTINEL.into()),
995                )])]),
996            ),
997            (
998                slot(ContractValue::sensitive(ContractValue::string(SENTINEL))),
999                OpaqueTree::String(SENTINEL.into()),
1000            ),
1001            (
1002                slot(ContractValue::opaque(OpaquePayload::new(
1003                    duplicate_tree.clone(),
1004                ))),
1005                duplicate_tree,
1006            ),
1007        ];
1008        let shape = enumeration("known", VariantShape::Unit);
1009
1010        for (payload, expected_capture) in cases {
1011            let input = enum_slot("future", payload);
1012            let error = conform_slot(&shape, DecodeRole::ProviderInput, input.clone()).unwrap_err();
1013            assert_eq!(
1014                error,
1015                ConformanceError {
1016                    path: vec![PathSegment::Variant("future".into())],
1017                    kind: ConformanceErrorKind::UnknownVariant("future".into()),
1018                }
1019            );
1020            assert!(!format!("{error:?} {error}").contains(SENTINEL));
1021
1022            let output = conform_slot(&shape, DecodeRole::ConsumerOutput, input.clone()).unwrap();
1023            let (tag, captured) = unknown_capture(&output);
1024            assert_eq!(tag, "future");
1025            assert_eq!(captured, &expected_capture);
1026            assert!(!format!("{output:?}").contains(SENTINEL));
1027            assert_eq!(
1028                conform_slot(&shape, DecodeRole::ConsumerOutput, input).unwrap(),
1029                output
1030            );
1031            assert_eq!(
1032                conform_slot(&shape, DecodeRole::ConsumerOutput, output.clone()).unwrap(),
1033                output
1034            );
1035        }
1036    }
1037
1038    #[test]
1039    fn rejected_map_values_do_not_leak_through_diagnostics() {
1040        const SENTINEL: &str = "rejected-map-runtime-value";
1041        let values = [
1042            ContractValue::string(SENTINEL),
1043            ContractValue::sensitive(ContractValue::string(SENTINEL)),
1044            ContractValue::opaque(OpaquePayload::new(OpaqueTree::String(SENTINEL.into()))),
1045        ];
1046        for value in values {
1047            let input = slot(object(vec![("safe-key", value)]));
1048            for role in ROLES {
1049                let error = conform_slot(&Shape::map(Shape::bool()).unwrap(), role, input.clone())
1050                    .unwrap_err();
1051                assert_eq!(error.kind, ConformanceErrorKind::KindMismatch);
1052                assert_eq!(error.path, vec![PathSegment::MapKey("safe-key".into())]);
1053                assert!(!format!("{error:?} {error}").contains(SENTINEL));
1054            }
1055        }
1056    }
1057
1058    #[test]
1059    fn sensitive_shape_rejects_a_direct_tri_state_inner() {
1060        let tri_state = Shape::tri_state(Shape::string()).unwrap();
1061        assert_eq!(
1062            Shape::sensitive(tri_state).err(),
1063            Some(ShapeError::TriStateSecretInner)
1064        );
1065    }
1066}