Skip to main content

rustrails_model/
naming.rs

1use std::any::type_name;
2
3use rustrails_support::inflector::{demodulize, humanize, pluralize, singularize, underscore};
4
5/// Model naming conventions shared across model-like types.
6///
7/// Override [`ModelNaming::model_name`] when a type needs custom naming metadata,
8/// such as namespaced routing or a non-standard external name.
9pub trait ModelNaming {
10    /// Returns cached naming metadata for the implementing type.
11    fn model_name() -> ModelName {
12        let type_name = type_name::<Self>();
13        let demodulized = demodulize(type_name);
14        ModelName::new(&demodulized)
15    }
16
17    /// Returns cached naming metadata from an instance.
18    fn model_name_instance(&self) -> ModelName {
19        Self::model_name()
20    }
21
22    /// Mirrors Rails' `to_model` naming helper for already-materialized records.
23    fn to_model(&self) -> &Self {
24        self
25    }
26
27    /// Returns the singular key for a record instance.
28    fn singular(&self) -> String {
29        Self::model_name().singular
30    }
31
32    /// Returns the plural key for a record instance.
33    fn plural(&self) -> String {
34        Self::model_name().plural
35    }
36
37    /// Returns the route collection key for a record instance.
38    fn route_key(&self) -> String {
39        Self::model_name().route_key
40    }
41
42    /// Returns the singular route key for a record instance.
43    fn singular_route_key(&self) -> String {
44        Self::model_name().singular_route_key
45    }
46
47    /// Returns the URL parameter key for a record instance.
48    fn param_key(&self) -> String {
49        Self::model_name().param_key
50    }
51
52    /// Returns `true` when the model name is uncountable.
53    fn uncountable() -> bool {
54        let model_name = Self::model_name();
55        model_name.singular == model_name.plural
56    }
57
58    /// Returns a human-readable label for an attribute name.
59    fn human_attribute_name(attribute: &str) -> String {
60        crate::naming::human_attribute_name(attribute)
61    }
62}
63
64/// Cached naming metadata derived from a model type name.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct ModelName {
67    /// Canonical type name.
68    pub name: String,
69    /// Singular underscored key.
70    pub singular: String,
71    /// Plural route key.
72    pub plural: String,
73    /// Underscored element name.
74    pub element: String,
75    /// Collection path used by partial lookup.
76    pub collection: String,
77    /// Human-friendly display name.
78    pub human: String,
79    /// URL parameter key.
80    pub param_key: String,
81    /// Route collection key.
82    pub route_key: String,
83    /// Singular route key.
84    pub singular_route_key: String,
85    /// I18n lookup key.
86    pub i18n_key: String,
87}
88
89impl ModelName {
90    /// Builds naming metadata from a Rust type name such as `User` or `BlogPost`.
91    pub fn new(name: &str) -> Self {
92        let underscored = underscore(name);
93        let singular = underscored.replace('/', "_");
94        let plural = pluralize(&singular);
95        let collection = pluralize(&underscored);
96        let element = underscore(&demodulize(name));
97        let human = humanize(&element);
98        let param_key = singular.clone();
99        let route_key = if singular == plural {
100            format!("{plural}_index")
101        } else {
102            plural.clone()
103        };
104        let singular_route_key = if singular == plural {
105            singular.clone()
106        } else {
107            singularize(&route_key)
108        };
109
110        Self {
111            name: name.to_string(),
112            singular,
113            plural,
114            element,
115            collection,
116            human,
117            param_key,
118            route_key,
119            singular_route_key,
120            i18n_key: underscored,
121        }
122    }
123}
124
125/// Converts an attribute identifier into a human-readable label.
126pub fn human_attribute_name(attribute: &str) -> String {
127    humanize(&underscore(attribute.trim()))
128}
129
130#[cfg(test)]
131mod tests {
132    use super::{ModelName, ModelNaming, human_attribute_name};
133
134    #[derive(Debug, Default)]
135    struct User;
136
137    impl ModelNaming for User {}
138
139    #[derive(Debug, Default)]
140    struct BlogPost;
141
142    impl ModelNaming for BlogPost {}
143
144    #[test]
145    fn builds_model_name_for_regular_noun() {
146        let model_name = ModelName::new("User");
147
148        assert_eq!(model_name.name, "User");
149        assert_eq!(model_name.singular, "user");
150        assert_eq!(model_name.plural, "users");
151        assert_eq!(model_name.element, "user");
152        assert_eq!(model_name.collection, "users");
153        assert_eq!(model_name.human, "User");
154        assert_eq!(model_name.param_key, "user");
155        assert_eq!(model_name.route_key, "users");
156        assert_eq!(model_name.singular_route_key, "user");
157        assert_eq!(model_name.i18n_key, "user");
158    }
159
160    #[test]
161    fn builds_model_name_for_compound_and_irregular_names() {
162        let compound = ModelName::new("BlogPost");
163        let irregular = ModelName::new("Person");
164
165        assert_eq!(compound.singular, "blog_post");
166        assert_eq!(compound.plural, "blog_posts");
167        assert_eq!(compound.human, "Blog post");
168        assert_eq!(irregular.singular, "person");
169        assert_eq!(irregular.plural, "people");
170        assert_eq!(irregular.route_key, "people");
171    }
172
173    #[test]
174    fn default_trait_model_name_uses_type_name() {
175        let user_name = User::model_name();
176        let blog_post_name = BlogPost::model_name();
177
178        assert_eq!(user_name.name, "User");
179        assert_eq!(blog_post_name.element, "blog_post");
180        assert_eq!(blog_post_name.route_key, "blog_posts");
181    }
182
183    #[test]
184    fn humanizes_attribute_names() {
185        assert_eq!(human_attribute_name("first_name"), "First name");
186        assert_eq!(User::human_attribute_name("account_id"), "Account");
187        assert_eq!(
188            BlogPost::human_attribute_name("publishedAt"),
189            "Published at"
190        );
191    }
192    #[test]
193    fn supports_namespaced_type_names() {
194        let model_name = ModelName::new("Admin::User");
195
196        assert_eq!(model_name.singular, "admin_user");
197        assert_eq!(model_name.route_key, "admin_users");
198        assert_eq!(model_name.element, "user");
199        assert_eq!(model_name.i18n_key, "admin/user");
200    }
201
202    #[test]
203    fn model_name_preserves_route_information_for_irregular_nouns() {
204        let model_name = ModelName::new("Person");
205
206        assert_eq!(model_name.param_key, "person");
207        assert_eq!(model_name.route_key, "people");
208        assert_eq!(model_name.singular_route_key, "person");
209    }
210
211    #[test]
212    fn human_attribute_name_trims_extra_whitespace() {
213        assert_eq!(human_attribute_name("  first_name  "), "First name");
214    }
215
216    #[test]
217    fn model_name_human_uses_demodulized_element() {
218        let model_name = ModelName::new("Admin::BillingAddress");
219        assert_eq!(model_name.human, "Billing address");
220    }
221
222    #[test]
223    fn model_name_name_returns_the_class_name() {
224        assert_eq!(ModelName::new("User").name, "User");
225    }
226
227    #[test]
228    fn model_name_singular_returns_the_singular_form() {
229        assert_eq!(ModelName::new("User").singular, "user");
230    }
231
232    #[test]
233    fn model_name_plural_returns_the_plural_form() {
234        assert_eq!(ModelName::new("User").plural, "users");
235    }
236
237    #[test]
238    fn model_name_element_returns_the_underscored_singular_name() {
239        assert_eq!(ModelName::new("User").element, "user");
240    }
241
242    #[test]
243    fn model_name_human_returns_the_humanized_name() {
244        assert_eq!(ModelName::new("User").human, "User");
245    }
246
247    #[test]
248    fn model_name_param_key_returns_the_parameter_key() {
249        assert_eq!(ModelName::new("User").param_key, "user");
250    }
251
252    #[test]
253    fn model_name_route_key_returns_the_route_key() {
254        assert_eq!(ModelName::new("User").route_key, "users");
255    }
256
257    #[test]
258    fn model_name_singular_route_key_returns_the_singular_route_key() {
259        assert_eq!(ModelName::new("User").singular_route_key, "user");
260    }
261
262    #[test]
263    fn model_name_i18n_key_returns_the_i18n_lookup_key() {
264        assert_eq!(ModelName::new("User").i18n_key, "user");
265    }
266
267    #[test]
268    fn compound_model_name_singular_underscores_words() {
269        assert_eq!(ModelName::new("BlogPost").singular, "blog_post");
270    }
271
272    #[test]
273    fn compound_model_name_plural_pluralizes_the_underscored_name() {
274        assert_eq!(ModelName::new("BlogPost").plural, "blog_posts");
275    }
276
277    #[test]
278    fn compound_model_name_element_uses_the_demodulized_name() {
279        assert_eq!(ModelName::new("BlogPost").element, "blog_post");
280    }
281
282    #[test]
283    fn compound_model_name_human_humanizes_words() {
284        assert_eq!(ModelName::new("BlogPost").human, "Blog post");
285    }
286
287    #[test]
288    fn irregular_model_name_pluralizes_to_people() {
289        assert_eq!(ModelName::new("Person").plural, "people");
290    }
291
292    #[test]
293    fn irregular_model_name_singular_route_key_stays_person() {
294        assert_eq!(ModelName::new("Person").singular_route_key, "person");
295    }
296
297    #[test]
298    fn namespaced_model_name_singular_preserves_namespace() {
299        assert_eq!(ModelName::new("Admin::User").singular, "admin_user");
300    }
301
302    #[test]
303    fn namespaced_model_name_element_uses_the_demodulized_name() {
304        assert_eq!(ModelName::new("Admin::User").element, "user");
305    }
306
307    #[test]
308    fn namespaced_model_name_route_key_pluralizes_the_namespace_aware_key() {
309        assert_eq!(ModelName::new("Admin::User").route_key, "admin_users");
310    }
311
312    #[test]
313    fn namespaced_model_name_i18n_key_uses_slashes() {
314        assert_eq!(ModelName::new("Admin::User").i18n_key, "admin/user");
315    }
316
317    #[test]
318    fn human_attribute_name_handles_snake_case_names() {
319        assert_eq!(
320            human_attribute_name("billing_address_line"),
321            "Billing address line"
322        );
323    }
324
325    #[test]
326    fn human_attribute_name_handles_camel_case_names_with_whitespace() {
327        assert_eq!(
328            human_attribute_name("  preferredContactMethod  "),
329            "Preferred contact method"
330        );
331    }
332
333    #[test]
334    fn default_trait_model_name_uses_the_type_name_for_regular_structs() {
335        #[derive(Debug, Default)]
336        struct InvoiceLine;
337
338        impl ModelNaming for InvoiceLine {}
339
340        let model_name = InvoiceLine::model_name();
341
342        assert_eq!(model_name.name, "InvoiceLine");
343        assert_eq!(model_name.element, "invoice_line");
344    }
345
346    #[test]
347    fn custom_model_name_override_can_customize_the_human_name() {
348        #[derive(Debug, Default)]
349        struct CustomerRecord;
350
351        impl ModelNaming for CustomerRecord {
352            fn model_name() -> ModelName {
353                let mut model_name = ModelName::new("User");
354                model_name.human = "Customer".to_string();
355                model_name.param_key = "customer".to_string();
356                model_name.route_key = "customers".to_string();
357                model_name.singular_route_key = "customer".to_string();
358                model_name
359            }
360        }
361
362        assert_eq!(CustomerRecord::model_name().human, "Customer");
363    }
364
365    #[test]
366    fn custom_model_name_override_can_customize_the_param_key() {
367        #[derive(Debug, Default)]
368        struct CustomerRecord;
369
370        impl ModelNaming for CustomerRecord {
371            fn model_name() -> ModelName {
372                let mut model_name = ModelName::new("User");
373                model_name.human = "Customer".to_string();
374                model_name.param_key = "customer".to_string();
375                model_name.route_key = "customers".to_string();
376                model_name.singular_route_key = "customer".to_string();
377                model_name
378            }
379        }
380
381        assert_eq!(CustomerRecord::model_name().param_key, "customer");
382        assert_eq!(CustomerRecord::model_name().route_key, "customers");
383    }
384
385    mod nested_models {
386        use super::{ModelName, ModelNaming};
387
388        #[derive(Debug, Default)]
389        pub(super) struct AuditEvent;
390
391        impl ModelNaming for AuditEvent {}
392
393        #[derive(Debug, Default)]
394        pub(super) struct BlogPostAlias;
395
396        impl ModelNaming for BlogPostAlias {
397            fn model_name() -> ModelName {
398                ModelName::new("Article")
399            }
400        }
401    }
402
403    #[test]
404    fn namespaced_compound_model_name_matches_track_back_reference() {
405        let model_name = ModelName::new("Post::TrackBack");
406
407        assert_eq!(model_name.singular, "post_track_back");
408        assert_eq!(model_name.plural, "post_track_backs");
409        assert_eq!(model_name.element, "track_back");
410        assert_eq!(model_name.human, "Track back");
411        assert_eq!(model_name.param_key, "post_track_back");
412        assert_eq!(model_name.route_key, "post_track_backs");
413        assert_eq!(model_name.i18n_key, "post/track_back");
414    }
415
416    #[test]
417    fn deeply_namespaced_irregular_model_name_pluralizes_the_final_segment() {
418        let model_name = ModelName::new("Admin::Support::Person");
419
420        assert_eq!(model_name.singular, "admin_support_person");
421        assert_eq!(model_name.element, "person");
422        assert_eq!(model_name.human, "Person");
423        assert_eq!(model_name.route_key, "admin_support_people");
424        assert_eq!(model_name.singular_route_key, "admin_support_person");
425        assert_eq!(model_name.i18n_key, "admin/support/person");
426    }
427
428    #[test]
429    fn default_trait_model_name_demodulizes_nested_type_names() {
430        let model_name = nested_models::AuditEvent::model_name();
431
432        assert_eq!(model_name.name, "AuditEvent");
433        assert_eq!(model_name.element, "audit_event");
434        assert_eq!(model_name.route_key, "audit_events");
435        assert_eq!(model_name.i18n_key, "audit_event");
436    }
437
438    #[test]
439    fn custom_model_name_override_can_replace_the_external_name() {
440        let model_name = nested_models::BlogPostAlias::model_name();
441
442        assert_eq!(model_name.name, "Article");
443        assert_eq!(model_name.singular, "article");
444        assert_eq!(model_name.element, "article");
445        assert_eq!(model_name.human, "Article");
446        assert_eq!(model_name.route_key, "articles");
447        assert_eq!(model_name.i18n_key, "article");
448    }
449
450    fn rails_track_back_model_name() -> ModelName {
451        ModelName::new("Post::TrackBack")
452    }
453
454    fn rails_isolated_blog_post_model_name() -> ModelName {
455        let mut model_name = ModelName::new("Blog::Post");
456        model_name.route_key = "posts".to_string();
457        model_name.param_key = "post".to_string();
458        model_name.singular_route_key = "post".to_string();
459        model_name
460    }
461
462    fn rails_shared_blog_post_model_name() -> ModelName {
463        ModelName::new("Blog::Post")
464    }
465
466    fn rails_supplied_article_model_name() -> ModelName {
467        ModelName::new("Article")
468    }
469
470    #[derive(Debug, Default)]
471    struct Contact;
472
473    impl ModelNaming for Contact {}
474
475    #[derive(Debug, Default)]
476    struct Sheep;
477
478    impl ModelNaming for Sheep {}
479
480    #[derive(Debug, Default)]
481    struct RelativeBlogPost;
482
483    impl ModelNaming for RelativeBlogPost {
484        fn model_name() -> ModelName {
485            rails_isolated_blog_post_model_name()
486        }
487    }
488
489    #[test]
490    fn test_rails_track_back_singular() {
491        assert_eq!(rails_track_back_model_name().singular, "post_track_back");
492    }
493
494    #[test]
495    fn test_rails_track_back_plural() {
496        assert_eq!(rails_track_back_model_name().plural, "post_track_backs");
497    }
498
499    #[test]
500    fn test_rails_track_back_element() {
501        assert_eq!(rails_track_back_model_name().element, "track_back");
502    }
503
504    #[test]
505    fn test_rails_track_back_collection() {
506        assert_eq!(rails_track_back_model_name().collection, "post/track_backs");
507    }
508
509    #[test]
510    fn test_rails_track_back_human() {
511        assert_eq!(rails_track_back_model_name().human, "Track back");
512    }
513
514    #[test]
515    fn test_rails_track_back_route_key() {
516        assert_eq!(rails_track_back_model_name().route_key, "post_track_backs");
517    }
518
519    #[test]
520    fn test_rails_track_back_param_key() {
521        assert_eq!(rails_track_back_model_name().param_key, "post_track_back");
522    }
523
524    #[test]
525    fn test_rails_track_back_i18n_key() {
526        assert_eq!(rails_track_back_model_name().i18n_key, "post/track_back");
527    }
528
529    #[test]
530    fn test_rails_track_back_is_not_uncountable() {
531        let model_name = rails_track_back_model_name();
532        assert_ne!(model_name.singular, model_name.plural);
533    }
534
535    #[test]
536    fn test_rails_namespaced_model_in_isolated_namespace_singular() {
537        assert_eq!(rails_isolated_blog_post_model_name().singular, "blog_post");
538    }
539
540    #[test]
541    fn test_rails_namespaced_model_in_isolated_namespace_plural() {
542        assert_eq!(rails_isolated_blog_post_model_name().plural, "blog_posts");
543    }
544
545    #[test]
546    fn test_rails_namespaced_model_in_isolated_namespace_element() {
547        assert_eq!(rails_isolated_blog_post_model_name().element, "post");
548    }
549
550    #[test]
551    fn test_rails_namespaced_model_in_isolated_namespace_collection() {
552        assert_eq!(
553            rails_isolated_blog_post_model_name().collection,
554            "blog/posts"
555        );
556    }
557
558    #[test]
559    fn test_rails_namespaced_model_in_isolated_namespace_human() {
560        assert_eq!(rails_isolated_blog_post_model_name().human, "Post");
561    }
562
563    #[test]
564    fn test_rails_namespaced_model_in_isolated_namespace_route_key() {
565        assert_eq!(rails_isolated_blog_post_model_name().route_key, "posts");
566    }
567
568    #[test]
569    fn test_rails_namespaced_model_in_isolated_namespace_param_key() {
570        assert_eq!(rails_isolated_blog_post_model_name().param_key, "post");
571    }
572
573    #[test]
574    fn test_rails_namespaced_model_in_isolated_namespace_i18n_key() {
575        assert_eq!(rails_isolated_blog_post_model_name().i18n_key, "blog/post");
576    }
577
578    #[test]
579    fn test_rails_namespaced_model_in_shared_namespace_singular() {
580        assert_eq!(rails_shared_blog_post_model_name().singular, "blog_post");
581    }
582
583    #[test]
584    fn test_rails_namespaced_model_in_shared_namespace_plural() {
585        assert_eq!(rails_shared_blog_post_model_name().plural, "blog_posts");
586    }
587
588    #[test]
589    fn test_rails_namespaced_model_in_shared_namespace_element() {
590        assert_eq!(rails_shared_blog_post_model_name().element, "post");
591    }
592
593    #[test]
594    fn test_rails_namespaced_model_in_shared_namespace_collection() {
595        assert_eq!(rails_shared_blog_post_model_name().collection, "blog/posts");
596    }
597
598    #[test]
599    fn test_rails_namespaced_model_in_shared_namespace_human() {
600        assert_eq!(rails_shared_blog_post_model_name().human, "Post");
601    }
602
603    #[test]
604    fn test_rails_namespaced_model_in_shared_namespace_route_key() {
605        assert_eq!(rails_shared_blog_post_model_name().route_key, "blog_posts");
606    }
607
608    #[test]
609    fn test_rails_namespaced_model_in_shared_namespace_param_key() {
610        assert_eq!(rails_shared_blog_post_model_name().param_key, "blog_post");
611    }
612
613    #[test]
614    fn test_rails_namespaced_model_in_shared_namespace_i18n_key() {
615        assert_eq!(rails_shared_blog_post_model_name().i18n_key, "blog/post");
616    }
617
618    #[test]
619    fn test_rails_supplied_model_name_singular() {
620        assert_eq!(rails_supplied_article_model_name().singular, "article");
621    }
622
623    #[test]
624    fn test_rails_supplied_model_name_plural() {
625        assert_eq!(rails_supplied_article_model_name().plural, "articles");
626    }
627
628    #[test]
629    fn test_rails_supplied_model_name_element() {
630        assert_eq!(rails_supplied_article_model_name().element, "article");
631    }
632
633    #[test]
634    fn test_rails_supplied_model_name_collection() {
635        assert_eq!(rails_supplied_article_model_name().collection, "articles");
636    }
637
638    #[test]
639    fn test_rails_supplied_model_name_human() {
640        assert_eq!(rails_supplied_article_model_name().human, "Article");
641    }
642
643    #[test]
644    fn test_rails_supplied_model_name_route_key() {
645        assert_eq!(rails_supplied_article_model_name().route_key, "articles");
646    }
647
648    #[test]
649    fn test_rails_supplied_model_name_param_key() {
650        assert_eq!(rails_supplied_article_model_name().param_key, "article");
651    }
652
653    #[test]
654    fn test_rails_supplied_model_name_i18n_key() {
655        assert_eq!(rails_supplied_article_model_name().i18n_key, "article");
656    }
657
658    #[test]
659    fn test_rails_supplied_locale_singular() {
660        assert_eq!(ModelName::new("Uzivatel").singular, "uzivatel");
661    }
662
663    #[test]
664    #[ignore = "Rails-specific: ModelName::new has no locale-specific inflection support"]
665    fn test_rails_supplied_locale_plural() {}
666
667    #[test]
668    fn test_rails_relative_model_name_singular() {
669        assert_eq!(RelativeBlogPost::model_name().singular, "blog_post");
670    }
671
672    #[test]
673    fn test_rails_relative_model_name_plural() {
674        assert_eq!(RelativeBlogPost::model_name().plural, "blog_posts");
675    }
676
677    #[test]
678    fn test_rails_relative_model_name_element() {
679        assert_eq!(RelativeBlogPost::model_name().element, "post");
680    }
681
682    #[test]
683    fn test_rails_relative_model_name_collection() {
684        assert_eq!(RelativeBlogPost::model_name().collection, "blog/posts");
685    }
686
687    #[test]
688    fn test_rails_relative_model_name_human() {
689        assert_eq!(RelativeBlogPost::model_name().human, "Post");
690    }
691
692    #[test]
693    fn test_rails_relative_model_name_route_key() {
694        assert_eq!(RelativeBlogPost::model_name().route_key, "posts");
695    }
696
697    #[test]
698    fn test_rails_relative_model_name_param_key() {
699        assert_eq!(RelativeBlogPost::model_name().param_key, "post");
700    }
701
702    #[test]
703    fn test_rails_relative_model_name_i18n_key() {
704        assert_eq!(RelativeBlogPost::model_name().i18n_key, "blog/post");
705    }
706
707    #[test]
708    fn test_rails_naming_helpers_to_model_called_on_record() {
709        let contact = Contact;
710        assert_eq!(contact.to_model().param_key(), "contact");
711    }
712
713    #[test]
714    fn test_rails_naming_helpers_singular_for_record() {
715        let contact = Contact;
716        assert_eq!(contact.singular(), "contact");
717    }
718
719    #[test]
720    fn test_rails_naming_helpers_singular_for_class() {
721        assert_eq!(Contact::model_name().singular, "contact");
722    }
723
724    #[test]
725    fn test_rails_naming_helpers_plural_for_record() {
726        let contact = Contact;
727        assert_eq!(contact.plural(), "contacts");
728    }
729
730    #[test]
731    fn test_rails_naming_helpers_plural_for_class() {
732        assert_eq!(Contact::model_name().plural, "contacts");
733    }
734
735    #[test]
736    fn test_rails_naming_helpers_route_key_for_record() {
737        let contact = Contact;
738        assert_eq!(contact.route_key(), "contacts");
739        assert_eq!(contact.singular_route_key(), "contact");
740    }
741
742    #[test]
743    fn test_rails_naming_helpers_route_key_for_class() {
744        let model_name = Contact::model_name();
745        assert_eq!(model_name.route_key, "contacts");
746        assert_eq!(model_name.singular_route_key, "contact");
747    }
748
749    #[test]
750    fn test_rails_naming_helpers_param_key_for_record() {
751        let contact = Contact;
752        assert_eq!(contact.param_key(), "contact");
753    }
754
755    #[test]
756    fn test_rails_naming_helpers_param_key_for_class() {
757        assert_eq!(Contact::model_name().param_key, "contact");
758    }
759
760    #[test]
761    fn test_rails_naming_helpers_uncountable() {
762        let sheep_name = Sheep::model_name();
763        let contact_name = Contact::model_name();
764
765        assert_eq!(sheep_name.singular, "sheep");
766        assert_eq!(sheep_name.plural, "sheep");
767        assert_ne!(contact_name.singular, contact_name.plural);
768    }
769
770    #[test]
771    fn test_rails_naming_helpers_uncountable_route_key() {
772        let sheep_name = Sheep::model_name();
773        assert_eq!(sheep_name.singular_route_key, "sheep");
774        assert_eq!(sheep_name.route_key, "sheep_index");
775        assert!(Sheep::uncountable());
776    }
777
778    #[test]
779    #[ignore = "Rails-specific: Rust types are always named and ModelName::new requires an explicit name"]
780    fn test_rails_anonymous_class_without_name_argument() {}
781
782    #[test]
783    fn test_rails_anonymous_class_with_name_argument() {
784        assert_eq!(ModelName::new("Anonymous").name, "Anonymous");
785    }
786
787    #[test]
788    fn test_rails_model_name_method_delegation() {
789        let contact = Contact;
790        assert_eq!(contact.model_name_instance(), Contact::model_name());
791    }
792
793    #[test]
794    fn test_rails_overriding_accessors_keys_for_supported_fields() {
795        let mut model_name = rails_track_back_model_name();
796        model_name.singular = "singular".to_string();
797        model_name.plural = "plural".to_string();
798        model_name.element = "element".to_string();
799        model_name.collection = "collection".to_string();
800        model_name.singular_route_key = "singular_route_key".to_string();
801        model_name.route_key = "route_key".to_string();
802        model_name.param_key = "param_key".to_string();
803        model_name.i18n_key = "i18n_key".to_string();
804        model_name.name = "name".to_string();
805
806        assert_eq!(model_name.singular, "singular");
807        assert_eq!(model_name.plural, "plural");
808        assert_eq!(model_name.element, "element");
809        assert_eq!(model_name.collection, "collection");
810        assert_eq!(model_name.singular_route_key, "singular_route_key");
811        assert_eq!(model_name.route_key, "route_key");
812        assert_eq!(model_name.param_key, "param_key");
813        assert_eq!(model_name.i18n_key, "i18n_key");
814        assert_eq!(model_name.name, "name");
815    }
816
817    #[test]
818    fn test_rails_overriding_accessors_collection_key() {
819        let mut model_name = rails_track_back_model_name();
820        model_name.collection = "custom/collection".to_string();
821        assert_eq!(model_name.collection, "custom/collection");
822    }
823
824    #[test]
825    fn test_rails_anonymous_class_with_name_argument_uses_the_supplied_name_for_metadata() {
826        let model_name = ModelName::new("Anonymous");
827
828        assert_eq!(model_name.name, "Anonymous");
829        assert_eq!(model_name.singular, "anonymous");
830        assert_eq!(model_name.element, "anonymous");
831        assert_eq!(model_name.human, "Anonymous");
832        assert_eq!(model_name.param_key, "anonymous");
833        assert_eq!(model_name.i18n_key, "anonymous");
834    }
835}