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#[derive(Debug, Clone, PartialEq, Eq)]
139#[non_exhaustive]
140pub enum ConformanceErrorKind {
141 MissingRequired,
143 UnexpectedNull,
145 UnexpectedMissing,
147 UnexpectedPayload,
149 KindMismatch,
151 UnknownField(String),
153 UnknownVariant(String),
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ConformanceError {
160 path: Vec<PathSegment>,
161 kind: ConformanceErrorKind,
162}
163
164impl ConformanceError {
165 pub fn kind(&self) -> &ConformanceErrorKind {
167 &self.kind
168 }
169
170 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}