1use crate::{
7 traits::RuntimeValueKind,
8 types::{Decimal, EntityTag},
9 value::Value,
10};
11use std::{borrow::Cow, cmp::Ordering};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum FieldStorageDecode {
25 ByKind,
27 Value,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum ScalarCodec {
42 Blob,
43 Bool,
44 Date,
45 Duration,
46 Float32,
47 Float64,
48 Int64,
49 Principal,
50 Subaccount,
51 Text,
52 Timestamp,
53 Uint64,
54 Ulid,
55 Unit,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum LeafCodec {
69 Scalar(ScalarCodec),
70 StructuralFallback,
71}
72
73#[derive(Clone, Copy, Debug)]
83pub struct EnumVariantModel {
84 pub(crate) ident: &'static str,
86 pub(crate) payload_kind: Option<&'static FieldKind>,
88 pub(crate) payload_storage_decode: FieldStorageDecode,
90}
91
92impl EnumVariantModel {
93 #[must_use]
95 pub const fn new(
96 ident: &'static str,
97 payload_kind: Option<&'static FieldKind>,
98 payload_storage_decode: FieldStorageDecode,
99 ) -> Self {
100 Self {
101 ident,
102 payload_kind,
103 payload_storage_decode,
104 }
105 }
106
107 #[must_use]
109 pub const fn ident(&self) -> &'static str {
110 self.ident
111 }
112
113 #[must_use]
115 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
116 self.payload_kind
117 }
118
119 #[must_use]
121 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
122 self.payload_storage_decode
123 }
124}
125
126#[derive(Debug)]
136pub struct FieldModel {
137 pub(crate) name: &'static str,
139 pub(crate) kind: FieldKind,
141 pub(crate) nullable: bool,
143 pub(crate) storage_decode: FieldStorageDecode,
145 pub(crate) leaf_codec: LeafCodec,
147 pub(crate) insert_generation: Option<FieldInsertGeneration>,
149 pub(crate) write_management: Option<FieldWriteManagement>,
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163pub enum FieldInsertGeneration {
164 Ulid,
166 Timestamp,
168}
169
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
180pub enum FieldWriteManagement {
181 CreatedAt,
183 UpdatedAt,
185}
186
187impl FieldModel {
188 #[must_use]
194 #[doc(hidden)]
195 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
196 Self::generated_with_storage_decode_and_nullability(
197 name,
198 kind,
199 FieldStorageDecode::ByKind,
200 false,
201 )
202 }
203
204 #[must_use]
206 #[doc(hidden)]
207 pub const fn generated_with_storage_decode(
208 name: &'static str,
209 kind: FieldKind,
210 storage_decode: FieldStorageDecode,
211 ) -> Self {
212 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
213 }
214
215 #[must_use]
217 #[doc(hidden)]
218 pub const fn generated_with_storage_decode_and_nullability(
219 name: &'static str,
220 kind: FieldKind,
221 storage_decode: FieldStorageDecode,
222 nullable: bool,
223 ) -> Self {
224 Self::generated_with_storage_decode_nullability_and_write_policies(
225 name,
226 kind,
227 storage_decode,
228 nullable,
229 None,
230 None,
231 )
232 }
233
234 #[must_use]
237 #[doc(hidden)]
238 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
239 name: &'static str,
240 kind: FieldKind,
241 storage_decode: FieldStorageDecode,
242 nullable: bool,
243 insert_generation: Option<FieldInsertGeneration>,
244 ) -> Self {
245 Self::generated_with_storage_decode_nullability_and_write_policies(
246 name,
247 kind,
248 storage_decode,
249 nullable,
250 insert_generation,
251 None,
252 )
253 }
254
255 #[must_use]
258 #[doc(hidden)]
259 pub const fn generated_with_storage_decode_nullability_and_write_policies(
260 name: &'static str,
261 kind: FieldKind,
262 storage_decode: FieldStorageDecode,
263 nullable: bool,
264 insert_generation: Option<FieldInsertGeneration>,
265 write_management: Option<FieldWriteManagement>,
266 ) -> Self {
267 Self {
268 name,
269 kind,
270 nullable,
271 storage_decode,
272 leaf_codec: leaf_codec_for(kind, storage_decode),
273 insert_generation,
274 write_management,
275 }
276 }
277
278 #[must_use]
280 pub const fn name(&self) -> &'static str {
281 self.name
282 }
283
284 #[must_use]
286 pub const fn kind(&self) -> FieldKind {
287 self.kind
288 }
289
290 #[must_use]
292 pub const fn nullable(&self) -> bool {
293 self.nullable
294 }
295
296 #[must_use]
298 pub const fn storage_decode(&self) -> FieldStorageDecode {
299 self.storage_decode
300 }
301
302 #[must_use]
304 pub const fn leaf_codec(&self) -> LeafCodec {
305 self.leaf_codec
306 }
307
308 #[must_use]
310 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
311 self.insert_generation
312 }
313
314 #[must_use]
316 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
317 self.write_management
318 }
319
320 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
328 if matches!(value, Value::Null) {
329 if self.nullable() {
330 return Ok(());
331 }
332
333 return Err("required field cannot store null".into());
334 }
335
336 let accepts = match self.storage_decode() {
337 FieldStorageDecode::Value => {
338 value_storage_kind_accepts_runtime_value(self.kind(), value)
339 }
340 FieldStorageDecode::ByKind => {
341 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
342 }
343 };
344 if !accepts {
345 return Err(format!(
346 "field kind {:?} does not accept runtime value {value:?}",
347 self.kind()
348 ));
349 }
350
351 ensure_decimal_scale_matches(self.kind(), value)?;
352 ensure_text_max_len_matches(self.kind(), value)?;
353 ensure_value_is_deterministic_for_storage(self.kind(), value)
354 }
355
356 pub(crate) fn normalize_runtime_value_for_storage<'a>(
360 &self,
361 value: &'a Value,
362 ) -> Result<Cow<'a, Value>, String> {
363 normalize_decimal_scale_for_storage(self.kind(), value)
364 }
365}
366
367const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
371 if matches!(storage_decode, FieldStorageDecode::Value) {
372 return LeafCodec::StructuralFallback;
373 }
374
375 match kind {
376 FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
377 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
378 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
379 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
380 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
381 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
382 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
383 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
384 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
385 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
386 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
387 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
388 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
389 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
390 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
391 FieldKind::Account
392 | FieldKind::Decimal { .. }
393 | FieldKind::Enum { .. }
394 | FieldKind::Int128
395 | FieldKind::IntBig
396 | FieldKind::List(_)
397 | FieldKind::Map { .. }
398 | FieldKind::Set(_)
399 | FieldKind::Structured { .. }
400 | FieldKind::Uint128
401 | FieldKind::UintBig => LeafCodec::StructuralFallback,
402 }
403}
404
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
412pub enum RelationStrength {
413 Strong,
414 Weak,
415}
416
417#[derive(Clone, Copy, Debug)]
427pub enum FieldKind {
428 Account,
430 Blob,
431 Bool,
432 Date,
433 Decimal {
434 scale: u32,
436 },
437 Duration,
438 Enum {
439 path: &'static str,
441 variants: &'static [EnumVariantModel],
443 },
444 Float32,
445 Float64,
446 Int,
447 Int128,
448 IntBig,
449 Principal,
450 Subaccount,
451 Text {
452 max_len: Option<u32>,
454 },
455 Timestamp,
456 Uint,
457 Uint128,
458 UintBig,
459 Ulid,
460 Unit,
461
462 Relation {
465 target_path: &'static str,
467 target_entity_name: &'static str,
469 target_entity_tag: EntityTag,
471 target_store_path: &'static str,
473 key_kind: &'static Self,
474 strength: RelationStrength,
475 },
476
477 List(&'static Self),
479 Set(&'static Self),
480 Map {
484 key: &'static Self,
485 value: &'static Self,
486 },
487
488 Structured {
492 queryable: bool,
493 },
494}
495
496impl FieldKind {
497 #[must_use]
498 pub const fn value_kind(&self) -> RuntimeValueKind {
499 match self {
500 Self::Account
501 | Self::Blob
502 | Self::Bool
503 | Self::Date
504 | Self::Duration
505 | Self::Enum { .. }
506 | Self::Float32
507 | Self::Float64
508 | Self::Int
509 | Self::Int128
510 | Self::IntBig
511 | Self::Principal
512 | Self::Subaccount
513 | Self::Text { .. }
514 | Self::Timestamp
515 | Self::Uint
516 | Self::Uint128
517 | Self::UintBig
518 | Self::Ulid
519 | Self::Unit
520 | Self::Decimal { .. }
521 | Self::Relation { .. } => RuntimeValueKind::Atomic,
522 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
523 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
524 Self::Structured { queryable } => RuntimeValueKind::Structured {
525 queryable: *queryable,
526 },
527 }
528 }
529
530 #[must_use]
538 pub const fn is_deterministic_collection_shape(&self) -> bool {
539 match self {
540 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
541
542 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
543
544 Self::Map { key, value } => {
545 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
546 }
547
548 _ => true,
549 }
550 }
551
552 #[must_use]
555 pub(crate) fn supports_group_probe(&self) -> bool {
556 match self {
557 Self::Enum { variants, .. } => variants.iter().all(|variant| {
558 variant
559 .payload_kind()
560 .is_none_or(Self::supports_group_probe)
561 }),
562 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
563 Self::List(_)
564 | Self::Set(_)
565 | Self::Map { .. }
566 | Self::Structured { .. }
567 | Self::Unit => false,
568 Self::Account
569 | Self::Blob
570 | Self::Bool
571 | Self::Date
572 | Self::Decimal { .. }
573 | Self::Duration
574 | Self::Float32
575 | Self::Float64
576 | Self::Int
577 | Self::Int128
578 | Self::IntBig
579 | Self::Principal
580 | Self::Subaccount
581 | Self::Text { .. }
582 | Self::Timestamp
583 | Self::Uint
584 | Self::Uint128
585 | Self::UintBig
586 | Self::Ulid => true,
587 }
588 }
589
590 #[must_use]
596 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
597 match (self, value) {
598 (Self::Account, Value::Account(_))
599 | (Self::Blob, Value::Blob(_))
600 | (Self::Bool, Value::Bool(_))
601 | (Self::Date, Value::Date(_))
602 | (Self::Decimal { .. }, Value::Decimal(_))
603 | (Self::Duration, Value::Duration(_))
604 | (Self::Enum { .. }, Value::Enum(_))
605 | (Self::Float32, Value::Float32(_))
606 | (Self::Float64, Value::Float64(_))
607 | (Self::Int, Value::Int(_))
608 | (Self::Int128, Value::Int128(_))
609 | (Self::IntBig, Value::IntBig(_))
610 | (Self::Principal, Value::Principal(_))
611 | (Self::Subaccount, Value::Subaccount(_))
612 | (Self::Text { .. }, Value::Text(_))
613 | (Self::Timestamp, Value::Timestamp(_))
614 | (Self::Uint, Value::Uint(_))
615 | (Self::Uint128, Value::Uint128(_))
616 | (Self::UintBig, Value::UintBig(_))
617 | (Self::Ulid, Value::Ulid(_))
618 | (Self::Unit, Value::Unit)
619 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
620 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
621 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
622 items.iter().all(|item| inner.accepts_value(item))
623 }
624 (Self::Map { key, value }, Value::Map(entries)) => {
625 if Value::validate_map_entries(entries.as_slice()).is_err() {
626 return false;
627 }
628
629 entries.iter().all(|(entry_key, entry_value)| {
630 key.accepts_value(entry_key) && value.accepts_value(entry_value)
631 })
632 }
633 _ => false,
634 }
635 }
636}
637
638fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
644 match (kind, value) {
645 (FieldKind::Relation { key_kind, .. }, value) => {
646 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
647 }
648 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
649 .iter()
650 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
651 (
652 FieldKind::Map {
653 key,
654 value: value_kind,
655 },
656 Value::Map(entries),
657 ) => {
658 if Value::validate_map_entries(entries.as_slice()).is_err() {
659 return false;
660 }
661
662 entries.iter().all(|(entry_key, entry_value)| {
663 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
664 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
665 })
666 }
667 (FieldKind::Structured { .. }, _) => false,
668 _ => kind.accepts_value(value),
669 }
670}
671
672fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
676 match (kind, value) {
677 (FieldKind::Structured { .. }, _) => true,
678 (FieldKind::Relation { key_kind, .. }, value) => {
679 value_storage_kind_accepts_runtime_value(*key_kind, value)
680 }
681 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
682 .iter()
683 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
684 (
685 FieldKind::Map {
686 key,
687 value: value_kind,
688 },
689 Value::Map(entries),
690 ) => {
691 if Value::validate_map_entries(entries.as_slice()).is_err() {
692 return false;
693 }
694
695 entries.iter().all(|(entry_key, entry_value)| {
696 value_storage_kind_accepts_runtime_value(*key, entry_key)
697 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
698 })
699 }
700 _ => kind.accepts_value(value),
701 }
702}
703
704fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
707 if matches!(value, Value::Null) {
708 return Ok(());
709 }
710
711 match (kind, value) {
712 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
713 if decimal.scale() != scale {
714 return Err(format!(
715 "decimal scale mismatch: expected {scale}, found {}",
716 decimal.scale()
717 ));
718 }
719
720 Ok(())
721 }
722 (FieldKind::Relation { key_kind, .. }, value) => {
723 ensure_decimal_scale_matches(*key_kind, value)
724 }
725 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
726 for item in items {
727 ensure_decimal_scale_matches(*inner, item)?;
728 }
729
730 Ok(())
731 }
732 (
733 FieldKind::Map {
734 key,
735 value: map_value,
736 },
737 Value::Map(entries),
738 ) => {
739 for (entry_key, entry_value) in entries {
740 ensure_decimal_scale_matches(*key, entry_key)?;
741 ensure_decimal_scale_matches(*map_value, entry_value)?;
742 }
743
744 Ok(())
745 }
746 _ => Ok(()),
747 }
748}
749
750fn normalize_decimal_scale_for_storage(
754 kind: FieldKind,
755 value: &Value,
756) -> Result<Cow<'_, Value>, String> {
757 if matches!(value, Value::Null) {
758 return Ok(Cow::Borrowed(value));
759 }
760
761 match (kind, value) {
762 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
763 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
764 format!(
765 "decimal scale mismatch: expected {scale}, found {}",
766 decimal.scale()
767 )
768 })?;
769
770 if normalized.scale() == decimal.scale() {
771 Ok(Cow::Borrowed(value))
772 } else {
773 Ok(Cow::Owned(Value::Decimal(normalized)))
774 }
775 }
776 (FieldKind::Relation { key_kind, .. }, value) => {
777 normalize_decimal_scale_for_storage(*key_kind, value)
778 }
779 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
780 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
781 items.map_or_else(
782 || Cow::Borrowed(value),
783 |items| Cow::Owned(Value::List(items)),
784 )
785 })
786 }
787 (
788 FieldKind::Map {
789 key,
790 value: map_value,
791 },
792 Value::Map(entries),
793 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
794 entries.map_or_else(
795 || Cow::Borrowed(value),
796 |entries| Cow::Owned(Value::Map(entries)),
797 )
798 }),
799 _ => Ok(Cow::Borrowed(value)),
800 }
801}
802
803fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
807 match decimal.scale().cmp(&scale) {
808 Ordering::Equal => Some(decimal),
809 Ordering::Less => decimal
810 .scale_to_integer(scale)
811 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
812 Ordering::Greater => Some(decimal.round_dp(scale)),
813 }
814}
815
816fn normalize_decimal_list_items(
819 kind: FieldKind,
820 items: &[Value],
821) -> Result<Option<Vec<Value>>, String> {
822 let mut normalized_items = None;
823
824 for (index, item) in items.iter().enumerate() {
825 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
826 if let Cow::Owned(value) = normalized {
827 let items = normalized_items.get_or_insert_with(|| items.to_vec());
828 items[index] = value;
829 }
830 }
831
832 Ok(normalized_items)
833}
834
835fn normalize_decimal_map_entries(
838 key_kind: FieldKind,
839 value_kind: FieldKind,
840 entries: &[(Value, Value)],
841) -> Result<Option<Vec<(Value, Value)>>, String> {
842 let mut normalized_entries = None;
843
844 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
845 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
846 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
847
848 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
849 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
850 if let Cow::Owned(value) = normalized_key {
851 entries[index].0 = value;
852 }
853 if let Cow::Owned(value) = normalized_value {
854 entries[index].1 = value;
855 }
856 }
857 }
858
859 Ok(normalized_entries)
860}
861
862fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
865 if matches!(value, Value::Null) {
866 return Ok(());
867 }
868
869 match (kind, value) {
870 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
871 let len = text.chars().count();
872 if len > max as usize {
873 return Err(format!(
874 "text length exceeds max_len: expected at most {max}, found {len}"
875 ));
876 }
877
878 Ok(())
879 }
880 (FieldKind::Relation { key_kind, .. }, value) => {
881 ensure_text_max_len_matches(*key_kind, value)
882 }
883 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
884 for item in items {
885 ensure_text_max_len_matches(*inner, item)?;
886 }
887
888 Ok(())
889 }
890 (
891 FieldKind::Map {
892 key,
893 value: map_value,
894 },
895 Value::Map(entries),
896 ) => {
897 for (entry_key, entry_value) in entries {
898 ensure_text_max_len_matches(*key, entry_key)?;
899 ensure_text_max_len_matches(*map_value, entry_value)?;
900 }
901
902 Ok(())
903 }
904 _ => Ok(()),
905 }
906}
907
908fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
911 match (kind, value) {
912 (FieldKind::Set(_), Value::List(items)) => {
913 for pair in items.windows(2) {
914 let [left, right] = pair else {
915 continue;
916 };
917 if Value::canonical_cmp(left, right) != Ordering::Less {
918 return Err("set payload must already be canonical and deduplicated".into());
919 }
920 }
921
922 Ok(())
923 }
924 (FieldKind::Map { .. }, Value::Map(entries)) => {
925 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
926
927 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
928 return Err("map payload must already be canonical and deduplicated".into());
929 }
930
931 Ok(())
932 }
933 _ => Ok(()),
934 }
935}
936
937#[cfg(test)]
942mod tests {
943 use crate::{
944 model::field::{FieldKind, FieldModel},
945 value::Value,
946 };
947
948 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
949
950 #[test]
951 fn text_max_len_accepts_unbounded_text() {
952 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
953
954 assert!(
955 field
956 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
957 .is_ok()
958 );
959 }
960
961 #[test]
962 fn text_max_len_counts_unicode_scalars_not_bytes() {
963 let field = FieldModel::generated("name", BOUNDED_TEXT);
964
965 assert!(
966 field
967 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
968 .is_ok()
969 );
970 assert!(
971 field
972 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
973 .is_err()
974 );
975 }
976
977 #[test]
978 fn text_max_len_recurses_through_collections() {
979 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
980 static TEXT_MAP: FieldKind = FieldKind::Map {
981 key: &BOUNDED_TEXT,
982 value: &BOUNDED_TEXT,
983 };
984
985 let list_field = FieldModel::generated("names", TEXT_LIST);
986 let map_field = FieldModel::generated("labels", TEXT_MAP);
987
988 assert!(
989 list_field
990 .validate_runtime_value_for_storage(&Value::List(vec![
991 Value::Text("Ada".into()),
992 Value::Text("Bob".into()),
993 ]))
994 .is_ok()
995 );
996 assert!(
997 list_field
998 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
999 .is_err()
1000 );
1001 assert!(
1002 map_field
1003 .validate_runtime_value_for_storage(&Value::Map(vec![(
1004 Value::Text("key".into()),
1005 Value::Text("val".into()),
1006 )]))
1007 .is_ok()
1008 );
1009 assert!(
1010 map_field
1011 .validate_runtime_value_for_storage(&Value::Map(vec![(
1012 Value::Text("long".into()),
1013 Value::Text("val".into()),
1014 )]))
1015 .is_err()
1016 );
1017 }
1018}