1use crate::{
7 traits::RuntimeValueKind,
8 types::{Decimal, EntityTag},
9 value::Value,
10};
11use std::{borrow::Cow, cmp::Ordering};
12
13pub const DEFAULT_BIG_INT_MAX_BYTES: u32 = 256;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum FieldStorageDecode {
28 ByKind,
30 Value,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum ScalarCodec {
45 Blob,
46 Bool,
47 Date,
48 Duration,
49 Float32,
50 Float64,
51 Int64,
52 Principal,
53 Subaccount,
54 Text,
55 Timestamp,
56 Nat64,
57 Ulid,
58 Unit,
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub enum LeafCodec {
72 Scalar(ScalarCodec),
73 StructuralFallback,
74}
75
76#[derive(Clone, Copy, Debug)]
86pub struct EnumVariantModel {
87 pub(crate) ident: &'static str,
89 pub(crate) payload_kind: Option<&'static FieldKind>,
91 pub(crate) payload_storage_decode: FieldStorageDecode,
93}
94
95impl EnumVariantModel {
96 #[must_use]
98 pub const fn new(
99 ident: &'static str,
100 payload_kind: Option<&'static FieldKind>,
101 payload_storage_decode: FieldStorageDecode,
102 ) -> Self {
103 Self {
104 ident,
105 payload_kind,
106 payload_storage_decode,
107 }
108 }
109
110 #[must_use]
112 pub const fn ident(&self) -> &'static str {
113 self.ident
114 }
115
116 #[must_use]
118 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
119 self.payload_kind
120 }
121
122 #[must_use]
124 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
125 self.payload_storage_decode
126 }
127}
128
129#[derive(Debug)]
139pub struct FieldModel {
140 pub(crate) name: &'static str,
142 pub(crate) kind: FieldKind,
144 pub(crate) nested_fields: &'static [Self],
146 pub(crate) nullable: bool,
148 pub(crate) storage_decode: FieldStorageDecode,
150 pub(crate) leaf_codec: LeafCodec,
152 pub(crate) insert_generation: Option<FieldInsertGeneration>,
154 pub(crate) write_management: Option<FieldWriteManagement>,
156 pub(crate) database_default: FieldDatabaseDefault,
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub enum FieldDatabaseDefault {
170 None,
172 EncodedSlotPayload(&'static [u8]),
178}
179
180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum FieldInsertGeneration {
191 Ulid,
193 Timestamp,
195}
196
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum FieldWriteManagement {
208 CreatedAt,
210 UpdatedAt,
212}
213
214impl FieldModel {
215 #[must_use]
221 #[doc(hidden)]
222 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
223 Self::generated_with_storage_decode_and_nullability(
224 name,
225 kind,
226 FieldStorageDecode::ByKind,
227 false,
228 )
229 }
230
231 #[must_use]
233 #[doc(hidden)]
234 pub const fn generated_with_storage_decode(
235 name: &'static str,
236 kind: FieldKind,
237 storage_decode: FieldStorageDecode,
238 ) -> Self {
239 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
240 }
241
242 #[must_use]
244 #[doc(hidden)]
245 pub const fn generated_with_storage_decode_and_nullability(
246 name: &'static str,
247 kind: FieldKind,
248 storage_decode: FieldStorageDecode,
249 nullable: bool,
250 ) -> Self {
251 Self::generated_with_storage_decode_nullability_and_write_policies(
252 name,
253 kind,
254 storage_decode,
255 nullable,
256 None,
257 None,
258 )
259 }
260
261 #[must_use]
264 #[doc(hidden)]
265 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
266 name: &'static str,
267 kind: FieldKind,
268 storage_decode: FieldStorageDecode,
269 nullable: bool,
270 insert_generation: Option<FieldInsertGeneration>,
271 ) -> Self {
272 Self::generated_with_storage_decode_nullability_and_write_policies(
273 name,
274 kind,
275 storage_decode,
276 nullable,
277 insert_generation,
278 None,
279 )
280 }
281
282 #[must_use]
285 #[doc(hidden)]
286 pub const fn generated_with_storage_decode_nullability_and_write_policies(
287 name: &'static str,
288 kind: FieldKind,
289 storage_decode: FieldStorageDecode,
290 nullable: bool,
291 insert_generation: Option<FieldInsertGeneration>,
292 write_management: Option<FieldWriteManagement>,
293 ) -> Self {
294 Self {
295 name,
296 kind,
297 nested_fields: &[],
298 nullable,
299 storage_decode,
300 leaf_codec: leaf_codec_for(kind, storage_decode),
301 insert_generation,
302 write_management,
303 database_default: FieldDatabaseDefault::None,
304 }
305 }
306
307 #[must_use]
309 #[doc(hidden)]
310 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
311 name: &'static str,
312 kind: FieldKind,
313 storage_decode: FieldStorageDecode,
314 nullable: bool,
315 insert_generation: Option<FieldInsertGeneration>,
316 write_management: Option<FieldWriteManagement>,
317 nested_fields: &'static [Self],
318 ) -> Self {
319 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
320 name,
321 kind,
322 storage_decode,
323 nullable,
324 insert_generation,
325 write_management,
326 FieldDatabaseDefault::None,
327 nested_fields,
328 )
329 }
330
331 #[must_use]
334 #[doc(hidden)]
335 #[expect(
336 clippy::too_many_arguments,
337 reason = "generated schema metadata keeps every field contract explicit"
338 )]
339 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
340 name: &'static str,
341 kind: FieldKind,
342 storage_decode: FieldStorageDecode,
343 nullable: bool,
344 insert_generation: Option<FieldInsertGeneration>,
345 write_management: Option<FieldWriteManagement>,
346 database_default: FieldDatabaseDefault,
347 nested_fields: &'static [Self],
348 ) -> Self {
349 Self {
350 name,
351 kind,
352 nested_fields,
353 nullable,
354 storage_decode,
355 leaf_codec: leaf_codec_for(kind, storage_decode),
356 insert_generation,
357 write_management,
358 database_default,
359 }
360 }
361
362 #[must_use]
364 pub const fn name(&self) -> &'static str {
365 self.name
366 }
367
368 #[must_use]
370 pub const fn kind(&self) -> FieldKind {
371 self.kind
372 }
373
374 #[must_use]
376 pub const fn nested_fields(&self) -> &'static [Self] {
377 self.nested_fields
378 }
379
380 #[must_use]
382 pub const fn nullable(&self) -> bool {
383 self.nullable
384 }
385
386 #[must_use]
388 pub const fn storage_decode(&self) -> FieldStorageDecode {
389 self.storage_decode
390 }
391
392 #[must_use]
394 pub const fn leaf_codec(&self) -> LeafCodec {
395 self.leaf_codec
396 }
397
398 #[must_use]
400 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
401 self.insert_generation
402 }
403
404 #[must_use]
406 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
407 self.write_management
408 }
409
410 #[must_use]
412 pub const fn database_default(&self) -> FieldDatabaseDefault {
413 self.database_default
414 }
415
416 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
424 if matches!(value, Value::Null) {
425 if self.nullable() {
426 return Ok(());
427 }
428
429 return Err("required field cannot store null".into());
430 }
431
432 let accepts = match self.storage_decode() {
433 FieldStorageDecode::Value => {
434 value_storage_kind_accepts_runtime_value(self.kind(), value)
435 }
436 FieldStorageDecode::ByKind => {
437 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
438 }
439 };
440 if !accepts {
441 return Err(format!(
442 "field kind {:?} does not accept runtime value {value:?}",
443 self.kind()
444 ));
445 }
446
447 ensure_decimal_scale_matches(self.kind(), value)?;
448 ensure_scalar_max_len_matches(self.kind(), value)?;
449 ensure_value_is_deterministic_for_storage(self.kind(), value)
450 }
451
452 pub(crate) fn normalize_runtime_value_for_storage<'a>(
456 &self,
457 value: &'a Value,
458 ) -> Result<Cow<'a, Value>, String> {
459 normalize_decimal_scale_for_storage(self.kind(), value)
460 }
461}
462
463const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
467 if matches!(storage_decode, FieldStorageDecode::Value) {
468 return LeafCodec::StructuralFallback;
469 }
470
471 match kind {
472 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
473 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
474 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
475 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
476 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
477 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
478 FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
479 LeafCodec::Scalar(ScalarCodec::Int64)
480 }
481 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
482 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
483 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
484 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
485 FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
486 LeafCodec::Scalar(ScalarCodec::Nat64)
487 }
488 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
489 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
490 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
491 FieldKind::Account
492 | FieldKind::Decimal { .. }
493 | FieldKind::Enum { .. }
494 | FieldKind::Int128
495 | FieldKind::IntBig { .. }
496 | FieldKind::List(_)
497 | FieldKind::Map { .. }
498 | FieldKind::Set(_)
499 | FieldKind::Structured { .. }
500 | FieldKind::Nat128
501 | FieldKind::NatBig { .. } => LeafCodec::StructuralFallback,
502 }
503}
504
505#[derive(Clone, Copy, Debug, Eq, PartialEq)]
512pub enum RelationStrength {
513 Strong,
514 Weak,
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 {
580 target_path: &'static str,
582 target_entity_name: &'static str,
584 target_entity_tag: EntityTag,
586 target_store_path: &'static str,
588 key_kind: &'static Self,
589 strength: RelationStrength,
590 },
591
592 List(&'static Self),
594 Set(&'static Self),
595 Map {
599 key: &'static Self,
600 value: &'static Self,
601 },
602
603 Structured {
607 queryable: bool,
608 },
609}
610
611impl FieldKind {
612 #[must_use]
613 pub const fn value_kind(&self) -> RuntimeValueKind {
614 match self {
615 Self::Account
616 | Self::Blob { .. }
617 | Self::Bool
618 | Self::Date
619 | Self::Duration
620 | Self::Enum { .. }
621 | Self::Float32
622 | Self::Float64
623 | Self::Int8
624 | Self::Int16
625 | Self::Int32
626 | Self::Int64
627 | Self::Int128
628 | Self::IntBig { .. }
629 | Self::Principal
630 | Self::Subaccount
631 | Self::Text { .. }
632 | Self::Timestamp
633 | Self::Nat8
634 | Self::Nat16
635 | Self::Nat32
636 | Self::Nat64
637 | Self::Nat128
638 | Self::NatBig { .. }
639 | Self::Ulid
640 | Self::Unit
641 | Self::Decimal { .. }
642 | Self::Relation { .. } => RuntimeValueKind::Atomic,
643 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
644 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
645 Self::Structured { queryable } => RuntimeValueKind::Structured {
646 queryable: *queryable,
647 },
648 }
649 }
650
651 #[must_use]
659 pub const fn is_deterministic_collection_shape(&self) -> bool {
660 match self {
661 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
662
663 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
664
665 Self::Map { key, value } => {
666 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
667 }
668
669 _ => true,
670 }
671 }
672
673 #[must_use]
676 pub(crate) fn supports_group_probe(&self) -> bool {
677 match self {
678 Self::Enum { variants, .. } => variants.iter().all(|variant| {
679 variant
680 .payload_kind()
681 .is_none_or(Self::supports_group_probe)
682 }),
683 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
684 Self::List(_)
685 | Self::Set(_)
686 | Self::Map { .. }
687 | Self::Structured { .. }
688 | Self::Unit => false,
689 Self::Account
690 | Self::Blob { .. }
691 | Self::Bool
692 | Self::Date
693 | Self::Decimal { .. }
694 | Self::Duration
695 | Self::Float32
696 | Self::Float64
697 | Self::Int8
698 | Self::Int16
699 | Self::Int32
700 | Self::Int64
701 | Self::Int128
702 | Self::IntBig { .. }
703 | Self::Principal
704 | Self::Subaccount
705 | Self::Text { .. }
706 | Self::Timestamp
707 | Self::Nat8
708 | Self::Nat16
709 | Self::Nat32
710 | Self::Nat64
711 | Self::Nat128
712 | Self::NatBig { .. }
713 | Self::Ulid => true,
714 }
715 }
716
717 #[must_use]
723 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
724 match (self, value) {
725 (Self::Account, Value::Account(_))
726 | (Self::Blob { .. }, Value::Blob(_))
727 | (Self::Bool, Value::Bool(_))
728 | (Self::Date, Value::Date(_))
729 | (Self::Decimal { .. }, Value::Decimal(_))
730 | (Self::Duration, Value::Duration(_))
731 | (Self::Enum { .. }, Value::Enum(_))
732 | (Self::Float32, Value::Float32(_))
733 | (Self::Float64, Value::Float64(_))
734 | (Self::Int64, Value::Int64(_))
735 | (Self::Int128, Value::Int128(_))
736 | (Self::Nat64, Value::Nat64(_))
737 | (Self::Principal, Value::Principal(_))
738 | (Self::Subaccount, Value::Subaccount(_))
739 | (Self::Text { .. }, Value::Text(_))
740 | (Self::Timestamp, Value::Timestamp(_))
741 | (Self::Nat128, Value::Nat128(_))
742 | (Self::Ulid, Value::Ulid(_))
743 | (Self::Unit, Value::Unit)
744 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
745 (Self::Int8, Value::Int64(value)) => i8::try_from(*value).is_ok(),
746 (Self::Int16, Value::Int64(value)) => i16::try_from(*value).is_ok(),
747 (Self::Int32, Value::Int64(value)) => i32::try_from(*value).is_ok(),
748 (Self::Nat8, Value::Nat64(value)) => u8::try_from(*value).is_ok(),
749 (Self::Nat16, Value::Nat64(value)) => u16::try_from(*value).is_ok(),
750 (Self::Nat32, Value::Nat64(value)) => u32::try_from(*value).is_ok(),
751 (Self::IntBig { max_bytes }, Value::IntBig(value)) => {
752 value.to_leb128().len() <= *max_bytes as usize
753 }
754 (Self::NatBig { max_bytes }, Value::NatBig(value)) => {
755 value.to_leb128().len() <= *max_bytes as usize
756 }
757 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
758 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
759 items.iter().all(|item| inner.accepts_value(item))
760 }
761 (Self::Map { key, value }, Value::Map(entries)) => {
762 if Value::validate_map_entries(entries.as_slice()).is_err() {
763 return false;
764 }
765
766 entries.iter().all(|(entry_key, entry_value)| {
767 key.accepts_value(entry_key) && value.accepts_value(entry_value)
768 })
769 }
770 _ => false,
771 }
772 }
773}
774
775fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
781 match (kind, value) {
782 (FieldKind::Relation { key_kind, .. }, value) => {
783 by_kind_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| by_kind_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 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
801 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
802 })
803 }
804 (FieldKind::Structured { .. }, _) => false,
805 _ => kind.accepts_value(value),
806 }
807}
808
809fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
813 match (kind, value) {
814 (FieldKind::Structured { .. }, _) => true,
815 (FieldKind::Relation { key_kind, .. }, value) => {
816 value_storage_kind_accepts_runtime_value(*key_kind, value)
817 }
818 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
819 .iter()
820 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
821 (
822 FieldKind::Map {
823 key,
824 value: value_kind,
825 },
826 Value::Map(entries),
827 ) => {
828 if Value::validate_map_entries(entries.as_slice()).is_err() {
829 return false;
830 }
831
832 entries.iter().all(|(entry_key, entry_value)| {
833 value_storage_kind_accepts_runtime_value(*key, entry_key)
834 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
835 })
836 }
837 _ => kind.accepts_value(value),
838 }
839}
840
841fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
844 if matches!(value, Value::Null) {
845 return Ok(());
846 }
847
848 match (kind, value) {
849 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
850 if decimal.scale() != scale {
851 return Err(format!(
852 "decimal scale mismatch: expected {scale}, found {}",
853 decimal.scale()
854 ));
855 }
856
857 Ok(())
858 }
859 (FieldKind::Relation { key_kind, .. }, value) => {
860 ensure_decimal_scale_matches(*key_kind, value)
861 }
862 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
863 for item in items {
864 ensure_decimal_scale_matches(*inner, item)?;
865 }
866
867 Ok(())
868 }
869 (
870 FieldKind::Map {
871 key,
872 value: map_value,
873 },
874 Value::Map(entries),
875 ) => {
876 for (entry_key, entry_value) in entries {
877 ensure_decimal_scale_matches(*key, entry_key)?;
878 ensure_decimal_scale_matches(*map_value, entry_value)?;
879 }
880
881 Ok(())
882 }
883 _ => Ok(()),
884 }
885}
886
887pub(crate) fn normalize_decimal_scale_for_storage(
892 kind: FieldKind,
893 value: &Value,
894) -> Result<Cow<'_, Value>, String> {
895 if matches!(value, Value::Null) {
896 return Ok(Cow::Borrowed(value));
897 }
898
899 match (kind, value) {
900 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
901 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
902 format!(
903 "decimal scale mismatch: expected {scale}, found {}",
904 decimal.scale()
905 )
906 })?;
907
908 if normalized.scale() == decimal.scale() {
909 Ok(Cow::Borrowed(value))
910 } else {
911 Ok(Cow::Owned(Value::Decimal(normalized)))
912 }
913 }
914 (FieldKind::Relation { key_kind, .. }, value) => {
915 normalize_decimal_scale_for_storage(*key_kind, value)
916 }
917 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
918 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
919 items.map_or_else(
920 || Cow::Borrowed(value),
921 |items| Cow::Owned(Value::List(items)),
922 )
923 })
924 }
925 (
926 FieldKind::Map {
927 key,
928 value: map_value,
929 },
930 Value::Map(entries),
931 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
932 entries.map_or_else(
933 || Cow::Borrowed(value),
934 |entries| Cow::Owned(Value::Map(entries)),
935 )
936 }),
937 _ => Ok(Cow::Borrowed(value)),
938 }
939}
940
941fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
945 match decimal.scale().cmp(&scale) {
946 Ordering::Equal => Some(decimal),
947 Ordering::Less => decimal
948 .scale_to_integer(scale)
949 .and_then(|mantissa| Decimal::try_from_i128_with_scale(mantissa, scale)),
950 Ordering::Greater => Some(decimal.round_dp(scale)),
951 }
952}
953
954fn normalize_decimal_list_items(
957 kind: FieldKind,
958 items: &[Value],
959) -> Result<Option<Vec<Value>>, String> {
960 let mut normalized_items = None;
961
962 for (index, item) in items.iter().enumerate() {
963 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
964 if let Cow::Owned(value) = normalized {
965 let items = normalized_items.get_or_insert_with(|| items.to_vec());
966 items[index] = value;
967 }
968 }
969
970 Ok(normalized_items)
971}
972
973fn normalize_decimal_map_entries(
976 key_kind: FieldKind,
977 value_kind: FieldKind,
978 entries: &[(Value, Value)],
979) -> Result<Option<Vec<(Value, Value)>>, String> {
980 let mut normalized_entries = None;
981
982 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
983 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
984 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
985
986 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
987 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
988 if let Cow::Owned(value) = normalized_key {
989 entries[index].0 = value;
990 }
991 if let Cow::Owned(value) = normalized_value {
992 entries[index].1 = value;
993 }
994 }
995 }
996
997 Ok(normalized_entries)
998}
999
1000fn ensure_scalar_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
1003 if matches!(value, Value::Null) {
1004 return Ok(());
1005 }
1006
1007 match (kind, value) {
1008 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
1009 let len = text.chars().count();
1010 if len > max as usize {
1011 return Err(format!(
1012 "text length exceeds max_len: expected at most {max}, found {len}"
1013 ));
1014 }
1015
1016 Ok(())
1017 }
1018 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
1019 let len = bytes.len();
1020 if len > max as usize {
1021 return Err(format!(
1022 "blob length exceeds max_len: expected at most {max}, found {len}"
1023 ));
1024 }
1025
1026 Ok(())
1027 }
1028 (FieldKind::Relation { key_kind, .. }, value) => {
1029 ensure_scalar_max_len_matches(*key_kind, value)
1030 }
1031 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
1032 for item in items {
1033 ensure_scalar_max_len_matches(*inner, item)?;
1034 }
1035
1036 Ok(())
1037 }
1038 (
1039 FieldKind::Map {
1040 key,
1041 value: map_value,
1042 },
1043 Value::Map(entries),
1044 ) => {
1045 for (entry_key, entry_value) in entries {
1046 ensure_scalar_max_len_matches(*key, entry_key)?;
1047 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1048 }
1049
1050 Ok(())
1051 }
1052 _ => Ok(()),
1053 }
1054}
1055
1056fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
1059 match (kind, value) {
1060 (FieldKind::Set(_), Value::List(items)) => {
1061 for pair in items.windows(2) {
1062 let [left, right] = pair else {
1063 continue;
1064 };
1065 if Value::canonical_cmp(left, right) != Ordering::Less {
1066 return Err("set payload must already be canonical and deduplicated".into());
1067 }
1068 }
1069
1070 Ok(())
1071 }
1072 (FieldKind::Map { .. }, Value::Map(entries)) => {
1073 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1074
1075 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1076 return Err("map payload must already be canonical and deduplicated".into());
1077 }
1078
1079 Ok(())
1080 }
1081 _ => Ok(()),
1082 }
1083}
1084
1085#[cfg(test)]
1090mod tests {
1091 use crate::{
1092 model::field::{FieldKind, FieldModel},
1093 value::Value,
1094 };
1095
1096 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1097 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1098
1099 #[test]
1100 fn text_max_len_accepts_unbounded_text() {
1101 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1102
1103 assert!(
1104 field
1105 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1106 .is_ok()
1107 );
1108 }
1109
1110 #[test]
1111 fn text_max_len_counts_unicode_scalars_not_bytes() {
1112 let field = FieldModel::generated("name", BOUNDED_TEXT);
1113
1114 assert!(
1115 field
1116 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1117 .is_ok()
1118 );
1119 assert!(
1120 field
1121 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1122 .is_err()
1123 );
1124 }
1125
1126 #[test]
1127 fn text_max_len_recurses_through_collections() {
1128 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1129 static TEXT_MAP: FieldKind = FieldKind::Map {
1130 key: &BOUNDED_TEXT,
1131 value: &BOUNDED_TEXT,
1132 };
1133
1134 let list_field = FieldModel::generated("names", TEXT_LIST);
1135 let map_field = FieldModel::generated("labels", TEXT_MAP);
1136
1137 assert!(
1138 list_field
1139 .validate_runtime_value_for_storage(&Value::List(vec![
1140 Value::Text("Ada".into()),
1141 Value::Text("Bob".into()),
1142 ]))
1143 .is_ok()
1144 );
1145 assert!(
1146 list_field
1147 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1148 .is_err()
1149 );
1150 assert!(
1151 map_field
1152 .validate_runtime_value_for_storage(&Value::Map(vec![(
1153 Value::Text("key".into()),
1154 Value::Text("val".into()),
1155 )]))
1156 .is_ok()
1157 );
1158 assert!(
1159 map_field
1160 .validate_runtime_value_for_storage(&Value::Map(vec![(
1161 Value::Text("long".into()),
1162 Value::Text("val".into()),
1163 )]))
1164 .is_err()
1165 );
1166 }
1167
1168 #[test]
1169 fn blob_max_len_counts_bytes() {
1170 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1171
1172 assert!(
1173 field
1174 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1175 .is_ok()
1176 );
1177 assert!(
1178 field
1179 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1180 .is_err()
1181 );
1182 }
1183
1184 #[test]
1185 fn blob_max_len_recurses_through_collections() {
1186 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1187
1188 let field = FieldModel::generated("payloads", BLOB_LIST);
1189
1190 assert!(
1191 field
1192 .validate_runtime_value_for_storage(&Value::List(vec![
1193 Value::Blob(vec![1, 2, 3]),
1194 Value::Blob(vec![4, 5]),
1195 ]))
1196 .is_ok()
1197 );
1198 assert!(
1199 field
1200 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1201 1, 2, 3, 4
1202 ])]))
1203 .is_err()
1204 );
1205 }
1206}