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_text_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 Bool,
519 Date,
520 Decimal {
521 scale: u32,
523 },
524 Duration,
525 Enum {
526 path: &'static str,
528 variants: &'static [EnumVariantModel],
530 },
531 Float32,
532 Float64,
533 Int,
534 Int128,
535 IntBig,
536 Principal,
537 Subaccount,
538 Text {
539 max_len: Option<u32>,
541 },
542 Timestamp,
543 Uint,
544 Uint128,
545 UintBig,
546 Ulid,
547 Unit,
548
549 Relation {
552 target_path: &'static str,
554 target_entity_name: &'static str,
556 target_entity_tag: EntityTag,
558 target_store_path: &'static str,
560 key_kind: &'static Self,
561 strength: RelationStrength,
562 },
563
564 List(&'static Self),
566 Set(&'static Self),
567 Map {
571 key: &'static Self,
572 value: &'static Self,
573 },
574
575 Structured {
579 queryable: bool,
580 },
581}
582
583impl FieldKind {
584 #[must_use]
585 pub const fn value_kind(&self) -> RuntimeValueKind {
586 match self {
587 Self::Account
588 | Self::Blob
589 | Self::Bool
590 | Self::Date
591 | Self::Duration
592 | Self::Enum { .. }
593 | Self::Float32
594 | Self::Float64
595 | Self::Int
596 | Self::Int128
597 | Self::IntBig
598 | Self::Principal
599 | Self::Subaccount
600 | Self::Text { .. }
601 | Self::Timestamp
602 | Self::Uint
603 | Self::Uint128
604 | Self::UintBig
605 | Self::Ulid
606 | Self::Unit
607 | Self::Decimal { .. }
608 | Self::Relation { .. } => RuntimeValueKind::Atomic,
609 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
610 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
611 Self::Structured { queryable } => RuntimeValueKind::Structured {
612 queryable: *queryable,
613 },
614 }
615 }
616
617 #[must_use]
625 pub const fn is_deterministic_collection_shape(&self) -> bool {
626 match self {
627 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
628
629 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
630
631 Self::Map { key, value } => {
632 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
633 }
634
635 _ => true,
636 }
637 }
638
639 #[must_use]
642 pub(crate) fn supports_group_probe(&self) -> bool {
643 match self {
644 Self::Enum { variants, .. } => variants.iter().all(|variant| {
645 variant
646 .payload_kind()
647 .is_none_or(Self::supports_group_probe)
648 }),
649 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
650 Self::List(_)
651 | Self::Set(_)
652 | Self::Map { .. }
653 | Self::Structured { .. }
654 | Self::Unit => false,
655 Self::Account
656 | Self::Blob
657 | Self::Bool
658 | Self::Date
659 | Self::Decimal { .. }
660 | Self::Duration
661 | Self::Float32
662 | Self::Float64
663 | Self::Int
664 | Self::Int128
665 | Self::IntBig
666 | Self::Principal
667 | Self::Subaccount
668 | Self::Text { .. }
669 | Self::Timestamp
670 | Self::Uint
671 | Self::Uint128
672 | Self::UintBig
673 | Self::Ulid => true,
674 }
675 }
676
677 #[must_use]
683 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
684 match (self, value) {
685 (Self::Account, Value::Account(_))
686 | (Self::Blob, Value::Blob(_))
687 | (Self::Bool, Value::Bool(_))
688 | (Self::Date, Value::Date(_))
689 | (Self::Decimal { .. }, Value::Decimal(_))
690 | (Self::Duration, Value::Duration(_))
691 | (Self::Enum { .. }, Value::Enum(_))
692 | (Self::Float32, Value::Float32(_))
693 | (Self::Float64, Value::Float64(_))
694 | (Self::Int, Value::Int(_))
695 | (Self::Int128, Value::Int128(_))
696 | (Self::IntBig, Value::IntBig(_))
697 | (Self::Principal, Value::Principal(_))
698 | (Self::Subaccount, Value::Subaccount(_))
699 | (Self::Text { .. }, Value::Text(_))
700 | (Self::Timestamp, Value::Timestamp(_))
701 | (Self::Uint, Value::Uint(_))
702 | (Self::Uint128, Value::Uint128(_))
703 | (Self::UintBig, Value::UintBig(_))
704 | (Self::Ulid, Value::Ulid(_))
705 | (Self::Unit, Value::Unit)
706 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
707 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
708 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
709 items.iter().all(|item| inner.accepts_value(item))
710 }
711 (Self::Map { key, value }, Value::Map(entries)) => {
712 if Value::validate_map_entries(entries.as_slice()).is_err() {
713 return false;
714 }
715
716 entries.iter().all(|(entry_key, entry_value)| {
717 key.accepts_value(entry_key) && value.accepts_value(entry_value)
718 })
719 }
720 _ => false,
721 }
722 }
723}
724
725fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
731 match (kind, value) {
732 (FieldKind::Relation { key_kind, .. }, value) => {
733 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
734 }
735 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
736 .iter()
737 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
738 (
739 FieldKind::Map {
740 key,
741 value: value_kind,
742 },
743 Value::Map(entries),
744 ) => {
745 if Value::validate_map_entries(entries.as_slice()).is_err() {
746 return false;
747 }
748
749 entries.iter().all(|(entry_key, entry_value)| {
750 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
751 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
752 })
753 }
754 (FieldKind::Structured { .. }, _) => false,
755 _ => kind.accepts_value(value),
756 }
757}
758
759fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
763 match (kind, value) {
764 (FieldKind::Structured { .. }, _) => true,
765 (FieldKind::Relation { key_kind, .. }, value) => {
766 value_storage_kind_accepts_runtime_value(*key_kind, value)
767 }
768 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
769 .iter()
770 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
771 (
772 FieldKind::Map {
773 key,
774 value: value_kind,
775 },
776 Value::Map(entries),
777 ) => {
778 if Value::validate_map_entries(entries.as_slice()).is_err() {
779 return false;
780 }
781
782 entries.iter().all(|(entry_key, entry_value)| {
783 value_storage_kind_accepts_runtime_value(*key, entry_key)
784 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
785 })
786 }
787 _ => kind.accepts_value(value),
788 }
789}
790
791fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
794 if matches!(value, Value::Null) {
795 return Ok(());
796 }
797
798 match (kind, value) {
799 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
800 if decimal.scale() != scale {
801 return Err(format!(
802 "decimal scale mismatch: expected {scale}, found {}",
803 decimal.scale()
804 ));
805 }
806
807 Ok(())
808 }
809 (FieldKind::Relation { key_kind, .. }, value) => {
810 ensure_decimal_scale_matches(*key_kind, value)
811 }
812 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
813 for item in items {
814 ensure_decimal_scale_matches(*inner, item)?;
815 }
816
817 Ok(())
818 }
819 (
820 FieldKind::Map {
821 key,
822 value: map_value,
823 },
824 Value::Map(entries),
825 ) => {
826 for (entry_key, entry_value) in entries {
827 ensure_decimal_scale_matches(*key, entry_key)?;
828 ensure_decimal_scale_matches(*map_value, entry_value)?;
829 }
830
831 Ok(())
832 }
833 _ => Ok(()),
834 }
835}
836
837fn normalize_decimal_scale_for_storage(
841 kind: FieldKind,
842 value: &Value,
843) -> Result<Cow<'_, Value>, String> {
844 if matches!(value, Value::Null) {
845 return Ok(Cow::Borrowed(value));
846 }
847
848 match (kind, value) {
849 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
850 let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
851 format!(
852 "decimal scale mismatch: expected {scale}, found {}",
853 decimal.scale()
854 )
855 })?;
856
857 if normalized.scale() == decimal.scale() {
858 Ok(Cow::Borrowed(value))
859 } else {
860 Ok(Cow::Owned(Value::Decimal(normalized)))
861 }
862 }
863 (FieldKind::Relation { key_kind, .. }, value) => {
864 normalize_decimal_scale_for_storage(*key_kind, value)
865 }
866 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
867 normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
868 items.map_or_else(
869 || Cow::Borrowed(value),
870 |items| Cow::Owned(Value::List(items)),
871 )
872 })
873 }
874 (
875 FieldKind::Map {
876 key,
877 value: map_value,
878 },
879 Value::Map(entries),
880 ) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
881 entries.map_or_else(
882 || Cow::Borrowed(value),
883 |entries| Cow::Owned(Value::Map(entries)),
884 )
885 }),
886 _ => Ok(Cow::Borrowed(value)),
887 }
888}
889
890fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
894 match decimal.scale().cmp(&scale) {
895 Ordering::Equal => Some(decimal),
896 Ordering::Less => decimal
897 .scale_to_integer(scale)
898 .map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
899 Ordering::Greater => Some(decimal.round_dp(scale)),
900 }
901}
902
903fn normalize_decimal_list_items(
906 kind: FieldKind,
907 items: &[Value],
908) -> Result<Option<Vec<Value>>, String> {
909 let mut normalized_items = None;
910
911 for (index, item) in items.iter().enumerate() {
912 let normalized = normalize_decimal_scale_for_storage(kind, item)?;
913 if let Cow::Owned(value) = normalized {
914 let items = normalized_items.get_or_insert_with(|| items.to_vec());
915 items[index] = value;
916 }
917 }
918
919 Ok(normalized_items)
920}
921
922fn normalize_decimal_map_entries(
925 key_kind: FieldKind,
926 value_kind: FieldKind,
927 entries: &[(Value, Value)],
928) -> Result<Option<Vec<(Value, Value)>>, String> {
929 let mut normalized_entries = None;
930
931 for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
932 let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
933 let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
934
935 if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
936 let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
937 if let Cow::Owned(value) = normalized_key {
938 entries[index].0 = value;
939 }
940 if let Cow::Owned(value) = normalized_value {
941 entries[index].1 = value;
942 }
943 }
944 }
945
946 Ok(normalized_entries)
947}
948
949fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
952 if matches!(value, Value::Null) {
953 return Ok(());
954 }
955
956 match (kind, value) {
957 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
958 let len = text.chars().count();
959 if len > max as usize {
960 return Err(format!(
961 "text length exceeds max_len: expected at most {max}, found {len}"
962 ));
963 }
964
965 Ok(())
966 }
967 (FieldKind::Relation { key_kind, .. }, value) => {
968 ensure_text_max_len_matches(*key_kind, value)
969 }
970 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
971 for item in items {
972 ensure_text_max_len_matches(*inner, item)?;
973 }
974
975 Ok(())
976 }
977 (
978 FieldKind::Map {
979 key,
980 value: map_value,
981 },
982 Value::Map(entries),
983 ) => {
984 for (entry_key, entry_value) in entries {
985 ensure_text_max_len_matches(*key, entry_key)?;
986 ensure_text_max_len_matches(*map_value, entry_value)?;
987 }
988
989 Ok(())
990 }
991 _ => Ok(()),
992 }
993}
994
995fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
998 match (kind, value) {
999 (FieldKind::Set(_), Value::List(items)) => {
1000 for pair in items.windows(2) {
1001 let [left, right] = pair else {
1002 continue;
1003 };
1004 if Value::canonical_cmp(left, right) != Ordering::Less {
1005 return Err("set payload must already be canonical and deduplicated".into());
1006 }
1007 }
1008
1009 Ok(())
1010 }
1011 (FieldKind::Map { .. }, Value::Map(entries)) => {
1012 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
1013
1014 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
1015 return Err("map payload must already be canonical and deduplicated".into());
1016 }
1017
1018 Ok(())
1019 }
1020 _ => Ok(()),
1021 }
1022}
1023
1024#[cfg(test)]
1029mod tests {
1030 use crate::{
1031 model::field::{FieldKind, FieldModel},
1032 value::Value,
1033 };
1034
1035 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
1036
1037 #[test]
1038 fn text_max_len_accepts_unbounded_text() {
1039 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
1040
1041 assert!(
1042 field
1043 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
1044 .is_ok()
1045 );
1046 }
1047
1048 #[test]
1049 fn text_max_len_counts_unicode_scalars_not_bytes() {
1050 let field = FieldModel::generated("name", BOUNDED_TEXT);
1051
1052 assert!(
1053 field
1054 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
1055 .is_ok()
1056 );
1057 assert!(
1058 field
1059 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
1060 .is_err()
1061 );
1062 }
1063
1064 #[test]
1065 fn text_max_len_recurses_through_collections() {
1066 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
1067 static TEXT_MAP: FieldKind = FieldKind::Map {
1068 key: &BOUNDED_TEXT,
1069 value: &BOUNDED_TEXT,
1070 };
1071
1072 let list_field = FieldModel::generated("names", TEXT_LIST);
1073 let map_field = FieldModel::generated("labels", TEXT_MAP);
1074
1075 assert!(
1076 list_field
1077 .validate_runtime_value_for_storage(&Value::List(vec![
1078 Value::Text("Ada".into()),
1079 Value::Text("Bob".into()),
1080 ]))
1081 .is_ok()
1082 );
1083 assert!(
1084 list_field
1085 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
1086 .is_err()
1087 );
1088 assert!(
1089 map_field
1090 .validate_runtime_value_for_storage(&Value::Map(vec![(
1091 Value::Text("key".into()),
1092 Value::Text("val".into()),
1093 )]))
1094 .is_ok()
1095 );
1096 assert!(
1097 map_field
1098 .validate_runtime_value_for_storage(&Value::Map(vec![(
1099 Value::Text("long".into()),
1100 Value::Text("val".into()),
1101 )]))
1102 .is_err()
1103 );
1104 }
1105}