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::HasOne`] metadata.
#[derive(Debug, Clone)]
pub struct HasOneBuilder {
    name: String,
    foreign_key: Option<String>,
    primary_key: Option<String>,
    dependent: Option<DependentAction>,
    through: Option<String>,
}

impl HasOneBuilder {
    /// 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 intermediary join target 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::HasOne,
            name,
        }
    }
}

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

    #[test]
    fn build_uses_default_metadata() {
        let meta = HasOneBuilder::new("profile").build();

        assert_eq!(meta.association_type, AssociationType::HasOne);
        assert_eq!(meta.target_table, "profiles");
        assert_eq!(meta.foreign_key, "profile_id");
        assert_eq!(meta.primary_key, "id");
    }

    #[test]
    fn build_supports_custom_primary_key_and_dependent_action() {
        let meta = HasOneBuilder::new("profile")
            .primary_key("uuid")
            .dependent(DependentAction::Nullify)
            .build();

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

    #[test]
    fn build_tracks_through_metadata() {
        let meta = HasOneBuilder::new("account")
            .through("memberships")
            .foreign_key("member_id")
            .build();

        assert_eq!(meta.through.as_deref(), Some("memberships"));
        assert_eq!(meta.foreign_key, "member_id");
    }
    #[test]
    fn build_preserves_association_name() {
        let meta = HasOneBuilder::new("profile").build();

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

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

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

    #[test]
    fn foreign_key_override_keeps_default_primary_key() {
        let meta = HasOneBuilder::new("profile")
            .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 = HasOneBuilder::new("profile").primary_key("uuid").build();

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

    #[test]
    fn through_and_dependent_can_coexist() {
        let meta = HasOneBuilder::new("profile")
            .through("memberships")
            .dependent(DependentAction::Destroy)
            .build();

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

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

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