1#[cfg(test)]
7use crate::value::Value;
8use crate::{types::EntityTag, value::RuntimeValueKind};
9#[cfg(test)]
10use std::cmp::Ordering;
11
12#[cfg(test)]
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub(crate) enum FieldStorageValidationError {
15 RequiredFieldNull,
16 StorageContract,
17 DecimalScaleMismatch,
18 ScalarMaxLen,
19 SetCanonicalOrder,
20 MapEntryContract,
21 MapCanonicalOrder,
22}
23
24pub const DEFAULT_BIG_INT_MAX_BYTES: u32 = 256;
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum FieldStorageDecode {
39 ByKind,
41 Value,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum ScalarCodec {
56 Blob,
57 Bool,
58 Date,
59 Duration,
60 Float32,
61 Float64,
62 Int64,
63 Principal,
64 Subaccount,
65 Text,
66 Timestamp,
67 Nat64,
68 Ulid,
69 Unit,
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub enum LeafCodec {
83 Scalar(ScalarCodec),
84 StructuralFallback,
85}
86
87#[derive(Clone, Copy, Debug)]
97pub struct EnumVariantModel {
98 pub(crate) ident: &'static str,
100 pub(crate) payload_kind: Option<&'static FieldKind>,
102 pub(crate) payload_storage_decode: FieldStorageDecode,
104}
105
106impl EnumVariantModel {
107 #[must_use]
109 pub const fn new(
110 ident: &'static str,
111 payload_kind: Option<&'static FieldKind>,
112 payload_storage_decode: FieldStorageDecode,
113 ) -> Self {
114 Self {
115 ident,
116 payload_kind,
117 payload_storage_decode,
118 }
119 }
120
121 #[must_use]
123 pub const fn ident(&self) -> &'static str {
124 self.ident
125 }
126
127 #[must_use]
129 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
130 self.payload_kind
131 }
132
133 #[must_use]
135 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
136 self.payload_storage_decode
137 }
138}
139
140#[derive(Debug)]
150pub struct FieldModel {
151 pub(crate) name: &'static str,
153 pub(crate) kind: FieldKind,
155 pub(crate) nested_fields: &'static [Self],
157 pub(crate) nullable: bool,
159 pub(crate) storage_decode: FieldStorageDecode,
161 pub(crate) leaf_codec: LeafCodec,
163 pub(crate) insert_generation: Option<FieldInsertGeneration>,
165 pub(crate) write_management: Option<FieldWriteManagement>,
167 pub(crate) database_default: FieldDatabaseDefault,
169}
170
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum FieldDatabaseDefault {
181 None,
183 EncodedSlotPayload(&'static [u8]),
189 AuthoredEnumUnit {
194 enum_path: &'static str,
196 variant: &'static str,
198 },
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum FieldInsertGeneration {
212 Ulid,
214 Timestamp,
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub enum FieldWriteManagement {
229 CreatedAt,
231 UpdatedAt,
233}
234
235impl FieldModel {
236 #[must_use]
242 #[doc(hidden)]
243 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
244 Self::generated_with_storage_decode_and_nullability(
245 name,
246 kind,
247 FieldStorageDecode::ByKind,
248 false,
249 )
250 }
251
252 #[must_use]
254 #[doc(hidden)]
255 pub const fn generated_with_storage_decode(
256 name: &'static str,
257 kind: FieldKind,
258 storage_decode: FieldStorageDecode,
259 ) -> Self {
260 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
261 }
262
263 #[must_use]
265 #[doc(hidden)]
266 pub const fn generated_with_storage_decode_and_nullability(
267 name: &'static str,
268 kind: FieldKind,
269 storage_decode: FieldStorageDecode,
270 nullable: bool,
271 ) -> Self {
272 Self::generated_with_storage_decode_nullability_and_write_policies(
273 name,
274 kind,
275 storage_decode,
276 nullable,
277 None,
278 None,
279 )
280 }
281
282 #[must_use]
285 #[doc(hidden)]
286 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
287 name: &'static str,
288 kind: FieldKind,
289 storage_decode: FieldStorageDecode,
290 nullable: bool,
291 insert_generation: Option<FieldInsertGeneration>,
292 ) -> Self {
293 Self::generated_with_storage_decode_nullability_and_write_policies(
294 name,
295 kind,
296 storage_decode,
297 nullable,
298 insert_generation,
299 None,
300 )
301 }
302
303 #[must_use]
306 #[doc(hidden)]
307 pub const fn generated_with_storage_decode_nullability_and_write_policies(
308 name: &'static str,
309 kind: FieldKind,
310 storage_decode: FieldStorageDecode,
311 nullable: bool,
312 insert_generation: Option<FieldInsertGeneration>,
313 write_management: Option<FieldWriteManagement>,
314 ) -> Self {
315 Self {
316 name,
317 kind,
318 nested_fields: &[],
319 nullable,
320 storage_decode,
321 leaf_codec: leaf_codec_for(kind, storage_decode),
322 insert_generation,
323 write_management,
324 database_default: FieldDatabaseDefault::None,
325 }
326 }
327
328 #[must_use]
330 #[doc(hidden)]
331 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
332 name: &'static str,
333 kind: FieldKind,
334 storage_decode: FieldStorageDecode,
335 nullable: bool,
336 insert_generation: Option<FieldInsertGeneration>,
337 write_management: Option<FieldWriteManagement>,
338 nested_fields: &'static [Self],
339 ) -> Self {
340 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
341 name,
342 kind,
343 storage_decode,
344 nullable,
345 insert_generation,
346 write_management,
347 FieldDatabaseDefault::None,
348 nested_fields,
349 )
350 }
351
352 #[must_use]
355 #[doc(hidden)]
356 #[expect(
357 clippy::too_many_arguments,
358 reason = "generated schema metadata keeps every field contract explicit"
359 )]
360 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
361 name: &'static str,
362 kind: FieldKind,
363 storage_decode: FieldStorageDecode,
364 nullable: bool,
365 insert_generation: Option<FieldInsertGeneration>,
366 write_management: Option<FieldWriteManagement>,
367 database_default: FieldDatabaseDefault,
368 nested_fields: &'static [Self],
369 ) -> Self {
370 Self {
371 name,
372 kind,
373 nested_fields,
374 nullable,
375 storage_decode,
376 leaf_codec: leaf_codec_for(kind, storage_decode),
377 insert_generation,
378 write_management,
379 database_default,
380 }
381 }
382
383 #[must_use]
385 pub const fn name(&self) -> &'static str {
386 self.name
387 }
388
389 #[must_use]
391 pub const fn kind(&self) -> FieldKind {
392 self.kind
393 }
394
395 #[must_use]
397 pub const fn nested_fields(&self) -> &'static [Self] {
398 self.nested_fields
399 }
400
401 #[must_use]
403 pub const fn nullable(&self) -> bool {
404 self.nullable
405 }
406
407 #[must_use]
409 pub const fn storage_decode(&self) -> FieldStorageDecode {
410 self.storage_decode
411 }
412
413 #[must_use]
415 pub const fn leaf_codec(&self) -> LeafCodec {
416 self.leaf_codec
417 }
418
419 #[must_use]
421 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
422 self.insert_generation
423 }
424
425 #[must_use]
427 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
428 self.write_management
429 }
430
431 #[must_use]
433 pub const fn database_default(&self) -> FieldDatabaseDefault {
434 self.database_default
435 }
436
437 #[cfg(test)]
445 pub(crate) fn validate_runtime_value_for_storage(
446 &self,
447 value: &Value,
448 ) -> Result<(), FieldStorageValidationError> {
449 if matches!(value, Value::Null) {
450 if self.nullable() {
451 return Ok(());
452 }
453
454 return Err(FieldStorageValidationError::RequiredFieldNull);
455 }
456
457 let accepts = match self.storage_decode() {
458 FieldStorageDecode::Value => {
459 value_storage_kind_accepts_runtime_value(self.kind(), value)
460 }
461 FieldStorageDecode::ByKind => {
462 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
463 }
464 };
465 if !accepts {
466 return Err(FieldStorageValidationError::StorageContract);
467 }
468
469 ensure_decimal_scale_matches(self.kind(), value)?;
470 ensure_scalar_max_len_matches(self.kind(), value)?;
471 ensure_value_is_deterministic_for_storage(self.kind(), value)
472 }
473}
474
475const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
479 if matches!(storage_decode, FieldStorageDecode::Value) {
480 return LeafCodec::StructuralFallback;
481 }
482
483 match kind {
484 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
485 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
486 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
487 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
488 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
489 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
490 FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
491 LeafCodec::Scalar(ScalarCodec::Int64)
492 }
493 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
494 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
495 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
496 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
497 FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
498 LeafCodec::Scalar(ScalarCodec::Nat64)
499 }
500 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
501 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
502 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
503 FieldKind::Account
504 | FieldKind::Decimal { .. }
505 | FieldKind::Enum { .. }
506 | FieldKind::Int128
507 | FieldKind::IntBig { .. }
508 | FieldKind::List(_)
509 | FieldKind::Map { .. }
510 | FieldKind::Set(_)
511 | FieldKind::Structured { .. }
512 | FieldKind::Nat128
513 | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
514 }
515}
516
517#[derive(Clone, Copy, Debug)]
527pub enum FieldKind {
528 Account,
530 Blob {
531 max_len: Option<u32>,
533 },
534 Bool,
535 Date,
536 Decimal {
537 scale: u32,
539 },
540 Duration,
541 Enum {
542 path: &'static str,
544 variants: &'static [EnumVariantModel],
546 },
547 Float32,
548 Float64,
549 Int8,
550 Int16,
551 Int32,
552 Int64,
553 Int128,
554 IntBig {
555 max_bytes: u32,
557 },
558 Principal,
559 Subaccount,
560 Text {
561 max_len: Option<u32>,
563 },
564 Timestamp,
565 Nat8,
566 Nat16,
567 Nat32,
568 Nat64,
569 Nat128,
570 NatBig {
571 max_bytes: u32,
573 },
574 Ulid,
575 Unit,
576
577 Relation {
579 target_path: &'static str,
581 target_entity_name: &'static str,
583 target_entity_tag: EntityTag,
585 target_store_path: &'static str,
587 key_kind: &'static Self,
588 },
589
590 List(&'static Self),
592 Set(&'static Self),
593 Map {
597 key: &'static Self,
598 value: &'static Self,
599 },
600
601 Structured {
605 queryable: bool,
606 },
607}
608
609impl FieldKind {
610 #[must_use]
611 pub const fn value_kind(&self) -> RuntimeValueKind {
612 match self {
613 Self::Account
614 | Self::Blob { .. }
615 | Self::Bool
616 | Self::Date
617 | Self::Duration
618 | Self::Enum { .. }
619 | Self::Float32
620 | Self::Float64
621 | Self::Int8
622 | Self::Int16
623 | Self::Int32
624 | Self::Int64
625 | Self::Int128
626 | Self::IntBig { .. }
627 | Self::Principal
628 | Self::Subaccount
629 | Self::Text { .. }
630 | Self::Timestamp
631 | Self::Nat8
632 | Self::Nat16
633 | Self::Nat32
634 | Self::Nat64
635 | Self::Nat128
636 | Self::NatBig { .. }
637 | Self::Ulid
638 | Self::Unit
639 | Self::Decimal { .. }
640 | Self::Relation { .. } => RuntimeValueKind::Atomic,
641 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
642 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
643 Self::Structured { queryable } => RuntimeValueKind::Structured {
644 queryable: *queryable,
645 },
646 }
647 }
648
649 #[must_use]
657 pub const fn is_deterministic_collection_shape(&self) -> bool {
658 match self {
659 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
660
661 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
662
663 Self::Map { key, value } => {
664 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
665 }
666
667 _ => true,
668 }
669 }
670
671 #[cfg(test)]
674 #[must_use]
675 pub(crate) fn supports_group_probe(&self) -> bool {
676 match self {
677 Self::Enum { variants, .. } => variants.iter().all(|variant| {
678 variant
679 .payload_kind()
680 .is_none_or(Self::supports_group_probe)
681 }),
682 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
683 Self::Decimal { .. } => true,
684 _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
685 }
686 }
687
688 #[cfg(test)]
690 #[must_use]
691 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
692 match (self, value) {
693 (Self::Account, Value::Account(_))
694 | (Self::Blob { .. }, Value::Blob(_))
695 | (Self::Bool, Value::Bool(_))
696 | (Self::Date, Value::Date(_))
697 | (Self::Decimal { .. }, Value::Decimal(_))
698 | (Self::Duration, Value::Duration(_))
699 | (Self::Enum { .. }, Value::Enum(_))
700 | (Self::Float32, Value::Float32(_))
701 | (Self::Float64, Value::Float64(_))
702 | (Self::Int64, Value::Int64(_))
703 | (Self::Int128, Value::Int128(_))
704 | (Self::Nat64, Value::Nat64(_))
705 | (Self::Principal, Value::Principal(_))
706 | (Self::Subaccount, Value::Subaccount(_))
707 | (Self::Text { .. }, Value::Text(_))
708 | (Self::Timestamp, Value::Timestamp(_))
709 | (Self::Nat128, Value::Nat128(_))
710 | (Self::Ulid, Value::Ulid(_))
711 | (Self::Unit, Value::Unit)
712 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
713 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
714 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
715 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
716 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
717 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
718 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
719 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
720 value.to_leb128().len() <= *max_bytes as usize
721 }
722 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
723 value.to_leb128().len() <= *max_bytes as usize
724 }
725 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
726 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
727 items.iter().all(|item| inner.accepts_value(item))
728 }
729 (Self::Map { key, value }, Value::Map(entries)) => {
730 Value::validate_map_entries(entries.as_slice()).is_ok()
731 && entries.iter().all(|(entry_key, entry_value)| {
732 key.accepts_value(entry_key) && value.accepts_value(entry_value)
733 })
734 }
735 _ => false,
736 }
737 }
738}
739
740#[cfg(test)]
746fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
747 match (kind, value) {
748 (FieldKind::Relation { key_kind, .. }, value) => {
749 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
750 }
751 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
752 .iter()
753 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
754 (
755 FieldKind::Map {
756 key,
757 value: value_kind,
758 },
759 Value::Map(entries),
760 ) => {
761 if Value::validate_map_entries(entries.as_slice()).is_err() {
762 return false;
763 }
764
765 entries.iter().all(|(entry_key, entry_value)| {
766 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
767 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
768 })
769 }
770 (FieldKind::Structured { .. }, _) => false,
771 _ => kind.accepts_value(value),
772 }
773}
774
775#[cfg(test)]
779fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
780 match (kind, value) {
781 (FieldKind::Structured { .. }, _) => true,
782 (FieldKind::Relation { key_kind, .. }, value) => {
783 value_storage_kind_accepts_runtime_value(*key_kind, value)
784 }
785 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
786 .iter()
787 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
788 (
789 FieldKind::Map {
790 key,
791 value: value_kind,
792 },
793 Value::Map(entries),
794 ) => {
795 if Value::validate_map_entries(entries.as_slice()).is_err() {
796 return false;
797 }
798
799 entries.iter().all(|(entry_key, entry_value)| {
800 value_storage_kind_accepts_runtime_value(*key, entry_key)
801 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
802 })
803 }
804 _ => kind.accepts_value(value),
805 }
806}
807
808#[cfg(test)]
811fn ensure_decimal_scale_matches(
812 kind: FieldKind,
813 value: &Value,
814) -> Result<(), FieldStorageValidationError> {
815 if matches!(value, Value::Null) {
816 return Ok(());
817 }
818
819 match (kind, value) {
820 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
821 if decimal.scale() != scale {
822 return Err(FieldStorageValidationError::DecimalScaleMismatch);
823 }
824
825 Ok(())
826 }
827 (FieldKind::Relation { key_kind, .. }, value) => {
828 ensure_decimal_scale_matches(*key_kind, value)
829 }
830 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
831 for item in items {
832 ensure_decimal_scale_matches(*inner, item)?;
833 }
834
835 Ok(())
836 }
837 (
838 FieldKind::Map {
839 key,
840 value: map_value,
841 },
842 Value::Map(entries),
843 ) => {
844 for (entry_key, entry_value) in entries {
845 ensure_decimal_scale_matches(*key, entry_key)?;
846 ensure_decimal_scale_matches(*map_value, entry_value)?;
847 }
848
849 Ok(())
850 }
851 _ => Ok(()),
852 }
853}
854
855#[cfg(test)]
858fn ensure_scalar_max_len_matches(
859 kind: FieldKind,
860 value: &Value,
861) -> Result<(), FieldStorageValidationError> {
862 if matches!(value, Value::Null) {
863 return Ok(());
864 }
865
866 match (kind, value) {
867 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
868 let len = text.chars().count();
869 if len > max as usize {
870 return Err(FieldStorageValidationError::ScalarMaxLen);
871 }
872
873 Ok(())
874 }
875 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
876 let len = bytes.len();
877 if len > max as usize {
878 return Err(FieldStorageValidationError::ScalarMaxLen);
879 }
880
881 Ok(())
882 }
883 (FieldKind::Relation { key_kind, .. }, value) => {
884 ensure_scalar_max_len_matches(*key_kind, value)
885 }
886 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
887 for item in items {
888 ensure_scalar_max_len_matches(*inner, item)?;
889 }
890
891 Ok(())
892 }
893 (
894 FieldKind::Map {
895 key,
896 value: map_value,
897 },
898 Value::Map(entries),
899 ) => {
900 for (entry_key, entry_value) in entries {
901 ensure_scalar_max_len_matches(*key, entry_key)?;
902 ensure_scalar_max_len_matches(*map_value, entry_value)?;
903 }
904
905 Ok(())
906 }
907 _ => Ok(()),
908 }
909}
910
911#[cfg(test)]
914fn ensure_value_is_deterministic_for_storage(
915 kind: FieldKind,
916 value: &Value,
917) -> Result<(), FieldStorageValidationError> {
918 match (kind, value) {
919 (FieldKind::Set(_), Value::List(items)) => {
920 for pair in items.windows(2) {
921 let [left, right] = pair else {
922 continue;
923 };
924 if Value::canonical_cmp(left, right) != Ordering::Less {
925 return Err(FieldStorageValidationError::SetCanonicalOrder);
926 }
927 }
928
929 Ok(())
930 }
931 (FieldKind::Map { .. }, Value::Map(entries)) => {
932 Value::validate_map_entries(entries.as_slice())
933 .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
934
935 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
936 return Err(FieldStorageValidationError::MapCanonicalOrder);
937 }
938
939 Ok(())
940 }
941 _ => Ok(()),
942 }
943}
944
945#[cfg(test)]
950mod tests {
951 use crate::{
952 model::field::{FieldKind, FieldModel},
953 value::Value,
954 };
955
956 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
957 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
958
959 #[test]
960 fn text_max_len_accepts_unbounded_text() {
961 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
962
963 assert!(
964 field
965 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
966 .is_ok()
967 );
968 }
969
970 #[test]
971 fn text_max_len_counts_unicode_scalars_not_bytes() {
972 let field = FieldModel::generated("name", BOUNDED_TEXT);
973
974 assert!(
975 field
976 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
977 .is_ok()
978 );
979 assert!(
980 field
981 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
982 .is_err()
983 );
984 }
985
986 #[test]
987 fn text_max_len_recurses_through_collections() {
988 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
989 static TEXT_MAP: FieldKind = FieldKind::Map {
990 key: &BOUNDED_TEXT,
991 value: &BOUNDED_TEXT,
992 };
993
994 let list_field = FieldModel::generated("names", TEXT_LIST);
995 let map_field = FieldModel::generated("labels", TEXT_MAP);
996
997 assert!(
998 list_field
999 .validate_runtime_value_for_storage(&Value::List(vec![
1000 Value::Text("Ada".into()),
1001 Value::Text("Bob".into()),
1002 ]))
1003 .is_ok()
1004 );
1005 assert!(
1006 list_field
1007 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1008 .is_err()
1009 );
1010 assert!(
1011 map_field
1012 .validate_runtime_value_for_storage(&Value::Map(vec![(
1013 Value::Text("key".into()),
1014 Value::Text("val".into()),
1015 )]))
1016 .is_ok()
1017 );
1018 assert!(
1019 map_field
1020 .validate_runtime_value_for_storage(&Value::Map(vec![(
1021 Value::Text("long".into()),
1022 Value::Text("val".into()),
1023 )]))
1024 .is_err()
1025 );
1026 }
1027
1028 #[test]
1029 fn blob_max_len_counts_bytes() {
1030 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1031
1032 assert!(
1033 field
1034 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1035 .is_ok()
1036 );
1037 assert!(
1038 field
1039 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1040 .is_err()
1041 );
1042 }
1043
1044 #[test]
1045 fn blob_max_len_recurses_through_collections() {
1046 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1047
1048 let field = FieldModel::generated("payloads", BLOB_LIST);
1049
1050 assert!(
1051 field
1052 .validate_runtime_value_for_storage(&Value::List(vec![
1053 Value::Blob(vec![1, 2, 3]),
1054 Value::Blob(vec![4, 5]),
1055 ]))
1056 .is_ok()
1057 );
1058 assert!(
1059 field
1060 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1061 1, 2, 3, 4
1062 ])]))
1063 .is_err()
1064 );
1065 }
1066}