use crate::db::models::fields::{FieldDescriptor, FieldType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
pub app_label: String,
pub model_name: String,
pub db_table: String,
pub verbose_name: String,
pub verbose_name_plural: String,
pub ordering: Vec<String>,
pub unique_together: Vec<Vec<String>>,
pub index_together: Vec<Vec<String>>,
pub fields: Vec<FieldDescriptor>,
pub abstract_model: bool,
pub managed: bool,
pub proxy: bool,
pub default_permissions: Vec<String>,
}
impl Options {
#[must_use]
pub fn new(app_label: &str, model_name: &str) -> Self {
let normalized_model_name = to_snake_case(model_name);
let verbose_name = normalized_model_name.replace('_', " ");
Self {
app_label: app_label.to_string(),
model_name: model_name.to_string(),
db_table: format!("{app_label}_{normalized_model_name}"),
verbose_name: verbose_name.clone(),
verbose_name_plural: format!("{verbose_name}s"),
ordering: Vec::new(),
unique_together: Vec::new(),
index_together: Vec::new(),
fields: Vec::new(),
abstract_model: false,
managed: true,
proxy: false,
default_permissions: ["add", "change", "delete", "view"]
.into_iter()
.map(str::to_string)
.collect(),
}
}
#[must_use]
pub fn get_field(&self, name: &str) -> Option<&FieldDescriptor> {
self.fields.iter().find(|field| field.name == name)
}
#[must_use]
pub fn concrete_fields(&self) -> Vec<&FieldDescriptor> {
self.fields
.iter()
.filter(|field| !is_relation_field(field))
.collect()
}
#[must_use]
pub fn related_fields(&self) -> Vec<&FieldDescriptor> {
self.fields
.iter()
.filter(|field| is_relation_field(field))
.collect()
}
#[must_use]
pub fn pk_field(&self) -> Option<&FieldDescriptor> {
self.fields.iter().find(|field| field.primary_key)
}
#[must_use]
pub fn label(&self) -> String {
format!("{}.{}", self.app_label, self.model_name)
}
#[must_use]
pub fn label_lower(&self) -> String {
self.label().to_lowercase()
}
#[must_use]
pub fn field_names(&self) -> Vec<&str> {
self.fields
.iter()
.map(|field| field.name.as_str())
.collect()
}
#[must_use]
pub fn many_to_many_fields(&self) -> Vec<&FieldDescriptor> {
self.fields
.iter()
.filter(|field| matches!(&field.field_type, FieldType::ManyToMany { .. }))
.collect()
}
}
fn is_relation_field(field: &FieldDescriptor) -> bool {
matches!(
&field.field_type,
FieldType::ForeignKey { .. } | FieldType::OneToOne { .. } | FieldType::ManyToMany { .. }
)
}
fn to_snake_case(value: &str) -> String {
let mut snake = String::with_capacity(value.len());
let mut previous_was_lower_or_digit = false;
for ch in value.chars() {
if ch == '_' || ch == ' ' {
if !snake.ends_with('_') && !snake.is_empty() {
snake.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
if ch.is_uppercase() && previous_was_lower_or_digit {
snake.push('_');
}
snake.extend(ch.to_lowercase());
previous_was_lower_or_digit = ch.is_lowercase() || ch.is_ascii_digit();
}
snake
}
#[cfg(test)]
mod tests {
use super::Options;
use crate::db::models::fields::{FieldDescriptor, FieldType, OnDelete};
fn scalar_field(name: &str) -> FieldDescriptor {
FieldDescriptor::new(name, FieldType::Char { max_length: 64 })
}
fn pk_field(name: &str) -> FieldDescriptor {
let mut field = FieldDescriptor::new(name, FieldType::Auto);
field.primary_key = true;
field
}
fn foreign_key(name: &str) -> FieldDescriptor {
FieldDescriptor::new(
name,
FieldType::ForeignKey {
to: "accounts.User".to_string(),
on_delete: OnDelete::Cascade,
},
)
}
fn many_to_many(name: &str) -> FieldDescriptor {
FieldDescriptor::new(
name,
FieldType::ManyToMany {
to: "blog.Tag".to_string(),
through: None,
},
)
}
#[test]
fn options_basic_construction() {
let options = Options::new("blog", "Article");
assert_eq!(options.app_label, "blog");
assert_eq!(options.model_name, "Article");
assert_eq!(options.db_table, "blog_article");
assert_eq!(options.verbose_name, "article");
assert_eq!(options.verbose_name_plural, "articles");
assert_eq!(
options.default_permissions,
vec!["add", "change", "delete", "view"]
);
}
#[test]
fn get_field_finds_by_name() {
let mut options = Options::new("blog", "Article");
options.fields.push(scalar_field("title"));
let field = options.get_field("title").expect("field should exist");
assert_eq!(field.name, "title");
}
#[test]
fn pk_field_returns_primary_key() {
let mut options = Options::new("blog", "Article");
options.fields.push(scalar_field("title"));
options.fields.push(pk_field("id"));
let field = options.pk_field().expect("primary key should exist");
assert_eq!(field.name, "id");
}
#[test]
fn concrete_fields_excludes_relations() {
let mut options = Options::new("blog", "Article");
options.fields.push(pk_field("id"));
options.fields.push(scalar_field("title"));
options.fields.push(foreign_key("author"));
options.fields.push(many_to_many("tags"));
let names: Vec<_> = options
.concrete_fields()
.into_iter()
.map(|field| field.name.as_str())
.collect();
assert_eq!(names, vec!["id", "title"]);
}
#[test]
fn related_fields_only_relations() {
let mut options = Options::new("blog", "Article");
options.fields.push(pk_field("id"));
options.fields.push(scalar_field("title"));
options.fields.push(foreign_key("author"));
options.fields.push(many_to_many("tags"));
let names: Vec<_> = options
.related_fields()
.into_iter()
.map(|field| field.name.as_str())
.collect();
assert_eq!(names, vec!["author", "tags"]);
}
#[test]
fn label_formats_correctly() {
let options = Options::new("blog", "Article");
assert_eq!(options.label(), "blog.Article");
assert_eq!(options.label_lower(), "blog.article");
}
#[test]
fn field_names_return_declared_order() {
let mut options = Options::new("blog", "Article");
options.fields.push(pk_field("id"));
options.fields.push(scalar_field("title"));
options.fields.push(foreign_key("author"));
assert_eq!(options.field_names(), vec!["id", "title", "author"]);
}
#[test]
fn many_to_many_fields_only_return_many_to_many_descriptors() {
let mut options = Options::new("blog", "Article");
options.fields.push(pk_field("id"));
options.fields.push(foreign_key("author"));
options.fields.push(many_to_many("tags"));
let names: Vec<_> = options
.many_to_many_fields()
.into_iter()
.map(|field| field.name.as_str())
.collect();
assert_eq!(names, vec!["tags"]);
}
}