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) nested_fields: &'static [Self],
143 pub(crate) nullable: bool,
145 pub(crate) storage_decode: FieldStorageDecode,
147 pub(crate) leaf_codec: LeafCodec,
149 pub(crate) insert_generation: Option<FieldInsertGeneration>,
151 pub(crate) write_management: Option<FieldWriteManagement>,
153}
154
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub enum FieldInsertGeneration {
166 Ulid,
168 Timestamp,
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum FieldWriteManagement {
183 CreatedAt,
185 UpdatedAt,
187}
188
189impl FieldModel {
190 #[must_use]
196 #[doc(hidden)]
197 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
198 Self::generated_with_storage_decode_and_nullability(
199 name,
200 kind,
201 FieldStorageDecode::ByKind,
202 false,
203 )
204 }
205
206 #[must_use]
208 #[doc(hidden)]
209 pub const fn generated_with_storage_decode(
210 name: &'static str,
211 kind: FieldKind,
212 storage_decode: FieldStorageDecode,
213 ) -> Self {
214 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
215 }
216
217 #[must_use]
219 #[doc(hidden)]
220 pub const fn generated_with_storage_decode_and_nullability(
221 name: &'static str,
222 kind: FieldKind,
223 storage_decode: FieldStorageDecode,
224 nullable: bool,
225 ) -> Self {
226 Self::generated_with_storage_decode_nullability_and_write_policies(
227 name,
228 kind,
229 storage_decode,
230 nullable,
231 None,
232 None,
233 )
234 }
235
236 #[must_use]
239 #[doc(hidden)]
240 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
241 name: &'static str,
242 kind: FieldKind,
243 storage_decode: FieldStorageDecode,
244 nullable: bool,
245 insert_generation: Option<FieldInsertGeneration>,
246 ) -> Self {
247 Self::generated_with_storage_decode_nullability_and_write_policies(
248 name,
249 kind,
250 storage_decode,
251 nullable,
252 insert_generation,
253 None,
254 )
255 }
256
257 #[must_use]
260 #[doc(hidden)]
261 pub const fn generated_with_storage_decode_nullability_and_write_policies(
262 name: &'static str,
263 kind: FieldKind,
264 storage_decode: FieldStorageDecode,
265 nullable: bool,
266 insert_generation: Option<FieldInsertGeneration>,
267 write_management: Option<FieldWriteManagement>,
268 ) -> Self {
269 Self {
270 name,
271 kind,
272 nested_fields: &[],
273 nullable,
274 storage_decode,
275 leaf_codec: leaf_codec_for(kind, storage_decode),
276 insert_generation,
277 write_management,
278 }
279 }
280
281 #[must_use]
283 #[doc(hidden)]
284 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
285 name: &'static str,
286 kind: FieldKind,
287 storage_decode: FieldStorageDecode,
288 nullable: bool,
289 insert_generation: Option<FieldInsertGeneration>,
290 write_management: Option<FieldWriteManagement>,
291 nested_fields: &'static [Self],
292 ) -> Self {
293 Self {
294 name,
295 kind,
296 nested_fields,
297 nullable,
298 storage_decode,
299 leaf_codec: leaf_codec_for(kind, storage_decode),
300 insert_generation,
301 write_management,
302 }
303 }
304
305 #[must_use]
307 pub const fn name(&self) -> &'static str {
308 self.name
309 }
310
311 #[must_use]
313 pub const fn kind(&self) -> FieldKind {
314 self.kind
315 }
316
317 #[must_use]
319 pub const fn nested_fields(&self) -> &'static [Self] {
320 self.nested_fields
321 }
322
323 #[must_use]
325 pub const fn nullable(&self) -> bool {
326 self.nullable
327 }
328
329 #[must_use]
331 pub const fn storage_decode(&self) -> FieldStorageDecode {
332 self.storage_decode
333 }
334
335 #[must_use]
337 pub const fn leaf_codec(&self) -> LeafCodec {
338 self.leaf_codec
339 }
340
341 #[must_use]
343 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
344 self.insert_generation
345 }
346
347 #[must_use]
349 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
350 self.write_management
351 }
352
353 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
361 if matches!(value, Value::Null) {
362 if self.nullable() {
363 return Ok(());
364 }
365
366 return Err("required field cannot store null".into());
367 }
368
369 let accepts = match self.storage_decode() {
370 FieldStorageDecode::Value => {
371 value_storage_kind_accepts_runtime_value(self.kind(), value)
372 }
373 FieldStorageDecode::ByKind => {
374 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
375 }
376 };
377 if !accepts {
378 return Err(format!(
379 "field kind {:?} does not accept runtime value {value:?}",
380 self.kind()
381 ));
382 }
383
384 ensure_decimal_scale_matches(self.kind(), value)?;
385 ensure_text_max_len_matches(self.kind(), value)?;
386 ensure_value_is_deterministic_for_storage(self.kind(), value)
387 }
388
389 pub(crate) fn normalize_runtime_value_for_storage<'a>(
393 &self,
394 value: &'a Value,
395 ) -> Result<Cow<'a, Value>, String> {
396 normalize_decimal_scale_for_storage(self.kind(), value)
397 }
398}
399
400const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
404 if matches!(storage_decode, FieldStorageDecode::Value) {
405 return LeafCodec::StructuralFallback;
406 }
407
408 match kind {
409 FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
410 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
411 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
412 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
413 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
414 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
415 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
416 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
417 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
418 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
419 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
420 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
421 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
422 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
423 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
424 FieldKind::Account
425 | FieldKind::Decimal { .. }
426 | FieldKind::Enum { .. }
427 | FieldKind::Int128
428 | FieldKind::IntBig
429 | FieldKind::List(_)
430 | FieldKind::Map { .. }
431 | FieldKind::Set(_)
432 | FieldKind::Structured { .. }
433 | FieldKind::Uint128
434 | FieldKind::UintBig => LeafCodec::StructuralFallback,
435 }
436}
437
438#[derive(Clone, Copy, Debug, Eq, PartialEq)]
445pub enum RelationStrength {
446 Strong,
447 Weak,
448}
449
450#[derive(Clone, Copy, Debug)]
460pub enum FieldKind {
461 Account,
463 Blob,
464 Bool,
465 Date,
466 Decimal {
467 scale: u32,
469 },
470 Duration,
471 Enum {
472 path: &'static str,
474 variants: &'static [EnumVariantModel],
476 },
477 Float32,
478 Float64,
479 Int,
480 Int128,
481 IntBig,
482 Principal,
483 Subaccount,
484 Text {
485 max_len: Option<u32>,
487 },
488 Timestamp,
489 Uint,
490 Uint128,
491 UintBig,
492 Ulid,
493 Unit,
494
495 Relation {
498 target_path: &'static str,
500 target_entity_name: &'static str,
502 target_entity_tag: EntityTag,
504 target_store_path: &'static str,
506 key_kind: &'static Self,
507 strength: RelationStrength,
508 },
509
510 List(&'static Self),
512 Set(&'static Self),
513 Map {
517 key: &'static Self,
518 value: &'static Self,
519 },
520
521 Structured {
525 queryable: bool,
526 },
527}
528
529impl FieldKind {
530 #[must_use]
531 pub const fn value_kind(&self) -> RuntimeValueKind {
532 match self {
533 Self::Account
534 | Self::Blob
535 | Self::Bool
536 | Self::Date
537 | Self::Duration
538 | Self::Enum { .. }
539 | Self::Float32
540 | Self::Float64
541 | Self::Int
542 | Self::Int128
543 | Self::IntBig
544 | Self::Principal
545 | Self::Subaccount
546 | Self::Text { .. }
547 | Self::Timestamp
548 | Self::Uint
549 | Self::Uint128
550 | Self::UintBig
551 | Self::Ulid
552 | Self::Unit
553 | Self::Decimal { .. }
554 | Self::Relation { .. } => RuntimeValueKind::Atomic,
555 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
556 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
557 Self::Structured { queryable } => RuntimeValueKind::Structured {
558 queryable: *queryable,
559 },
560 }
561 }
562
563 #[must_use]
571 pub const fn is_deterministic_collection_shape(&self) -> bool {
572 match self {
573 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
574
575 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
576
577 Self::Map { key, value } => {
578 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
579 }
580
581 _ => true,
582 }
583 }
584
585 #[must_use]
588 pub(crate) fn supports_group_probe(&self) -> bool {
589 match self {
590 Self::Enum { variants, .. } => variants.iter().all(|variant| {
591 variant
592 .payload_kind()
593 .is_none_or(Self::supports_group_probe)
594 }),
595 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
596 Self::List(_)
597 | Self::Set(_)
598 | Self::Map { .. }
599 | Self::Structured { .. }
600 | Self::Unit => false,
601 Self::Account
602 | Self::Blob
603 | Self::Bool
604 | Self::Date
605 | Self::Decimal { .. }
606 | Self::Duration
607 | Self::Float32
608 | Self::Float64
609 | Self::Int
610 | Self::Int128
611 | Self::IntBig
612 | Self::Principal
613 | Self::Subaccount
614 | Self::Text { .. }
615 | Self::Timestamp
616 | Self::Uint
617 | Self::Uint128
618 | Self::UintBig
619 | Self::Ulid => true,
620 }
621 }
622
623 #[must_use]
629 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
630 match (self, value) {
631 (Self::Account, Value::Account(_))
632 | (Self::Blob, Value::Blob(_))
633 | (Self::Bool, Value::Bool(_))
634 | (Self::Date, Value::Date(_))
635 | (Self::Decimal { .. }, Value::Decimal(_))
636 | (Self::Duration, Value::Duration(_))
637 | (Self::Enum { .. }, Value::Enum(_))
638 | (Self::Float32, Value::Float32(_))
639 | (Self::Float64, Value::Float64(_))
640 | (Self::Int, Value::Int(_))
641 | (Self::Int128, Value::Int128(_))
642 | (Self::IntBig, Value::IntBig(_))
643 | (Self::Principal, Value::Principal(_))
644 | (Self::Subaccount, Value::Subaccount(_))
645 | (Self::Text { .. }, Value::Text(_))
646 | (Self::Timestamp, Value::Timestamp(_))
647 | (Self::Uint, Value::Uint(_))
648 | (Self::Uint128, Value::Uint128(_))
649 | (Self::UintBig, Value::UintBig(_))
650 | (Self::Ulid, Value::Ulid(_))
651 | (Self::Unit, Value::Unit)
652 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
653 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
654 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
655 items.iter().all(|item| inner.accepts_value(item))
656 }
657 (Self::Map { key, value }, Value::Map(entries)) => {
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 key.accepts_value(entry_key) && value.accepts_value(entry_value)
664 })
665 }
666 _ => false,
667 }
668 }
669}
670
671fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
677 match (kind, value) {
678 (FieldKind::Relation { key_kind, .. }, value) => {
679 by_kind_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| by_kind_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 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
697 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
698 })
699 }
700 (FieldKind::Structured { .. }, _) => false,
701 _ => kind.accepts_value(value),
702 }
703}
704
705fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
709 match (kind, value) {
710 (FieldKind::Structured { .. }, _) => true,
711 (FieldKind::Relation { key_kind, .. }, value) => {
712 value_storage_kind_accepts_runtime_value(*key_kind, value)
713 }
714 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
715 .iter()
716 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
717 (
718 FieldKind::Map {
719 key,
720 value: value_kind,
721 },
722 Value::Map(entries),
723 ) => {
724 if Value::validate_map_entries(entries.as_slice()).is_err() {
725 return false;
726 }
727
728 entries.iter().all(|(entry_key, entry_value)| {
729 value_storage_kind_accepts_runtime_value(*key, entry_key)
730 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
731 })
732 }
733 _ => kind.accepts_value(value),
734 }
735}
736
737fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
740 if matches!(value, Value::Null) {
741 return Ok(());
742 }
743
744 match (kind, value) {
745 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
746 if decimal.scale() != scale {
747 return Err(format!(
748 "decimal scale mismatch: expected {scale}, found {}",
749 decimal.scale()
750 ));
751 }
752
753 Ok(())
754 }
755 (FieldKind::Relation { key_kind, .. }, value) => {
756 ensure_decimal_scale_matches(*key_kind, value)
757 }
758 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
759 for item in items {
760 ensure_decimal_scale_matches(*inner, item)?;
761 }
762
763 Ok(())
764 }
765 (
766 FieldKind::Map {
767 key,
768 value: map_value,
769 },
770 Value::Map(entries),
771 ) => {
772 for (entry_key, entry_value) in entries {
773 ensure_decimal_scale_matches(*key, entry_key)?;
774 ensure_decimal_scale_matches(*map_value, entry_value)?;
775 }
776
777 Ok(())
778 }
779 _ => Ok(()),
780 }
781}
782
783fn normalize_decimal_scale_for_storage(
787 kind: FieldKind,
788 value: &Value,
789) -> Result<Cow<'_, Value>, String> {
790 if matches!(value, Value::Null) {
791 return Ok(Cow::Borrowed(value));
792 }
793
794 match (kind, value) {
795 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
796 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
797 format!(
798 "decimal scale mismatch: expected {scale}, found {}",
799 decimal.scale()
800 )
801 })?;
802
803 if normalized.scale() == decimal.scale() {
804 Ok(Cow::Borrowed(value))
805 } else {
806 Ok(Cow::Owned(Value::Decimal(normalized)))
807 }
808 }
809 (FieldKind::Relation { key_kind, .. }, value) => {
810 normalize_decimal_scale_for_storage(*key_kind, value)
811 }
812 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
813 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
814 items.map_or_else(
815 || Cow::Borrowed(value),
816 |items| Cow::Owned(Value::List(items)),
817 )
818 })
819 }
820 (
821 FieldKind::Map {
822 key,
823 value: map_value,
824 },
825 Value::Map(entries),
826 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
827 entries.map_or_else(
828 || Cow::Borrowed(value),
829 |entries| Cow::Owned(Value::Map(entries)),
830 )
831 }),
832 _ => Ok(Cow::Borrowed(value)),
833 }
834}
835
836fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
840 match decimal.scale().cmp(&scale) {
841 Ordering::Equal => Some(decimal),
842 Ordering::Less => decimal
843 .scale_to_integer(scale)
844 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
845 Ordering::Greater => Some(decimal.round_dp(scale)),
846 }
847}
848
849fn normalize_decimal_list_items(
852 kind: FieldKind,
853 items: &[Value],
854) -> Result<Option<Vec<Value>>, String> {
855 let mut normalized_items = None;
856
857 for (index, item) in items.iter().enumerate() {
858 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
859 if let Cow::Owned(value) = normalized {
860 let items = normalized_items.get_or_insert_with(|| items.to_vec());
861 items[index] = value;
862 }
863 }
864
865 Ok(normalized_items)
866}
867
868fn normalize_decimal_map_entries(
871 key_kind: FieldKind,
872 value_kind: FieldKind,
873 entries: &[(Value, Value)],
874) -> Result<Option<Vec<(Value, Value)>>, String> {
875 let mut normalized_entries = None;
876
877 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
878 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
879 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
880
881 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
882 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
883 if let Cow::Owned(value) = normalized_key {
884 entries[index].0 = value;
885 }
886 if let Cow::Owned(value) = normalized_value {
887 entries[index].1 = value;
888 }
889 }
890 }
891
892 Ok(normalized_entries)
893}
894
895fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
898 if matches!(value, Value::Null) {
899 return Ok(());
900 }
901
902 match (kind, value) {
903 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
904 let len = text.chars().count();
905 if len > max as usize {
906 return Err(format!(
907 "text length exceeds max_len: expected at most {max}, found {len}"
908 ));
909 }
910
911 Ok(())
912 }
913 (FieldKind::Relation { key_kind, .. }, value) => {
914 ensure_text_max_len_matches(*key_kind, value)
915 }
916 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
917 for item in items {
918 ensure_text_max_len_matches(*inner, item)?;
919 }
920
921 Ok(())
922 }
923 (
924 FieldKind::Map {
925 key,
926 value: map_value,
927 },
928 Value::Map(entries),
929 ) => {
930 for (entry_key, entry_value) in entries {
931 ensure_text_max_len_matches(*key, entry_key)?;
932 ensure_text_max_len_matches(*map_value, entry_value)?;
933 }
934
935 Ok(())
936 }
937 _ => Ok(()),
938 }
939}
940
941fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
944 match (kind, value) {
945 (FieldKind::Set(_), Value::List(items)) => {
946 for pair in items.windows(2) {
947 let [left, right] = pair else {
948 continue;
949 };
950 if Value::canonical_cmp(left, right) != Ordering::Less {
951 return Err("set payload must already be canonical and deduplicated".into());
952 }
953 }
954
955 Ok(())
956 }
957 (FieldKind::Map { .. }, Value::Map(entries)) => {
958 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
959
960 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
961 return Err("map payload must already be canonical and deduplicated".into());
962 }
963
964 Ok(())
965 }
966 _ => Ok(()),
967 }
968}
969
970#[cfg(test)]
975mod tests {
976 use crate::{
977 model::field::{FieldKind, FieldModel},
978 value::Value,
979 };
980
981 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
982
983 #[test]
984 fn text_max_len_accepts_unbounded_text() {
985 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
986
987 assert!(
988 field
989 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
990 .is_ok()
991 );
992 }
993
994 #[test]
995 fn text_max_len_counts_unicode_scalars_not_bytes() {
996 let field = FieldModel::generated("name", BOUNDED_TEXT);
997
998 assert!(
999 field
1000 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1001 .is_ok()
1002 );
1003 assert!(
1004 field
1005 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1006 .is_err()
1007 );
1008 }
1009
1010 #[test]
1011 fn text_max_len_recurses_through_collections() {
1012 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1013 static TEXT_MAP: FieldKind = FieldKind::Map {
1014 key: &BOUNDED_TEXT,
1015 value: &BOUNDED_TEXT,
1016 };
1017
1018 let list_field = FieldModel::generated("names", TEXT_LIST);
1019 let map_field = FieldModel::generated("labels", TEXT_MAP);
1020
1021 assert!(
1022 list_field
1023 .validate_runtime_value_for_storage(&Value::List(vec![
1024 Value::Text("Ada".into()),
1025 Value::Text("Bob".into()),
1026 ]))
1027 .is_ok()
1028 );
1029 assert!(
1030 list_field
1031 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1032 .is_err()
1033 );
1034 assert!(
1035 map_field
1036 .validate_runtime_value_for_storage(&Value::Map(vec![(
1037 Value::Text("key".into()),
1038 Value::Text("val".into()),
1039 )]))
1040 .is_ok()
1041 );
1042 assert!(
1043 map_field
1044 .validate_runtime_value_for_storage(&Value::Map(vec![(
1045 Value::Text("long".into()),
1046 Value::Text("val".into()),
1047 )]))
1048 .is_err()
1049 );
1050 }
1051}