Skip to main content

doido_model/
association.rs

1//! Declarative association conventions (Rails `belongs_to`/`has_many`/…).
2//!
3//! sea-orm already models relations at the type level; this adds the Rails
4//! naming conventions (foreign keys, target tables, HABTM join tables) that
5//! generators and query helpers reuse, derived from [`doido_core::Inflector`].
6
7use doido_core::Inflector;
8
9/// The kind of association between two models.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum AssociationKind {
12    BelongsTo,
13    HasOne,
14    HasMany,
15    HasAndBelongsToMany,
16}
17
18/// A resolved association: its name plus the conventional foreign key and table.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Association {
21    pub name: String,
22    pub kind: AssociationKind,
23    pub foreign_key: String,
24    pub table: String,
25}
26
27impl Association {
28    /// `belongs_to :author` — this table holds `author_id`; target is `authors`.
29    pub fn belongs_to(name: &str) -> Self {
30        Self {
31            name: name.to_string(),
32            kind: AssociationKind::BelongsTo,
33            foreign_key: Inflector::foreign_key(name),
34            table: Inflector::tableize(name),
35        }
36    }
37
38    /// `has_one :profile` on `owner` — child table holds `{owner}_id`.
39    pub fn has_one(owner: &str, name: &str) -> Self {
40        Self {
41            name: name.to_string(),
42            kind: AssociationKind::HasOne,
43            foreign_key: Inflector::foreign_key(owner),
44            table: Inflector::tableize(name),
45        }
46    }
47
48    /// `has_many :comments` on `owner` — child table holds `{owner}_id`. The
49    /// association name is already plural, so it is singularized before tableizing.
50    pub fn has_many(owner: &str, name: &str) -> Self {
51        Self {
52            name: name.to_string(),
53            kind: AssociationKind::HasMany,
54            foreign_key: Inflector::foreign_key(owner),
55            table: Inflector::tableize(&Inflector::singularize(name)),
56        }
57    }
58
59    /// `has_and_belongs_to_many :tags` — target table `tags`, join FK `tag_id`.
60    pub fn has_and_belongs_to_many(name: &str) -> Self {
61        Self {
62            name: name.to_string(),
63            kind: AssociationKind::HasAndBelongsToMany,
64            foreign_key: Inflector::foreign_key(&Inflector::singularize(name)),
65            table: Inflector::tableize(&Inflector::singularize(name)),
66        }
67    }
68}
69
70/// The conventional HABTM join-table name: the two tables, sorted, joined by `_`
71/// (e.g. `("Article", "Tag") → "articles_tags"`).
72pub fn join_table(a: &str, b: &str) -> String {
73    let mut tables = [Inflector::tableize(a), Inflector::tableize(b)];
74    tables.sort();
75    format!("{}_{}", tables[0], tables[1])
76}
77
78/// A polymorphic `belongs_to` (Rails `belongs_to :commentable, polymorphic: true`),
79/// which stores the target's class name in `{name}_type` and its id in `{name}_id`.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct PolymorphicAssociation {
82    pub name: String,
83    pub type_column: String,
84    pub id_column: String,
85}
86
87impl PolymorphicAssociation {
88    pub fn belongs_to(name: &str) -> Self {
89        Self {
90            name: name.to_string(),
91            type_column: format!("{name}_type"),
92            id_column: format!("{name}_id"),
93        }
94    }
95}
96
97/// The default single-table-inheritance discriminator column (Rails `type`).
98pub fn sti_type_column() -> &'static str {
99    "type"
100}