use crate::model::{field::FieldModel, index::IndexModel};
#[derive(Debug)]
pub struct PrimaryKeyModel {
fields: PrimaryKeyModelFields,
}
impl PrimaryKeyModel {
#[must_use]
pub const fn scalar(field: &'static FieldModel) -> Self {
Self {
fields: PrimaryKeyModelFields::Scalar(field),
}
}
#[must_use]
pub const fn ordered(fields: &'static [&'static FieldModel]) -> Self {
assert!(!fields.is_empty(), "primary key model requires fields");
Self {
fields: PrimaryKeyModelFields::Ordered(fields),
}
}
#[must_use]
pub const fn len(&self) -> usize {
match self.fields {
PrimaryKeyModelFields::Scalar(_) => 1,
PrimaryKeyModelFields::Ordered(fields) => fields.len(),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub const fn is_scalar(&self) -> bool {
self.len() == 1
}
#[must_use]
pub const fn first_field(&self) -> &'static FieldModel {
match self.fields {
PrimaryKeyModelFields::Scalar(field) => field,
PrimaryKeyModelFields::Ordered(fields) => fields[0],
}
}
#[must_use]
pub const fn fields(&self) -> PrimaryKeyModelFields {
self.fields
}
}
#[derive(Clone, Copy, Debug)]
pub enum PrimaryKeyModelFields {
Scalar(&'static FieldModel),
Ordered(&'static [&'static FieldModel]),
}
impl PrimaryKeyModelFields {
#[must_use]
pub const fn len(self) -> usize {
match self {
Self::Scalar(_) => 1,
Self::Ordered(fields) => fields.len(),
}
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.len() == 0
}
#[must_use]
pub fn get(self, index: usize) -> Option<&'static FieldModel> {
match self {
Self::Scalar(field) => (index == 0).then_some(field),
Self::Ordered(fields) => fields.get(index).copied(),
}
}
#[must_use]
pub const fn iter(self) -> PrimaryKeyModelFieldIter {
PrimaryKeyModelFieldIter {
fields: self,
index: 0,
}
}
}
#[derive(Clone, Debug)]
pub struct PrimaryKeyModelFieldIter {
fields: PrimaryKeyModelFields,
index: usize,
}
impl Iterator for PrimaryKeyModelFieldIter {
type Item = &'static FieldModel;
fn next(&mut self) -> Option<Self::Item> {
let item = self.fields.get(self.index)?;
self.index += 1;
Some(item)
}
}
#[cfg(test)]
mod primary_key_model_tests {
use super::{PrimaryKeyModel, PrimaryKeyModelFields};
use crate::model::FieldModel;
static ID_FIELD: FieldModel = FieldModel::generated("id", crate::model::FieldKind::Nat64);
static TENANT_FIELD: FieldModel =
FieldModel::generated("tenant_id", crate::model::FieldKind::Nat64);
static ORDERED_FIELDS: [&FieldModel; 2] = [&ID_FIELD, &TENANT_FIELD];
#[test]
fn scalar_primary_key_model_exposes_one_field() {
let model = PrimaryKeyModel::scalar(&ID_FIELD);
assert_eq!(model.len(), 1);
assert!(model.is_scalar());
assert_eq!(model.first_field().name(), "id");
assert_eq!(
model
.fields()
.iter()
.map(FieldModel::name)
.collect::<Vec<_>>(),
["id"]
);
}
#[test]
fn ordered_primary_key_model_preserves_field_order() {
let model = PrimaryKeyModel::ordered(&ORDERED_FIELDS);
assert_eq!(model.len(), 2);
assert!(!model.is_scalar());
assert_eq!(model.first_field().name(), "id");
assert_eq!(
model
.fields()
.iter()
.map(FieldModel::name)
.collect::<Vec<_>>(),
["id", "tenant_id"],
);
std::assert_matches!(model.fields(), PrimaryKeyModelFields::Ordered(_));
}
}
#[derive(Debug)]
pub struct RelationEdgeModel {
name: &'static str,
target_path: &'static str,
local_fields: &'static [&'static FieldModel],
}
impl RelationEdgeModel {
#[must_use]
pub const fn generated(
name: &'static str,
target_path: &'static str,
local_fields: &'static [&'static FieldModel],
) -> Self {
Self {
name,
target_path,
local_fields,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn target_path(&self) -> &'static str {
self.target_path
}
#[must_use]
pub const fn local_fields(&self) -> &'static [&'static FieldModel] {
self.local_fields
}
}
#[cfg(test)]
mod relation_edge_model_tests {
use super::RelationEdgeModel;
use crate::model::{FieldKind, FieldModel};
static TENANT_FIELD: FieldModel = FieldModel::generated("tenant_id", FieldKind::Nat64);
static USER_FIELD: FieldModel = FieldModel::generated("user_id", FieldKind::Ulid);
static LOCAL_FIELDS: [&FieldModel; 2] = [&TENANT_FIELD, &USER_FIELD];
#[test]
fn relation_edge_model_preserves_ordered_local_fields() {
let relation = RelationEdgeModel::generated("author", "example::User", &LOCAL_FIELDS);
assert_eq!(relation.name(), "author");
assert_eq!(relation.target_path(), "example::User");
assert_eq!(
relation
.local_fields()
.iter()
.map(|field| field.name())
.collect::<Vec<_>>(),
["tenant_id", "user_id"],
);
}
}
#[derive(Debug)]
pub struct EntityModel {
pub(crate) path: &'static str,
pub(crate) entity_name: &'static str,
pub(crate) schema_version: u32,
pub(crate) primary_key: &'static FieldModel,
pub(crate) primary_key_slot: usize,
pub(crate) primary_key_model: PrimaryKeyModel,
pub(crate) fields: &'static [FieldModel],
pub(crate) indexes: &'static [&'static IndexModel],
pub(crate) relations: &'static [RelationEdgeModel],
}
impl EntityModel {
#[must_use]
#[doc(hidden)]
pub const fn generated(
path: &'static str,
entity_name: &'static str,
schema_version: u32,
primary_key: &'static FieldModel,
primary_key_slot: usize,
fields: &'static [FieldModel],
indexes: &'static [&'static IndexModel],
) -> Self {
Self {
path,
entity_name,
schema_version,
primary_key,
primary_key_slot,
primary_key_model: PrimaryKeyModel::scalar(primary_key),
fields,
indexes,
relations: &[],
}
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_primary_key_model(
path: &'static str,
entity_name: &'static str,
schema_version: u32,
primary_key_model: PrimaryKeyModel,
primary_key_slot: usize,
fields: &'static [FieldModel],
indexes: &'static [&'static IndexModel],
) -> Self {
Self::generated_with_primary_key_model_and_relations(
path,
entity_name,
schema_version,
primary_key_model,
primary_key_slot,
fields,
indexes,
&[],
)
}
#[must_use]
#[doc(hidden)]
#[expect(
clippy::too_many_arguments,
reason = "generated entity model construction keeps path, declared version, key, field, index, and relation metadata explicit"
)]
pub const fn generated_with_primary_key_model_and_relations(
path: &'static str,
entity_name: &'static str,
schema_version: u32,
primary_key_model: PrimaryKeyModel,
primary_key_slot: usize,
fields: &'static [FieldModel],
indexes: &'static [&'static IndexModel],
relations: &'static [RelationEdgeModel],
) -> Self {
assert!(
schema_version > 0,
"generated schema_version must be positive"
);
Self {
path,
entity_name,
schema_version,
primary_key: primary_key_model.first_field(),
primary_key_slot,
primary_key_model,
fields,
indexes,
relations,
}
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.entity_name
}
#[must_use]
pub const fn declared_schema_version(&self) -> u32 {
self.schema_version
}
#[must_use]
pub const fn primary_key(&self) -> &'static FieldModel {
self.primary_key
}
#[must_use]
pub const fn primary_key_model(&self) -> &PrimaryKeyModel {
&self.primary_key_model
}
#[must_use]
pub fn primary_key_names(&self) -> Vec<&'static str> {
self.primary_key_model()
.fields()
.iter()
.map(crate::model::field::FieldModel::name)
.collect()
}
#[must_use]
pub const fn primary_key_slot(&self) -> usize {
self.primary_key_slot
}
#[must_use]
pub const fn fields(&self) -> &'static [FieldModel] {
self.fields
}
#[must_use]
pub const fn indexes(&self) -> &'static [&'static IndexModel] {
self.indexes
}
#[must_use]
pub const fn relations(&self) -> &'static [RelationEdgeModel] {
self.relations
}
#[must_use]
pub(crate) fn resolve_field_slot(&self, field_name: &str) -> Option<usize> {
self.fields
.iter()
.position(|field| field.name == field_name)
}
}