1use std::collections::BTreeMap;
5
6use rustev_contract::decimal::Decimal;
7use rustev_contract::definition::{Literal, TypeDecl};
8use rustev_contract::grade::{AbsentReason, PresentWhen};
9use rustev_contract::judgment::{Filtered, OutValue, Ranking, Shortlist};
10use rustev_contract::time::{DurationMs, Timestamp};
11use serde_json::Value as Json;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Ty {
16 Bool,
17 Integer,
18 Decimal,
19 Text {
20 max_bytes: u64,
21 },
22 Enum(Vec<String>),
23 Timestamp,
24 Duration,
25 Maybe(Box<Ty>),
29 List {
30 item: Box<Ty>,
31 max: u64,
32 },
33 Record(Vec<(String, Ty)>),
34 Filtered {
36 max: u64,
37 id_bytes: u64,
38 },
39 Shortlist {
40 max: u64,
41 id_bytes: u64,
42 },
43 Ranking {
44 max: u64,
45 },
46 Nullable(Box<Ty>),
50 Map {
53 key_max: u64,
54 value: Box<Ty>,
55 max: u64,
56 },
57 Joined {
61 value: Box<Ty>,
62 max: u64,
63 id_bytes: u64,
64 },
65}
66
67impl Ty {
68 pub fn from_decl(d: &TypeDecl) -> Ty {
69 match d {
70 TypeDecl::Bool => Ty::Bool,
71 TypeDecl::Integer => Ty::Integer,
72 TypeDecl::Decimal => Ty::Decimal,
73 TypeDecl::Text { max_bytes } => Ty::Text {
74 max_bytes: *max_bytes,
75 },
76 TypeDecl::Timestamp => Ty::Timestamp,
77 TypeDecl::Enum { options } => Ty::Enum(options.clone()),
78 TypeDecl::List { item, max_items } => Ty::List {
79 item: Box::new(Ty::from_decl(item)),
80 max: *max_items,
81 },
82 TypeDecl::Record { fields, .. } => Ty::Record(
83 fields
84 .iter()
85 .map(|f| (f.name.clone(), Ty::from_decl(&f.ty)))
86 .collect(),
87 ),
88 TypeDecl::Nullable { ty } => Ty::Nullable(Box::new(Ty::from_decl(ty))),
94 TypeDecl::Map {
95 key_max_bytes,
96 value,
97 max_entries,
98 } => Ty::Map {
99 key_max: *key_max_bytes,
100 value: Box::new(Ty::from_decl(value)),
101 max: *max_entries,
102 },
103 }
104 }
105
106 pub fn same_shape(&self, other: &Ty) -> bool {
109 match (self, other) {
110 (Ty::Text { .. }, Ty::Text { .. }) => true,
111 (Ty::Enum(_), Ty::Enum(_)) => true,
112 (Ty::Maybe(a), Ty::Maybe(b)) => a.same_shape(b),
113 (Ty::List { item: a, .. }, Ty::List { item: b, .. }) => a.same_shape(b),
114 (Ty::Record(a), Ty::Record(b)) => {
115 a.len() == b.len()
116 && a.iter()
117 .zip(b)
118 .all(|((n1, t1), (n2, t2))| n1 == n2 && t1.same_shape(t2))
119 }
120 (Ty::Filtered { .. }, Ty::Filtered { .. }) => true,
121 (Ty::Shortlist { .. }, Ty::Shortlist { .. }) => true,
122 (Ty::Ranking { .. }, Ty::Ranking { .. }) => true,
123 (Ty::Nullable(a), Ty::Nullable(b)) => a.same_shape(b),
124 (Ty::Map { value: a, .. }, Ty::Map { value: b, .. }) => a.same_shape(b),
125 (Ty::Joined { value: a, .. }, Ty::Joined { value: b, .. }) => a.same_shape(b),
126 (a, b) => a == b,
127 }
128 }
129
130 pub fn name(&self) -> String {
132 match self {
133 Ty::Bool => "bool".into(),
134 Ty::Integer => "integer".into(),
135 Ty::Decimal => "decimal".into(),
136 Ty::Text { .. } => "text".into(),
137 Ty::Enum(_) => "enum".into(),
138 Ty::Timestamp => "timestamp".into(),
139 Ty::Duration => "duration".into(),
140 Ty::Maybe(t) => format!("maybe<{}>", t.name()),
141 Ty::List { item, .. } => format!("list<{}>", item.name()),
142 Ty::Record(_) => "record".into(),
143 Ty::Filtered { .. } => "filtered".into(),
144 Ty::Shortlist { .. } => "shortlist".into(),
145 Ty::Ranking { .. } => "ranking".into(),
146 Ty::Nullable(t) => format!("nullable<{}>", t.name()),
147 Ty::Map { value, .. } => format!("map<{}>", value.name()),
148 Ty::Joined { value, .. } => format!("joined<{}>", value.name()),
149 }
150 }
151
152 pub fn join_item(value: &Ty, id_bytes: u64) -> Ty {
154 Ty::Record(vec![
155 (
156 "id".into(),
157 Ty::Text {
158 max_bytes: id_bytes,
159 },
160 ),
161 ("match".into(), value.clone()),
162 ])
163 }
164
165 pub fn max_json_bytes(&self) -> u64 {
168 let text = |n: u64| 2u64.saturating_add(n.saturating_mul(6));
169 match self {
170 Ty::Bool => 5,
171 Ty::Integer => 20,
172 Ty::Decimal => 44,
173 Ty::Text { max_bytes } => text(*max_bytes),
174 Ty::Enum(opts) => text(opts.iter().map(|o| o.len() as u64).max().unwrap_or(0)),
175 Ty::Timestamp => 15,
176 Ty::Duration => 20,
177 Ty::Maybe(t) => t.max_json_bytes().max(4),
178 Ty::List { item, max } => {
179 2u64.saturating_add(max.saturating_mul(item.max_json_bytes().saturating_add(1)))
180 }
181 Ty::Record(fields) => fields.iter().fold(2u64, |acc, (n, t)| {
182 acc.saturating_add(text(n.len() as u64))
183 .saturating_add(2)
184 .saturating_add(t.max_json_bytes())
185 }),
186 Ty::Filtered { max, id_bytes } | Ty::Shortlist { max, id_bytes } => {
188 64u64.saturating_add(max.saturating_mul(text(*id_bytes).saturating_add(64)))
189 }
190 Ty::Ranking { max } => 64u64.saturating_add(max.saturating_mul(1024)),
191 Ty::Nullable(t) => t.max_json_bytes().max(4),
192 Ty::Map {
193 key_max,
194 value,
195 max,
196 } => 2u64.saturating_add(
197 max.saturating_mul(
198 text(*key_max)
199 .saturating_add(2)
200 .saturating_add(value.max_json_bytes()),
201 ),
202 ),
203 Ty::Joined {
204 value,
205 max,
206 id_bytes,
207 } => 2u64.saturating_add(
208 max.saturating_mul(
209 Ty::join_item(value, *id_bytes)
210 .max_json_bytes()
211 .saturating_add(1),
212 ),
213 ),
214 }
215 }
216
217 pub fn is_orderable(&self) -> bool {
218 matches!(
219 self,
220 Ty::Integer | Ty::Decimal | Ty::Timestamp | Ty::Duration
221 )
222 }
223}
224
225#[derive(Debug, Clone, PartialEq)]
227pub enum Value {
228 Bool(bool),
229 Integer(i64),
230 Decimal(Decimal),
231 Text(String),
232 Enum(String),
233 Timestamp(Timestamp),
234 Duration(DurationMs),
235 Maybe(Option<Box<Value>>),
236 List(Vec<Value>),
237 Record(BTreeMap<String, Value>),
238 Filtered(Filtered),
239 Shortlist(Shortlist),
240 Ranking(Ranking),
241 Selection(rustev_contract::ranking_result::Selection),
242 Null,
244 Absent(Absence),
248 Map(BTreeMap<String, Value>),
249 Joined(Vec<JoinEntry>),
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct Absence {
255 pub reason: AbsentReason,
256 pub predicate: Option<PresentWhen>,
258}
259
260#[derive(Debug, Clone, PartialEq)]
262pub struct JoinEntry {
263 pub id: String,
264 pub matched: JoinMatch,
265}
266
267#[derive(Debug, Clone, PartialEq)]
268pub enum JoinMatch {
269 Matched(Box<Value>),
270 Unmatched {
272 key_path: Option<String>,
273 },
274 MatchedAbsent(Absence),
275}
276
277impl JoinEntry {
278 pub fn item(&self) -> Option<Value> {
281 match &self.matched {
282 JoinMatch::Matched(v) => Some(Value::Record(BTreeMap::from([
283 ("id".to_string(), Value::Text(self.id.clone())),
284 ("match".to_string(), (**v).clone()),
285 ]))),
286 _ => None,
287 }
288 }
289}
290
291impl Value {
292 pub fn from_literal(l: &Literal) -> Value {
293 match l {
294 Literal::Bool(b) => Value::Bool(*b),
295 Literal::Integer(i) => Value::Integer(*i),
296 Literal::Decimal(d) => Value::Decimal(*d),
297 Literal::Text(s) => Value::Text(s.clone()),
298 Literal::Timestamp(t) => Value::Timestamp(*t),
299 Literal::DurationMs(d) => Value::Duration(DurationMs(*d)),
300 Literal::Enum(s) => Value::Enum(s.clone()),
301 }
302 }
303
304 pub fn literal_ty(l: &Literal) -> Ty {
305 match l {
306 Literal::Bool(_) => Ty::Bool,
307 Literal::Integer(_) => Ty::Integer,
308 Literal::Decimal(_) => Ty::Decimal,
309 Literal::Text(s) => Ty::Text {
310 max_bytes: s.len() as u64,
311 },
312 Literal::Timestamp(_) => Ty::Timestamp,
313 Literal::DurationMs(_) => Ty::Duration,
314 Literal::Enum(s) => Ty::Enum(vec![s.clone()]),
315 }
316 }
317
318 pub fn to_out(&self) -> OutValue {
320 match self {
321 Value::Bool(b) => OutValue::Bool(*b),
322 Value::Integer(i) => OutValue::Integer(*i),
323 Value::Decimal(d) => OutValue::Decimal(*d),
324 Value::Text(s) => OutValue::Text(s.clone()),
325 Value::Enum(s) => OutValue::Enum(s.clone()),
326 Value::Timestamp(t) => OutValue::Timestamp(*t),
327 Value::Duration(d) => OutValue::Duration(*d),
328 Value::Maybe(None) => OutValue::None,
329 Value::Maybe(Some(v)) => v.to_out(),
330 Value::List(items) => OutValue::List(items.iter().map(Value::to_out).collect()),
331 Value::Record(m) => {
332 OutValue::Record(m.iter().map(|(k, v)| (k.clone(), v.to_out())).collect())
333 }
334 Value::Filtered(f) => OutValue::Filtered(f.clone()),
335 Value::Shortlist(s) => OutValue::Shortlist(s.clone()),
336 Value::Ranking(r) => OutValue::Ranking(r.clone()),
337 Value::Selection(s) => OutValue::Selection(s.clone()),
338 Value::Null | Value::Absent(_) => OutValue::None,
341 Value::Map(m) => {
342 OutValue::Record(m.iter().map(|(k, v)| (k.clone(), v.to_out())).collect())
343 }
344 Value::Joined(entries) => OutValue::List(
345 entries
346 .iter()
347 .filter_map(|e| e.item().map(|v| v.to_out()))
348 .collect(),
349 ),
350 }
351 }
352
353 pub fn to_json(&self) -> Json {
356 match self {
357 Value::Bool(b) => Json::Bool(*b),
358 Value::Integer(i) => Json::from(*i),
359 Value::Decimal(d) => Json::String(d.to_canonical_string()),
360 Value::Text(s) | Value::Enum(s) => Json::String(s.clone()),
361 Value::Timestamp(t) => Json::from(t.as_ms()),
362 Value::Duration(d) => Json::from(d.0),
363 Value::Maybe(None) => Json::Null,
364 Value::Maybe(Some(v)) => v.to_json(),
365 Value::List(items) => Json::Array(items.iter().map(Value::to_json).collect()),
366 Value::Record(m) => {
367 Json::Object(m.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
368 }
369 Value::Filtered(f) => serde_json::to_value(f).unwrap_or(Json::Null),
370 Value::Shortlist(s) => serde_json::to_value(s).unwrap_or(Json::Null),
371 Value::Ranking(_) => Json::Null,
372 Value::Selection(_) => Json::Null,
373 Value::Null | Value::Absent(_) => Json::Null,
376 Value::Map(m) => {
377 Json::Object(m.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
378 }
379 Value::Joined(entries) => Json::Array(
380 entries
381 .iter()
382 .map(|e| match &e.matched {
383 JoinMatch::Matched(v) => {
384 serde_json::json!({"id": e.id, "match": v.to_json()})
385 }
386 _ => serde_json::json!({"id": e.id, "match": null}),
391 })
392 .collect(),
393 ),
394 }
395 }
396
397 pub fn id_bounds(ty: &Ty) -> Option<(u64, u64)> {
399 match ty {
400 Ty::Filtered { max, id_bytes } | Ty::Shortlist { max, id_bytes } => {
401 Some((*max, *id_bytes))
402 }
403 Ty::List { item, max } => match item.as_ref() {
404 Ty::Text { max_bytes } => Some((*max, *max_bytes)),
405 Ty::Enum(o) => Some((*max, o.iter().map(|s| s.len() as u64).max().unwrap_or(0))),
406 _ => None,
407 },
408 _ => None,
409 }
410 }
411
412 pub fn ids(&self) -> Option<Vec<String>> {
415 match self {
416 Value::Filtered(f) => Some(f.eligible.clone()),
417 Value::Shortlist(s) => Some(s.ids.clone()),
418 Value::List(items) => items
419 .iter()
420 .map(|v| match v {
421 Value::Text(s) | Value::Enum(s) => Some(s.clone()),
422 _ => None,
423 })
424 .collect(),
425 _ => None,
426 }
427 }
428}
429
430pub fn decode_v2(decl: &TypeDecl, json: &Json) -> Result<Value, String> {
435 match (decl, json) {
436 (TypeDecl::Nullable { .. }, Json::Null) => Ok(Value::Null),
437 (TypeDecl::Nullable { ty }, v) => decode_v2(ty, v),
438 (TypeDecl::List { item, max_items }, Json::Array(a)) => {
439 if a.len() as u64 > *max_items {
440 return Err(format!("list of {} items exceeds {max_items}", a.len()));
441 }
442 a.iter()
443 .enumerate()
444 .map(|(i, v)| decode_v2(item, v).map_err(|e| format!("[{i}]: {e}")))
445 .collect::<Result<Vec<_>, _>>()
446 .map(Value::List)
447 }
448 (
449 TypeDecl::Map {
450 key_max_bytes,
451 value,
452 max_entries,
453 },
454 Json::Object(o),
455 ) => {
456 if o.len() as u64 > *max_entries {
457 return Err(format!("map of {} entries exceeds {max_entries}", o.len()));
458 }
459 let mut out = BTreeMap::new();
460 for (k, v) in o {
461 if k.len() as u64 > *key_max_bytes {
462 return Err(format!("key of {} bytes exceeds {key_max_bytes}", k.len()));
463 }
464 out.insert(
465 k.clone(),
466 decode_v2(value, v).map_err(|e| format!("[{k:?}]: {e}"))?,
467 );
468 }
469 Ok(Value::Map(out))
470 }
471 (
472 TypeDecl::Record {
473 fields,
474 present_when,
475 },
476 Json::Object(o),
477 ) => {
478 if let Some(extra) = o.keys().find(|k| !fields.iter().any(|f| &f.name == *k)) {
479 return Err(format!("undeclared field {extra:?}"));
480 }
481 let mut out = BTreeMap::new();
482 for f in fields {
483 let v = match (o.get(&f.name), &f.ty) {
484 (None, TypeDecl::Nullable { .. }) => Value::Null,
485 (None, _) => return Err(format!("missing field {:?}", f.name)),
486 (Some(v), t) => decode_v2(t, v).map_err(|e| format!(".{}: {e}", f.name))?,
487 };
488 out.insert(f.name.clone(), v);
489 }
490 if let Some(p) = present_when {
491 let mut named = Vec::new();
494 p.fields(&mut named);
495 if let Some(n) = named.iter().find(|n| !out.contains_key(*n)) {
496 return Err(format!("present_when names undeclared field {n:?}"));
497 }
498 let is_null = |n: &str| matches!(out.get(n), Some(Value::Null | Value::Absent(_)));
501 if !p.holds(&is_null) {
502 return Ok(Value::Absent(Absence {
503 reason: AbsentReason::PredicateFailed,
504 predicate: Some(p.canonical()),
505 }));
506 }
507 }
508 Ok(Value::Record(out))
509 }
510 (d, j) => decode(&Ty::from_decl(d), j),
511 }
512}
513
514pub fn decode(ty: &Ty, json: &Json) -> Result<Value, String> {
517 match (ty, json) {
518 (Ty::Nullable(_), Json::Null) => Ok(Value::Null),
520 (Ty::Nullable(t), v) => decode(t, v),
521 (Ty::Bool, Json::Bool(b)) => Ok(Value::Bool(*b)),
522 (Ty::Integer, Json::Number(n)) => n
523 .as_i64()
524 .map(Value::Integer)
525 .ok_or_else(|| "not an i64 integer".into()),
526 (Ty::Decimal, Json::String(s)) => Decimal::parse(s)
527 .map(Value::Decimal)
528 .map_err(|e| e.to_string()),
529 (Ty::Text { max_bytes }, Json::String(s)) => {
530 if s.len() as u64 > *max_bytes {
531 Err(format!("text of {} bytes exceeds {max_bytes}", s.len()))
532 } else {
533 Ok(Value::Text(s.clone()))
534 }
535 }
536 (Ty::Enum(opts), Json::String(s)) => {
537 if opts.contains(s) {
538 Ok(Value::Enum(s.clone()))
539 } else {
540 Err(format!("{s:?} is not a declared option"))
541 }
542 }
543 (Ty::Timestamp, Json::Number(n)) => n
544 .as_i64()
545 .and_then(|ms| Timestamp::from_ms(ms).ok())
546 .map(Value::Timestamp)
547 .ok_or_else(|| "timestamp out of range".into()),
548 (Ty::List { item, max }, Json::Array(a)) => {
549 if a.len() as u64 > *max {
550 return Err(format!("list of {} items exceeds {max}", a.len()));
551 }
552 a.iter()
553 .enumerate()
554 .map(|(i, v)| decode(item, v).map_err(|e| format!("[{i}]: {e}")))
555 .collect::<Result<Vec<_>, _>>()
556 .map(Value::List)
557 }
558 (
559 Ty::Map {
560 key_max,
561 value,
562 max,
563 },
564 Json::Object(o),
565 ) => {
566 if o.len() as u64 > *max {
567 return Err(format!("map of {} entries exceeds {max}", o.len()));
568 }
569 let mut out = BTreeMap::new();
570 for (k, v) in o {
571 if k.len() as u64 > *key_max {
572 return Err(format!("key of {} bytes exceeds {key_max}", k.len()));
573 }
574 out.insert(
575 k.clone(),
576 decode(value, v).map_err(|e| format!("[{k:?}]: {e}"))?,
577 );
578 }
579 Ok(Value::Map(out))
580 }
581 (Ty::Record(fields), Json::Object(o)) => {
582 if let Some(extra) = o.keys().find(|k| !fields.iter().any(|(n, _)| n == *k)) {
583 return Err(format!("undeclared field {extra:?}"));
584 }
585 let mut out = BTreeMap::new();
586 for (name, fty) in fields {
587 let v = o
588 .get(name)
589 .ok_or_else(|| format!("missing field {name:?}"))?;
590 out.insert(
591 name.clone(),
592 decode(fty, v).map_err(|e| format!(".{name}: {e}"))?,
593 );
594 }
595 Ok(Value::Record(out))
596 }
597 (t, _) => Err(format!("expected {}", t.name())),
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use serde_json::json;
605
606 #[test]
607 fn version_2_decoding_separates_null_absent_and_present() {
608 use rustev_contract::definition::FieldDecl;
609 use rustev_contract::grade::PresentWhen;
610 let nullable = |ty| TypeDecl::Nullable { ty: Box::new(ty) };
611 let rec = TypeDecl::Record {
612 fields: vec![
613 FieldDecl {
614 name: "total".into(),
615 ty: nullable(TypeDecl::Integer),
616 },
617 FieldDecl {
618 name: "unit".into(),
619 ty: nullable(TypeDecl::Integer),
620 },
621 ],
622 present_when: Some(PresentWhen::AnyNonNull(vec!["total".into(), "unit".into()])),
623 };
624 let present = decode_v2(&rec, &json!({"total": 3})).unwrap();
625 assert_eq!(
626 present,
627 Value::Record(BTreeMap::from([
628 ("total".into(), Value::Integer(3)),
629 ("unit".into(), Value::Null),
630 ])),
631 "a missing nullable key is null"
632 );
633 assert!(matches!(
635 decode_v2(&rec, &json!({"total": null, "unit": null})),
636 Ok(Value::Absent(Absence {
637 reason: AbsentReason::PredicateFailed,
638 predicate: Some(_),
639 }))
640 ));
641 assert!(
642 decode_v2(&rec, &json!({"total": 1, "x": 1})).is_err(),
643 "extra field"
644 );
645 let mut stray = rec.clone();
647 if let TypeDecl::Record { present_when, .. } = &mut stray {
648 *present_when = Some(PresentWhen::AnyNonNull(vec!["total".into(), "x".into()]));
649 }
650 assert!(decode_v2(&stray, &json!({"total": 1})).is_err());
651 let n = Ty::Nullable(Box::new(Ty::Integer));
653 assert_eq!(decode(&n, &json!(null)), Ok(Value::Null));
654 assert_eq!(decode(&n, &json!(2)), Ok(Value::Integer(2)));
655 let parent = TypeDecl::Record {
657 fields: vec![FieldDecl {
658 name: "quote".into(),
659 ty: nullable(rec.clone()),
660 }],
661 present_when: Some(PresentWhen::AnyNonNull(vec!["quote".into()])),
662 };
663 assert!(matches!(
664 decode_v2(&parent, &json!({"quote": {"total": null, "unit": null}})),
665 Ok(Value::Absent(_))
666 ));
667 assert!(matches!(
668 decode_v2(&parent, &json!({"quote": {"total": 1}})),
669 Ok(Value::Record(_))
670 ));
671 let strict = TypeDecl::Record {
672 fields: vec![FieldDecl {
673 name: "n".into(),
674 ty: TypeDecl::Integer,
675 }],
676 present_when: None,
677 };
678 assert!(
679 decode_v2(&strict, &json!({})).is_err(),
680 "missing non-nullable field"
681 );
682 let map = TypeDecl::Map {
683 key_max_bytes: 2,
684 value: Box::new(nullable(TypeDecl::Integer)),
685 max_entries: 1,
686 };
687 assert_eq!(
688 decode_v2(&map, &json!({"a": null})),
689 Ok(Value::Map(BTreeMap::from([("a".into(), Value::Null)])))
690 );
691 assert!(
692 decode_v2(&map, &json!({"a": 1, "b": 2})).is_err(),
693 "entries"
694 );
695 assert!(decode_v2(&map, &json!({"abc": 1})).is_err(), "key bytes");
696 }
697
698 #[test]
699 fn a_map_decodes_by_key_within_its_bounds() {
700 let map = Ty::from_decl(&TypeDecl::Map {
701 key_max_bytes: 4,
702 value: Box::new(TypeDecl::Integer),
703 max_entries: 2,
704 });
705 assert_eq!(
706 decode(&map, &json!({"a": 1, "b": 2})),
707 Ok(Value::Map(BTreeMap::from([
708 ("a".into(), Value::Integer(1)),
709 ("b".into(), Value::Integer(2)),
710 ])))
711 );
712 assert!(
713 decode(&map, &json!({"a": 1, "b": 2, "c": 3})).is_err(),
714 "entries"
715 );
716 assert!(decode(&map, &json!({"long_key": 1})).is_err(), "key bytes");
717 assert!(decode(&map, &json!({"a": "1"})).is_err(), "value type");
718 assert!(decode(&map, &json!([1])).is_err(), "not an object");
719 }
720
721 #[test]
722 fn decoding_is_strict() {
723 let rec = Ty::Record(vec![("a".into(), Ty::Integer), ("b".into(), Ty::Decimal)]);
724 assert!(decode(&rec, &json!({"a": 1, "b": "2.5"})).is_ok());
725 assert!(decode(&rec, &json!({"a": 1})).is_err(), "missing field");
726 assert!(
727 decode(&rec, &json!({"a": 1, "b": "2", "c": 0})).is_err(),
728 "extra field"
729 );
730 assert!(
731 decode(&rec, &json!({"a": "1", "b": "2"})).is_err(),
732 "no coercion"
733 );
734 assert!(
735 decode(&Ty::Decimal, &json!(2)).is_err(),
736 "decimal must be a string"
737 );
738 assert!(
739 decode(&Ty::Decimal, &json!("0.0000000001")).is_err(),
740 "excess precision"
741 );
742 assert!(decode(&Ty::Text { max_bytes: 3 }, &json!("abcd")).is_err());
743 assert!(decode(&Ty::Text { max_bytes: 4 }, &json!("abcd")).is_ok());
744 assert!(decode(&Ty::Enum(vec!["x".into()]), &json!("y")).is_err());
745 let list = Ty::List {
746 item: Box::new(Ty::Bool),
747 max: 2,
748 };
749 assert!(decode(&list, &json!([true, false])).is_ok());
750 assert!(decode(&list, &json!([true, false, true])).is_err());
751 assert!(decode(&Ty::Timestamp, &json!(-1)).is_err());
752 assert!(decode(&Ty::Integer, &json!(9223372036854775808u64)).is_err());
753 }
754
755 #[test]
756 fn size_bounds_cover_worst_case_escaping() {
757 let t = Ty::Text { max_bytes: 3 };
758 let v = Value::Text("\u{1}\u{1}\u{1}".into());
759 let bytes = rustev_contract::canonical::canonical_value_bytes(&v.to_json()).unwrap();
760 assert!(bytes.len() as u64 <= t.max_json_bytes());
761 let d = Value::Decimal(Decimal::from_units(i128::MIN));
762 let bytes = rustev_contract::canonical::canonical_value_bytes(&d.to_json()).unwrap();
763 assert!(bytes.len() as u64 <= Ty::Decimal.max_json_bytes());
764 }
765}