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::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
311 name,
312 kind,
313 storage_decode,
314 nullable,
315 insert_generation,
316 write_management,
317 FieldDatabaseDefault::None,
318 nested_fields,
319 )
320 }
321
322 #[must_use]
325 #[doc(hidden)]
326 #[expect(
327 clippy::too_many_arguments,
328 reason = "generated schema metadata keeps every field contract explicit"
329 )]
330 pub const fn generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
331 name: &'static str,
332 kind: FieldKind,
333 storage_decode: FieldStorageDecode,
334 nullable: bool,
335 insert_generation: Option<FieldInsertGeneration>,
336 write_management: Option<FieldWriteManagement>,
337 database_default: FieldDatabaseDefault,
338 nested_fields: &'static [Self],
339 ) -> Self {
340 Self {
341 name,
342 kind,
343 nested_fields,
344 nullable,
345 storage_decode,
346 leaf_codec: leaf_codec_for(kind, storage_decode),
347 insert_generation,
348 write_management,
349 database_default,
350 }
351 }
352
353 #[must_use]
355 pub const fn name(&self) -> &'static str {
356 self.name
357 }
358
359 #[must_use]
361 pub const fn kind(&self) -> FieldKind {
362 self.kind
363 }
364
365 #[must_use]
367 pub const fn nested_fields(&self) -> &'static [Self] {
368 self.nested_fields
369 }
370
371 #[must_use]
373 pub const fn nullable(&self) -> bool {
374 self.nullable
375 }
376
377 #[must_use]
379 pub const fn storage_decode(&self) -> FieldStorageDecode {
380 self.storage_decode
381 }
382
383 #[must_use]
385 pub const fn leaf_codec(&self) -> LeafCodec {
386 self.leaf_codec
387 }
388
389 #[must_use]
391 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
392 self.insert_generation
393 }
394
395 #[must_use]
397 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
398 self.write_management
399 }
400
401 #[must_use]
403 pub const fn database_default(&self) -> FieldDatabaseDefault {
404 self.database_default
405 }
406
407 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
415 if matches!(value, Value::Null) {
416 if self.nullable() {
417 return Ok(());
418 }
419
420 return Err("required field cannot store null".into());
421 }
422
423 let accepts = match self.storage_decode() {
424 FieldStorageDecode::Value => {
425 value_storage_kind_accepts_runtime_value(self.kind(), value)
426 }
427 FieldStorageDecode::ByKind => {
428 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
429 }
430 };
431 if !accepts {
432 return Err(format!(
433 "field kind {:?} does not accept runtime value {value:?}",
434 self.kind()
435 ));
436 }
437
438 ensure_decimal_scale_matches(self.kind(), value)?;
439 ensure_scalar_max_len_matches(self.kind(), value)?;
440 ensure_value_is_deterministic_for_storage(self.kind(), value)
441 }
442
443 pub(crate) fn normalize_runtime_value_for_storage<'a>(
447 &self,
448 value: &'a Value,
449 ) -> Result<Cow<'a, Value>, String> {
450 normalize_decimal_scale_for_storage(self.kind(), value)
451 }
452}
453
454const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
458 if matches!(storage_decode, FieldStorageDecode::Value) {
459 return LeafCodec::StructuralFallback;
460 }
461
462 match kind {
463 FieldKind::Blob { .. } => LeafCodec::Scalar(ScalarCodec::Blob),
464 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
465 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
466 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
467 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
468 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
469 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
470 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
471 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
472 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
473 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
474 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
475 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
476 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
477 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
478 FieldKind::Account
479 | FieldKind::Decimal { .. }
480 | FieldKind::Enum { .. }
481 | FieldKind::Int128
482 | FieldKind::IntBig
483 | FieldKind::List(_)
484 | FieldKind::Map { .. }
485 | FieldKind::Set(_)
486 | FieldKind::Structured { .. }
487 | FieldKind::Uint128
488 | FieldKind::UintBig => LeafCodec::StructuralFallback,
489 }
490}
491
492#[derive(Clone, Copy, Debug, Eq, PartialEq)]
499pub enum RelationStrength {
500 Strong,
501 Weak,
502}
503
504#[derive(Clone, Copy, Debug)]
514pub enum FieldKind {
515 Account,
517 Blob {
518 max_len: Option<u32>,
520 },
521 Bool,
522 Date,
523 Decimal {
524 scale: u32,
526 },
527 Duration,
528 Enum {
529 path: &'static str,
531 variants: &'static [EnumVariantModel],
533 },
534 Float32,
535 Float64,
536 Int,
537 Int128,
538 IntBig,
539 Principal,
540 Subaccount,
541 Text {
542 max_len: Option<u32>,
544 },
545 Timestamp,
546 Uint,
547 Uint128,
548 UintBig,
549 Ulid,
550 Unit,
551
552 Relation {
555 target_path: &'static str,
557 target_entity_name: &'static str,
559 target_entity_tag: EntityTag,
561 target_store_path: &'static str,
563 key_kind: &'static Self,
564 strength: RelationStrength,
565 },
566
567 List(&'static Self),
569 Set(&'static Self),
570 Map {
574 key: &'static Self,
575 value: &'static Self,
576 },
577
578 Structured {
582 queryable: bool,
583 },
584}
585
586impl FieldKind {
587 #[must_use]
588 pub const fn value_kind(&self) -> RuntimeValueKind {
589 match self {
590 Self::Account
591 | Self::Blob { .. }
592 | Self::Bool
593 | Self::Date
594 | Self::Duration
595 | Self::Enum { .. }
596 | Self::Float32
597 | Self::Float64
598 | Self::Int
599 | Self::Int128
600 | Self::IntBig
601 | Self::Principal
602 | Self::Subaccount
603 | Self::Text { .. }
604 | Self::Timestamp
605 | Self::Uint
606 | Self::Uint128
607 | Self::UintBig
608 | Self::Ulid
609 | Self::Unit
610 | Self::Decimal { .. }
611 | Self::Relation { .. } => RuntimeValueKind::Atomic,
612 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
613 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
614 Self::Structured { queryable } => RuntimeValueKind::Structured {
615 queryable: *queryable,
616 },
617 }
618 }
619
620 #[must_use]
628 pub const fn is_deterministic_collection_shape(&self) -> bool {
629 match self {
630 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
631
632 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
633
634 Self::Map { key, value } => {
635 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
636 }
637
638 _ => true,
639 }
640 }
641
642 #[must_use]
645 pub(crate) fn supports_group_probe(&self) -> bool {
646 match self {
647 Self::Enum { variants, .. } => variants.iter().all(|variant| {
648 variant
649 .payload_kind()
650 .is_none_or(Self::supports_group_probe)
651 }),
652 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
653 Self::List(_)
654 | Self::Set(_)
655 | Self::Map { .. }
656 | Self::Structured { .. }
657 | Self::Unit => false,
658 Self::Account
659 | Self::Blob { .. }
660 | Self::Bool
661 | Self::Date
662 | Self::Decimal { .. }
663 | Self::Duration
664 | Self::Float32
665 | Self::Float64
666 | Self::Int
667 | Self::Int128
668 | Self::IntBig
669 | Self::Principal
670 | Self::Subaccount
671 | Self::Text { .. }
672 | Self::Timestamp
673 | Self::Uint
674 | Self::Uint128
675 | Self::UintBig
676 | Self::Ulid => true,
677 }
678 }
679
680 #[must_use]
686 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
687 match (self, value) {
688 (Self::Account, Value::Account(_))
689 | (Self::Blob { .. }, Value::Blob(_))
690 | (Self::Bool, Value::Bool(_))
691 | (Self::Date, Value::Date(_))
692 | (Self::Decimal { .. }, Value::Decimal(_))
693 | (Self::Duration, Value::Duration(_))
694 | (Self::Enum { .. }, Value::Enum(_))
695 | (Self::Float32, Value::Float32(_))
696 | (Self::Float64, Value::Float64(_))
697 | (Self::Int, Value::Int(_))
698 | (Self::Int128, Value::Int128(_))
699 | (Self::IntBig, Value::IntBig(_))
700 | (Self::Principal, Value::Principal(_))
701 | (Self::Subaccount, Value::Subaccount(_))
702 | (Self::Text { .. }, Value::Text(_))
703 | (Self::Timestamp, Value::Timestamp(_))
704 | (Self::Uint, Value::Uint(_))
705 | (Self::Uint128, Value::Uint128(_))
706 | (Self::UintBig, Value::UintBig(_))
707 | (Self::Ulid, Value::Ulid(_))
708 | (Self::Unit, Value::Unit)
709 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
710 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
711 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
712 items.iter().all(|item| inner.accepts_value(item))
713 }
714 (Self::Map { key, value }, Value::Map(entries)) => {
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 key.accepts_value(entry_key) && value.accepts_value(entry_value)
721 })
722 }
723 _ => false,
724 }
725 }
726}
727
728fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
734 match (kind, value) {
735 (FieldKind::Relation { key_kind, .. }, value) => {
736 by_kind_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| by_kind_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 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
754 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
755 })
756 }
757 (FieldKind::Structured { .. }, _) => false,
758 _ => kind.accepts_value(value),
759 }
760}
761
762fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
766 match (kind, value) {
767 (FieldKind::Structured { .. }, _) => true,
768 (FieldKind::Relation { key_kind, .. }, value) => {
769 value_storage_kind_accepts_runtime_value(*key_kind, value)
770 }
771 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
772 .iter()
773 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
774 (
775 FieldKind::Map {
776 key,
777 value: value_kind,
778 },
779 Value::Map(entries),
780 ) => {
781 if Value::validate_map_entries(entries.as_slice()).is_err() {
782 return false;
783 }
784
785 entries.iter().all(|(entry_key, entry_value)| {
786 value_storage_kind_accepts_runtime_value(*key, entry_key)
787 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
788 })
789 }
790 _ => kind.accepts_value(value),
791 }
792}
793
794fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
797 if matches!(value, Value::Null) {
798 return Ok(());
799 }
800
801 match (kind, value) {
802 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
803 if decimal.scale() != scale {
804 return Err(format!(
805 "decimal scale mismatch: expected {scale}, found {}",
806 decimal.scale()
807 ));
808 }
809
810 Ok(())
811 }
812 (FieldKind::Relation { key_kind, .. }, value) => {
813 ensure_decimal_scale_matches(*key_kind, value)
814 }
815 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
816 for item in items {
817 ensure_decimal_scale_matches(*inner, item)?;
818 }
819
820 Ok(())
821 }
822 (
823 FieldKind::Map {
824 key,
825 value: map_value,
826 },
827 Value::Map(entries),
828 ) => {
829 for (entry_key, entry_value) in entries {
830 ensure_decimal_scale_matches(*key, entry_key)?;
831 ensure_decimal_scale_matches(*map_value, entry_value)?;
832 }
833
834 Ok(())
835 }
836 _ => Ok(()),
837 }
838}
839
840pub(crate) fn normalize_decimal_scale_for_storage(
845 kind: FieldKind,
846 value: &Value,
847) -> Result<Cow<'_, Value>, String> {
848 if matches!(value, Value::Null) {
849 return Ok(Cow::Borrowed(value));
850 }
851
852 match (kind, value) {
853 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
854 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
855 format!(
856 "decimal scale mismatch: expected {scale}, found {}",
857 decimal.scale()
858 )
859 })?;
860
861 if normalized.scale() == decimal.scale() {
862 Ok(Cow::Borrowed(value))
863 } else {
864 Ok(Cow::Owned(Value::Decimal(normalized)))
865 }
866 }
867 (FieldKind::Relation { key_kind, .. }, value) => {
868 normalize_decimal_scale_for_storage(*key_kind, value)
869 }
870 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
871 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
872 items.map_or_else(
873 || Cow::Borrowed(value),
874 |items| Cow::Owned(Value::List(items)),
875 )
876 })
877 }
878 (
879 FieldKind::Map {
880 key,
881 value: map_value,
882 },
883 Value::Map(entries),
884 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
885 entries.map_or_else(
886 || Cow::Borrowed(value),
887 |entries| Cow::Owned(Value::Map(entries)),
888 )
889 }),
890 _ => Ok(Cow::Borrowed(value)),
891 }
892}
893
894fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
898 match decimal.scale().cmp(&scale) {
899 Ordering::Equal => Some(decimal),
900 Ordering::Less => decimal
901 .scale_to_integer(scale)
902 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
903 Ordering::Greater => Some(decimal.round_dp(scale)),
904 }
905}
906
907fn normalize_decimal_list_items(
910 kind: FieldKind,
911 items: &[Value],
912) -> Result<Option<Vec<Value>>, String> {
913 let mut normalized_items = None;
914
915 for (index, item) in items.iter().enumerate() {
916 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
917 if let Cow::Owned(value) = normalized {
918 let items = normalized_items.get_or_insert_with(|| items.to_vec());
919 items[index] = value;
920 }
921 }
922
923 Ok(normalized_items)
924}
925
926fn normalize_decimal_map_entries(
929 key_kind: FieldKind,
930 value_kind: FieldKind,
931 entries: &[(Value, Value)],
932) -> Result<Option<Vec<(Value, Value)>>, String> {
933 let mut normalized_entries = None;
934
935 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
936 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
937 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
938
939 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
940 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
941 if let Cow::Owned(value) = normalized_key {
942 entries[index].0 = value;
943 }
944 if let Cow::Owned(value) = normalized_value {
945 entries[index].1 = value;
946 }
947 }
948 }
949
950 Ok(normalized_entries)
951}
952
953fn ensure_scalar_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
956 if matches!(value, Value::Null) {
957 return Ok(());
958 }
959
960 match (kind, value) {
961 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
962 let len = text.chars().count();
963 if len > max as usize {
964 return Err(format!(
965 "text length exceeds max_len: expected at most {max}, found {len}"
966 ));
967 }
968
969 Ok(())
970 }
971 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
972 let len = bytes.len();
973 if len > max as usize {
974 return Err(format!(
975 "blob length exceeds max_len: expected at most {max}, found {len}"
976 ));
977 }
978
979 Ok(())
980 }
981 (FieldKind::Relation { key_kind, .. }, value) => {
982 ensure_scalar_max_len_matches(*key_kind, value)
983 }
984 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
985 for item in items {
986 ensure_scalar_max_len_matches(*inner, item)?;
987 }
988
989 Ok(())
990 }
991 (
992 FieldKind::Map {
993 key,
994 value: map_value,
995 },
996 Value::Map(entries),
997 ) => {
998 for (entry_key, entry_value) in entries {
999 ensure_scalar_max_len_matches(*key, entry_key)?;
1000 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1001 }
1002
1003 Ok(())
1004 }
1005 _ => Ok(()),
1006 }
1007}
1008
1009fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
1012 match (kind, value) {
1013 (FieldKind::Set(_), Value::List(items)) => {
1014 for pair in items.windows(2) {
1015 let [left, right] = pair else {
1016 continue;
1017 };
1018 if Value::canonical_cmp(left, right) != Ordering::Less {
1019 return Err("set payload must already be canonical and deduplicated".into());
1020 }
1021 }
1022
1023 Ok(())
1024 }
1025 (FieldKind::Map { .. }, Value::Map(entries)) => {
1026 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1027
1028 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1029 return Err("map payload must already be canonical and deduplicated".into());
1030 }
1031
1032 Ok(())
1033 }
1034 _ => Ok(()),
1035 }
1036}
1037
1038#[cfg(test)]
1043mod tests {
1044 use crate::{
1045 model::field::{FieldKind, FieldModel},
1046 value::Value,
1047 };
1048
1049 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1050 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1051
1052 #[test]
1053 fn text_max_len_accepts_unbounded_text() {
1054 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1055
1056 assert!(
1057 field
1058 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1059 .is_ok()
1060 );
1061 }
1062
1063 #[test]
1064 fn text_max_len_counts_unicode_scalars_not_bytes() {
1065 let field = FieldModel::generated("name", BOUNDED_TEXT);
1066
1067 assert!(
1068 field
1069 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1070 .is_ok()
1071 );
1072 assert!(
1073 field
1074 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1075 .is_err()
1076 );
1077 }
1078
1079 #[test]
1080 fn text_max_len_recurses_through_collections() {
1081 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1082 static TEXT_MAP: FieldKind = FieldKind::Map {
1083 key: &BOUNDED_TEXT,
1084 value: &BOUNDED_TEXT,
1085 };
1086
1087 let list_field = FieldModel::generated("names", TEXT_LIST);
1088 let map_field = FieldModel::generated("labels", TEXT_MAP);
1089
1090 assert!(
1091 list_field
1092 .validate_runtime_value_for_storage(&Value::List(vec![
1093 Value::Text("Ada".into()),
1094 Value::Text("Bob".into()),
1095 ]))
1096 .is_ok()
1097 );
1098 assert!(
1099 list_field
1100 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1101 .is_err()
1102 );
1103 assert!(
1104 map_field
1105 .validate_runtime_value_for_storage(&Value::Map(vec![(
1106 Value::Text("key".into()),
1107 Value::Text("val".into()),
1108 )]))
1109 .is_ok()
1110 );
1111 assert!(
1112 map_field
1113 .validate_runtime_value_for_storage(&Value::Map(vec![(
1114 Value::Text("long".into()),
1115 Value::Text("val".into()),
1116 )]))
1117 .is_err()
1118 );
1119 }
1120
1121 #[test]
1122 fn blob_max_len_counts_bytes() {
1123 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1124
1125 assert!(
1126 field
1127 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1128 .is_ok()
1129 );
1130 assert!(
1131 field
1132 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1133 .is_err()
1134 );
1135 }
1136
1137 #[test]
1138 fn blob_max_len_recurses_through_collections() {
1139 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1140
1141 let field = FieldModel::generated("payloads", BLOB_LIST);
1142
1143 assert!(
1144 field
1145 .validate_runtime_value_for_storage(&Value::List(vec![
1146 Value::Blob(vec![1, 2, 3]),
1147 Value::Blob(vec![4, 5]),
1148 ]))
1149 .is_ok()
1150 );
1151 assert!(
1152 field
1153 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1154 1, 2, 3, 4
1155 ])]))
1156 .is_err()
1157 );
1158 }
1159}