{"id":13877999,"url":"https://github.com/waymondo/activerecord-exclusive-arc","last_synced_at":"2025-04-10T00:44:31.445Z","repository":{"id":152069340,"uuid":"624628744","full_name":"waymondo/activerecord-exclusive-arc","owner":"waymondo","description":":dizzy: An ActiveRecord extension for implementing polymorphic relationships as exclusive arcs","archived":false,"fork":false,"pushed_at":"2024-10-12T16:32:42.000Z","size":43,"stargazers_count":22,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-10T00:44:25.607Z","etag":null,"topics":["activerecord","exclusive-arc","polymorphism","rails"],"latest_commit_sha":null,"homepage":"https://waymondo.com/posts/are-exclusive-arcs-evil","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/waymondo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-06T22:36:15.000Z","updated_at":"2025-03-10T16:20:58.000Z","dependencies_parsed_at":"2023-12-26T00:25:37.745Z","dependency_job_id":"cd204654-5ae3-426e-922c-7d69395b1172","html_url":"https://github.com/waymondo/activerecord-exclusive-arc","commit_stats":null,"previous_names":["waymondo/exclusive-arc"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waymondo%2Factiverecord-exclusive-arc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waymondo%2Factiverecord-exclusive-arc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waymondo%2Factiverecord-exclusive-arc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/waymondo%2Factiverecord-exclusive-arc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/waymondo","download_url":"https://codeload.github.com/waymondo/activerecord-exclusive-arc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248137997,"owners_count":21053775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","exclusive-arc","polymorphism","rails"],"created_at":"2024-08-06T08:01:37.123Z","updated_at":"2025-04-10T00:44:31.416Z","avatar_url":"https://github.com/waymondo.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"## 💫 `activerecord-exclusive-arc` 💫\n\nA RubyGem that allows an ActiveRecord model to exclusively belong to one of any number of different\ntypes of ActiveRecord models.\n\n### Doesn’t Rails already provide a way to do this?\n\nYes but [here’s a post about why this exists](https://waymondo.com/posts/are-exclusive-arcs-evil/).\n\n### So how does this work?\n\nIt reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an\n_Exclusive Arc_, where each potential polymorphic reference has its own foreign key. This maps\nnicely to a set of optional `belongs_to` relationships, some polymorphic convenience methods, and a\ndatabase check constraint with a matching `ActiveRecord` validation.\n\n## How to use\n\nFirstly, add the gem to your `Gemfile` and `bundle install`:\n\n```ruby\ngem \"activerecord-exclusive-arc\"\n```\n\nThe feature set of this gem is offered via a Rails generator command:\n\n```\nbin/rails g exclusive_arc \u003cModel\u003e \u003carc\u003e \u003cbelongs_to1\u003e \u003cbelongs_to2\u003e ...\n```\n\nThis assumes you already have a `\u003cModel\u003e`. The `\u003carc\u003e` is the name of the polymorphic association\nyou want to establish that may either be a `\u003cbelongs_to1\u003e`, `\u003cbelongs_to2\u003e`, etc. Say we ran:\n\n```\nbin/rails g exclusive_arc Comment commentable post comment\n```\n\nThis will inject code into your `Comment` Model:\n\n```ruby\nclass Comment \u003c ApplicationRecord\n  include ExclusiveArc::Model\n  has_exclusive_arc :commentable, [:post, :comment]\nend\n```\n\nAt a high-level, this essentially transpiles to the following:\n\n```ruby\nclass Comment \u003c ApplicationRecord\n  belongs_to :post, optional: true\n  belongs_to :comment, optional: true\n  validate :post_or_comment_present?\n\n  def commentable\n    @commentable ||= (post || comment)\n  end\n\n  def commentable=(post_or_comment)\n    @commentable = post_or_comment\n  end\nend\n```\n\nIt's a bit more involved than that, but it demonstrates the essense of the API as an `ActiveRecord` user.\n\nIf you need to customize a specific `belongs_to` relationship, you can do so by declaring it before\n`has_exclusive_arc`:\n\n```ruby\nclass Comment \u003c ApplicationRecord\n  include ExclusiveArc::Model\n  belongs_to :post, -\u003e { where(comments_enabled: true) }, optional: true\n  has_exclusive_arc :commentable, [:post, :comment]\nend\n```\n\nContinuing with our example, the generator command would also produce a migration that looks like\nthis:\n\n```ruby\nclass CommentCommentableExclusiveArcPostComment \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_reference :comments, :post, foreign_key: true, index: {where: \"post_id IS NOT NULL\"}\n    add_reference :comments, :comment, foreign_key: true, index: {where: \"comment_id IS NOT NULL\"}\n    add_check_constraint(\n      :comments,\n      \"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1\",\n      name: \"commentable\"\n    )\n  end\nend\n```\n\nThe check constraint ensures `ActiveRecord` validations can’t be bypassed to break the fabeled\nrule - \"There Can Only Be One️\". Traditional foreign key constraints can be used and the partial\nindexes provide improved lookup performance for each individual polymorphic assoication.\n\n### Exclusive Arc Options\n\nSome options are available to the generator command. You can see them with:\n\n```\n$ bin/rails g exclusive_arc --help\nUsage:\n  rails generate exclusive_arc NAME [arc belongs_to1 belongs_to2 ...] [options]\n\nOptions:\n  [--optional], [--no-optional]                                          # Exclusive arc is optional\n  [--skip-foreign-key-constraints], [--no-skip-foreign-key-constraints]  # Skip foreign key constraints\n  [--skip-foreign-key-indexes], [--no-skip-foreign-key-indexes]          # Skip foreign key partial indexes\n  [--skip-check-constraint], [--no-skip-check-constraint]                # Skip check constraint\n\nAdds an Exclusive Arc to an ActiveRecord model and generates the migration for it\n```\n\nNotably, if you want to make an Exclusive Arc optional, you can use the `--optional` flag. This will\nadjust the definition in your `ActiveRecord` model and loosen both the validation and database check\nconstraint so that there can be 0 or 1 foreign keys set for the polymorphic reference.\n\n### Updating an existing exclusive arc\n\nIf you need to add an additional polymorphic option to an existing exclusive arc, you can simply run\nthe generator command again with the additional target. Existing references will be skipped and the\ncheck constraint will be removed and re-added in a reversible manner.\n\n```\nbin/rails g exclusive_arc Comment commentable post comment page\n```\n\n``` ruby\nclass CommentCommentableExclusiveArcPostCommentPage \u003c ActiveRecord::Migration[7.0]\n  def change\n    add_reference :comments, :page, foreign_key: true, index: {where: \"page_id IS NOT NULL\"}\n    remove_check_constraint(\n      :comments,\n      \"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1\",\n      name: \"commentable\"\n    )\n    add_check_constraint(\n      :comments,\n      \"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END + CASE WHEN page_id IS NULL THEN 0 ELSE 1 END) = 1\",\n      name: \"commentable\"\n    )\n  end\nend\n```\n\nThe registration in the model will be updated as well.\n\n``` ruby\nclass Comment \u003c ApplicationRecord\n  include ExclusiveArc::Model\n  has_exclusive_arc :commentable, [:post, :comment, :page]\nend\n```\n\n### Compatibility\n\nCurrently `activerecord-exclusive-arc` is tested against a matrix of:\n* Ruby 3.2 and 3.3\n* Rails 6.1, 7.0, 7.1, 7.2, 8.0\n* `postgresql`, `sqlite3`, and `mysql2` database adapters\n\n### Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/waymondo/activerecord-exclusive-arc.\n\n### License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaymondo%2Factiverecord-exclusive-arc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwaymondo%2Factiverecord-exclusive-arc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwaymondo%2Factiverecord-exclusive-arc/lists"}