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 Null,
243 Absent(Absence),
247 Map(BTreeMap<String, Value>),
248 Joined(Vec<JoinEntry>),
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct Absence {
254 pub reason: AbsentReason,
255 pub predicate: Option<PresentWhen>,
257}
258
259#[derive(Debug, Clone, PartialEq)]
261pub struct JoinEntry {
262 pub id: String,
263 pub matched: JoinMatch,
264}
265
266#[derive(Debug, Clone, PartialEq)]
267pub enum JoinMatch {
268 Matched(Box<Value>),
269 Unmatched {
271 key_path: Option<String>,
272 },
273 MatchedAbsent(Absence),
274}
275
276impl JoinEntry {
277 pub fn item(&self) -> Option<Value> {
280 match &self.matched {
281 JoinMatch::Matched(v) => Some(Value::Record(BTreeMap::from([
282 ("id".to_string(), Value::Text(self.id.clone())),
283 ("match".to_string(), (**v).clone()),
284 ]))),
285 _ => None,
286 }
287 }
288}
289
290impl Value {
291 pub fn from_literal(l: &Literal) -> Value {
292 match l {
293 Literal::Bool(b) => Value::Bool(*b),
294 Literal::Integer(i) => Value::Integer(*i),
295 Literal::Decimal(d) => Value::Decimal(*d),
296 Literal::Text(s) => Value::Text(s.clone()),
297 Literal::Timestamp(t) => Value::Timestamp(*t),
298 Literal::DurationMs(d) => Value::Duration(DurationMs(*d)),
299 Literal::Enum(s) => Value::Enum(s.clone()),
300 }
301 }
302
303 pub fn literal_ty(l: &Literal) -> Ty {
304 match l {
305 Literal::Bool(_) => Ty::Bool,
306 Literal::Integer(_) => Ty::Integer,
307 Literal::Decimal(_) => Ty::Decimal,
308 Literal::Text(s) => Ty::Text {
309 max_bytes: s.len() as u64,
310 },
311 Literal::Timestamp(_) => Ty::Timestamp,
312 Literal::DurationMs(_) => Ty::Duration,
313 Literal::Enum(s) => Ty::Enum(vec![s.clone()]),
314 }
315 }
316
317 pub fn to_out(&self) -> OutValue {
319 match self {
320 Value::Bool(b) => OutValue::Bool(*b),
321 Value::Integer(i) => OutValue::Integer(*i),
322 Value::Decimal(d) => OutValue::Decimal(*d),
323 Value::Text(s) => OutValue::Text(s.clone()),
324 Value::Enum(s) => OutValue::Enum(s.clone()),
325 Value::Timestamp(t) => OutValue::Timestamp(*t),
326 Value::Duration(d) => OutValue::Duration(*d),
327 Value::Maybe(None) => OutValue::None,
328 Value::Maybe(Some(v)) => v.to_out(),
329 Value::List(items) => OutValue::List(items.iter().map(Value::to_out).collect()),
330 Value::Record(m) => {
331 OutValue::Record(m.iter().map(|(k, v)| (k.clone(), v.to_out())).collect())
332 }
333 Value::Filtered(f) => OutValue::Filtered(f.clone()),
334 Value::Shortlist(s) => OutValue::Shortlist(s.clone()),
335 Value::Ranking(r) => OutValue::Ranking(r.clone()),
336 Value::Null | Value::Absent(_) => OutValue::None,
339 Value::Map(m) => {
340 OutValue::Record(m.iter().map(|(k, v)| (k.clone(), v.to_out())).collect())
341 }
342 Value::Joined(entries) => OutValue::List(
343 entries
344 .iter()
345 .filter_map(|e| e.item().map(|v| v.to_out()))
346 .collect(),
347 ),
348 }
349 }
350
351 pub fn to_json(&self) -> Json {
354 match self {
355 Value::Bool(b) => Json::Bool(*b),
356 Value::Integer(i) => Json::from(*i),
357 Value::Decimal(d) => Json::String(d.to_canonical_string()),
358 Value::Text(s) | Value::Enum(s) => Json::String(s.clone()),
359 Value::Timestamp(t) => Json::from(t.as_ms()),
360 Value::Duration(d) => Json::from(d.0),
361 Value::Maybe(None) => Json::Null,
362 Value::Maybe(Some(v)) => v.to_json(),
363 Value::List(items) => Json::Array(items.iter().map(Value::to_json).collect()),
364 Value::Record(m) => {
365 Json::Object(m.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
366 }
367 Value::Filtered(f) => serde_json::to_value(f).unwrap_or(Json::Null),
368 Value::Shortlist(s) => serde_json::to_value(s).unwrap_or(Json::Null),
369 Value::Ranking(_) => Json::Null,
370 Value::Null | Value::Absent(_) => Json::Null,
373 Value::Map(m) => {
374 Json::Object(m.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
375 }
376 Value::Joined(entries) => Json::Array(
377 entries
378 .iter()
379 .map(|e| match &e.matched {
380 JoinMatch::Matched(v) => {
381 serde_json::json!({"id": e.id, "match": v.to_json()})
382 }
383 _ => serde_json::json!({"id": e.id, "match": null}),
388 })
389 .collect(),
390 ),
391 }
392 }
393
394 pub fn id_bounds(ty: &Ty) -> Option<(u64, u64)> {
396 match ty {
397 Ty::Filtered { max, id_bytes } | Ty::Shortlist { max, id_bytes } => {
398 Some((*max, *id_bytes))
399 }
400 Ty::List { item, max } => match item.as_ref() {
401 Ty::Text { max_bytes } => Some((*max, *max_bytes)),
402 Ty::Enum(o) => Some((*max, o.iter().map(|s| s.len() as u64).max().unwrap_or(0))),
403 _ => None,
404 },
405 _ => None,
406 }
407 }
408
409 pub fn ids(&self) -> Option<Vec<String>> {
412 match self {
413 Value::Filtered(f) => Some(f.eligible.clone()),
414 Value::Shortlist(s) => Some(s.ids.clone()),
415 Value::List(items) => items
416 .iter()
417 .map(|v| match v {
418 Value::Text(s) | Value::Enum(s) => Some(s.clone()),
419 _ => None,
420 })
421 .collect(),
422 _ => None,
423 }
424 }
425}
426
427pub fn decode_v2(decl: &TypeDecl, json: &Json) -> Result<Value, String> {
432 match (decl, json) {
433 (TypeDecl::Nullable { .. }, Json::Null) => Ok(Value::Null),
434 (TypeDecl::Nullable { ty }, v) => decode_v2(ty, v),
435 (TypeDecl::List { item, max_items }, Json::Array(a)) => {
436 if a.len() as u64 > *max_items {
437 return Err(format!("list of {} items exceeds {max_items}", a.len()));
438 }
439 a.iter()
440 .enumerate()
441 .map(|(i, v)| decode_v2(item, v).map_err(|e| format!("[{i}]: {e}")))
442 .collect::<Result<Vec<_>, _>>()
443 .map(Value::List)
444 }
445 (
446 TypeDecl::Map {
447 key_max_bytes,
448 value,
449 max_entries,
450 },
451 Json::Object(o),
452 ) => {
453 if o.len() as u64 > *max_entries {
454 return Err(format!("map of {} entries exceeds {max_entries}", o.len()));
455 }
456 let mut out = BTreeMap::new();
457 for (k, v) in o {
458 if k.len() as u64 > *key_max_bytes {
459 return Err(format!("key of {} bytes exceeds {key_max_bytes}", k.len()));
460 }
461 out.insert(
462 k.clone(),
463 decode_v2(value, v).map_err(|e| format!("[{k:?}]: {e}"))?,
464 );
465 }
466 Ok(Value::Map(out))
467 }
468 (
469 TypeDecl::Record {
470 fields,
471 present_when,
472 },
473 Json::Object(o),
474 ) => {
475 if let Some(extra) = o.keys().find(|k| !fields.iter().any(|f| &f.name == *k)) {
476 return Err(format!("undeclared field {extra:?}"));
477 }
478 let mut out = BTreeMap::new();
479 for f in fields {
480 let v = match (o.get(&f.name), &f.ty) {
481 (None, TypeDecl::Nullable { .. }) => Value::Null,
482 (None, _) => return Err(format!("missing field {:?}", f.name)),
483 (Some(v), t) => decode_v2(t, v).map_err(|e| format!(".{}: {e}", f.name))?,
484 };
485 out.insert(f.name.clone(), v);
486 }
487 if let Some(p) = present_when {
488 let mut named = Vec::new();
491 p.fields(&mut named);
492 if let Some(n) = named.iter().find(|n| !out.contains_key(*n)) {
493 return Err(format!("present_when names undeclared field {n:?}"));
494 }
495 let is_null = |n: &str| matches!(out.get(n), Some(Value::Null | Value::Absent(_)));
498 if !p.holds(&is_null) {
499 return Ok(Value::Absent(Absence {
500 reason: AbsentReason::PredicateFailed,
501 predicate: Some(p.canonical()),
502 }));
503 }
504 }
505 Ok(Value::Record(out))
506 }
507 (d, j) => decode(&Ty::from_decl(d), j),
508 }
509}
510
511pub fn decode(ty: &Ty, json: &Json) -> Result<Value, String> {
514 match (ty, json) {
515 (Ty::Nullable(_), Json::Null) => Ok(Value::Null),
517 (Ty::Nullable(t), v) => decode(t, v),
518 (Ty::Bool, Json::Bool(b)) => Ok(Value::Bool(*b)),
519 (Ty::Integer, Json::Number(n)) => n
520 .as_i64()
521 .map(Value::Integer)
522 .ok_or_else(|| "not an i64 integer".into()),
523 (Ty::Decimal, Json::String(s)) => Decimal::parse(s)
524 .map(Value::Decimal)
525 .map_err(|e| e.to_string()),
526 (Ty::Text { max_bytes }, Json::String(s)) => {
527 if s.len() as u64 > *max_bytes {
528 Err(format!("text of {} bytes exceeds {max_bytes}", s.len()))
529 } else {
530 Ok(Value::Text(s.clone()))
531 }
532 }
533 (Ty::Enum(opts), Json::String(s)) => {
534 if opts.contains(s) {
535 Ok(Value::Enum(s.clone()))
536 } else {
537 Err(format!("{s:?} is not a declared option"))
538 }
539 }
540 (Ty::Timestamp, Json::Number(n)) => n
541 .as_i64()
542 .and_then(|ms| Timestamp::from_ms(ms).ok())
543 .map(Value::Timestamp)
544 .ok_or_else(|| "timestamp out of range".into()),
545 (Ty::List { item, max }, Json::Array(a)) => {
546 if a.len() as u64 > *max {
547 return Err(format!("list of {} items exceeds {max}", a.len()));
548 }
549 a.iter()
550 .enumerate()
551 .map(|(i, v)| decode(item, v).map_err(|e| format!("[{i}]: {e}")))
552 .collect::<Result<Vec<_>, _>>()
553 .map(Value::List)
554 }
555 (
556 Ty::Map {
557 key_max,
558 value,
559 max,
560 },
561 Json::Object(o),
562 ) => {
563 if o.len() as u64 > *max {
564 return Err(format!("map of {} entries exceeds {max}", o.len()));
565 }
566 let mut out = BTreeMap::new();
567 for (k, v) in o {
568 if k.len() as u64 > *key_max {
569 return Err(format!("key of {} bytes exceeds {key_max}", k.len()));
570 }
571 out.insert(
572 k.clone(),
573 decode(value, v).map_err(|e| format!("[{k:?}]: {e}"))?,
574 );
575 }
576 Ok(Value::Map(out))
577 }
578 (Ty::Record(fields), Json::Object(o)) => {
579 if let Some(extra) = o.keys().find(|k| !fields.iter().any(|(n, _)| n == *k)) {
580 return Err(format!("undeclared field {extra:?}"));
581 }
582 let mut out = BTreeMap::new();
583 for (name, fty) in fields {
584 let v = o
585 .get(name)
586 .ok_or_else(|| format!("missing field {name:?}"))?;
587 out.insert(
588 name.clone(),
589 decode(fty, v).map_err(|e| format!(".{name}: {e}"))?,
590 );
591 }
592 Ok(Value::Record(out))
593 }
594 (t, _) => Err(format!("expected {}", t.name())),
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use serde_json::json;
602
603 #[test]
604 fn version_2_decoding_separates_null_absent_and_present() {
605 use rustev_contract::definition::FieldDecl;
606 use rustev_contract::grade::PresentWhen;
607 let nullable = |ty| TypeDecl::Nullable { ty: Box::new(ty) };
608 let rec = TypeDecl::Record {
609 fields: vec![
610 FieldDecl {
611 name: "total".into(),
612 ty: nullable(TypeDecl::Integer),
613 },
614 FieldDecl {
615 name: "unit".into(),
616 ty: nullable(TypeDecl::Integer),
617 },
618 ],
619 present_when: Some(PresentWhen::AnyNonNull(vec!["total".into(), "unit".into()])),
620 };
621 let present = decode_v2(&rec, &json!({"total": 3})).unwrap();
622 assert_eq!(
623 present,
624 Value::Record(BTreeMap::from([
625 ("total".into(), Value::Integer(3)),
626 ("unit".into(), Value::Null),
627 ])),
628 "a missing nullable key is null"
629 );
630 assert!(matches!(
632 decode_v2(&rec, &json!({"total": null, "unit": null})),
633 Ok(Value::Absent(Absence {
634 reason: AbsentReason::PredicateFailed,
635 predicate: Some(_),
636 }))
637 ));
638 assert!(
639 decode_v2(&rec, &json!({"total": 1, "x": 1})).is_err(),
640 "extra field"
641 );
642 let mut stray = rec.clone();
644 if let TypeDecl::Record { present_when, .. } = &mut stray {
645 *present_when = Some(PresentWhen::AnyNonNull(vec!["total".into(), "x".into()]));
646 }
647 assert!(decode_v2(&stray, &json!({"total": 1})).is_err());
648 let n = Ty::Nullable(Box::new(Ty::Integer));
650 assert_eq!(decode(&n, &json!(null)), Ok(Value::Null));
651 assert_eq!(decode(&n, &json!(2)), Ok(Value::Integer(2)));
652 let parent = TypeDecl::Record {
654 fields: vec![FieldDecl {
655 name: "quote".into(),
656 ty: nullable(rec.clone()),
657 }],
658 present_when: Some(PresentWhen::AnyNonNull(vec!["quote".into()])),
659 };
660 assert!(matches!(
661 decode_v2(&parent, &json!({"quote": {"total": null, "unit": null}})),
662 Ok(Value::Absent(_))
663 ));
664 assert!(matches!(
665 decode_v2(&parent, &json!({"quote": {"total": 1}})),
666 Ok(Value::Record(_))
667 ));
668 let strict = TypeDecl::Record {
669 fields: vec![FieldDecl {
670 name: "n".into(),
671 ty: TypeDecl::Integer,
672 }],
673 present_when: None,
674 };
675 assert!(
676 decode_v2(&strict, &json!({})).is_err(),
677 "missing non-nullable field"
678 );
679 let map = TypeDecl::Map {
680 key_max_bytes: 2,
681 value: Box::new(nullable(TypeDecl::Integer)),
682 max_entries: 1,
683 };
684 assert_eq!(
685 decode_v2(&map, &json!({"a": null})),
686 Ok(Value::Map(BTreeMap::from([("a".into(), Value::Null)])))
687 );
688 assert!(
689 decode_v2(&map, &json!({"a": 1, "b": 2})).is_err(),
690 "entries"
691 );
692 assert!(decode_v2(&map, &json!({"abc": 1})).is_err(), "key bytes");
693 }
694
695 #[test]
696 fn a_map_decodes_by_key_within_its_bounds() {
697 let map = Ty::from_decl(&TypeDecl::Map {
698 key_max_bytes: 4,
699 value: Box::new(TypeDecl::Integer),
700 max_entries: 2,
701 });
702 assert_eq!(
703 decode(&map, &json!({"a": 1, "b": 2})),
704 Ok(Value::Map(BTreeMap::from([
705 ("a".into(), Value::Integer(1)),
706 ("b".into(), Value::Integer(2)),
707 ])))
708 );
709 assert!(
710 decode(&map, &json!({"a": 1, "b": 2, "c": 3})).is_err(),
711 "entries"
712 );
713 assert!(decode(&map, &json!({"long_key": 1})).is_err(), "key bytes");
714 assert!(decode(&map, &json!({"a": "1"})).is_err(), "value type");
715 assert!(decode(&map, &json!([1])).is_err(), "not an object");
716 }
717
718 #[test]
719 fn decoding_is_strict() {
720 let rec = Ty::Record(vec![("a".into(), Ty::Integer), ("b".into(), Ty::Decimal)]);
721 assert!(decode(&rec, &json!({"a": 1, "b": "2.5"})).is_ok());
722 assert!(decode(&rec, &json!({"a": 1})).is_err(), "missing field");
723 assert!(
724 decode(&rec, &json!({"a": 1, "b": "2", "c": 0})).is_err(),
725 "extra field"
726 );
727 assert!(
728 decode(&rec, &json!({"a": "1", "b": "2"})).is_err(),
729 "no coercion"
730 );
731 assert!(
732 decode(&Ty::Decimal, &json!(2)).is_err(),
733 "decimal must be a string"
734 );
735 assert!(
736 decode(&Ty::Decimal, &json!("0.0000000001")).is_err(),
737 "excess precision"
738 );
739 assert!(decode(&Ty::Text { max_bytes: 3 }, &json!("abcd")).is_err());
740 assert!(decode(&Ty::Text { max_bytes: 4 }, &json!("abcd")).is_ok());
741 assert!(decode(&Ty::Enum(vec!["x".into()]), &json!("y")).is_err());
742 let list = Ty::List {
743 item: Box::new(Ty::Bool),
744 max: 2,
745 };
746 assert!(decode(&list, &json!([true, false])).is_ok());
747 assert!(decode(&list, &json!([true, false, true])).is_err());
748 assert!(decode(&Ty::Timestamp, &json!(-1)).is_err());
749 assert!(decode(&Ty::Integer, &json!(9223372036854775808u64)).is_err());
750 }
751
752 #[test]
753 fn size_bounds_cover_worst_case_escaping() {
754 let t = Ty::Text { max_bytes: 3 };
755 let v = Value::Text("\u{1}\u{1}\u{1}".into());
756 let bytes = rustev_contract::canonical::canonical_value_bytes(&v.to_json()).unwrap();
757 assert!(bytes.len() as u64 <= t.max_json_bytes());
758 let d = Value::Decimal(Decimal::from_units(i128::MIN));
759 let bytes = rustev_contract::canonical::canonical_value_bytes(&d.to_json()).unwrap();
760 assert!(bytes.len() as u64 <= Ty::Decimal.max_json_bytes());
761 }
762}