use super::FieldType;
#[derive(Debug, Clone, Copy)]
pub struct FieldSchema {
pub name: &'static str,
pub column: &'static str,
pub ty: FieldType,
pub nullable: bool,
pub primary_key: bool,
pub relation: Option<Relation>,
pub max_length: Option<u32>,
pub min: Option<i64>,
pub max: Option<i64>,
pub default: Option<&'static str>,
pub auto: bool,
pub unique: bool,
pub generated_as: Option<&'static str>,
pub help_text: Option<&'static str>,
pub choices: Option<&'static [(&'static str, &'static str)]>,
pub db_comment: Option<&'static str>,
pub verbose_name: Option<&'static str>,
pub editable: bool,
pub blank: bool,
pub case_insensitive: bool,
pub fk_on_delete: Option<OnDeleteAction>,
pub validators: &'static [&'static str],
}
impl FieldSchema {
#[must_use]
pub fn display_label(&self) -> &'static str {
self.verbose_name.unwrap_or(self.name)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Relation {
Fk { to: &'static str, on: &'static str },
O2O { to: &'static str, on: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnDeleteAction {
Cascade,
Restrict,
SetNull,
SetDefault,
NoAction,
}
impl OnDeleteAction {
#[must_use]
pub const fn as_sql(self) -> &'static str {
match self {
Self::Cascade => "CASCADE",
Self::Restrict => "RESTRICT",
Self::SetNull => "SET NULL",
Self::SetDefault => "SET DEFAULT",
Self::NoAction => "NO ACTION",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GenericRelation {
pub name: &'static str,
pub ct_column: &'static str,
pub pk_column: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct ReverseRelation {
pub name: &'static str,
pub child_schema: &'static ModelSchema,
pub child_fk_column: &'static str,
pub self_pk_column: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct GenericReverseRelation {
pub name: &'static str,
pub child_schema: &'static ModelSchema,
pub ct_column: &'static str,
pub pk_column: &'static str,
pub self_pk_column: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct CompositeFkRelation {
pub name: &'static str,
pub to: &'static str,
pub from: &'static [&'static str],
pub on: &'static [&'static str],
}
#[derive(Debug, Clone, Copy)]
pub struct M2MRelation {
pub name: &'static str,
pub to: &'static str,
pub through: &'static str,
pub src_col: &'static str,
pub dst_col: &'static str,
pub auto_create: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ModelSchema {
pub name: &'static str,
pub table: &'static str,
pub fields: &'static [FieldSchema],
pub display: Option<&'static str>,
pub app_label: Option<&'static str>,
pub admin: Option<&'static AdminConfig>,
pub soft_delete_column: Option<&'static str>,
pub permissions: bool,
pub audit_track: Option<&'static [&'static str]>,
pub m2m: &'static [M2MRelation],
pub indexes: &'static [IndexSchema],
pub check_constraints: &'static [CheckConstraint],
pub exclusion_constraints: &'static [ExclusionConstraint],
pub default_permissions: &'static [&'static str],
pub composite_relations: &'static [CompositeFkRelation],
pub generic_relations: &'static [GenericRelation],
pub scope: ModelScope,
pub default_order: &'static [(&'static str, bool)],
pub is_view: bool,
pub verbose_name: Option<&'static str>,
pub verbose_name_plural: Option<&'static str>,
pub managed: bool,
pub db_table_comment: Option<&'static str>,
pub default_related_name: Option<&'static str>,
pub base_manager_name: Option<&'static str>,
pub required_db_vendor: Option<&'static str>,
pub required_db_features: &'static [&'static str],
pub order_with_respect_to: Option<&'static str>,
pub proxy: bool,
pub get_latest_by: Option<(&'static str, bool)>,
pub extra_permissions: &'static [(&'static str, &'static str)],
pub global_scopes: &'static [GlobalScope],
}
#[derive(Debug, Clone, Copy)]
pub struct GlobalScope {
pub name: &'static str,
pub apply: fn() -> crate::core::WhereExpr,
}
impl ModelSchema {
#[must_use]
pub fn display_label(&self) -> &'static str {
self.verbose_name.unwrap_or(self.name)
}
#[must_use]
pub fn display_label_plural(&self) -> String {
if let Some(plural) = self.verbose_name_plural {
return plural.to_owned();
}
let base = self.verbose_name.unwrap_or(self.name);
format!("{base}s")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ModelScope {
Registry,
#[default]
Tenant,
}
impl ModelScope {
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"registry" => Some(Self::Registry),
"tenant" => Some(Self::Tenant),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Registry => "registry",
Self::Tenant => "tenant",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CheckConstraint {
pub name: &'static str,
pub expr: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct ExclusionConstraint {
pub name: &'static str,
pub using: &'static str,
pub elements: &'static [(&'static str, &'static str)],
pub where_clause: Option<&'static str>,
}
#[derive(Debug, Clone, Copy)]
pub struct IndexSchema {
pub name: &'static str,
pub columns: &'static [&'static str],
pub unique: bool,
pub method: IndexMethod,
pub where_clause: Option<&'static str>,
pub include: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexMethod {
#[default]
BTree,
Gin,
Gist,
Brin,
SpGist,
Hash,
Bloom,
}
impl IndexMethod {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::BTree => "btree",
Self::Gin => "gin",
Self::Gist => "gist",
Self::Brin => "brin",
Self::SpGist => "spgist",
Self::Hash => "hash",
Self::Bloom => "bloom",
}
}
#[must_use]
pub fn from_token(s: &str) -> Self {
match s {
"gin" => Self::Gin,
"gist" => Self::Gist,
"brin" => Self::Brin,
"spgist" => Self::SpGist,
"hash" => Self::Hash,
"bloom" => Self::Bloom,
_ => Self::BTree,
}
}
#[must_use]
pub const fn is_postgres_only(self) -> bool {
matches!(
self,
Self::Gin | Self::Gist | Self::Brin | Self::SpGist | Self::Bloom
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdminConfig {
pub list_display: &'static [&'static str],
pub search_fields: &'static [&'static str],
pub list_per_page: usize,
pub ordering: &'static [(&'static str, bool)],
pub readonly_fields: &'static [&'static str],
pub list_filter: &'static [&'static str],
pub actions: &'static [&'static str],
pub fieldsets: &'static [Fieldset],
pub list_display_links: &'static [&'static str],
pub search_help_text: &'static str,
pub actions_on_top: bool,
pub actions_on_bottom: bool,
pub date_hierarchy: &'static str,
pub prepopulated_fields: &'static [PrepopulatedField],
pub raw_id_fields: &'static [&'static str],
pub autocomplete_fields: &'static [&'static str],
pub list_select_related: ListSelectRelated,
pub formfield_overrides: &'static [(&'static str, &'static str)],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListSelectRelated {
All,
None,
Only(&'static [&'static str]),
}
#[derive(Debug, Clone, Copy)]
pub struct PrepopulatedField {
pub target: &'static str,
pub sources: &'static [&'static str],
}
#[derive(Debug, Clone, Copy)]
pub struct Fieldset {
pub title: &'static str,
pub fields: &'static [&'static str],
}
impl AdminConfig {
pub const DEFAULT: AdminConfig = AdminConfig {
list_display: &[],
search_fields: &[],
list_per_page: 0,
ordering: &[],
readonly_fields: &[],
list_filter: &[],
actions: &[],
fieldsets: &[],
list_display_links: &[],
search_help_text: "",
actions_on_top: true,
actions_on_bottom: false,
date_hierarchy: "",
prepopulated_fields: &[],
raw_id_fields: &[],
autocomplete_fields: &[],
list_select_related: ListSelectRelated::All,
formfield_overrides: &[],
};
}
impl ModelSchema {
#[must_use]
pub fn field(&self, name: &str) -> Option<&'static FieldSchema> {
self.fields.iter().find(|f| f.name == name)
}
#[must_use]
pub fn field_by_column(&self, column: &str) -> Option<&'static FieldSchema> {
self.fields.iter().find(|f| f.column == column)
}
#[must_use]
pub fn primary_key(&self) -> Option<&'static FieldSchema> {
self.fields.iter().find(|f| f.primary_key)
}
pub fn scalar_fields(&self) -> impl Iterator<Item = &'static FieldSchema> {
self.fields.iter()
}
#[must_use]
pub fn display_field(&self) -> Option<&'static FieldSchema> {
if let Some(name) = self.display {
return self.field(name);
}
self.primary_key()
}
pub fn searchable_fields(&self) -> impl Iterator<Item = &'static FieldSchema> {
self.fields.iter().filter(|f| {
matches!(f.ty, FieldType::String) && f.max_length.is_some() && f.relation.is_none()
})
}
}
pub trait Model: Sized + Send + Sync + 'static {
const SCHEMA: &'static ModelSchema;
fn reverse_relations() -> &'static [ReverseRelation] {
&[]
}
fn generic_reverse_relations() -> &'static [GenericReverseRelation] {
&[]
}
}
#[doc(hidden)]
pub struct ModelEntry {
pub schema: &'static ModelSchema,
pub module_path: &'static str,
}
impl ModelEntry {
#[must_use]
pub fn resolved_app_label(&self) -> Option<&'static str> {
if let Some(label) = self.schema.app_label {
return Some(label);
}
infer_app_label_from_module_path(self.module_path)
}
}
#[must_use]
pub fn infer_app_label_from_module_path(path: &'static str) -> Option<&'static str> {
let mut parts = path.split("::");
let _crate_name = parts.next()?;
let candidate = parts.next()?;
if matches!(candidate, "models" | "views" | "urls" | "main") {
return None;
}
Some(candidate)
}
inventory::collect!(ModelEntry);
#[cfg(test)]
mod tests {
use super::infer_app_label_from_module_path as infer;
#[test]
fn infers_app_from_submodule() {
assert_eq!(infer("my_app::blog::models"), Some("blog"));
assert_eq!(infer("my_app::shop::models"), Some("shop"));
assert_eq!(infer("my_app::auth"), Some("auth"));
}
#[test]
fn returns_none_for_project_root_models() {
assert_eq!(infer("my_app"), None);
assert_eq!(infer("my_app::models"), None);
assert_eq!(infer("my_app::views"), None);
}
}