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 EncodedSlotPayload(&'static [u8]),
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum FieldInsertGeneration {
188 Ulid,
190 Timestamp,
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum FieldWriteManagement {
205 CreatedAt,
207 UpdatedAt,
209}
210
211impl FieldModel {
212 #[must_use]
218 #[doc(hidden)]
219 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
220 Self::generated_with_storage_decode_and_nullability(
221 name,
222 kind,
223 FieldStorageDecode::ByKind,
224 false,
225 )
226 }
227
228 #[must_use]
230 #[doc(hidden)]
231 pub const fn generated_with_storage_decode(
232 name: &'static str,
233 kind: FieldKind,
234 storage_decode: FieldStorageDecode,
235 ) -> Self {
236 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
237 }
238
239 #[must_use]
241 #[doc(hidden)]
242 pub const fn generated_with_storage_decode_and_nullability(
243 name: &'static str,
244 kind: FieldKind,
245 storage_decode: FieldStorageDecode,
246 nullable: bool,
247 ) -> Self {
248 Self::generated_with_storage_decode_nullability_and_write_policies(
249 name,
250 kind,
251 storage_decode,
252 nullable,
253 None,
254 None,
255 )
256 }
257
258 #[must_use]
261 #[doc(hidden)]
262 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
263 name: &'static str,
264 kind: FieldKind,
265 storage_decode: FieldStorageDecode,
266 nullable: bool,
267 insert_generation: Option<FieldInsertGeneration>,
268 ) -> Self {
269 Self::generated_with_storage_decode_nullability_and_write_policies(
270 name,
271 kind,
272 storage_decode,
273 nullable,
274 insert_generation,
275 None,
276 )
277 }
278
279 #[must_use]
282 #[doc(hidden)]
283 pub const fn generated_with_storage_decode_nullability_and_write_policies(
284 name: &'static str,
285 kind: FieldKind,
286 storage_decode: FieldStorageDecode,
287 nullable: bool,
288 insert_generation: Option<FieldInsertGeneration>,
289 write_management: Option<FieldWriteManagement>,
290 ) -> Self {
291 Self {
292 name,
293 kind,
294 nested_fields: &[],
295 nullable,
296 storage_decode,
297 leaf_codec: leaf_codec_for(kind, storage_decode),
298 insert_generation,
299 write_management,
300 database_default: FieldDatabaseDefault::None,
301 }
302 }
303
304 #[must_use]
306 #[doc(hidden)]
307 pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
308 name: &'static str,
309 kind: FieldKind,
310 storage_decode: FieldStorageDecode,
311 nullable: bool,
312 insert_generation: Option<FieldInsertGeneration>,
313 write_management: Option<FieldWriteManagement>,
314 nested_fields: &'static [Self],
315 ) -> Self {
316 Self::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
317 name,
318 kind,
319 storage_decode,
320 nullable,
321 insert_generation,
322 write_management,
323 FieldDatabaseDefault::None,
324 nested_fields,
325 )
326 }
327
328 #[must_use]
331 #[doc(hidden)]
332 #[expect(
333 clippy::too_many_arguments,
334 reason = "generated schema metadata keeps every field contract explicit"
335 )]
336 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
337 name: &'static str,
338 kind: FieldKind,
339 storage_decode: FieldStorageDecode,
340 nullable: bool,
341 insert_generation: Option<FieldInsertGeneration>,
342 write_management: Option<FieldWriteManagement>,
343 database_default: FieldDatabaseDefault,
344 nested_fields: &'static [Self],
345 ) -> Self {
346 Self {
347 name,
348 kind,
349 nested_fields,
350 nullable,
351 storage_decode,
352 leaf_codec: leaf_codec_for(kind, storage_decode),
353 insert_generation,
354 write_management,
355 database_default,
356 }
357 }
358
359 #[must_use]
361 pub const fn name(&self) -> &'static str {
362 self.name
363 }
364
365 #[must_use]
367 pub const fn kind(&self) -> FieldKind {
368 self.kind
369 }
370
371 #[must_use]
373 pub const fn nested_fields(&self) -> &'static [Self] {
374 self.nested_fields
375 }
376
377 #[must_use]
379 pub const fn nullable(&self) -> bool {
380 self.nullable
381 }
382
383 #[must_use]
385 pub const fn storage_decode(&self) -> FieldStorageDecode {
386 self.storage_decode
387 }
388
389 #[must_use]
391 pub const fn leaf_codec(&self) -> LeafCodec {
392 self.leaf_codec
393 }
394
395 #[must_use]
397 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
398 self.insert_generation
399 }
400
401 #[must_use]
403 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
404 self.write_management
405 }
406
407 #[must_use]
409 pub const fn database_default(&self) -> FieldDatabaseDefault {
410 self.database_default
411 }
412
413 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
421 if matches!(value, Value::Null) {
422 if self.nullable() {
423 return Ok(());
424 }
425
426 return Err("required field cannot store null".into());
427 }
428
429 let accepts = match self.storage_decode() {
430 FieldStorageDecode::Value => {
431 value_storage_kind_accepts_runtime_value(self.kind(), value)
432 }
433 FieldStorageDecode::ByKind => {
434 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
435 }
436 };
437 if !accepts {
438 return Err(format!(
439 "field kind {:?} does not accept runtime value {value:?}",
440 self.kind()
441 ));
442 }
443
444 ensure_decimal_scale_matches(self.kind(), value)?;
445 ensure_scalar_max_len_matches(self.kind(), value)?;
446 ensure_value_is_deterministic_for_storage(self.kind(), value)
447 }
448
449 pub(crate) fn normalize_runtime_value_for_storage<'a>(
453 &self,
454 value: &'a Value,
455 ) -> Result<Cow<'a, Value>, String> {
456 normalize_decimal_scale_for_storage(self.kind(), value)
457 }
458}
459
460const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
464 if matches!(storage_decode, FieldStorageDecode::Value) {
465 return LeafCodec::StructuralFallback;
466 }
467
468 match kind {
469 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
470 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
471 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
472 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
473 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
474 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
475 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
476 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
477 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
478 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
479 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
480 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
481 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
482 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
483 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
484 FieldKind::Account
485 | FieldKind::Decimal { .. }
486 | FieldKind::Enum { .. }
487 | FieldKind::Int128
488 | FieldKind::IntBig
489 | FieldKind::List(_)
490 | FieldKind::Map { .. }
491 | FieldKind::Set(_)
492 | FieldKind::Structured { .. }
493 | FieldKind::Uint128
494 | FieldKind::UintBig => LeafCodec::StructuralFallback,
495 }
496}
497
498#[derive(Clone, Copy, Debug, Eq, PartialEq)]
505pub enum RelationStrength {
506 Strong,
507 Weak,
508}
509
510#[derive(Clone, Copy, Debug)]
520pub enum FieldKind {
521 Account,
523 Blob {
524 max_len: Option<u32>,
526 },
527 Bool,
528 Date,
529 Decimal {
530 scale: u32,
532 },
533 Duration,
534 Enum {
535 path: &'static str,
537 variants: &'static [EnumVariantModel],
539 },
540 Float32,
541 Float64,
542 Int,
543 Int128,
544 IntBig,
545 Principal,
546 Subaccount,
547 Text {
548 max_len: Option<u32>,
550 },
551 Timestamp,
552 Uint,
553 Uint128,
554 UintBig,
555 Ulid,
556 Unit,
557
558 Relation {
561 target_path: &'static str,
563 target_entity_name: &'static str,
565 target_entity_tag: EntityTag,
567 target_store_path: &'static str,
569 key_kind: &'static Self,
570 strength: RelationStrength,
571 },
572
573 List(&'static Self),
575 Set(&'static Self),
576 Map {
580 key: &'static Self,
581 value: &'static Self,
582 },
583
584 Structured {
588 queryable: bool,
589 },
590}
591
592impl FieldKind {
593 #[must_use]
594 pub const fn value_kind(&self) -> RuntimeValueKind {
595 match self {
596 Self::Account
597 | Self::Blob { .. }
598 | Self::Bool
599 | Self::Date
600 | Self::Duration
601 | Self::Enum { .. }
602 | Self::Float32
603 | Self::Float64
604 | Self::Int
605 | Self::Int128
606 | Self::IntBig
607 | Self::Principal
608 | Self::Subaccount
609 | Self::Text { .. }
610 | Self::Timestamp
611 | Self::Uint
612 | Self::Uint128
613 | Self::UintBig
614 | Self::Ulid
615 | Self::Unit
616 | Self::Decimal { .. }
617 | Self::Relation { .. } => RuntimeValueKind::Atomic,
618 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
619 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
620 Self::Structured { queryable } => RuntimeValueKind::Structured {
621 queryable: *queryable,
622 },
623 }
624 }
625
626 #[must_use]
634 pub const fn is_deterministic_collection_shape(&self) -> bool {
635 match self {
636 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
637
638 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
639
640 Self::Map { key, value } => {
641 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
642 }
643
644 _ => true,
645 }
646 }
647
648 #[must_use]
651 pub(crate) fn supports_group_probe(&self) -> bool {
652 match self {
653 Self::Enum { variants, .. } => variants.iter().all(|variant| {
654 variant
655 .payload_kind()
656 .is_none_or(Self::supports_group_probe)
657 }),
658 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
659 Self::List(_)
660 | Self::Set(_)
661 | Self::Map { .. }
662 | Self::Structured { .. }
663 | Self::Unit => false,
664 Self::Account
665 | Self::Blob { .. }
666 | Self::Bool
667 | Self::Date
668 | Self::Decimal { .. }
669 | Self::Duration
670 | Self::Float32
671 | Self::Float64
672 | Self::Int
673 | Self::Int128
674 | Self::IntBig
675 | Self::Principal
676 | Self::Subaccount
677 | Self::Text { .. }
678 | Self::Timestamp
679 | Self::Uint
680 | Self::Uint128
681 | Self::UintBig
682 | Self::Ulid => true,
683 }
684 }
685
686 #[must_use]
692 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
693 match (self, value) {
694 (Self::Account, Value::Account(_))
695 | (Self::Blob { .. }, Value::Blob(_))
696 | (Self::Bool, Value::Bool(_))
697 | (Self::Date, Value::Date(_))
698 | (Self::Decimal { .. }, Value::Decimal(_))
699 | (Self::Duration, Value::Duration(_))
700 | (Self::Enum { .. }, Value::Enum(_))
701 | (Self::Float32, Value::Float32(_))
702 | (Self::Float64, Value::Float64(_))
703 | (Self::Int, Value::Int(_))
704 | (Self::Int128, Value::Int128(_))
705 | (Self::IntBig, Value::IntBig(_))
706 | (Self::Principal, Value::Principal(_))
707 | (Self::Subaccount, Value::Subaccount(_))
708 | (Self::Text { .. }, Value::Text(_))
709 | (Self::Timestamp, Value::Timestamp(_))
710 | (Self::Uint, Value::Uint(_))
711 | (Self::Uint128, Value::Uint128(_))
712 | (Self::UintBig, Value::UintBig(_))
713 | (Self::Ulid, Value::Ulid(_))
714 | (Self::Unit, Value::Unit)
715 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
716 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
717 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
718 items.iter().all(|item| inner.accepts_value(item))
719 }
720 (Self::Map { key, value }, Value::Map(entries)) => {
721 if Value::validate_map_entries(entries.as_slice()).is_err() {
722 return false;
723 }
724
725 entries.iter().all(|(entry_key, entry_value)| {
726 key.accepts_value(entry_key) && value.accepts_value(entry_value)
727 })
728 }
729 _ => false,
730 }
731 }
732}
733
734fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
740 match (kind, value) {
741 (FieldKind::Relation { key_kind, .. }, value) => {
742 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
743 }
744 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
745 .iter()
746 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
747 (
748 FieldKind::Map {
749 key,
750 value: value_kind,
751 },
752 Value::Map(entries),
753 ) => {
754 if Value::validate_map_entries(entries.as_slice()).is_err() {
755 return false;
756 }
757
758 entries.iter().all(|(entry_key, entry_value)| {
759 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
760 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
761 })
762 }
763 (FieldKind::Structured { .. }, _) => false,
764 _ => kind.accepts_value(value),
765 }
766}
767
768fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
772 match (kind, value) {
773 (FieldKind::Structured { .. }, _) => true,
774 (FieldKind::Relation { key_kind, .. }, value) => {
775 value_storage_kind_accepts_runtime_value(*key_kind, value)
776 }
777 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
778 .iter()
779 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
780 (
781 FieldKind::Map {
782 key,
783 value: value_kind,
784 },
785 Value::Map(entries),
786 ) => {
787 if Value::validate_map_entries(entries.as_slice()).is_err() {
788 return false;
789 }
790
791 entries.iter().all(|(entry_key, entry_value)| {
792 value_storage_kind_accepts_runtime_value(*key, entry_key)
793 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
794 })
795 }
796 _ => kind.accepts_value(value),
797 }
798}
799
800fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
803 if matches!(value, Value::Null) {
804 return Ok(());
805 }
806
807 match (kind, value) {
808 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
809 if decimal.scale() != scale {
810 return Err(format!(
811 "decimal scale mismatch: expected {scale}, found {}",
812 decimal.scale()
813 ));
814 }
815
816 Ok(())
817 }
818 (FieldKind::Relation { key_kind, .. }, value) => {
819 ensure_decimal_scale_matches(*key_kind, value)
820 }
821 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
822 for item in items {
823 ensure_decimal_scale_matches(*inner, item)?;
824 }
825
826 Ok(())
827 }
828 (
829 FieldKind::Map {
830 key,
831 value: map_value,
832 },
833 Value::Map(entries),
834 ) => {
835 for (entry_key, entry_value) in entries {
836 ensure_decimal_scale_matches(*key, entry_key)?;
837 ensure_decimal_scale_matches(*map_value, entry_value)?;
838 }
839
840 Ok(())
841 }
842 _ => Ok(()),
843 }
844}
845
846pub(crate) fn normalize_decimal_scale_for_storage(
851 kind: FieldKind,
852 value: &Value,
853) -> Result<Cow<'_, Value>, String> {
854 if matches!(value, Value::Null) {
855 return Ok(Cow::Borrowed(value));
856 }
857
858 match (kind, value) {
859 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
860 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
861 format!(
862 "decimal scale mismatch: expected {scale}, found {}",
863 decimal.scale()
864 )
865 })?;
866
867 if normalized.scale() == decimal.scale() {
868 Ok(Cow::Borrowed(value))
869 } else {
870 Ok(Cow::Owned(Value::Decimal(normalized)))
871 }
872 }
873 (FieldKind::Relation { key_kind, .. }, value) => {
874 normalize_decimal_scale_for_storage(*key_kind, value)
875 }
876 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
877 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
878 items.map_or_else(
879 || Cow::Borrowed(value),
880 |items| Cow::Owned(Value::List(items)),
881 )
882 })
883 }
884 (
885 FieldKind::Map {
886 key,
887 value: map_value,
888 },
889 Value::Map(entries),
890 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
891 entries.map_or_else(
892 || Cow::Borrowed(value),
893 |entries| Cow::Owned(Value::Map(entries)),
894 )
895 }),
896 _ => Ok(Cow::Borrowed(value)),
897 }
898}
899
900fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
904 match decimal.scale().cmp(&scale) {
905 Ordering::Equal => Some(decimal),
906 Ordering::Less => decimal
907 .scale_to_integer(scale)
908 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
909 Ordering::Greater => Some(decimal.round_dp(scale)),
910 }
911}
912
913fn normalize_decimal_list_items(
916 kind: FieldKind,
917 items: &[Value],
918) -> Result<Option<Vec<Value>>, String> {
919 let mut normalized_items = None;
920
921 for (index, item) in items.iter().enumerate() {
922 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
923 if let Cow::Owned(value) = normalized {
924 let items = normalized_items.get_or_insert_with(|| items.to_vec());
925 items[index] = value;
926 }
927 }
928
929 Ok(normalized_items)
930}
931
932fn normalize_decimal_map_entries(
935 key_kind: FieldKind,
936 value_kind: FieldKind,
937 entries: &[(Value, Value)],
938) -> Result<Option<Vec<(Value, Value)>>, String> {
939 let mut normalized_entries = None;
940
941 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
942 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
943 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
944
945 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
946 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
947 if let Cow::Owned(value) = normalized_key {
948 entries[index].0 = value;
949 }
950 if let Cow::Owned(value) = normalized_value {
951 entries[index].1 = value;
952 }
953 }
954 }
955
956 Ok(normalized_entries)
957}
958
959fn ensure_scalar_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
962 if matches!(value, Value::Null) {
963 return Ok(());
964 }
965
966 match (kind, value) {
967 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
968 let len = text.chars().count();
969 if len > max as usize {
970 return Err(format!(
971 "text length exceeds max_len: expected at most {max}, found {len}"
972 ));
973 }
974
975 Ok(())
976 }
977 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
978 let len = bytes.len();
979 if len > max as usize {
980 return Err(format!(
981 "blob length exceeds max_len: expected at most {max}, found {len}"
982 ));
983 }
984
985 Ok(())
986 }
987 (FieldKind::Relation { key_kind, .. }, value) => {
988 ensure_scalar_max_len_matches(*key_kind, value)
989 }
990 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
991 for item in items {
992 ensure_scalar_max_len_matches(*inner, item)?;
993 }
994
995 Ok(())
996 }
997 (
998 FieldKind::Map {
999 key,
1000 value: map_value,
1001 },
1002 Value::Map(entries),
1003 ) => {
1004 for (entry_key, entry_value) in entries {
1005 ensure_scalar_max_len_matches(*key, entry_key)?;
1006 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1007 }
1008
1009 Ok(())
1010 }
1011 _ => Ok(()),
1012 }
1013}
1014
1015fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
1018 match (kind, value) {
1019 (FieldKind::Set(_), Value::List(items)) => {
1020 for pair in items.windows(2) {
1021 let [left, right] = pair else {
1022 continue;
1023 };
1024 if Value::canonical_cmp(left, right) != Ordering::Less {
1025 return Err("set payload must already be canonical and deduplicated".into());
1026 }
1027 }
1028
1029 Ok(())
1030 }
1031 (FieldKind::Map { .. }, Value::Map(entries)) => {
1032 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1033
1034 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1035 return Err("map payload must already be canonical and deduplicated".into());
1036 }
1037
1038 Ok(())
1039 }
1040 _ => Ok(()),
1041 }
1042}
1043
1044#[cfg(test)]
1049mod tests {
1050 use crate::{
1051 model::field::{FieldKind, FieldModel},
1052 value::Value,
1053 };
1054
1055 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1056 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1057
1058 #[test]
1059 fn text_max_len_accepts_unbounded_text() {
1060 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1061
1062 assert!(
1063 field
1064 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1065 .is_ok()
1066 );
1067 }
1068
1069 #[test]
1070 fn text_max_len_counts_unicode_scalars_not_bytes() {
1071 let field = FieldModel::generated("name", BOUNDED_TEXT);
1072
1073 assert!(
1074 field
1075 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1076 .is_ok()
1077 );
1078 assert!(
1079 field
1080 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1081 .is_err()
1082 );
1083 }
1084
1085 #[test]
1086 fn text_max_len_recurses_through_collections() {
1087 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1088 static TEXT_MAP: FieldKind = FieldKind::Map {
1089 key: &BOUNDED_TEXT,
1090 value: &BOUNDED_TEXT,
1091 };
1092
1093 let list_field = FieldModel::generated("names", TEXT_LIST);
1094 let map_field = FieldModel::generated("labels", TEXT_MAP);
1095
1096 assert!(
1097 list_field
1098 .validate_runtime_value_for_storage(&Value::List(vec![
1099 Value::Text("Ada".into()),
1100 Value::Text("Bob".into()),
1101 ]))
1102 .is_ok()
1103 );
1104 assert!(
1105 list_field
1106 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1107 .is_err()
1108 );
1109 assert!(
1110 map_field
1111 .validate_runtime_value_for_storage(&Value::Map(vec![(
1112 Value::Text("key".into()),
1113 Value::Text("val".into()),
1114 )]))
1115 .is_ok()
1116 );
1117 assert!(
1118 map_field
1119 .validate_runtime_value_for_storage(&Value::Map(vec![(
1120 Value::Text("long".into()),
1121 Value::Text("val".into()),
1122 )]))
1123 .is_err()
1124 );
1125 }
1126
1127 #[test]
1128 fn blob_max_len_counts_bytes() {
1129 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1130
1131 assert!(
1132 field
1133 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1134 .is_ok()
1135 );
1136 assert!(
1137 field
1138 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1139 .is_err()
1140 );
1141 }
1142
1143 #[test]
1144 fn blob_max_len_recurses_through_collections() {
1145 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1146
1147 let field = FieldModel::generated("payloads", BLOB_LIST);
1148
1149 assert!(
1150 field
1151 .validate_runtime_value_for_storage(&Value::List(vec![
1152 Value::Blob(vec![1, 2, 3]),
1153 Value::Blob(vec![4, 5]),
1154 ]))
1155 .is_ok()
1156 );
1157 assert!(
1158 field
1159 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1160 1, 2, 3, 4
1161 ])]))
1162 .is_err()
1163 );
1164 }
1165}