rustrails-record 0.1.2

ORM layer (ActiveRecord equivalent)
Documentation
use rustrails_support::inflector::{foreign_key, singularize, tableize};

use super::{AssociationMeta, AssociationType, DependentAction};

/// Builder for [`AssociationType::HasMany`] metadata.
#[derive(Debug, Clone)]
pub struct HasManyBuilder {
    name: String,
    foreign_key: Option<String>,
    primary_key: Option<String>,
    dependent: Option<DependentAction>,
    through: Option<String>,
}

impl HasManyBuilder {
    /// Creates a builder for the named association.
    #[must_use]
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_owned(),
            foreign_key: None,
            primary_key: None,
            dependent: None,
            through: None,
        }
    }

    /// Overrides the inferred foreign-key column name.
    #[must_use]
    pub fn foreign_key(mut self, fk: &str) -> Self {
        self.foreign_key = Some(fk.to_owned());
        self
    }

    /// Overrides the owner primary-key column name.
    #[must_use]
    pub fn primary_key(mut self, key: &str) -> Self {
        self.primary_key = Some(key.to_owned());
        self
    }

    /// Sets the dependent action for the association.
    #[must_use]
    pub fn dependent(mut self, action: DependentAction) -> Self {
        self.dependent = Some(action);
        self
    }

    /// Declares the join model or table used by the association.
    #[must_use]
    pub fn through(mut self, junction: &str) -> Self {
        self.through = Some(junction.to_owned());
        self
    }

    /// Builds association metadata with best-effort ActiveRecord-style defaults.
    #[must_use]
    pub fn build(self) -> AssociationMeta {
        let name = self.name;
        let inferred_key = foreign_key(&singularize(&name));

        AssociationMeta {
            target_table: tableize(&name),
            foreign_key: self.foreign_key.unwrap_or(inferred_key),
            primary_key: self.primary_key.unwrap_or_else(|| "id".to_owned()),
            dependent: self.dependent,
            through: self.through,
            polymorphic: false,
            association_type: AssociationType::HasMany,
            name,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::HasManyBuilder;
    use crate::associations::{AssociationType, DependentAction};

    #[test]
    fn build_uses_plural_target_table_and_default_keys() {
        let meta = HasManyBuilder::new("comments").build();

        assert_eq!(meta.name, "comments");
        assert_eq!(meta.association_type, AssociationType::HasMany);
        assert_eq!(meta.target_table, "comments");
        assert_eq!(meta.foreign_key, "comment_id");
        assert_eq!(meta.primary_key, "id");
        assert_eq!(meta.through, None);
    }

    #[test]
    fn build_applies_custom_keys_and_dependent_action() {
        let meta = HasManyBuilder::new("children")
            .foreign_key("parent_uuid")
            .primary_key("uuid")
            .dependent(DependentAction::Delete)
            .build();

        assert_eq!(meta.foreign_key, "parent_uuid");
        assert_eq!(meta.primary_key, "uuid");
        assert_eq!(meta.dependent, Some(DependentAction::Delete));
    }

    #[test]
    fn build_tracks_through_association() {
        let meta = HasManyBuilder::new("tags").through("taggings").build();

        assert_eq!(meta.through.as_deref(), Some("taggings"));
    }
    #[test]
    fn build_preserves_association_name() {
        let meta = HasManyBuilder::new("comments").build();

        assert_eq!(meta.name, "comments");
    }

    #[test]
    fn dependent_defaults_to_none() {
        let meta = HasManyBuilder::new("comments").build();

        assert!(meta.dependent.is_none());
    }

    #[test]
    fn foreign_key_override_keeps_default_primary_key() {
        let meta = HasManyBuilder::new("comments")
            .foreign_key("owner_id")
            .build();

        assert_eq!(meta.foreign_key, "owner_id");
        assert_eq!(meta.primary_key, "id");
    }

    #[test]
    fn primary_key_override_keeps_inferred_foreign_key() {
        let meta = HasManyBuilder::new("comments").primary_key("uuid").build();

        assert_eq!(meta.foreign_key, "comment_id");
        assert_eq!(meta.primary_key, "uuid");
    }

    #[test]
    fn through_and_dependent_can_coexist() {
        let meta = HasManyBuilder::new("tags")
            .through("taggings")
            .dependent(DependentAction::Destroy)
            .build();

        assert_eq!(meta.through.as_deref(), Some("taggings"));
        assert_eq!(meta.dependent, Some(DependentAction::Destroy));
    }

    #[test]
    fn restrict_dependent_action_is_preserved() {
        let meta = HasManyBuilder::new("comments")
            .dependent(DependentAction::Restrict)
            .build();

        assert_eq!(meta.dependent, Some(DependentAction::Restrict));
    }
}