use crate::{
db::{Db, identity::EntityName},
error::InternalError,
model::entity::EntityModel,
model::field::{FieldKind, RelationStrength},
traits::CanisterKind,
types::EntityTag,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum RelationDescriptorCardinality {
Single,
List,
Set,
}
#[derive(Clone, Copy)]
pub(in crate::db) struct RelationDescriptor<'model> {
field_index: usize,
field_name: &'static str,
field_kind: &'model FieldKind,
target_path: &'static str,
target_entity_name: &'static str,
target_entity_tag: EntityTag,
target_store_path: &'static str,
key_kind: &'model FieldKind,
strength: RelationStrength,
cardinality: RelationDescriptorCardinality,
}
impl<'model> RelationDescriptor<'model> {
const fn new(
field_index: usize,
field_name: &'static str,
field_kind: &'model FieldKind,
target: RelationTargetInfo<'model>,
) -> Self {
Self {
field_index,
field_name,
field_kind,
target_path: target.path,
target_entity_name: target.entity_name,
target_entity_tag: target.entity_tag,
target_store_path: target.store_path,
key_kind: target.key_kind,
strength: target.strength,
cardinality: target.cardinality,
}
}
#[must_use]
pub(in crate::db) const fn field_index(self) -> usize {
self.field_index
}
#[must_use]
pub(in crate::db) const fn field_name(self) -> &'static str {
self.field_name
}
#[must_use]
pub(in crate::db) const fn field_kind(self) -> &'model FieldKind {
self.field_kind
}
#[must_use]
pub(in crate::db) const fn target_path(self) -> &'static str {
self.target_path
}
#[must_use]
pub(in crate::db) const fn target_entity_name(self) -> &'static str {
self.target_entity_name
}
#[must_use]
pub(in crate::db) const fn target_entity_tag(self) -> EntityTag {
self.target_entity_tag
}
#[must_use]
pub(in crate::db) const fn target_store_path(self) -> &'static str {
self.target_store_path
}
#[must_use]
pub(in crate::db) const fn key_kind(self) -> &'model FieldKind {
self.key_kind
}
#[must_use]
pub(in crate::db) const fn strength(self) -> RelationStrength {
self.strength
}
#[must_use]
pub(in crate::db) const fn cardinality(self) -> RelationDescriptorCardinality {
self.cardinality
}
}
pub(super) struct StrongRelationMetadataError {
field_name: &'static str,
target_path: &'static str,
target_entity_name: &'static str,
source: crate::db::identity::EntityNameError,
}
impl StrongRelationMetadataError {
#[must_use]
pub(super) const fn field_name(&self) -> &'static str {
self.field_name
}
#[must_use]
pub(super) const fn target_path(&self) -> &'static str {
self.target_path
}
#[must_use]
pub(super) const fn target_entity_name(&self) -> &'static str {
self.target_entity_name
}
#[must_use]
pub(super) const fn source(&self) -> &crate::db::identity::EntityNameError {
&self.source
}
}
#[derive(Clone, Copy)]
pub(super) struct StrongRelationTargetIdentity {
path: &'static str,
entity_name: EntityName,
entity_tag: EntityTag,
store_path: &'static str,
key_kind: &'static FieldKind,
}
impl StrongRelationTargetIdentity {
fn try_from_descriptor(
descriptor: RelationDescriptor<'static>,
) -> Result<Self, StrongRelationMetadataError> {
let entity_name =
EntityName::try_from_str(descriptor.target_entity_name()).map_err(|source| {
StrongRelationMetadataError {
field_name: descriptor.field_name(),
target_path: descriptor.target_path(),
target_entity_name: descriptor.target_entity_name(),
source,
}
})?;
Ok(Self {
path: descriptor.target_path(),
entity_name,
entity_tag: descriptor.target_entity_tag(),
store_path: descriptor.target_store_path(),
key_kind: descriptor.key_kind(),
})
}
#[must_use]
pub(super) const fn path(self) -> &'static str {
self.path
}
#[must_use]
pub(super) const fn entity_name(self) -> EntityName {
self.entity_name
}
#[must_use]
pub(super) const fn entity_tag(self) -> EntityTag {
self.entity_tag
}
#[must_use]
pub(super) const fn store_path(self) -> &'static str {
self.store_path
}
#[must_use]
pub(super) const fn key_kind(self) -> &'static FieldKind {
self.key_kind
}
pub(super) fn validate_against_db<C>(
self,
db: &Db<C>,
source_path: &str,
field_name: &str,
) -> Result<(), InternalError>
where
C: CanisterKind,
{
if !db.has_runtime_hooks() {
return Ok(());
}
let hook = db
.runtime_hook_for_entity_tag(self.entity_tag)
.map_err(|err| {
InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path,
format!(
"target_entity_tag={} is not registered: {err}",
self.entity_tag.value()
),
)
})?;
if hook.entity_path != self.path {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path,
format!(
"target_entity_tag={} resolves to entity_path={} but relation declares {}",
self.entity_tag.value(),
hook.entity_path,
self.path,
),
));
}
if hook.model.name() != self.entity_name.as_str() {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path,
format!(
"target_entity_tag={} resolves to entity_name={} but relation declares {}",
self.entity_tag.value(),
hook.model.name(),
self.entity_name.as_str(),
),
));
}
if hook.store_path != self.store_path {
return Err(InternalError::strong_relation_target_identity_mismatch(
source_path,
field_name,
self.path,
format!(
"target_store_path={} does not match runtime store {} for target_entity_tag={}",
self.store_path,
hook.store_path,
self.entity_tag.value(),
),
));
}
Ok(())
}
}
#[derive(Clone, Copy)]
pub(super) struct StrongRelationInfo {
pub(super) field_index: usize,
pub(super) field_name: &'static str,
pub(super) field_kind: &'static FieldKind,
target: StrongRelationTargetIdentity,
}
impl StrongRelationInfo {
#[must_use]
pub(super) const fn target(self) -> StrongRelationTargetIdentity {
self.target
}
pub(super) fn validate_target_identity<C>(
self,
db: &Db<C>,
source_path: &str,
) -> Result<(), InternalError>
where
C: CanisterKind,
{
self.target
.validate_against_db(db, source_path, self.field_name)
}
}
#[derive(Clone, Copy)]
struct RelationTargetInfo<'model> {
path: &'static str,
entity_name: &'static str,
entity_tag: EntityTag,
store_path: &'static str,
key_kind: &'model FieldKind,
strength: RelationStrength,
cardinality: RelationDescriptorCardinality,
}
const fn relation_target_from_kind(kind: &FieldKind) -> Option<RelationTargetInfo<'_>> {
match kind {
FieldKind::Relation {
target_path,
target_entity_name,
target_entity_tag,
target_store_path,
key_kind,
strength,
..
} => Some(RelationTargetInfo {
path: target_path,
entity_name: target_entity_name,
entity_tag: *target_entity_tag,
store_path: target_store_path,
key_kind,
strength: *strength,
cardinality: RelationDescriptorCardinality::Single,
}),
FieldKind::List(FieldKind::Relation {
target_path,
target_entity_name,
target_entity_tag,
target_store_path,
key_kind,
strength,
..
}) => Some(RelationTargetInfo {
path: target_path,
entity_name: target_entity_name,
entity_tag: *target_entity_tag,
store_path: target_store_path,
key_kind,
strength: *strength,
cardinality: RelationDescriptorCardinality::List,
}),
FieldKind::Set(FieldKind::Relation {
target_path,
target_entity_name,
target_entity_tag,
target_store_path,
key_kind,
strength,
..
}) => Some(RelationTargetInfo {
path: target_path,
entity_name: target_entity_name,
entity_tag: *target_entity_tag,
store_path: target_store_path,
key_kind,
strength: *strength,
cardinality: RelationDescriptorCardinality::Set,
}),
_ => None,
}
}
const fn relation_descriptor_from_field<'model>(
field_index: usize,
field_name: &'static str,
kind: &'model FieldKind,
) -> Option<RelationDescriptor<'model>> {
let Some(target) = relation_target_from_kind(kind) else {
return None;
};
Some(RelationDescriptor::new(
field_index,
field_name,
kind,
target,
))
}
pub(in crate::db) fn relation_descriptors_for_model_iter(
model: &EntityModel,
) -> impl Iterator<Item = RelationDescriptor<'_>> + '_ {
model
.fields
.iter()
.enumerate()
.filter_map(|(field_index, field)| {
relation_descriptor_from_field(field_index, field.name, &field.kind)
})
}
fn strong_relation_from_descriptor(
descriptor: RelationDescriptor<'static>,
) -> Result<Option<StrongRelationInfo>, StrongRelationMetadataError> {
if descriptor.strength() != RelationStrength::Strong {
return Ok(None);
}
Ok(Some(StrongRelationInfo {
field_index: descriptor.field_index(),
field_name: descriptor.field_name(),
field_kind: descriptor.field_kind(),
target: StrongRelationTargetIdentity::try_from_descriptor(descriptor)?,
}))
}
pub(super) fn strong_relations_for_model_iter<'a>(
model: &'static EntityModel,
target_path_filter: Option<&'a str>,
) -> impl Iterator<Item = Result<StrongRelationInfo, StrongRelationMetadataError>> + 'a {
relation_descriptors_for_model_iter(model)
.filter_map(move |descriptor| {
if descriptor.strength() != RelationStrength::Strong {
return None;
}
if target_path_filter.is_some_and(|target_path| descriptor.target_path() != target_path)
{
return None;
}
Some(strong_relation_from_descriptor(descriptor))
})
.map(|relation| relation.map(|relation| relation.expect("strong relation filtered")))
}
impl EntityModel {
#[must_use]
pub(in crate::db) fn has_any_strong_relations(&'static self) -> bool {
relation_descriptors_for_model_iter(self)
.any(|descriptor| descriptor.strength() == RelationStrength::Strong)
}
}
pub(in crate::db) fn model_has_strong_relations_to_target(
model: &'static EntityModel,
target_path: &str,
) -> bool {
relation_descriptors_for_model_iter(model).any(|descriptor| {
descriptor.strength() == RelationStrength::Strong && descriptor.target_path() == target_path
})
}