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 pub(crate) database_default: FieldDatabaseDefault,
155}
156
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166pub enum FieldDatabaseDefault {
167 None,
169}
170
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
181pub enum FieldInsertGeneration {
182 Ulid,
184 Timestamp,
186}
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum FieldWriteManagement {
199 CreatedAt,
201 UpdatedAt,
203}
204
205impl FieldModel {
206 #[must_use]
212 #[doc(hidden)]
213 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
214 Self::generated_with_storage_decode_and_nullability(
215 name,
216 kind,
217 FieldStorageDecode::ByKind,
218 false,
219 )
220 }
221
222 #[must_use]
224 #[doc(hidden)]
225 pub const fn generated_with_storage_decode(
226 name: &'static str,
227 kind: FieldKind,
228 storage_decode: FieldStorageDecode,
229 ) -> Self {
230 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
231 }
232
233 #[must_use]
235 #[doc(hidden)]
236 pub const fn generated_with_storage_decode_and_nullability(
237 name: &'static str,
238 kind: FieldKind,
239 storage_decode: FieldStorageDecode,
240 nullable: bool,
241 ) -> Self {
242 Self::generated_with_storage_decode_nullability_and_write_policies(
243 name,
244 kind,
245 storage_decode,
246 nullable,
247 None,
248 None,
249 )
250 }
251
252 #[must_use]
255 #[doc(hidden)]
256 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
257 name: &'static str,
258 kind: FieldKind,
259 storage_decode: FieldStorageDecode,
260 nullable: bool,
261 insert_generation: Option<FieldInsertGeneration>,
262 ) -> Self {
263 Self::generated_with_storage_decode_nullability_and_write_policies(
264 name,
265 kind,
266 storage_decode,
267 nullable,
268 insert_generation,
269 None,
270 )
271 }
272
273 #[must_use]
276 #[doc(hidden)]
277 pub const fn generated_with_storage_decode_nullability_and_write_policies(
278 name: &'static str,
279 kind: FieldKind,
280 storage_decode: FieldStorageDecode,
281 nullable: bool,
282 insert_generation: Option<FieldInsertGeneration>,
283 write_management: Option<FieldWriteManagement>,
284 ) -> Self {
285 Self {
286 name,
287 kind,
288 nested_fields: &[],
289 nullable,
290 storage_decode,
291 leaf_codec: leaf_codec_for(kind, storage_decode),
292 insert_generation,
293 write_management,
294 database_default: FieldDatabaseDefault::None,
295 }
296 }
297
298 #[must_use]
300 #[doc(hidden)]
301 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
302 name: &'static str,
303 kind: FieldKind,
304 storage_decode: FieldStorageDecode,
305 nullable: bool,
306 insert_generation: Option<FieldInsertGeneration>,
307 write_management: Option<FieldWriteManagement>,
308 nested_fields: &'static [Self],
309 ) -> Self {
310 Self {
311 name,
312 kind,
313 nested_fields,
314 nullable,
315 storage_decode,
316 leaf_codec: leaf_codec_for(kind, storage_decode),
317 insert_generation,
318 write_management,
319 database_default: FieldDatabaseDefault::None,
320 }
321 }
322
323 #[must_use]
325 pub const fn name(&self) -> &'static str {
326 self.name
327 }
328
329 #[must_use]
331 pub const fn kind(&self) -> FieldKind {
332 self.kind
333 }
334
335 #[must_use]
337 pub const fn nested_fields(&self) -> &'static [Self] {
338 self.nested_fields
339 }
340
341 #[must_use]
343 pub const fn nullable(&self) -> bool {
344 self.nullable
345 }
346
347 #[must_use]
349 pub const fn storage_decode(&self) -> FieldStorageDecode {
350 self.storage_decode
351 }
352
353 #[must_use]
355 pub const fn leaf_codec(&self) -> LeafCodec {
356 self.leaf_codec
357 }
358
359 #[must_use]
361 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
362 self.insert_generation
363 }
364
365 #[must_use]
367 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
368 self.write_management
369 }
370
371 #[must_use]
373 pub const fn database_default(&self) -> FieldDatabaseDefault {
374 self.database_default
375 }
376
377 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
385 if matches!(value, Value::Null) {
386 if self.nullable() {
387 return Ok(());
388 }
389
390 return Err("required field cannot store null".into());
391 }
392
393 let accepts = match self.storage_decode() {
394 FieldStorageDecode::Value => {
395 value_storage_kind_accepts_runtime_value(self.kind(), value)
396 }
397 FieldStorageDecode::ByKind => {
398 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
399 }
400 };
401 if !accepts {
402 return Err(format!(
403 "field kind {:?} does not accept runtime value {value:?}",
404 self.kind()
405 ));
406 }
407
408 ensure_decimal_scale_matches(self.kind(), value)?;
409 ensure_text_max_len_matches(self.kind(), value)?;
410 ensure_value_is_deterministic_for_storage(self.kind(), value)
411 }
412
413 pub(crate) fn normalize_runtime_value_for_storage<'a>(
417 &self,
418 value: &'a Value,
419 ) -> Result<Cow<'a, Value>, String> {
420 normalize_decimal_scale_for_storage(self.kind(), value)
421 }
422}
423
424const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
428 if matches!(storage_decode, FieldStorageDecode::Value) {
429 return LeafCodec::StructuralFallback;
430 }
431
432 match kind {
433 FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
434 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
435 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
436 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
437 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
438 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
439 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
440 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
441 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
442 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
443 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
444 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
445 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
446 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
447 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
448 FieldKind::Account
449 | FieldKind::Decimal { .. }
450 | FieldKind::Enum { .. }
451 | FieldKind::Int128
452 | FieldKind::IntBig
453 | FieldKind::List(_)
454 | FieldKind::Map { .. }
455 | FieldKind::Set(_)
456 | FieldKind::Structured { .. }
457 | FieldKind::Uint128
458 | FieldKind::UintBig => LeafCodec::StructuralFallback,
459 }
460}
461
462#[derive(Clone, Copy, Debug, Eq, PartialEq)]
469pub enum RelationStrength {
470 Strong,
471 Weak,
472}
473
474#[derive(Clone, Copy, Debug)]
484pub enum FieldKind {
485 Account,
487 Blob,
488 Bool,
489 Date,
490 Decimal {
491 scale: u32,
493 },
494 Duration,
495 Enum {
496 path: &'static str,
498 variants: &'static [EnumVariantModel],
500 },
501 Float32,
502 Float64,
503 Int,
504 Int128,
505 IntBig,
506 Principal,
507 Subaccount,
508 Text {
509 max_len: Option<u32>,
511 },
512 Timestamp,
513 Uint,
514 Uint128,
515 UintBig,
516 Ulid,
517 Unit,
518
519 Relation {
522 target_path: &'static str,
524 target_entity_name: &'static str,
526 target_entity_tag: EntityTag,
528 target_store_path: &'static str,
530 key_kind: &'static Self,
531 strength: RelationStrength,
532 },
533
534 List(&'static Self),
536 Set(&'static Self),
537 Map {
541 key: &'static Self,
542 value: &'static Self,
543 },
544
545 Structured {
549 queryable: bool,
550 },
551}
552
553impl FieldKind {
554 #[must_use]
555 pub const fn value_kind(&self) -> RuntimeValueKind {
556 match self {
557 Self::Account
558 | Self::Blob
559 | Self::Bool
560 | Self::Date
561 | Self::Duration
562 | Self::Enum { .. }
563 | Self::Float32
564 | Self::Float64
565 | Self::Int
566 | Self::Int128
567 | Self::IntBig
568 | Self::Principal
569 | Self::Subaccount
570 | Self::Text { .. }
571 | Self::Timestamp
572 | Self::Uint
573 | Self::Uint128
574 | Self::UintBig
575 | Self::Ulid
576 | Self::Unit
577 | Self::Decimal { .. }
578 | Self::Relation { .. } => RuntimeValueKind::Atomic,
579 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
580 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
581 Self::Structured { queryable } => RuntimeValueKind::Structured {
582 queryable: *queryable,
583 },
584 }
585 }
586
587 #[must_use]
595 pub const fn is_deterministic_collection_shape(&self) -> bool {
596 match self {
597 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
598
599 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
600
601 Self::Map { key, value } => {
602 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
603 }
604
605 _ => true,
606 }
607 }
608
609 #[must_use]
612 pub(crate) fn supports_group_probe(&self) -> bool {
613 match self {
614 Self::Enum { variants, .. } => variants.iter().all(|variant| {
615 variant
616 .payload_kind()
617 .is_none_or(Self::supports_group_probe)
618 }),
619 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
620 Self::List(_)
621 | Self::Set(_)
622 | Self::Map { .. }
623 | Self::Structured { .. }
624 | Self::Unit => false,
625 Self::Account
626 | Self::Blob
627 | Self::Bool
628 | Self::Date
629 | Self::Decimal { .. }
630 | Self::Duration
631 | Self::Float32
632 | Self::Float64
633 | Self::Int
634 | Self::Int128
635 | Self::IntBig
636 | Self::Principal
637 | Self::Subaccount
638 | Self::Text { .. }
639 | Self::Timestamp
640 | Self::Uint
641 | Self::Uint128
642 | Self::UintBig
643 | Self::Ulid => true,
644 }
645 }
646
647 #[must_use]
653 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
654 match (self, value) {
655 (Self::Account, Value::Account(_))
656 | (Self::Blob, Value::Blob(_))
657 | (Self::Bool, Value::Bool(_))
658 | (Self::Date, Value::Date(_))
659 | (Self::Decimal { .. }, Value::Decimal(_))
660 | (Self::Duration, Value::Duration(_))
661 | (Self::Enum { .. }, Value::Enum(_))
662 | (Self::Float32, Value::Float32(_))
663 | (Self::Float64, Value::Float64(_))
664 | (Self::Int, Value::Int(_))
665 | (Self::Int128, Value::Int128(_))
666 | (Self::IntBig, Value::IntBig(_))
667 | (Self::Principal, Value::Principal(_))
668 | (Self::Subaccount, Value::Subaccount(_))
669 | (Self::Text { .. }, Value::Text(_))
670 | (Self::Timestamp, Value::Timestamp(_))
671 | (Self::Uint, Value::Uint(_))
672 | (Self::Uint128, Value::Uint128(_))
673 | (Self::UintBig, Value::UintBig(_))
674 | (Self::Ulid, Value::Ulid(_))
675 | (Self::Unit, Value::Unit)
676 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
677 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
678 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
679 items.iter().all(|item| inner.accepts_value(item))
680 }
681 (Self::Map { key, value }, Value::Map(entries)) => {
682 if Value::validate_map_entries(entries.as_slice()).is_err() {
683 return false;
684 }
685
686 entries.iter().all(|(entry_key, entry_value)| {
687 key.accepts_value(entry_key) && value.accepts_value(entry_value)
688 })
689 }
690 _ => false,
691 }
692 }
693}
694
695fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
701 match (kind, value) {
702 (FieldKind::Relation { key_kind, .. }, value) => {
703 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
704 }
705 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
706 .iter()
707 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
708 (
709 FieldKind::Map {
710 key,
711 value: value_kind,
712 },
713 Value::Map(entries),
714 ) => {
715 if Value::validate_map_entries(entries.as_slice()).is_err() {
716 return false;
717 }
718
719 entries.iter().all(|(entry_key, entry_value)| {
720 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
721 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
722 })
723 }
724 (FieldKind::Structured { .. }, _) => false,
725 _ => kind.accepts_value(value),
726 }
727}
728
729fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
733 match (kind, value) {
734 (FieldKind::Structured { .. }, _) => true,
735 (FieldKind::Relation { key_kind, .. }, value) => {
736 value_storage_kind_accepts_runtime_value(*key_kind, value)
737 }
738 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
739 .iter()
740 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
741 (
742 FieldKind::Map {
743 key,
744 value: value_kind,
745 },
746 Value::Map(entries),
747 ) => {
748 if Value::validate_map_entries(entries.as_slice()).is_err() {
749 return false;
750 }
751
752 entries.iter().all(|(entry_key, entry_value)| {
753 value_storage_kind_accepts_runtime_value(*key, entry_key)
754 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
755 })
756 }
757 _ => kind.accepts_value(value),
758 }
759}
760
761fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
764 if matches!(value, Value::Null) {
765 return Ok(());
766 }
767
768 match (kind, value) {
769 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
770 if decimal.scale() != scale {
771 return Err(format!(
772 "decimal scale mismatch: expected {scale}, found {}",
773 decimal.scale()
774 ));
775 }
776
777 Ok(())
778 }
779 (FieldKind::Relation { key_kind, .. }, value) => {
780 ensure_decimal_scale_matches(*key_kind, value)
781 }
782 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
783 for item in items {
784 ensure_decimal_scale_matches(*inner, item)?;
785 }
786
787 Ok(())
788 }
789 (
790 FieldKind::Map {
791 key,
792 value: map_value,
793 },
794 Value::Map(entries),
795 ) => {
796 for (entry_key, entry_value) in entries {
797 ensure_decimal_scale_matches(*key, entry_key)?;
798 ensure_decimal_scale_matches(*map_value, entry_value)?;
799 }
800
801 Ok(())
802 }
803 _ => Ok(()),
804 }
805}
806
807fn normalize_decimal_scale_for_storage(
811 kind: FieldKind,
812 value: &Value,
813) -> Result<Cow<'_, Value>, String> {
814 if matches!(value, Value::Null) {
815 return Ok(Cow::Borrowed(value));
816 }
817
818 match (kind, value) {
819 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
820 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
821 format!(
822 "decimal scale mismatch: expected {scale}, found {}",
823 decimal.scale()
824 )
825 })?;
826
827 if normalized.scale() == decimal.scale() {
828 Ok(Cow::Borrowed(value))
829 } else {
830 Ok(Cow::Owned(Value::Decimal(normalized)))
831 }
832 }
833 (FieldKind::Relation { key_kind, .. }, value) => {
834 normalize_decimal_scale_for_storage(*key_kind, value)
835 }
836 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
837 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
838 items.map_or_else(
839 || Cow::Borrowed(value),
840 |items| Cow::Owned(Value::List(items)),
841 )
842 })
843 }
844 (
845 FieldKind::Map {
846 key,
847 value: map_value,
848 },
849 Value::Map(entries),
850 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
851 entries.map_or_else(
852 || Cow::Borrowed(value),
853 |entries| Cow::Owned(Value::Map(entries)),
854 )
855 }),
856 _ => Ok(Cow::Borrowed(value)),
857 }
858}
859
860fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
864 match decimal.scale().cmp(&scale) {
865 Ordering::Equal => Some(decimal),
866 Ordering::Less => decimal
867 .scale_to_integer(scale)
868 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
869 Ordering::Greater => Some(decimal.round_dp(scale)),
870 }
871}
872
873fn normalize_decimal_list_items(
876 kind: FieldKind,
877 items: &[Value],
878) -> Result<Option<Vec<Value>>, String> {
879 let mut normalized_items = None;
880
881 for (index, item) in items.iter().enumerate() {
882 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
883 if let Cow::Owned(value) = normalized {
884 let items = normalized_items.get_or_insert_with(|| items.to_vec());
885 items[index] = value;
886 }
887 }
888
889 Ok(normalized_items)
890}
891
892fn normalize_decimal_map_entries(
895 key_kind: FieldKind,
896 value_kind: FieldKind,
897 entries: &[(Value, Value)],
898) -> Result<Option<Vec<(Value, Value)>>, String> {
899 let mut normalized_entries = None;
900
901 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
902 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
903 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
904
905 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
906 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
907 if let Cow::Owned(value) = normalized_key {
908 entries[index].0 = value;
909 }
910 if let Cow::Owned(value) = normalized_value {
911 entries[index].1 = value;
912 }
913 }
914 }
915
916 Ok(normalized_entries)
917}
918
919fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
922 if matches!(value, Value::Null) {
923 return Ok(());
924 }
925
926 match (kind, value) {
927 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
928 let len = text.chars().count();
929 if len > max as usize {
930 return Err(format!(
931 "text length exceeds max_len: expected at most {max}, found {len}"
932 ));
933 }
934
935 Ok(())
936 }
937 (FieldKind::Relation { key_kind, .. }, value) => {
938 ensure_text_max_len_matches(*key_kind, value)
939 }
940 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
941 for item in items {
942 ensure_text_max_len_matches(*inner, item)?;
943 }
944
945 Ok(())
946 }
947 (
948 FieldKind::Map {
949 key,
950 value: map_value,
951 },
952 Value::Map(entries),
953 ) => {
954 for (entry_key, entry_value) in entries {
955 ensure_text_max_len_matches(*key, entry_key)?;
956 ensure_text_max_len_matches(*map_value, entry_value)?;
957 }
958
959 Ok(())
960 }
961 _ => Ok(()),
962 }
963}
964
965fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
968 match (kind, value) {
969 (FieldKind::Set(_), Value::List(items)) => {
970 for pair in items.windows(2) {
971 let [left, right] = pair else {
972 continue;
973 };
974 if Value::canonical_cmp(left, right) != Ordering::Less {
975 return Err("set payload must already be canonical and deduplicated".into());
976 }
977 }
978
979 Ok(())
980 }
981 (FieldKind::Map { .. }, Value::Map(entries)) => {
982 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
983
984 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
985 return Err("map payload must already be canonical and deduplicated".into());
986 }
987
988 Ok(())
989 }
990 _ => Ok(()),
991 }
992}
993
994#[cfg(test)]
999mod tests {
1000 use crate::{
1001 model::field::{FieldKind, FieldModel},
1002 value::Value,
1003 };
1004
1005 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1006
1007 #[test]
1008 fn text_max_len_accepts_unbounded_text() {
1009 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1010
1011 assert!(
1012 field
1013 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1014 .is_ok()
1015 );
1016 }
1017
1018 #[test]
1019 fn text_max_len_counts_unicode_scalars_not_bytes() {
1020 let field = FieldModel::generated("name", BOUNDED_TEXT);
1021
1022 assert!(
1023 field
1024 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1025 .is_ok()
1026 );
1027 assert!(
1028 field
1029 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1030 .is_err()
1031 );
1032 }
1033
1034 #[test]
1035 fn text_max_len_recurses_through_collections() {
1036 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1037 static TEXT_MAP: FieldKind = FieldKind::Map {
1038 key: &BOUNDED_TEXT,
1039 value: &BOUNDED_TEXT,
1040 };
1041
1042 let list_field = FieldModel::generated("names", TEXT_LIST);
1043 let map_field = FieldModel::generated("labels", TEXT_MAP);
1044
1045 assert!(
1046 list_field
1047 .validate_runtime_value_for_storage(&Value::List(vec![
1048 Value::Text("Ada".into()),
1049 Value::Text("Bob".into()),
1050 ]))
1051 .is_ok()
1052 );
1053 assert!(
1054 list_field
1055 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1056 .is_err()
1057 );
1058 assert!(
1059 map_field
1060 .validate_runtime_value_for_storage(&Value::Map(vec![(
1061 Value::Text("key".into()),
1062 Value::Text("val".into()),
1063 )]))
1064 .is_ok()
1065 );
1066 assert!(
1067 map_field
1068 .validate_runtime_value_for_storage(&Value::Map(vec![(
1069 Value::Text("long".into()),
1070 Value::Text("val".into()),
1071 )]))
1072 .is_err()
1073 );
1074 }
1075}