#![deny(clippy::exhaustive_structs)]
use super::FieldType;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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 auto_now: 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 const fn new(name: &'static str, column: &'static str, ty: FieldType) -> Self {
Self {
name,
column,
ty,
nullable: false,
primary_key: false,
relation: None,
max_length: None,
min: None,
max: None,
default: None,
auto: false,
auto_now: false,
unique: false,
generated_as: None,
help_text: None,
choices: None,
db_comment: None,
verbose_name: None,
editable: true,
blank: false,
case_insensitive: false,
fk_on_delete: None,
validators: &[],
}
}
#[must_use]
pub fn display_label(&self) -> &'static str {
self.verbose_name.unwrap_or(self.name)
}
#[must_use]
pub fn is_auto_timestamp(&self) -> bool {
self.auto && !self.primary_key && matches!(self.ty, FieldType::DateTime)
}
#[must_use]
pub fn is_serial(&self) -> bool {
self.auto && matches!(self.ty, FieldType::I16 | FieldType::I32 | FieldType::I64)
}
#[must_use]
pub fn is_rust_side_uuid(&self) -> bool {
self.auto && matches!(self.ty, FieldType::Uuid) && self.default.is_none()
}
#[must_use]
pub fn accepts_input(&self, kind: WriteKind) -> bool {
!self.auto
&& self.generated_as.is_none()
&& !(self.primary_key && kind == WriteKind::Update)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WriteKind {
Insert,
Update,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum Relation {
#[non_exhaustive]
Fk { to: &'static str, on: &'static str },
#[non_exhaustive]
O2O { to: &'static str, on: &'static str },
}
impl Relation {
#[must_use]
pub const fn fk(to: &'static str, on: &'static str) -> Self {
Self::Fk { to, on }
}
#[must_use]
pub const fn o2o(to: &'static str, on: &'static str) -> Self {
Self::O2O { to, on }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
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)]
#[non_exhaustive]
pub struct GenericRelation {
pub name: &'static str,
pub ct_column: &'static str,
pub pk_column: &'static str,
}
impl GenericRelation {
#[must_use]
pub const fn new(name: &'static str, ct_column: &'static str, pk_column: &'static str) -> Self {
Self {
name,
ct_column,
pk_column,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ReverseRelation {
pub name: &'static str,
pub child_schema: &'static ModelSchema,
pub child_fk_column: &'static str,
pub self_pk_column: &'static str,
}
impl ReverseRelation {
#[must_use]
pub const fn new(
name: &'static str,
child_schema: &'static ModelSchema,
child_fk_column: &'static str,
self_pk_column: &'static str,
) -> Self {
Self {
name,
child_schema,
child_fk_column,
self_pk_column,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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,
}
impl GenericReverseRelation {
#[must_use]
pub const fn new(
name: &'static str,
child_schema: &'static ModelSchema,
ct_column: &'static str,
pk_column: &'static str,
self_pk_column: &'static str,
) -> Self {
Self {
name,
child_schema,
ct_column,
pk_column,
self_pk_column,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct CompositeFkRelation {
pub name: &'static str,
pub to: &'static str,
pub from: &'static [&'static str],
pub on: &'static [&'static str],
}
impl CompositeFkRelation {
#[must_use]
pub const fn new(
name: &'static str,
to: &'static str,
from: &'static [&'static str],
on: &'static [&'static str],
) -> Self {
Self { name, to, from, on }
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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,
}
impl M2MRelation {
#[must_use]
pub const fn new(
name: &'static str,
to: &'static str,
through: &'static str,
src_col: &'static str,
dst_col: &'static str,
) -> Self {
Self {
name,
to,
through,
src_col,
dst_col,
auto_create: true,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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)]
#[non_exhaustive]
pub struct GlobalScope {
pub name: &'static str,
pub apply: fn() -> crate::core::WhereExpr,
}
impl GlobalScope {
#[must_use]
pub const fn new(name: &'static str, apply: fn() -> crate::core::WhereExpr) -> Self {
Self { name, apply }
}
}
impl ModelSchema {
pub(crate) fn global_scope_exprs(&self, skip: &[&str]) -> Vec<crate::core::WhereExpr> {
self.global_scopes
.iter()
.filter(|s| !skip.contains(&s.name))
.map(|s| (s.apply)())
.collect()
}
#[must_use]
pub fn with_global_scopes(
&self,
where_clause: crate::core::WhereExpr,
) -> crate::core::WhereExpr {
let mut all = self.global_scope_exprs(&[]);
if all.is_empty() {
return where_clause;
}
if !matches!(&where_clause, crate::core::WhereExpr::And(v) if v.is_empty()) {
all.push(where_clause);
}
crate::core::WhereExpr::And(all)
}
}
impl ModelSchema {
#[must_use]
pub const fn new(name: &'static str, table: &'static str) -> Self {
Self {
name,
table,
fields: &[],
display: None,
app_label: None,
admin: None,
soft_delete_column: None,
permissions: false,
audit_track: None,
m2m: &[],
indexes: &[],
check_constraints: &[],
exclusion_constraints: &[],
default_permissions: &[],
composite_relations: &[],
generic_relations: &[],
scope: ModelScope::Tenant,
default_order: &[],
is_view: false,
verbose_name: None,
verbose_name_plural: None,
managed: true,
db_table_comment: None,
default_related_name: None,
base_manager_name: None,
required_db_vendor: None,
required_db_features: &[],
order_with_respect_to: None,
proxy: false,
get_latest_by: None,
extra_permissions: &[],
global_scopes: &[],
}
}
#[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)]
#[non_exhaustive]
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)]
#[non_exhaustive]
pub struct CheckConstraint {
pub name: &'static str,
pub expr: &'static str,
}
impl CheckConstraint {
#[must_use]
pub const fn new(name: &'static str, expr: &'static str) -> Self {
Self { name, expr }
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ExclusionConstraint {
pub name: &'static str,
pub using: &'static str,
pub elements: &'static [(&'static str, &'static str)],
pub where_clause: Option<&'static str>,
}
impl ExclusionConstraint {
#[must_use]
pub const fn new(
name: &'static str,
using: &'static str,
elements: &'static [(&'static str, &'static str)],
) -> Self {
Self {
name,
using,
elements,
where_clause: None,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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],
}
impl IndexSchema {
#[must_use]
pub const fn new(name: &'static str, columns: &'static [&'static str]) -> Self {
Self {
name,
columns,
unique: false,
method: IndexMethod::BTree,
where_clause: None,
include: &[],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
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)]
#[non_exhaustive]
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)]
#[non_exhaustive]
pub enum ListSelectRelated {
All,
None,
Only(&'static [&'static str]),
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct PrepopulatedField {
pub target: &'static str,
pub sources: &'static [&'static str],
}
impl PrepopulatedField {
#[must_use]
pub const fn new(target: &'static str, sources: &'static [&'static str]) -> Self {
Self { target, sources }
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Fieldset {
pub title: &'static str,
pub fields: &'static [&'static str],
}
impl Fieldset {
#[must_use]
pub const fn new(title: &'static str, fields: &'static [&'static str]) -> Self {
Self { title, fields }
}
}
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)
}
#[must_use]
#[cfg(any(feature = "admin", feature = "tenancy", feature = "template_views"))]
pub(crate) fn with_pk_tiebreak(
&self,
mut order: Vec<super::OrderItem>,
) -> Vec<super::OrderItem> {
for pk in self.fields.iter().filter(|f| f.primary_key) {
if !order.iter().any(|o| o.column_name() == Some(pk.column)) {
order.push(super::OrderItem::column(pk.column, false));
}
}
order
}
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)]
fn __rustango_audited_update() -> Option<crate::audit::AuditedUpdate> {
None
}
#[doc(hidden)]
fn __rustango_audited_delete() -> Option<crate::audit::AuditedDelete> {
None
}
#[doc(hidden)]
fn __rustango_audited_create() -> Option<crate::audit::AuditedCreate> {
None
}
}
#[doc(hidden)]
#[non_exhaustive]
pub struct ModelEntry {
pub schema: &'static ModelSchema,
pub module_path: &'static str,
audited_update: fn() -> Option<crate::audit::AuditedUpdate>,
audited_delete: fn() -> Option<crate::audit::AuditedDelete>,
audited_create: fn() -> Option<crate::audit::AuditedCreate>,
}
impl ModelEntry {
#[must_use]
pub const fn new(schema: &'static ModelSchema, module_path: &'static str) -> Self {
Self {
schema,
module_path,
audited_update: || None,
audited_delete: || None,
audited_create: || None,
}
}
#[must_use]
pub const fn with_audited(
mut self,
update: fn() -> Option<crate::audit::AuditedUpdate>,
delete: fn() -> Option<crate::audit::AuditedDelete>,
) -> Self {
self.audited_update = update;
self.audited_delete = delete;
self
}
#[must_use]
pub const fn with_audited_create(
mut self,
create: fn() -> Option<crate::audit::AuditedCreate>,
) -> Self {
self.audited_create = create;
self
}
#[must_use]
pub(crate) fn is_framework(&self) -> bool {
self.module_path == "rustango" || self.module_path.starts_with("rustango::")
}
#[must_use]
pub fn for_schema(model: &ModelSchema) -> Option<&'static Self> {
inventory::iter::<Self>
.into_iter()
.find(|e| e.schema.table == model.table && e.schema.name == model.name)
}
#[must_use]
pub fn for_table(table: &str) -> Option<&'static Self> {
inventory::iter::<Self>
.into_iter()
.find(|e| e.schema.table == table)
}
#[must_use]
pub fn audited_update(&self) -> Option<crate::audit::AuditedUpdate> {
(self.audited_update)()
}
#[must_use]
pub fn audited_delete(&self) -> Option<crate::audit::AuditedDelete> {
(self.audited_delete)()
}
#[must_use]
pub fn audited_create(&self) -> Option<crate::audit::AuditedCreate> {
(self.audited_create)()
}
#[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);
#[doc(hidden)]
#[allow(clippy::exhaustive_structs)]
#[derive(Debug, Clone, Copy)]
pub struct FieldSchemaParts {
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 auto_now: 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 {
#[doc(hidden)]
#[must_use]
pub const fn from_parts(parts: FieldSchemaParts) -> Self {
let FieldSchemaParts {
name,
column,
ty,
nullable,
primary_key,
relation,
max_length,
min,
max,
default,
auto,
auto_now,
unique,
generated_as,
help_text,
choices,
db_comment,
verbose_name,
editable,
blank,
case_insensitive,
fk_on_delete,
validators,
} = parts;
Self {
name,
column,
ty,
nullable,
primary_key,
relation,
max_length,
min,
max,
default,
auto,
auto_now,
unique,
generated_as,
help_text,
choices,
db_comment,
verbose_name,
editable,
blank,
case_insensitive,
fk_on_delete,
validators,
}
}
}
#[doc(hidden)]
#[allow(clippy::exhaustive_structs)]
#[derive(Debug, Clone, Copy)]
pub struct ModelSchemaParts {
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],
}
impl ModelSchema {
#[doc(hidden)]
#[must_use]
pub const fn from_parts(parts: ModelSchemaParts) -> Self {
let ModelSchemaParts {
name,
table,
fields,
display,
app_label,
admin,
soft_delete_column,
permissions,
audit_track,
m2m,
indexes,
check_constraints,
exclusion_constraints,
default_permissions,
composite_relations,
generic_relations,
scope,
default_order,
is_view,
verbose_name,
verbose_name_plural,
managed,
db_table_comment,
default_related_name,
base_manager_name,
required_db_vendor,
required_db_features,
order_with_respect_to,
proxy,
get_latest_by,
extra_permissions,
global_scopes,
} = parts;
Self {
name,
table,
fields,
display,
app_label,
admin,
soft_delete_column,
permissions,
audit_track,
m2m,
indexes,
check_constraints,
exclusion_constraints,
default_permissions,
composite_relations,
generic_relations,
scope,
default_order,
is_view,
verbose_name,
verbose_name_plural,
managed,
db_table_comment,
default_related_name,
base_manager_name,
required_db_vendor,
required_db_features,
order_with_respect_to,
proxy,
get_latest_by,
extra_permissions,
global_scopes,
}
}
}
#[doc(hidden)]
#[allow(clippy::exhaustive_structs)]
#[derive(Debug, Clone, Copy)]
pub struct AdminConfigParts {
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)],
}
impl AdminConfig {
#[doc(hidden)]
#[must_use]
pub const fn from_parts(parts: AdminConfigParts) -> Self {
let AdminConfigParts {
list_display,
search_fields,
list_per_page,
ordering,
readonly_fields,
list_filter,
actions,
fieldsets,
list_display_links,
search_help_text,
actions_on_top,
actions_on_bottom,
date_hierarchy,
prepopulated_fields,
raw_id_fields,
autocomplete_fields,
list_select_related,
formfield_overrides,
} = parts;
Self {
list_display,
search_fields,
list_per_page,
ordering,
readonly_fields,
list_filter,
actions,
fieldsets,
list_display_links,
search_help_text,
actions_on_top,
actions_on_bottom,
date_hierarchy,
prepopulated_fields,
raw_id_fields,
autocomplete_fields,
list_select_related,
formfield_overrides,
}
}
}
#[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);
}
}