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, Eq, PartialEq)]
524pub enum RelationStrength {
525 Strong,
526 Weak,
527}
528
529#[derive(Clone, Copy, Debug)]
539pub enum FieldKind {
540 Account,
542 Blob {
543 max_len: Option<u32>,
545 },
546 Bool,
547 Date,
548 Decimal {
549 scale: u32,
551 },
552 Duration,
553 Enum {
554 path: &'static str,
556 variants: &'static [EnumVariantModel],
558 },
559 Float32,
560 Float64,
561 Int8,
562 Int16,
563 Int32,
564 Int64,
565 Int128,
566 IntBig {
567 max_bytes: u32,
569 },
570 Principal,
571 Subaccount,
572 Text {
573 max_len: Option<u32>,
575 },
576 Timestamp,
577 Nat8,
578 Nat16,
579 Nat32,
580 Nat64,
581 Nat128,
582 NatBig {
583 max_bytes: u32,
585 },
586 Ulid,
587 Unit,
588
589 Relation {
592 target_path: &'static str,
594 target_entity_name: &'static str,
596 target_entity_tag: EntityTag,
598 target_store_path: &'static str,
600 key_kind: &'static Self,
601 strength: RelationStrength,
602 },
603
604 List(&'static Self),
606 Set(&'static Self),
607 Map {
611 key: &'static Self,
612 value: &'static Self,
613 },
614
615 Structured {
619 queryable: bool,
620 },
621}
622
623impl FieldKind {
624 #[must_use]
625 pub const fn value_kind(&self) -> RuntimeValueKind {
626 match self {
627 Self::Account
628 | Self::Blob { .. }
629 | Self::Bool
630 | Self::Date
631 | Self::Duration
632 | Self::Enum { .. }
633 | Self::Float32
634 | Self::Float64
635 | Self::Int8
636 | Self::Int16
637 | Self::Int32
638 | Self::Int64
639 | Self::Int128
640 | Self::IntBig { .. }
641 | Self::Principal
642 | Self::Subaccount
643 | Self::Text { .. }
644 | Self::Timestamp
645 | Self::Nat8
646 | Self::Nat16
647 | Self::Nat32
648 | Self::Nat64
649 | Self::Nat128
650 | Self::NatBig { .. }
651 | Self::Ulid
652 | Self::Unit
653 | Self::Decimal { .. }
654 | Self::Relation { .. } => RuntimeValueKind::Atomic,
655 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
656 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
657 Self::Structured { queryable } => RuntimeValueKind::Structured {
658 queryable: *queryable,
659 },
660 }
661 }
662
663 #[must_use]
671 pub const fn is_deterministic_collection_shape(&self) -> bool {
672 match self {
673 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
674
675 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
676
677 Self::Map { key, value } => {
678 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
679 }
680
681 _ => true,
682 }
683 }
684
685 #[cfg(test)]
688 #[must_use]
689 pub(crate) fn supports_group_probe(&self) -> bool {
690 match self {
691 Self::Enum { variants, .. } => variants.iter().all(|variant| {
692 variant
693 .payload_kind()
694 .is_none_or(Self::supports_group_probe)
695 }),
696 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
697 Self::Decimal { .. } => true,
698 _ => super::field_kind_semantics::field_kind_has_identity_group_canonical_form(*self),
699 }
700 }
701
702 #[cfg(test)]
704 #[must_use]
705 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
706 match (self, value) {
707 (Self::Account, Value::Account(_))
708 | (Self::Blob { .. }, Value::Blob(_))
709 | (Self::Bool, Value::Bool(_))
710 | (Self::Date, Value::Date(_))
711 | (Self::Decimal { .. }, Value::Decimal(_))
712 | (Self::Duration, Value::Duration(_))
713 | (Self::Enum { .. }, Value::Enum(_))
714 | (Self::Float32, Value::Float32(_))
715 | (Self::Float64, Value::Float64(_))
716 | (Self::Int64, Value::Int64(_))
717 | (Self::Int128, Value::Int128(_))
718 | (Self::Nat64, Value::Nat64(_))
719 | (Self::Principal, Value::Principal(_))
720 | (Self::Subaccount, Value::Subaccount(_))
721 | (Self::Text { .. }, Value::Text(_))
722 | (Self::Timestamp, Value::Timestamp(_))
723 | (Self::Nat128, Value::Nat128(_))
724 | (Self::Ulid, Value::Ulid(_))
725 | (Self::Unit, Value::Unit)
726 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
727 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
728 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
729 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
730 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
731 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
732 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
733 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
734 value.to_leb128().len() <= *max_bytes as usize
735 }
736 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
737 value.to_leb128().len() <= *max_bytes as usize
738 }
739 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
740 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
741 items.iter().all(|item| inner.accepts_value(item))
742 }
743 (Self::Map { key, value }, Value::Map(entries)) => {
744 Value::validate_map_entries(entries.as_slice()).is_ok()
745 && entries.iter().all(|(entry_key, entry_value)| {
746 key.accepts_value(entry_key) && value.accepts_value(entry_value)
747 })
748 }
749 _ => false,
750 }
751 }
752}
753
754#[cfg(test)]
760fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
761 match (kind, value) {
762 (FieldKind::Relation { key_kind, .. }, value) => {
763 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
764 }
765 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
766 .iter()
767 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
768 (
769 FieldKind::Map {
770 key,
771 value: value_kind,
772 },
773 Value::Map(entries),
774 ) => {
775 if Value::validate_map_entries(entries.as_slice()).is_err() {
776 return false;
777 }
778
779 entries.iter().all(|(entry_key, entry_value)| {
780 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
781 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
782 })
783 }
784 (FieldKind::Structured { .. }, _) => false,
785 _ => kind.accepts_value(value),
786 }
787}
788
789#[cfg(test)]
793fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
794 match (kind, value) {
795 (FieldKind::Structured { .. }, _) => true,
796 (FieldKind::Relation { key_kind, .. }, value) => {
797 value_storage_kind_accepts_runtime_value(*key_kind, value)
798 }
799 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
800 .iter()
801 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
802 (
803 FieldKind::Map {
804 key,
805 value: value_kind,
806 },
807 Value::Map(entries),
808 ) => {
809 if Value::validate_map_entries(entries.as_slice()).is_err() {
810 return false;
811 }
812
813 entries.iter().all(|(entry_key, entry_value)| {
814 value_storage_kind_accepts_runtime_value(*key, entry_key)
815 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
816 })
817 }
818 _ => kind.accepts_value(value),
819 }
820}
821
822#[cfg(test)]
825fn ensure_decimal_scale_matches(
826 kind: FieldKind,
827 value: &Value,
828) -> Result<(), FieldStorageValidationError> {
829 if matches!(value, Value::Null) {
830 return Ok(());
831 }
832
833 match (kind, value) {
834 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
835 if decimal.scale() != scale {
836 return Err(FieldStorageValidationError::DecimalScaleMismatch);
837 }
838
839 Ok(())
840 }
841 (FieldKind::Relation { key_kind, .. }, value) => {
842 ensure_decimal_scale_matches(*key_kind, value)
843 }
844 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
845 for item in items {
846 ensure_decimal_scale_matches(*inner, item)?;
847 }
848
849 Ok(())
850 }
851 (
852 FieldKind::Map {
853 key,
854 value: map_value,
855 },
856 Value::Map(entries),
857 ) => {
858 for (entry_key, entry_value) in entries {
859 ensure_decimal_scale_matches(*key, entry_key)?;
860 ensure_decimal_scale_matches(*map_value, entry_value)?;
861 }
862
863 Ok(())
864 }
865 _ => Ok(()),
866 }
867}
868
869#[cfg(test)]
872fn ensure_scalar_max_len_matches(
873 kind: FieldKind,
874 value: &Value,
875) -> Result<(), FieldStorageValidationError> {
876 if matches!(value, Value::Null) {
877 return Ok(());
878 }
879
880 match (kind, value) {
881 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
882 let len = text.chars().count();
883 if len > max as usize {
884 return Err(FieldStorageValidationError::ScalarMaxLen);
885 }
886
887 Ok(())
888 }
889 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
890 let len = bytes.len();
891 if len > max as usize {
892 return Err(FieldStorageValidationError::ScalarMaxLen);
893 }
894
895 Ok(())
896 }
897 (FieldKind::Relation { key_kind, .. }, value) => {
898 ensure_scalar_max_len_matches(*key_kind, value)
899 }
900 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
901 for item in items {
902 ensure_scalar_max_len_matches(*inner, item)?;
903 }
904
905 Ok(())
906 }
907 (
908 FieldKind::Map {
909 key,
910 value: map_value,
911 },
912 Value::Map(entries),
913 ) => {
914 for (entry_key, entry_value) in entries {
915 ensure_scalar_max_len_matches(*key, entry_key)?;
916 ensure_scalar_max_len_matches(*map_value, entry_value)?;
917 }
918
919 Ok(())
920 }
921 _ => Ok(()),
922 }
923}
924
925#[cfg(test)]
928fn ensure_value_is_deterministic_for_storage(
929 kind: FieldKind,
930 value: &Value,
931) -> Result<(), FieldStorageValidationError> {
932 match (kind, value) {
933 (FieldKind::Set(_), Value::List(items)) => {
934 for pair in items.windows(2) {
935 let [left, right] = pair else {
936 continue;
937 };
938 if Value::canonical_cmp(left, right) != Ordering::Less {
939 return Err(FieldStorageValidationError::SetCanonicalOrder);
940 }
941 }
942
943 Ok(())
944 }
945 (FieldKind::Map { .. }, Value::Map(entries)) => {
946 Value::validate_map_entries(entries.as_slice())
947 .map_err(|_| FieldStorageValidationError::MapEntryContract)?;
948
949 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
950 return Err(FieldStorageValidationError::MapCanonicalOrder);
951 }
952
953 Ok(())
954 }
955 _ => Ok(()),
956 }
957}
958
959#[cfg(test)]
964mod tests {
965 use crate::{
966 model::field::{FieldKind, FieldModel},
967 value::Value,
968 };
969
970 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
971 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
972
973 #[test]
974 fn text_max_len_accepts_unbounded_text() {
975 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
976
977 assert!(
978 field
979 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
980 .is_ok()
981 );
982 }
983
984 #[test]
985 fn text_max_len_counts_unicode_scalars_not_bytes() {
986 let field = FieldModel::generated("name", BOUNDED_TEXT);
987
988 assert!(
989 field
990 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
991 .is_ok()
992 );
993 assert!(
994 field
995 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
996 .is_err()
997 );
998 }
999
1000 #[test]
1001 fn text_max_len_recurses_through_collections() {
1002 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1003 static TEXT_MAP: FieldKind = FieldKind::Map {
1004 key: &BOUNDED_TEXT,
1005 value: &BOUNDED_TEXT,
1006 };
1007
1008 let list_field = FieldModel::generated("names", TEXT_LIST);
1009 let map_field = FieldModel::generated("labels", TEXT_MAP);
1010
1011 assert!(
1012 list_field
1013 .validate_runtime_value_for_storage(&Value::List(vec![
1014 Value::Text("Ada".into()),
1015 Value::Text("Bob".into()),
1016 ]))
1017 .is_ok()
1018 );
1019 assert!(
1020 list_field
1021 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1022 .is_err()
1023 );
1024 assert!(
1025 map_field
1026 .validate_runtime_value_for_storage(&Value::Map(vec![(
1027 Value::Text("key".into()),
1028 Value::Text("val".into()),
1029 )]))
1030 .is_ok()
1031 );
1032 assert!(
1033 map_field
1034 .validate_runtime_value_for_storage(&Value::Map(vec![(
1035 Value::Text("long".into()),
1036 Value::Text("val".into()),
1037 )]))
1038 .is_err()
1039 );
1040 }
1041
1042 #[test]
1043 fn blob_max_len_counts_bytes() {
1044 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1045
1046 assert!(
1047 field
1048 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1049 .is_ok()
1050 );
1051 assert!(
1052 field
1053 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1054 .is_err()
1055 );
1056 }
1057
1058 #[test]
1059 fn blob_max_len_recurses_through_collections() {
1060 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1061
1062 let field = FieldModel::generated("payloads", BLOB_LIST);
1063
1064 assert!(
1065 field
1066 .validate_runtime_value_for_storage(&Value::List(vec![
1067 Value::Blob(vec![1, 2, 3]),
1068 Value::Blob(vec![4, 5]),
1069 ]))
1070 .is_ok()
1071 );
1072 assert!(
1073 field
1074 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1075 1, 2, 3, 4
1076 ])]))
1077 .is_err()
1078 );
1079 }
1080}