use crate::{
traits::RuntimeValueKind,
types::{Decimal, EntityTag},
value::Value,
};
use std::{borrow::Cow, cmp::Ordering};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldStorageDecode {
ByKind,
Value,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScalarCodec {
Blob,
Bool,
Date,
Duration,
Float32,
Float64,
Int64,
Principal,
Subaccount,
Text,
Timestamp,
Uint64,
Ulid,
Unit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LeafCodec {
Scalar(ScalarCodec),
StructuralFallback,
}
#[derive(Clone, Copy, Debug)]
pub struct EnumVariantModel {
pub(crate) ident: &'static str,
pub(crate) payload_kind: Option<&'static FieldKind>,
pub(crate) payload_storage_decode: FieldStorageDecode,
}
impl EnumVariantModel {
#[must_use]
pub const fn new(
ident: &'static str,
payload_kind: Option<&'static FieldKind>,
payload_storage_decode: FieldStorageDecode,
) -> Self {
Self {
ident,
payload_kind,
payload_storage_decode,
}
}
#[must_use]
pub const fn ident(&self) -> &'static str {
self.ident
}
#[must_use]
pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
self.payload_kind
}
#[must_use]
pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
self.payload_storage_decode
}
}
#[derive(Debug)]
pub struct FieldModel {
pub(crate) name: &'static str,
pub(crate) kind: FieldKind,
pub(crate) nested_fields: &'static [Self],
pub(crate) nullable: bool,
pub(crate) storage_decode: FieldStorageDecode,
pub(crate) leaf_codec: LeafCodec,
pub(crate) insert_generation: Option<FieldInsertGeneration>,
pub(crate) write_management: Option<FieldWriteManagement>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldInsertGeneration {
Ulid,
Timestamp,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldWriteManagement {
CreatedAt,
UpdatedAt,
}
impl FieldModel {
#[must_use]
#[doc(hidden)]
pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
Self::generated_with_storage_decode_and_nullability(
name,
kind,
FieldStorageDecode::ByKind,
false,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
) -> Self {
Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_and_nullability(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
) -> Self {
Self::generated_with_storage_decode_nullability_and_write_policies(
name,
kind,
storage_decode,
nullable,
None,
None,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_nullability_and_insert_generation(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
insert_generation: Option<FieldInsertGeneration>,
) -> Self {
Self::generated_with_storage_decode_nullability_and_write_policies(
name,
kind,
storage_decode,
nullable,
insert_generation,
None,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_nullability_and_write_policies(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
insert_generation: Option<FieldInsertGeneration>,
write_management: Option<FieldWriteManagement>,
) -> Self {
Self {
name,
kind,
nested_fields: &[],
nullable,
storage_decode,
leaf_codec: leaf_codec_for(kind, storage_decode),
insert_generation,
write_management,
}
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_nullability_write_policies_and_nested_fields(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
insert_generation: Option<FieldInsertGeneration>,
write_management: Option<FieldWriteManagement>,
nested_fields: &'static [Self],
) -> Self {
Self {
name,
kind,
nested_fields,
nullable,
storage_decode,
leaf_codec: leaf_codec_for(kind, storage_decode),
insert_generation,
write_management,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn kind(&self) -> FieldKind {
self.kind
}
#[must_use]
pub const fn nested_fields(&self) -> &'static [Self] {
self.nested_fields
}
#[must_use]
pub const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub const fn storage_decode(&self) -> FieldStorageDecode {
self.storage_decode
}
#[must_use]
pub const fn leaf_codec(&self) -> LeafCodec {
self.leaf_codec
}
#[must_use]
pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
self.insert_generation
}
#[must_use]
pub const fn write_management(&self) -> Option<FieldWriteManagement> {
self.write_management
}
pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
if matches!(value, Value::Null) {
if self.nullable() {
return Ok(());
}
return Err("required field cannot store null".into());
}
let accepts = match self.storage_decode() {
FieldStorageDecode::Value => {
value_storage_kind_accepts_runtime_value(self.kind(), value)
}
FieldStorageDecode::ByKind => {
by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
}
};
if !accepts {
return Err(format!(
"field kind {:?} does not accept runtime value {value:?}",
self.kind()
));
}
ensure_decimal_scale_matches(self.kind(), value)?;
ensure_text_max_len_matches(self.kind(), value)?;
ensure_value_is_deterministic_for_storage(self.kind(), value)
}
pub(crate) fn normalize_runtime_value_for_storage<'a>(
&self,
value: &'a Value,
) -> Result<Cow<'a, Value>, String> {
normalize_decimal_scale_for_storage(self.kind(), value)
}
}
const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
if matches!(storage_decode, FieldStorageDecode::Value) {
return LeafCodec::StructuralFallback;
}
match kind {
FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
FieldKind::Text { .. } => LeafCodec::Scalar(ScalarCodec::Text),
FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
FieldKind::Account
| FieldKind::Decimal { .. }
| FieldKind::Enum { .. }
| FieldKind::Int128
| FieldKind::IntBig
| FieldKind::List(_)
| FieldKind::Map { .. }
| FieldKind::Set(_)
| FieldKind::Structured { .. }
| FieldKind::Uint128
| FieldKind::UintBig => LeafCodec::StructuralFallback,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RelationStrength {
Strong,
Weak,
}
#[derive(Clone, Copy, Debug)]
pub enum FieldKind {
Account,
Blob,
Bool,
Date,
Decimal {
scale: u32,
},
Duration,
Enum {
path: &'static str,
variants: &'static [EnumVariantModel],
},
Float32,
Float64,
Int,
Int128,
IntBig,
Principal,
Subaccount,
Text {
max_len: Option<u32>,
},
Timestamp,
Uint,
Uint128,
UintBig,
Ulid,
Unit,
Relation {
target_path: &'static str,
target_entity_name: &'static str,
target_entity_tag: EntityTag,
target_store_path: &'static str,
key_kind: &'static Self,
strength: RelationStrength,
},
List(&'static Self),
Set(&'static Self),
Map {
key: &'static Self,
value: &'static Self,
},
Structured {
queryable: bool,
},
}
impl FieldKind {
#[must_use]
pub const fn value_kind(&self) -> RuntimeValueKind {
match self {
Self::Account
| Self::Blob
| Self::Bool
| Self::Date
| Self::Duration
| Self::Enum { .. }
| Self::Float32
| Self::Float64
| Self::Int
| Self::Int128
| Self::IntBig
| Self::Principal
| Self::Subaccount
| Self::Text { .. }
| Self::Timestamp
| Self::Uint
| Self::Uint128
| Self::UintBig
| Self::Ulid
| Self::Unit
| Self::Decimal { .. }
| Self::Relation { .. } => RuntimeValueKind::Atomic,
Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
Self::Structured { queryable } => RuntimeValueKind::Structured {
queryable: *queryable,
},
}
}
#[must_use]
pub const fn is_deterministic_collection_shape(&self) -> bool {
match self {
Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
Self::Map { key, value } => {
key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
}
_ => true,
}
}
#[must_use]
pub(crate) fn supports_group_probe(&self) -> bool {
match self {
Self::Enum { variants, .. } => variants.iter().all(|variant| {
variant
.payload_kind()
.is_none_or(Self::supports_group_probe)
}),
Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
Self::List(_)
| Self::Set(_)
| Self::Map { .. }
| Self::Structured { .. }
| Self::Unit => false,
Self::Account
| Self::Blob
| Self::Bool
| Self::Date
| Self::Decimal { .. }
| Self::Duration
| Self::Float32
| Self::Float64
| Self::Int
| Self::Int128
| Self::IntBig
| Self::Principal
| Self::Subaccount
| Self::Text { .. }
| Self::Timestamp
| Self::Uint
| Self::Uint128
| Self::UintBig
| Self::Ulid => true,
}
}
#[must_use]
pub(crate) fn accepts_value(&self, value: &Value) -> bool {
match (self, value) {
(Self::Account, Value::Account(_))
| (Self::Blob, Value::Blob(_))
| (Self::Bool, Value::Bool(_))
| (Self::Date, Value::Date(_))
| (Self::Decimal { .. }, Value::Decimal(_))
| (Self::Duration, Value::Duration(_))
| (Self::Enum { .. }, Value::Enum(_))
| (Self::Float32, Value::Float32(_))
| (Self::Float64, Value::Float64(_))
| (Self::Int, Value::Int(_))
| (Self::Int128, Value::Int128(_))
| (Self::IntBig, Value::IntBig(_))
| (Self::Principal, Value::Principal(_))
| (Self::Subaccount, Value::Subaccount(_))
| (Self::Text { .. }, Value::Text(_))
| (Self::Timestamp, Value::Timestamp(_))
| (Self::Uint, Value::Uint(_))
| (Self::Uint128, Value::Uint128(_))
| (Self::UintBig, Value::UintBig(_))
| (Self::Ulid, Value::Ulid(_))
| (Self::Unit, Value::Unit)
| (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
(Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
(Self::List(inner) | Self::Set(inner), Value::List(items)) => {
items.iter().all(|item| inner.accepts_value(item))
}
(Self::Map { key, value }, Value::Map(entries)) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
key.accepts_value(entry_key) && value.accepts_value(entry_value)
})
}
_ => false,
}
}
}
fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
match (kind, value) {
(FieldKind::Relation { key_kind, .. }, value) => {
by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
.iter()
.all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
(
FieldKind::Map {
key,
value: value_kind,
},
Value::Map(entries),
) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
&& by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
})
}
(FieldKind::Structured { .. }, _) => false,
_ => kind.accepts_value(value),
}
}
fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
match (kind, value) {
(FieldKind::Structured { .. }, _) => true,
(FieldKind::Relation { key_kind, .. }, value) => {
value_storage_kind_accepts_runtime_value(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
.iter()
.all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
(
FieldKind::Map {
key,
value: value_kind,
},
Value::Map(entries),
) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
value_storage_kind_accepts_runtime_value(*key, entry_key)
&& value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
})
}
_ => kind.accepts_value(value),
}
}
fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
if matches!(value, Value::Null) {
return Ok(());
}
match (kind, value) {
(FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
if decimal.scale() != scale {
return Err(format!(
"decimal scale mismatch: expected {scale}, found {}",
decimal.scale()
));
}
Ok(())
}
(FieldKind::Relation { key_kind, .. }, value) => {
ensure_decimal_scale_matches(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
for item in items {
ensure_decimal_scale_matches(*inner, item)?;
}
Ok(())
}
(
FieldKind::Map {
key,
value: map_value,
},
Value::Map(entries),
) => {
for (entry_key, entry_value) in entries {
ensure_decimal_scale_matches(*key, entry_key)?;
ensure_decimal_scale_matches(*map_value, entry_value)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn normalize_decimal_scale_for_storage(
kind: FieldKind,
value: &Value,
) -> Result<Cow<'_, Value>, String> {
if matches!(value, Value::Null) {
return Ok(Cow::Borrowed(value));
}
match (kind, value) {
(FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
let normalized = decimal_with_storage_scale(*decimal, scale).ok_or_else(|| {
format!(
"decimal scale mismatch: expected {scale}, found {}",
decimal.scale()
)
})?;
if normalized.scale() == decimal.scale() {
Ok(Cow::Borrowed(value))
} else {
Ok(Cow::Owned(Value::Decimal(normalized)))
}
}
(FieldKind::Relation { key_kind, .. }, value) => {
normalize_decimal_scale_for_storage(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
normalize_decimal_list_items(*inner, items.as_slice()).map(|items| {
items.map_or_else(
|| Cow::Borrowed(value),
|items| Cow::Owned(Value::List(items)),
)
})
}
(
FieldKind::Map {
key,
value: map_value,
},
Value::Map(entries),
) => normalize_decimal_map_entries(*key, *map_value, entries.as_slice()).map(|entries| {
entries.map_or_else(
|| Cow::Borrowed(value),
|entries| Cow::Owned(Value::Map(entries)),
)
}),
_ => Ok(Cow::Borrowed(value)),
}
}
fn decimal_with_storage_scale(decimal: Decimal, scale: u32) -> Option<Decimal> {
match decimal.scale().cmp(&scale) {
Ordering::Equal => Some(decimal),
Ordering::Less => decimal
.scale_to_integer(scale)
.map(|mantissa| Decimal::from_i128_with_scale(mantissa, scale)),
Ordering::Greater => Some(decimal.round_dp(scale)),
}
}
fn normalize_decimal_list_items(
kind: FieldKind,
items: &[Value],
) -> Result<Option<Vec<Value>>, String> {
let mut normalized_items = None;
for (index, item) in items.iter().enumerate() {
let normalized = normalize_decimal_scale_for_storage(kind, item)?;
if let Cow::Owned(value) = normalized {
let items = normalized_items.get_or_insert_with(|| items.to_vec());
items[index] = value;
}
}
Ok(normalized_items)
}
fn normalize_decimal_map_entries(
key_kind: FieldKind,
value_kind: FieldKind,
entries: &[(Value, Value)],
) -> Result<Option<Vec<(Value, Value)>>, String> {
let mut normalized_entries = None;
for (index, (entry_key, entry_value)) in entries.iter().enumerate() {
let normalized_key = normalize_decimal_scale_for_storage(key_kind, entry_key)?;
let normalized_value = normalize_decimal_scale_for_storage(value_kind, entry_value)?;
if matches!(normalized_key, Cow::Owned(_)) || matches!(normalized_value, Cow::Owned(_)) {
let entries = normalized_entries.get_or_insert_with(|| entries.to_vec());
if let Cow::Owned(value) = normalized_key {
entries[index].0 = value;
}
if let Cow::Owned(value) = normalized_value {
entries[index].1 = value;
}
}
}
Ok(normalized_entries)
}
fn ensure_text_max_len_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
if matches!(value, Value::Null) {
return Ok(());
}
match (kind, value) {
(FieldKind::Text { max_len: Some(max) }, Value::Text(text)) => {
let len = text.chars().count();
if len > max as usize {
return Err(format!(
"text length exceeds max_len: expected at most {max}, found {len}"
));
}
Ok(())
}
(FieldKind::Relation { key_kind, .. }, value) => {
ensure_text_max_len_matches(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
for item in items {
ensure_text_max_len_matches(*inner, item)?;
}
Ok(())
}
(
FieldKind::Map {
key,
value: map_value,
},
Value::Map(entries),
) => {
for (entry_key, entry_value) in entries {
ensure_text_max_len_matches(*key, entry_key)?;
ensure_text_max_len_matches(*map_value, entry_value)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
match (kind, value) {
(FieldKind::Set(_), Value::List(items)) => {
for pair in items.windows(2) {
let [left, right] = pair else {
continue;
};
if Value::canonical_cmp(left, right) != Ordering::Less {
return Err("set payload must already be canonical and deduplicated".into());
}
}
Ok(())
}
(FieldKind::Map { .. }, Value::Map(entries)) => {
Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
return Err("map payload must already be canonical and deduplicated".into());
}
Ok(())
}
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use crate::{
model::field::{FieldKind, FieldModel},
value::Value,
};
static BOUNDED_TEXT: FieldKind = FieldKind::Text { max_len: Some(3) };
#[test]
fn text_max_len_accepts_unbounded_text() {
let field = FieldModel::generated("name", FieldKind::Text { max_len: None });
assert!(
field
.validate_runtime_value_for_storage(&Value::Text("Ada Lovelace".into()))
.is_ok()
);
}
#[test]
fn text_max_len_counts_unicode_scalars_not_bytes() {
let field = FieldModel::generated("name", BOUNDED_TEXT);
assert!(
field
.validate_runtime_value_for_storage(&Value::Text("ééé".into()))
.is_ok()
);
assert!(
field
.validate_runtime_value_for_storage(&Value::Text("éééé".into()))
.is_err()
);
}
#[test]
fn text_max_len_recurses_through_collections() {
static TEXT_LIST: FieldKind = FieldKind::List(&BOUNDED_TEXT);
static TEXT_MAP: FieldKind = FieldKind::Map {
key: &BOUNDED_TEXT,
value: &BOUNDED_TEXT,
};
let list_field = FieldModel::generated("names", TEXT_LIST);
let map_field = FieldModel::generated("labels", TEXT_MAP);
assert!(
list_field
.validate_runtime_value_for_storage(&Value::List(vec![
Value::Text("Ada".into()),
Value::Text("Bob".into()),
]))
.is_ok()
);
assert!(
list_field
.validate_runtime_value_for_storage(&Value::List(vec![Value::Text("Grace".into())]))
.is_err()
);
assert!(
map_field
.validate_runtime_value_for_storage(&Value::Map(vec![(
Value::Text("key".into()),
Value::Text("val".into()),
)]))
.is_ok()
);
assert!(
map_field
.validate_runtime_value_for_storage(&Value::Map(vec![(
Value::Text("long".into()),
Value::Text("val".into()),
)]))
.is_err()
);
}
}