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
840fn normalize_decimal_scale_for_storage(
844 kind: FieldKind,
845 value: &Value,
846) -> Result<Cow<'_, Value>, String> {
847 if matches!(value, Value::Null) {
848 return Ok(Cow::Borrowed(value));
849 }
850
851 match (kind, value) {
852 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
853 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
854 format!(
855 "decimal scale mismatch: expected {scale}, found {}",
856 decimal.scale()
857 )
858 })?;
859
860 if normalized.scale() == decimal.scale() {
861 Ok(Cow::Borrowed(value))
862 } else {
863 Ok(Cow::Owned(Value::Decimal(normalized)))
864 }
865 }
866 (FieldKind::Relation { key_kind, .. }, value) => {
867 normalize_decimal_scale_for_storage(*key_kind, value)
868 }
869 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
870 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
871 items.map_or_else(
872 || Cow::Borrowed(value),
873 |items| Cow::Owned(Value::List(items)),
874 )
875 })
876 }
877 (
878 FieldKind::Map {
879 key,
880 value: map_value,
881 },
882 Value::Map(entries),
883 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
884 entries.map_or_else(
885 || Cow::Borrowed(value),
886 |entries| Cow::Owned(Value::Map(entries)),
887 )
888 }),
889 _ => Ok(Cow::Borrowed(value)),
890 }
891}
892
893fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
897 match decimal.scale().cmp(&scale) {
898 Ordering::Equal => Some(decimal),
899 Ordering::Less => decimal
900 .scale_to_integer(scale)
901 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
902 Ordering::Greater => Some(decimal.round_dp(scale)),
903 }
904}
905
906fn normalize_decimal_list_items(
909 kind: FieldKind,
910 items: &[Value],
911) -> Result<Option<Vec<Value>>, String> {
912 let mut normalized_items = None;
913
914 for (index, item) in items.iter().enumerate() {
915 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
916 if let Cow::Owned(value) = normalized {
917 let items = normalized_items.get_or_insert_with(|| items.to_vec());
918 items[index] = value;
919 }
920 }
921
922 Ok(normalized_items)
923}
924
925fn normalize_decimal_map_entries(
928 key_kind: FieldKind,
929 value_kind: FieldKind,
930 entries: &[(Value, Value)],
931) -> Result<Option<Vec<(Value, Value)>>, String> {
932 let mut normalized_entries = None;
933
934 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
935 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
936 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
937
938 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
939 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
940 if let Cow::Owned(value) = normalized_key {
941 entries[index].0 = value;
942 }
943 if let Cow::Owned(value) = normalized_value {
944 entries[index].1 = value;
945 }
946 }
947 }
948
949 Ok(normalized_entries)
950}
951
952fn ensure_scalar_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
955 if matches!(value, Value::Null) {
956 return Ok(());
957 }
958
959 match (kind, value) {
960 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
961 let len = text.chars().count();
962 if len > max as usize {
963 return Err(format!(
964 "text length exceeds max_len: expected at most {max}, found {len}"
965 ));
966 }
967
968 Ok(())
969 }
970 (FieldKind::Blob { max_len: Some(max) }, Value::Blob(bytes)) => {
971 let len = bytes.len();
972 if len > max as usize {
973 return Err(format!(
974 "blob length exceeds max_len: expected at most {max}, found {len}"
975 ));
976 }
977
978 Ok(())
979 }
980 (FieldKind::Relation { key_kind, .. }, value) => {
981 ensure_scalar_max_len_matches(*key_kind, value)
982 }
983 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
984 for item in items {
985 ensure_scalar_max_len_matches(*inner, item)?;
986 }
987
988 Ok(())
989 }
990 (
991 FieldKind::Map {
992 key,
993 value: map_value,
994 },
995 Value::Map(entries),
996 ) => {
997 for (entry_key, entry_value) in entries {
998 ensure_scalar_max_len_matches(*key, entry_key)?;
999 ensure_scalar_max_len_matches(*map_value, entry_value)?;
1000 }
1001
1002 Ok(())
1003 }
1004 _ => Ok(()),
1005 }
1006}
1007
1008fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
1011 match (kind, value) {
1012 (FieldKind::Set(_), Value::List(items)) => {
1013 for pair in items.windows(2) {
1014 let [left, right] = pair else {
1015 continue;
1016 };
1017 if Value::canonical_cmp(left, right) != Ordering::Less {
1018 return Err("set payload must already be canonical and deduplicated".into());
1019 }
1020 }
1021
1022 Ok(())
1023 }
1024 (FieldKind::Map { .. }, Value::Map(entries)) => {
1025 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1026
1027 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1028 return Err("map payload must already be canonical and deduplicated".into());
1029 }
1030
1031 Ok(())
1032 }
1033 _ => Ok(()),
1034 }
1035}
1036
1037#[cfg(test)]
1042mod tests {
1043 use crate::{
1044 model::field::{FieldKind, FieldModel},
1045 value::Value,
1046 };
1047
1048 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1049 static BOUNDED_BLOB: FieldKind = FieldKind::Blob { max_len: Some(3) };
1050
1051 #[test]
1052 fn text_max_len_accepts_unbounded_text() {
1053 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1054
1055 assert!(
1056 field
1057 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1058 .is_ok()
1059 );
1060 }
1061
1062 #[test]
1063 fn text_max_len_counts_unicode_scalars_not_bytes() {
1064 let field = FieldModel::generated("name", BOUNDED_TEXT);
1065
1066 assert!(
1067 field
1068 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1069 .is_ok()
1070 );
1071 assert!(
1072 field
1073 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1074 .is_err()
1075 );
1076 }
1077
1078 #[test]
1079 fn text_max_len_recurses_through_collections() {
1080 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1081 static TEXT_MAP: FieldKind = FieldKind::Map {
1082 key: &BOUNDED_TEXT,
1083 value: &BOUNDED_TEXT,
1084 };
1085
1086 let list_field = FieldModel::generated("names", TEXT_LIST);
1087 let map_field = FieldModel::generated("labels", TEXT_MAP);
1088
1089 assert!(
1090 list_field
1091 .validate_runtime_value_for_storage(&Value::List(vec![
1092 Value::Text("Ada".into()),
1093 Value::Text("Bob".into()),
1094 ]))
1095 .is_ok()
1096 );
1097 assert!(
1098 list_field
1099 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1100 .is_err()
1101 );
1102 assert!(
1103 map_field
1104 .validate_runtime_value_for_storage(&Value::Map(vec![(
1105 Value::Text("key".into()),
1106 Value::Text("val".into()),
1107 )]))
1108 .is_ok()
1109 );
1110 assert!(
1111 map_field
1112 .validate_runtime_value_for_storage(&Value::Map(vec![(
1113 Value::Text("long".into()),
1114 Value::Text("val".into()),
1115 )]))
1116 .is_err()
1117 );
1118 }
1119
1120 #[test]
1121 fn blob_max_len_counts_bytes() {
1122 let field = FieldModel::generated("payload", BOUNDED_BLOB);
1123
1124 assert!(
1125 field
1126 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3]))
1127 .is_ok()
1128 );
1129 assert!(
1130 field
1131 .validate_runtime_value_for_storage(&Value::Blob(vec![1, 2, 3, 4]))
1132 .is_err()
1133 );
1134 }
1135
1136 #[test]
1137 fn blob_max_len_recurses_through_collections() {
1138 static BLOB_LIST: FieldKind = FieldKind::List(&BOUNDED_BLOB);
1139
1140 let field = FieldModel::generated("payloads", BLOB_LIST);
1141
1142 assert!(
1143 field
1144 .validate_runtime_value_for_storage(&Value::List(vec![
1145 Value::Blob(vec![1, 2, 3]),
1146 Value::Blob(vec![4, 5]),
1147 ]))
1148 .is_ok()
1149 );
1150 assert!(
1151 field
1152 .validate_runtime_value_for_storage(&Value::List(vec![Value::Blob(vec![
1153 1, 2, 3, 4
1154 ])]))
1155 .is_err()
1156 );
1157 }
1158}