1use crate::{traits::RuntimeValueKind, types::EntityTag, value::Value};
7use std::cmp::Ordering;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum FieldStorageDecode {
21 ByKind,
23 Value,
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ScalarCodec {
38 Blob,
39 Bool,
40 Date,
41 Duration,
42 Float32,
43 Float64,
44 Int64,
45 Principal,
46 Subaccount,
47 Text,
48 Timestamp,
49 Uint64,
50 Ulid,
51 Unit,
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub enum LeafCodec {
65 Scalar(ScalarCodec),
66 StructuralFallback,
67}
68
69#[derive(Clone, Copy, Debug)]
79pub struct EnumVariantModel {
80 pub(crate) ident: &'static str,
82 pub(crate) payload_kind: Option<&'static FieldKind>,
84 pub(crate) payload_storage_decode: FieldStorageDecode,
86}
87
88impl EnumVariantModel {
89 #[must_use]
91 pub const fn new(
92 ident: &'static str,
93 payload_kind: Option<&'static FieldKind>,
94 payload_storage_decode: FieldStorageDecode,
95 ) -> Self {
96 Self {
97 ident,
98 payload_kind,
99 payload_storage_decode,
100 }
101 }
102
103 #[must_use]
105 pub const fn ident(&self) -> &'static str {
106 self.ident
107 }
108
109 #[must_use]
111 pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
112 self.payload_kind
113 }
114
115 #[must_use]
117 pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
118 self.payload_storage_decode
119 }
120}
121
122#[derive(Debug)]
132pub struct FieldModel {
133 pub(crate) name: &'static str,
135 pub(crate) kind: FieldKind,
137 pub(crate) nullable: bool,
139 pub(crate) storage_decode: FieldStorageDecode,
141 pub(crate) leaf_codec: LeafCodec,
143 pub(crate) insert_generation: Option<FieldInsertGeneration>,
145 pub(crate) write_management: Option<FieldWriteManagement>,
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum FieldInsertGeneration {
160 Ulid,
162 Timestamp,
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum FieldWriteManagement {
177 CreatedAt,
179 UpdatedAt,
181}
182
183impl FieldModel {
184 #[must_use]
190 #[doc(hidden)]
191 pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
192 Self::generated_with_storage_decode_and_nullability(
193 name,
194 kind,
195 FieldStorageDecode::ByKind,
196 false,
197 )
198 }
199
200 #[must_use]
202 #[doc(hidden)]
203 pub const fn generated_with_storage_decode(
204 name: &'static str,
205 kind: FieldKind,
206 storage_decode: FieldStorageDecode,
207 ) -> Self {
208 Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
209 }
210
211 #[must_use]
213 #[doc(hidden)]
214 pub const fn generated_with_storage_decode_and_nullability(
215 name: &'static str,
216 kind: FieldKind,
217 storage_decode: FieldStorageDecode,
218 nullable: bool,
219 ) -> Self {
220 Self::generated_with_storage_decode_nullability_and_write_policies(
221 name,
222 kind,
223 storage_decode,
224 nullable,
225 None,
226 None,
227 )
228 }
229
230 #[must_use]
233 #[doc(hidden)]
234 pub const fn generated_with_storage_decode_nullability_and_insert_generation(
235 name: &'static str,
236 kind: FieldKind,
237 storage_decode: FieldStorageDecode,
238 nullable: bool,
239 insert_generation: Option<FieldInsertGeneration>,
240 ) -> Self {
241 Self::generated_with_storage_decode_nullability_and_write_policies(
242 name,
243 kind,
244 storage_decode,
245 nullable,
246 insert_generation,
247 None,
248 )
249 }
250
251 #[must_use]
254 #[doc(hidden)]
255 pub const fn generated_with_storage_decode_nullability_and_write_policies(
256 name: &'static str,
257 kind: FieldKind,
258 storage_decode: FieldStorageDecode,
259 nullable: bool,
260 insert_generation: Option<FieldInsertGeneration>,
261 write_management: Option<FieldWriteManagement>,
262 ) -> Self {
263 Self {
264 name,
265 kind,
266 nullable,
267 storage_decode,
268 leaf_codec: leaf_codec_for(kind, storage_decode),
269 insert_generation,
270 write_management,
271 }
272 }
273
274 #[must_use]
276 pub const fn name(&self) -> &'static str {
277 self.name
278 }
279
280 #[must_use]
282 pub const fn kind(&self) -> FieldKind {
283 self.kind
284 }
285
286 #[must_use]
288 pub const fn nullable(&self) -> bool {
289 self.nullable
290 }
291
292 #[must_use]
294 pub const fn storage_decode(&self) -> FieldStorageDecode {
295 self.storage_decode
296 }
297
298 #[must_use]
300 pub const fn leaf_codec(&self) -> LeafCodec {
301 self.leaf_codec
302 }
303
304 #[must_use]
306 pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
307 self.insert_generation
308 }
309
310 #[must_use]
312 pub const fn write_management(&self) -> Option<FieldWriteManagement> {
313 self.write_management
314 }
315
316 pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
324 if matches!(value, Value::Null) {
325 if self.nullable() {
326 return Ok(());
327 }
328
329 return Err("required field cannot store null".into());
330 }
331
332 let accepts = match self.storage_decode() {
333 FieldStorageDecode::Value => {
334 value_storage_kind_accepts_runtime_value(self.kind(), value)
335 }
336 FieldStorageDecode::ByKind => {
337 by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
338 }
339 };
340 if !accepts {
341 return Err(format!(
342 "field kind {:?} does not accept runtime value {value:?}",
343 self.kind()
344 ));
345 }
346
347 ensure_decimal_scale_matches(self.kind(), value)?;
348 ensure_text_max_len_matches(self.kind(), value)?;
349 ensure_value_is_deterministic_for_storage(self.kind(), value)
350 }
351}
352
353const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
357 if matches!(storage_decode, FieldStorageDecode::Value) {
358 return LeafCodec::StructuralFallback;
359 }
360
361 match kind {
362 FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
363 FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
364 FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
365 FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
366 FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
367 FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
368 FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
369 FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
370 FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
371 FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
372 FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
373 FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
374 FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
375 FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
376 FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
377 FieldKind::Account
378 | FieldKind::Decimal { .. }
379 | FieldKind::Enum { .. }
380 | FieldKind::Int128
381 | FieldKind::IntBig
382 | FieldKind::List(_)
383 | FieldKind::Map { .. }
384 | FieldKind::Set(_)
385 | FieldKind::Structured { .. }
386 | FieldKind::Uint128
387 | FieldKind::UintBig => LeafCodec::StructuralFallback,
388 }
389}
390
391#[derive(Clone, Copy, Debug, Eq, PartialEq)]
398pub enum RelationStrength {
399 Strong,
400 Weak,
401}
402
403#[derive(Clone, Copy, Debug)]
413pub enum FieldKind {
414 Account,
416 Blob,
417 Bool,
418 Date,
419 Decimal {
420 scale: u32,
422 },
423 Duration,
424 Enum {
425 path: &'static str,
427 variants: &'static [EnumVariantModel],
429 },
430 Float32,
431 Float64,
432 Int,
433 Int128,
434 IntBig,
435 Principal,
436 Subaccount,
437 Text {
438 max_len: Option<u32>,
440 },
441 Timestamp,
442 Uint,
443 Uint128,
444 UintBig,
445 Ulid,
446 Unit,
447
448 Relation {
451 target_path: &'static str,
453 target_entity_name: &'static str,
455 target_entity_tag: EntityTag,
457 target_store_path: &'static str,
459 key_kind: &'static Self,
460 strength: RelationStrength,
461 },
462
463 List(&'static Self),
465 Set(&'static Self),
466 Map {
470 key: &'static Self,
471 value: &'static Self,
472 },
473
474 Structured {
478 queryable: bool,
479 },
480}
481
482impl FieldKind {
483 #[must_use]
484 pub const fn value_kind(&self) -> RuntimeValueKind {
485 match self {
486 Self::Account
487 | Self::Blob
488 | Self::Bool
489 | Self::Date
490 | Self::Duration
491 | Self::Enum { .. }
492 | Self::Float32
493 | Self::Float64
494 | Self::Int
495 | Self::Int128
496 | Self::IntBig
497 | Self::Principal
498 | Self::Subaccount
499 | Self::Text { .. }
500 | Self::Timestamp
501 | Self::Uint
502 | Self::Uint128
503 | Self::UintBig
504 | Self::Ulid
505 | Self::Unit
506 | Self::Decimal { .. }
507 | Self::Relation { .. } => RuntimeValueKind::Atomic,
508 Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
509 Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
510 Self::Structured { queryable } => RuntimeValueKind::Structured {
511 queryable: *queryable,
512 },
513 }
514 }
515
516 #[must_use]
524 pub const fn is_deterministic_collection_shape(&self) -> bool {
525 match self {
526 Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
527
528 Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
529
530 Self::Map { key, value } => {
531 key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
532 }
533
534 _ => true,
535 }
536 }
537
538 #[must_use]
541 pub(crate) fn supports_group_probe(&self) -> bool {
542 match self {
543 Self::Enum { variants, .. } => variants.iter().all(|variant| {
544 variant
545 .payload_kind()
546 .is_none_or(Self::supports_group_probe)
547 }),
548 Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
549 Self::List(_)
550 | Self::Set(_)
551 | Self::Map { .. }
552 | Self::Structured { .. }
553 | Self::Unit => false,
554 Self::Account
555 | Self::Blob
556 | Self::Bool
557 | Self::Date
558 | Self::Decimal { .. }
559 | Self::Duration
560 | Self::Float32
561 | Self::Float64
562 | Self::Int
563 | Self::Int128
564 | Self::IntBig
565 | Self::Principal
566 | Self::Subaccount
567 | Self::Text { .. }
568 | Self::Timestamp
569 | Self::Uint
570 | Self::Uint128
571 | Self::UintBig
572 | Self::Ulid => true,
573 }
574 }
575
576 #[must_use]
582 pub(crate) fn accepts_value(&self, value: &Value) -> bool {
583 match (self, value) {
584 (Self::Account, Value::Account(_))
585 | (Self::Blob, Value::Blob(_))
586 | (Self::Bool, Value::Bool(_))
587 | (Self::Date, Value::Date(_))
588 | (Self::Decimal { .. }, Value::Decimal(_))
589 | (Self::Duration, Value::Duration(_))
590 | (Self::Enum { .. }, Value::Enum(_))
591 | (Self::Float32, Value::Float32(_))
592 | (Self::Float64, Value::Float64(_))
593 | (Self::Int, Value::Int(_))
594 | (Self::Int128, Value::Int128(_))
595 | (Self::IntBig, Value::IntBig(_))
596 | (Self::Principal, Value::Principal(_))
597 | (Self::Subaccount, Value::Subaccount(_))
598 | (Self::Text { .. }, Value::Text(_))
599 | (Self::Timestamp, Value::Timestamp(_))
600 | (Self::Uint, Value::Uint(_))
601 | (Self::Uint128, Value::Uint128(_))
602 | (Self::UintBig, Value::UintBig(_))
603 | (Self::Ulid, Value::Ulid(_))
604 | (Self::Unit, Value::Unit)
605 | (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
606 (Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
607 (Self::List(inner) | Self::Set(inner), Value::List(items)) => {
608 items.iter().all(|item| inner.accepts_value(item))
609 }
610 (Self::Map { key, value }, Value::Map(entries)) => {
611 if Value::validate_map_entries(entries.as_slice()).is_err() {
612 return false;
613 }
614
615 entries.iter().all(|(entry_key, entry_value)| {
616 key.accepts_value(entry_key) && value.accepts_value(entry_value)
617 })
618 }
619 _ => false,
620 }
621 }
622}
623
624fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
630 match (kind, value) {
631 (FieldKind::Relation { key_kind, .. }, value) => {
632 by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
633 }
634 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
635 .iter()
636 .all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
637 (
638 FieldKind::Map {
639 key,
640 value: value_kind,
641 },
642 Value::Map(entries),
643 ) => {
644 if Value::validate_map_entries(entries.as_slice()).is_err() {
645 return false;
646 }
647
648 entries.iter().all(|(entry_key, entry_value)| {
649 by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
650 && by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
651 })
652 }
653 (FieldKind::Structured { .. }, _) => false,
654 _ => kind.accepts_value(value),
655 }
656}
657
658fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
662 match (kind, value) {
663 (FieldKind::Structured { .. }, _) => true,
664 (FieldKind::Relation { key_kind, .. }, value) => {
665 value_storage_kind_accepts_runtime_value(*key_kind, value)
666 }
667 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
668 .iter()
669 .all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
670 (
671 FieldKind::Map {
672 key,
673 value: value_kind,
674 },
675 Value::Map(entries),
676 ) => {
677 if Value::validate_map_entries(entries.as_slice()).is_err() {
678 return false;
679 }
680
681 entries.iter().all(|(entry_key, entry_value)| {
682 value_storage_kind_accepts_runtime_value(*key, entry_key)
683 && value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
684 })
685 }
686 _ => kind.accepts_value(value),
687 }
688}
689
690fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
693 if matches!(value, Value::Null) {
694 return Ok(());
695 }
696
697 match (kind, value) {
698 (FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
699 if decimal.scale() != scale {
700 return Err(format!(
701 "decimal scale mismatch: expected {scale}, found {}",
702 decimal.scale()
703 ));
704 }
705
706 Ok(())
707 }
708 (FieldKind::Relation { key_kind, .. }, value) => {
709 ensure_decimal_scale_matches(*key_kind, value)
710 }
711 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
712 for item in items {
713 ensure_decimal_scale_matches(*inner, item)?;
714 }
715
716 Ok(())
717 }
718 (
719 FieldKind::Map {
720 key,
721 value: map_value,
722 },
723 Value::Map(entries),
724 ) => {
725 for (entry_key, entry_value) in entries {
726 ensure_decimal_scale_matches(*key, entry_key)?;
727 ensure_decimal_scale_matches(*map_value, entry_value)?;
728 }
729
730 Ok(())
731 }
732 _ => Ok(()),
733 }
734}
735
736fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
739 if matches!(value, Value::Null) {
740 return Ok(());
741 }
742
743 match (kind, value) {
744 (FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
745 let len = text.chars().count();
746 if len > max as usize {
747 return Err(format!(
748 "text length exceeds max_len: expected at most {max}, found {len}"
749 ));
750 }
751
752 Ok(())
753 }
754 (FieldKind::Relation { key_kind, .. }, value) => {
755 ensure_text_max_len_matches(*key_kind, value)
756 }
757 (FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
758 for item in items {
759 ensure_text_max_len_matches(*inner, item)?;
760 }
761
762 Ok(())
763 }
764 (
765 FieldKind::Map {
766 key,
767 value: map_value,
768 },
769 Value::Map(entries),
770 ) => {
771 for (entry_key, entry_value) in entries {
772 ensure_text_max_len_matches(*key, entry_key)?;
773 ensure_text_max_len_matches(*map_value, entry_value)?;
774 }
775
776 Ok(())
777 }
778 _ => Ok(()),
779 }
780}
781
782fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
785 match (kind, value) {
786 (FieldKind::Set(_), Value::List(items)) => {
787 for pair in items.windows(2) {
788 let [left, right] = pair else {
789 continue;
790 };
791 if Value::canonical_cmp(left, right) != Ordering::Less {
792 return Err("set payload must already be canonical and deduplicated".into());
793 }
794 }
795
796 Ok(())
797 }
798 (FieldKind::Map { .. }, Value::Map(entries)) => {
799 Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
800
801 if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
802 return Err("map payload must already be canonical and deduplicated".into());
803 }
804
805 Ok(())
806 }
807 _ => Ok(()),
808 }
809}
810
811#[cfg(test)]
816mod tests {
817 use crate::{
818 model::field::{FieldKind, FieldModel},
819 value::Value,
820 };
821
822 static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
823
824 #[test]
825 fn text_max_len_accepts_unbounded_text() {
826 let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
827
828 assert!(
829 field
830 .validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
831 .is_ok()
832 );
833 }
834
835 #[test]
836 fn text_max_len_counts_unicode_scalars_not_bytes() {
837 let field = FieldModel::generated("name", BOUNDED_TEXT);
838
839 assert!(
840 field
841 .validate_runtime_value_for_storage(&Value::Text("ééé".into()))
842 .is_ok()
843 );
844 assert!(
845 field
846 .validate_runtime_value_for_storage(&Value::Text("éééé".into()))
847 .is_err()
848 );
849 }
850
851 #[test]
852 fn text_max_len_recurses_through_collections() {
853 static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
854 static TEXT_MAP: FieldKind = FieldKind::Map {
855 key: &BOUNDED_TEXT,
856 value: &BOUNDED_TEXT,
857 };
858
859 let list_field = FieldModel::generated("names", TEXT_LIST);
860 let map_field = FieldModel::generated("labels", TEXT_MAP);
861
862 assert!(
863 list_field
864 .validate_runtime_value_for_storage(&Value::List(vec![
865 Value::Text("Ada".into()),
866 Value::Text("Bob".into()),
867 ]))
868 .is_ok()
869 );
870 assert!(
871 list_field
872 .validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
873 .is_err()
874 );
875 assert!(
876 map_field
877 .validate_runtime_value_for_storage(&Value::Map(vec![(
878 Value::Text("key".into()),
879 Value::Text("val".into()),
880 )]))
881 .is_ok()
882 );
883 assert!(
884 map_field
885 .validate_runtime_value_for_storage(&Value::Map(vec![(
886 Value::Text("long".into()),
887 Value::Text("val".into()),
888 )]))
889 .is_err()
890 );
891 }
892}