use std::any::type_name;
use rustrails_support::inflector::{demodulize, humanize, pluralize, singularize, underscore};
pub trait ModelNaming {
fn model_name() -> ModelName {
let type_name = type_name::<Self>();
let demodulized = demodulize(type_name);
ModelName::new(&demodulized)
}
fn model_name_instance(&self) -> ModelName {
Self::model_name()
}
fn to_model(&self) -> &Self {
self
}
fn singular(&self) -> String {
Self::model_name().singular
}
fn plural(&self) -> String {
Self::model_name().plural
}
fn route_key(&self) -> String {
Self::model_name().route_key
}
fn singular_route_key(&self) -> String {
Self::model_name().singular_route_key
}
fn param_key(&self) -> String {
Self::model_name().param_key
}
fn uncountable() -> bool {
let model_name = Self::model_name();
model_name.singular == model_name.plural
}
fn human_attribute_name(attribute: &str) -> String {
crate::naming::human_attribute_name(attribute)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelName {
pub name: String,
pub singular: String,
pub plural: String,
pub element: String,
pub collection: String,
pub human: String,
pub param_key: String,
pub route_key: String,
pub singular_route_key: String,
pub i18n_key: String,
}
impl ModelName {
pub fn new(name: &str) -> Self {
let underscored = underscore(name);
let singular = underscored.replace('/', "_");
let plural = pluralize(&singular);
let collection = pluralize(&underscored);
let element = underscore(&demodulize(name));
let human = humanize(&element);
let param_key = singular.clone();
let route_key = if singular == plural {
format!("{plural}_index")
} else {
plural.clone()
};
let singular_route_key = if singular == plural {
singular.clone()
} else {
singularize(&route_key)
};
Self {
name: name.to_string(),
singular,
plural,
element,
collection,
human,
param_key,
route_key,
singular_route_key,
i18n_key: underscored,
}
}
}
pub fn human_attribute_name(attribute: &str) -> String {
humanize(&underscore(attribute.trim()))
}
#[cfg(test)]
mod tests {
use super::{ModelName, ModelNaming, human_attribute_name};
#[derive(Debug, Default)]
struct User;
impl ModelNaming for User {}
#[derive(Debug, Default)]
struct BlogPost;
impl ModelNaming for BlogPost {}
#[test]
fn builds_model_name_for_regular_noun() {
let model_name = ModelName::new("User");
assert_eq!(model_name.name, "User");
assert_eq!(model_name.singular, "user");
assert_eq!(model_name.plural, "users");
assert_eq!(model_name.element, "user");
assert_eq!(model_name.collection, "users");
assert_eq!(model_name.human, "User");
assert_eq!(model_name.param_key, "user");
assert_eq!(model_name.route_key, "users");
assert_eq!(model_name.singular_route_key, "user");
assert_eq!(model_name.i18n_key, "user");
}
#[test]
fn builds_model_name_for_compound_and_irregular_names() {
let compound = ModelName::new("BlogPost");
let irregular = ModelName::new("Person");
assert_eq!(compound.singular, "blog_post");
assert_eq!(compound.plural, "blog_posts");
assert_eq!(compound.human, "Blog post");
assert_eq!(irregular.singular, "person");
assert_eq!(irregular.plural, "people");
assert_eq!(irregular.route_key, "people");
}
#[test]
fn default_trait_model_name_uses_type_name() {
let user_name = User::model_name();
let blog_post_name = BlogPost::model_name();
assert_eq!(user_name.name, "User");
assert_eq!(blog_post_name.element, "blog_post");
assert_eq!(blog_post_name.route_key, "blog_posts");
}
#[test]
fn humanizes_attribute_names() {
assert_eq!(human_attribute_name("first_name"), "First name");
assert_eq!(User::human_attribute_name("account_id"), "Account");
assert_eq!(
BlogPost::human_attribute_name("publishedAt"),
"Published at"
);
}
#[test]
fn supports_namespaced_type_names() {
let model_name = ModelName::new("Admin::User");
assert_eq!(model_name.singular, "admin_user");
assert_eq!(model_name.route_key, "admin_users");
assert_eq!(model_name.element, "user");
assert_eq!(model_name.i18n_key, "admin/user");
}
#[test]
fn model_name_preserves_route_information_for_irregular_nouns() {
let model_name = ModelName::new("Person");
assert_eq!(model_name.param_key, "person");
assert_eq!(model_name.route_key, "people");
assert_eq!(model_name.singular_route_key, "person");
}
#[test]
fn human_attribute_name_trims_extra_whitespace() {
assert_eq!(human_attribute_name(" first_name "), "First name");
}
#[test]
fn model_name_human_uses_demodulized_element() {
let model_name = ModelName::new("Admin::BillingAddress");
assert_eq!(model_name.human, "Billing address");
}
#[test]
fn model_name_name_returns_the_class_name() {
assert_eq!(ModelName::new("User").name, "User");
}
#[test]
fn model_name_singular_returns_the_singular_form() {
assert_eq!(ModelName::new("User").singular, "user");
}
#[test]
fn model_name_plural_returns_the_plural_form() {
assert_eq!(ModelName::new("User").plural, "users");
}
#[test]
fn model_name_element_returns_the_underscored_singular_name() {
assert_eq!(ModelName::new("User").element, "user");
}
#[test]
fn model_name_human_returns_the_humanized_name() {
assert_eq!(ModelName::new("User").human, "User");
}
#[test]
fn model_name_param_key_returns_the_parameter_key() {
assert_eq!(ModelName::new("User").param_key, "user");
}
#[test]
fn model_name_route_key_returns_the_route_key() {
assert_eq!(ModelName::new("User").route_key, "users");
}
#[test]
fn model_name_singular_route_key_returns_the_singular_route_key() {
assert_eq!(ModelName::new("User").singular_route_key, "user");
}
#[test]
fn model_name_i18n_key_returns_the_i18n_lookup_key() {
assert_eq!(ModelName::new("User").i18n_key, "user");
}
#[test]
fn compound_model_name_singular_underscores_words() {
assert_eq!(ModelName::new("BlogPost").singular, "blog_post");
}
#[test]
fn compound_model_name_plural_pluralizes_the_underscored_name() {
assert_eq!(ModelName::new("BlogPost").plural, "blog_posts");
}
#[test]
fn compound_model_name_element_uses_the_demodulized_name() {
assert_eq!(ModelName::new("BlogPost").element, "blog_post");
}
#[test]
fn compound_model_name_human_humanizes_words() {
assert_eq!(ModelName::new("BlogPost").human, "Blog post");
}
#[test]
fn irregular_model_name_pluralizes_to_people() {
assert_eq!(ModelName::new("Person").plural, "people");
}
#[test]
fn irregular_model_name_singular_route_key_stays_person() {
assert_eq!(ModelName::new("Person").singular_route_key, "person");
}
#[test]
fn namespaced_model_name_singular_preserves_namespace() {
assert_eq!(ModelName::new("Admin::User").singular, "admin_user");
}
#[test]
fn namespaced_model_name_element_uses_the_demodulized_name() {
assert_eq!(ModelName::new("Admin::User").element, "user");
}
#[test]
fn namespaced_model_name_route_key_pluralizes_the_namespace_aware_key() {
assert_eq!(ModelName::new("Admin::User").route_key, "admin_users");
}
#[test]
fn namespaced_model_name_i18n_key_uses_slashes() {
assert_eq!(ModelName::new("Admin::User").i18n_key, "admin/user");
}
#[test]
fn human_attribute_name_handles_snake_case_names() {
assert_eq!(
human_attribute_name("billing_address_line"),
"Billing address line"
);
}
#[test]
fn human_attribute_name_handles_camel_case_names_with_whitespace() {
assert_eq!(
human_attribute_name(" preferredContactMethod "),
"Preferred contact method"
);
}
#[test]
fn default_trait_model_name_uses_the_type_name_for_regular_structs() {
#[derive(Debug, Default)]
struct InvoiceLine;
impl ModelNaming for InvoiceLine {}
let model_name = InvoiceLine::model_name();
assert_eq!(model_name.name, "InvoiceLine");
assert_eq!(model_name.element, "invoice_line");
}
#[test]
fn custom_model_name_override_can_customize_the_human_name() {
#[derive(Debug, Default)]
struct CustomerRecord;
impl ModelNaming for CustomerRecord {
fn model_name() -> ModelName {
let mut model_name = ModelName::new("User");
model_name.human = "Customer".to_string();
model_name.param_key = "customer".to_string();
model_name.route_key = "customers".to_string();
model_name.singular_route_key = "customer".to_string();
model_name
}
}
assert_eq!(CustomerRecord::model_name().human, "Customer");
}
#[test]
fn custom_model_name_override_can_customize_the_param_key() {
#[derive(Debug, Default)]
struct CustomerRecord;
impl ModelNaming for CustomerRecord {
fn model_name() -> ModelName {
let mut model_name = ModelName::new("User");
model_name.human = "Customer".to_string();
model_name.param_key = "customer".to_string();
model_name.route_key = "customers".to_string();
model_name.singular_route_key = "customer".to_string();
model_name
}
}
assert_eq!(CustomerRecord::model_name().param_key, "customer");
assert_eq!(CustomerRecord::model_name().route_key, "customers");
}
mod nested_models {
use super::{ModelName, ModelNaming};
#[derive(Debug, Default)]
pub(super) struct AuditEvent;
impl ModelNaming for AuditEvent {}
#[derive(Debug, Default)]
pub(super) struct BlogPostAlias;
impl ModelNaming for BlogPostAlias {
fn model_name() -> ModelName {
ModelName::new("Article")
}
}
}
#[test]
fn namespaced_compound_model_name_matches_track_back_reference() {
let model_name = ModelName::new("Post::TrackBack");
assert_eq!(model_name.singular, "post_track_back");
assert_eq!(model_name.plural, "post_track_backs");
assert_eq!(model_name.element, "track_back");
assert_eq!(model_name.human, "Track back");
assert_eq!(model_name.param_key, "post_track_back");
assert_eq!(model_name.route_key, "post_track_backs");
assert_eq!(model_name.i18n_key, "post/track_back");
}
#[test]
fn deeply_namespaced_irregular_model_name_pluralizes_the_final_segment() {
let model_name = ModelName::new("Admin::Support::Person");
assert_eq!(model_name.singular, "admin_support_person");
assert_eq!(model_name.element, "person");
assert_eq!(model_name.human, "Person");
assert_eq!(model_name.route_key, "admin_support_people");
assert_eq!(model_name.singular_route_key, "admin_support_person");
assert_eq!(model_name.i18n_key, "admin/support/person");
}
#[test]
fn default_trait_model_name_demodulizes_nested_type_names() {
let model_name = nested_models::AuditEvent::model_name();
assert_eq!(model_name.name, "AuditEvent");
assert_eq!(model_name.element, "audit_event");
assert_eq!(model_name.route_key, "audit_events");
assert_eq!(model_name.i18n_key, "audit_event");
}
#[test]
fn custom_model_name_override_can_replace_the_external_name() {
let model_name = nested_models::BlogPostAlias::model_name();
assert_eq!(model_name.name, "Article");
assert_eq!(model_name.singular, "article");
assert_eq!(model_name.element, "article");
assert_eq!(model_name.human, "Article");
assert_eq!(model_name.route_key, "articles");
assert_eq!(model_name.i18n_key, "article");
}
fn rails_track_back_model_name() -> ModelName {
ModelName::new("Post::TrackBack")
}
fn rails_isolated_blog_post_model_name() -> ModelName {
let mut model_name = ModelName::new("Blog::Post");
model_name.route_key = "posts".to_string();
model_name.param_key = "post".to_string();
model_name.singular_route_key = "post".to_string();
model_name
}
fn rails_shared_blog_post_model_name() -> ModelName {
ModelName::new("Blog::Post")
}
fn rails_supplied_article_model_name() -> ModelName {
ModelName::new("Article")
}
#[derive(Debug, Default)]
struct Contact;
impl ModelNaming for Contact {}
#[derive(Debug, Default)]
struct Sheep;
impl ModelNaming for Sheep {}
#[derive(Debug, Default)]
struct RelativeBlogPost;
impl ModelNaming for RelativeBlogPost {
fn model_name() -> ModelName {
rails_isolated_blog_post_model_name()
}
}
#[test]
fn test_rails_track_back_singular() {
assert_eq!(rails_track_back_model_name().singular, "post_track_back");
}
#[test]
fn test_rails_track_back_plural() {
assert_eq!(rails_track_back_model_name().plural, "post_track_backs");
}
#[test]
fn test_rails_track_back_element() {
assert_eq!(rails_track_back_model_name().element, "track_back");
}
#[test]
fn test_rails_track_back_collection() {
assert_eq!(rails_track_back_model_name().collection, "post/track_backs");
}
#[test]
fn test_rails_track_back_human() {
assert_eq!(rails_track_back_model_name().human, "Track back");
}
#[test]
fn test_rails_track_back_route_key() {
assert_eq!(rails_track_back_model_name().route_key, "post_track_backs");
}
#[test]
fn test_rails_track_back_param_key() {
assert_eq!(rails_track_back_model_name().param_key, "post_track_back");
}
#[test]
fn test_rails_track_back_i18n_key() {
assert_eq!(rails_track_back_model_name().i18n_key, "post/track_back");
}
#[test]
fn test_rails_track_back_is_not_uncountable() {
let model_name = rails_track_back_model_name();
assert_ne!(model_name.singular, model_name.plural);
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_singular() {
assert_eq!(rails_isolated_blog_post_model_name().singular, "blog_post");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_plural() {
assert_eq!(rails_isolated_blog_post_model_name().plural, "blog_posts");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_element() {
assert_eq!(rails_isolated_blog_post_model_name().element, "post");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_collection() {
assert_eq!(
rails_isolated_blog_post_model_name().collection,
"blog/posts"
);
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_human() {
assert_eq!(rails_isolated_blog_post_model_name().human, "Post");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_route_key() {
assert_eq!(rails_isolated_blog_post_model_name().route_key, "posts");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_param_key() {
assert_eq!(rails_isolated_blog_post_model_name().param_key, "post");
}
#[test]
fn test_rails_namespaced_model_in_isolated_namespace_i18n_key() {
assert_eq!(rails_isolated_blog_post_model_name().i18n_key, "blog/post");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_singular() {
assert_eq!(rails_shared_blog_post_model_name().singular, "blog_post");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_plural() {
assert_eq!(rails_shared_blog_post_model_name().plural, "blog_posts");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_element() {
assert_eq!(rails_shared_blog_post_model_name().element, "post");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_collection() {
assert_eq!(rails_shared_blog_post_model_name().collection, "blog/posts");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_human() {
assert_eq!(rails_shared_blog_post_model_name().human, "Post");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_route_key() {
assert_eq!(rails_shared_blog_post_model_name().route_key, "blog_posts");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_param_key() {
assert_eq!(rails_shared_blog_post_model_name().param_key, "blog_post");
}
#[test]
fn test_rails_namespaced_model_in_shared_namespace_i18n_key() {
assert_eq!(rails_shared_blog_post_model_name().i18n_key, "blog/post");
}
#[test]
fn test_rails_supplied_model_name_singular() {
assert_eq!(rails_supplied_article_model_name().singular, "article");
}
#[test]
fn test_rails_supplied_model_name_plural() {
assert_eq!(rails_supplied_article_model_name().plural, "articles");
}
#[test]
fn test_rails_supplied_model_name_element() {
assert_eq!(rails_supplied_article_model_name().element, "article");
}
#[test]
fn test_rails_supplied_model_name_collection() {
assert_eq!(rails_supplied_article_model_name().collection, "articles");
}
#[test]
fn test_rails_supplied_model_name_human() {
assert_eq!(rails_supplied_article_model_name().human, "Article");
}
#[test]
fn test_rails_supplied_model_name_route_key() {
assert_eq!(rails_supplied_article_model_name().route_key, "articles");
}
#[test]
fn test_rails_supplied_model_name_param_key() {
assert_eq!(rails_supplied_article_model_name().param_key, "article");
}
#[test]
fn test_rails_supplied_model_name_i18n_key() {
assert_eq!(rails_supplied_article_model_name().i18n_key, "article");
}
#[test]
fn test_rails_supplied_locale_singular() {
assert_eq!(ModelName::new("Uzivatel").singular, "uzivatel");
}
#[test]
#[ignore = "Rails-specific: ModelName::new has no locale-specific inflection support"]
fn test_rails_supplied_locale_plural() {}
#[test]
fn test_rails_relative_model_name_singular() {
assert_eq!(RelativeBlogPost::model_name().singular, "blog_post");
}
#[test]
fn test_rails_relative_model_name_plural() {
assert_eq!(RelativeBlogPost::model_name().plural, "blog_posts");
}
#[test]
fn test_rails_relative_model_name_element() {
assert_eq!(RelativeBlogPost::model_name().element, "post");
}
#[test]
fn test_rails_relative_model_name_collection() {
assert_eq!(RelativeBlogPost::model_name().collection, "blog/posts");
}
#[test]
fn test_rails_relative_model_name_human() {
assert_eq!(RelativeBlogPost::model_name().human, "Post");
}
#[test]
fn test_rails_relative_model_name_route_key() {
assert_eq!(RelativeBlogPost::model_name().route_key, "posts");
}
#[test]
fn test_rails_relative_model_name_param_key() {
assert_eq!(RelativeBlogPost::model_name().param_key, "post");
}
#[test]
fn test_rails_relative_model_name_i18n_key() {
assert_eq!(RelativeBlogPost::model_name().i18n_key, "blog/post");
}
#[test]
fn test_rails_naming_helpers_to_model_called_on_record() {
let contact = Contact;
assert_eq!(contact.to_model().param_key(), "contact");
}
#[test]
fn test_rails_naming_helpers_singular_for_record() {
let contact = Contact;
assert_eq!(contact.singular(), "contact");
}
#[test]
fn test_rails_naming_helpers_singular_for_class() {
assert_eq!(Contact::model_name().singular, "contact");
}
#[test]
fn test_rails_naming_helpers_plural_for_record() {
let contact = Contact;
assert_eq!(contact.plural(), "contacts");
}
#[test]
fn test_rails_naming_helpers_plural_for_class() {
assert_eq!(Contact::model_name().plural, "contacts");
}
#[test]
fn test_rails_naming_helpers_route_key_for_record() {
let contact = Contact;
assert_eq!(contact.route_key(), "contacts");
assert_eq!(contact.singular_route_key(), "contact");
}
#[test]
fn test_rails_naming_helpers_route_key_for_class() {
let model_name = Contact::model_name();
assert_eq!(model_name.route_key, "contacts");
assert_eq!(model_name.singular_route_key, "contact");
}
#[test]
fn test_rails_naming_helpers_param_key_for_record() {
let contact = Contact;
assert_eq!(contact.param_key(), "contact");
}
#[test]
fn test_rails_naming_helpers_param_key_for_class() {
assert_eq!(Contact::model_name().param_key, "contact");
}
#[test]
fn test_rails_naming_helpers_uncountable() {
let sheep_name = Sheep::model_name();
let contact_name = Contact::model_name();
assert_eq!(sheep_name.singular, "sheep");
assert_eq!(sheep_name.plural, "sheep");
assert_ne!(contact_name.singular, contact_name.plural);
}
#[test]
fn test_rails_naming_helpers_uncountable_route_key() {
let sheep_name = Sheep::model_name();
assert_eq!(sheep_name.singular_route_key, "sheep");
assert_eq!(sheep_name.route_key, "sheep_index");
assert!(Sheep::uncountable());
}
#[test]
#[ignore = "Rails-specific: Rust types are always named and ModelName::new requires an explicit name"]
fn test_rails_anonymous_class_without_name_argument() {}
#[test]
fn test_rails_anonymous_class_with_name_argument() {
assert_eq!(ModelName::new("Anonymous").name, "Anonymous");
}
#[test]
fn test_rails_model_name_method_delegation() {
let contact = Contact;
assert_eq!(contact.model_name_instance(), Contact::model_name());
}
#[test]
fn test_rails_overriding_accessors_keys_for_supported_fields() {
let mut model_name = rails_track_back_model_name();
model_name.singular = "singular".to_string();
model_name.plural = "plural".to_string();
model_name.element = "element".to_string();
model_name.collection = "collection".to_string();
model_name.singular_route_key = "singular_route_key".to_string();
model_name.route_key = "route_key".to_string();
model_name.param_key = "param_key".to_string();
model_name.i18n_key = "i18n_key".to_string();
model_name.name = "name".to_string();
assert_eq!(model_name.singular, "singular");
assert_eq!(model_name.plural, "plural");
assert_eq!(model_name.element, "element");
assert_eq!(model_name.collection, "collection");
assert_eq!(model_name.singular_route_key, "singular_route_key");
assert_eq!(model_name.route_key, "route_key");
assert_eq!(model_name.param_key, "param_key");
assert_eq!(model_name.i18n_key, "i18n_key");
assert_eq!(model_name.name, "name");
}
#[test]
fn test_rails_overriding_accessors_collection_key() {
let mut model_name = rails_track_back_model_name();
model_name.collection = "custom/collection".to_string();
assert_eq!(model_name.collection, "custom/collection");
}
#[test]
fn test_rails_anonymous_class_with_name_argument_uses_the_supplied_name_for_metadata() {
let model_name = ModelName::new("Anonymous");
assert_eq!(model_name.name, "Anonymous");
assert_eq!(model_name.singular, "anonymous");
assert_eq!(model_name.element, "anonymous");
assert_eq!(model_name.human, "Anonymous");
assert_eq!(model_name.param_key, "anonymous");
assert_eq!(model_name.i18n_key, "anonymous");
}
}