{"id":22354287,"url":"https://github.com/jonmagic/arca","last_synced_at":"2025-07-23T05:34:45.940Z","repository":{"id":56842533,"uuid":"38952574","full_name":"jonmagic/arca","owner":"jonmagic","description":"Arca is a callback analyzer for ActiveRecord ideally suited for digging yourself out of callback hell","archived":false,"fork":false,"pushed_at":"2024-04-17T20:23:40.000Z","size":86,"stargazers_count":39,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-12T10:51:33.707Z","etag":null,"topics":["activerecord","ruby"],"latest_commit_sha":null,"homepage":"","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/jonmagic.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":"2015-07-12T06:35:51.000Z","updated_at":"2025-05-21T12:34:50.000Z","dependencies_parsed_at":"2024-02-24T19:46:23.345Z","dependency_job_id":"4908385a-d36f-4b90-9d21-5b08b1e4beb7","html_url":"https://github.com/jonmagic/arca","commit_stats":{"total_commits":87,"total_committers":3,"mean_commits":29.0,"dds":0.04597701149425293,"last_synced_commit":"b421a285e58ef08b6d586e87e3b822ca35637aef"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jonmagic/arca","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonmagic%2Farca","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonmagic%2Farca/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonmagic%2Farca/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonmagic%2Farca/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonmagic","download_url":"https://codeload.github.com/jonmagic/arca/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonmagic%2Farca/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266624823,"owners_count":23958301,"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","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","ruby"],"created_at":"2024-12-04T13:12:13.072Z","updated_at":"2025-07-23T05:34:45.919Z","avatar_url":"https://github.com/jonmagic.png","language":"Ruby","readme":"# ActiveRecord Callback Analyzer\n\nArca is a callback analyzer for ActiveRecord models ideally suited for digging yourself out of callback hell. At best it will help you move towards a [more maintainable design](https://web.archive.org/web/20161016162603/http://adequate.io/culling-the-activerecord-lifecycle) and at worst it can be used in your test suite to give you feedback when callbacks change.\n\nArca helps you answer questions like:\n\n* how spread out callbacks are for each model\n* how many callbacks use conditionals (`:if`, `:unless`, and `:on`)\n* how many possible permutations exist per callback type (`:commit`, `:create`, `:destroy`, `:find`, `:initialize`, `:rollback`, `:save`, `:touch`, `:update`, `:validation`) taking conditionals into consideration\n\nThe Arca library has two main components, the collector and the reporter. Include the collector module in ActiveRecord::Base before your models are loaded.\n\nAt GitHub, we test callbacks by whitelisting existing callbacks, and adding a lint test to ensure new callbacks are not added without review. The [examples](examples) folder is a good starting point.\n\n## Requirements\n\n![travis-ci build status](https://travis-ci.org/jonmagic/arca.svg)\n\nArca is tested against ActiveRecord 3.2 and 4.2 running on Ruby 1.9.3, 2.0.0, 2.1.0, and 2.2.0.\n\n## Usage\n\nAdd the gem to your Gemfile and run `bundle`.\n\n```\ngem 'arca'\n```\n\nIn your test helper (`test/test_helper.rb` for example) require the Arca library and include the `Arca::Collector` in `ActiveRecord::Base`.\n\n```\nrequire \"active_record\"\nrequire \"arca\"\n\nclass ActiveRecord::Base\n  include Arca::Collector\nend\n\n# load your app. It's important to setup before loading your models because Arca\n# works by wrapping itself around the callback method definitions (before_save,\n# after_save, etc) and then records how and where those methods are used.\n```\n\nIn this example the `Annoucements` module is included in `Ticket` and defines it's own callback.\n\n\n```ruby\nclass Ticket \u003c ActiveRecord::Base\n  include Announcements\n\n  before_save :set_title, :set_body\n  before_save :upcase_title, :if =\u003e :title_is_a_shout?\n\n  def set_title\n    self.title ||= \"Ticket id #{SecureRandom.hex(2)}\"\n  end\n\n  def set_body\n    self.body ||= \"Everything is broken.\"\n  end\n\n  def upcase_title\n    self.title = title.upcase\n  end\n\n  def title_is_a_shout?\n    self.title.split(\" \").size == 1\n  end\nend\n```\n\n```ruby\nmodule Announcements\n  def self.included(base)\n    base.class_eval do\n      after_save :announce_save\n    end\n  end\n\n  def announce_save\n    puts \"saved #{self.class.name.downcase}!\"\n  end\nend\n```\n\nUse `Arca[Ticket].report` to analyze the callbacks for the `Ticket` class.\n\n```ruby\n\u003e Arca[Ticket].report\n{\n                   :model_name =\u003e \"Ticket\",\n              :model_file_path =\u003e \"test/fixtures/ticket.rb\",\n              :callbacks_count =\u003e 4,\n           :conditionals_count =\u003e 1,\n          :lines_between_count =\u003e 6,\n     :external_callbacks_count =\u003e 1,\n       :external_targets_count =\u003e 0,\n  :external_conditionals_count =\u003e 0,\n      :calculated_permutations =\u003e 2\n}\n```\n\nTry out `Arca[Ticket].analyzed_callbacks` to see where and how each callback works and the order they run in.\n\n```ruby\n\u003e Arca[Ticket].analyzed_callbacks\n{\n  :before_save =\u003e [\n    {\n      :callback                       =\u003e :before_save,\n      :callback_file_path             =\u003e \"test/fixtures/ticket.rb\",\n      :callback_line_number           =\u003e 5,\n      :external_callback              =\u003e false,\n      :target                         =\u003e :set_title,\n      :target_file_path               =\u003e \"test/fixtures/ticket.rb\",\n      :target_line_number             =\u003e 8,\n      :external_target                =\u003e false,\n      :lines_to_target                =\u003e 3,\n      :conditional                    =\u003e nil,\n      :conditional_target             =\u003e nil,\n      :conditional_target_file_path   =\u003e nil,\n      :conditional_target_line_number =\u003e nil,\n      :external_conditional_target    =\u003e nil,\n      :lines_to_conditional_target    =\u003e nil\n    },\n    {\n      :callback                       =\u003e :before_save,\n      :callback_file_path             =\u003e \"test/fixtures/ticket.rb\",\n      :callback_line_number           =\u003e 5,\n      :external_callback              =\u003e false,\n      :target                         =\u003e :set_body,\n      :target_file_path               =\u003e \"test/fixtures/ticket.rb\",\n      :target_line_number             =\u003e 12,\n      :external_target                =\u003e false,\n      :lines_to_target                =\u003e 7,\n      :conditional                    =\u003e nil,\n      :conditional_target             =\u003e nil,\n      :conditional_target_file_path   =\u003e nil,\n      :conditional_target_line_number =\u003e nil,\n      :external_conditional_target    =\u003e nil,\n      :lines_to_conditional_target    =\u003e nil\n    },\n    {\n      :callback                       =\u003e :before_save,\n      :callback_file_path             =\u003e \"test/fixtures/ticket.rb\",\n      :callback_line_number           =\u003e 6,\n      :external_callback              =\u003e false,\n      :target                         =\u003e :upcase_title,\n      :target_file_path               =\u003e \"test/fixtures/ticket.rb\",\n      :target_line_number             =\u003e 16,\n      :external_target                =\u003e false,\n      :lines_to_target                =\u003e 10,\n      :conditional                    =\u003e :if,\n      :conditional_target             =\u003e :title_is_a_shout?,\n      :conditional_target_file_path   =\u003e \"test/fixtures/ticket.rb\",\n      :conditional_target_line_number =\u003e 20,\n      :external_conditional_target    =\u003e false,\n      :lines_to_conditional_target    =\u003e nil\n    }\n  ],\n  :after_save  =\u003e [\n    {\n      :callback                       =\u003e :after_save,\n      :callback_file_path             =\u003e \"test/fixtures/announcements.rb\",\n      :callback_line_number           =\u003e 4,\n      :external_callback              =\u003e true,\n      :target                         =\u003e :announce_save,\n      :target_file_path               =\u003e \"test/fixtures/announcements.rb\",\n      :target_line_number             =\u003e 8,\n      :external_target                =\u003e false,\n      :lines_to_target                =\u003e 4,\n      :conditional                    =\u003e nil,\n      :conditional_target             =\u003e nil,\n      :conditional_target_file_path   =\u003e nil,\n      :conditional_target_line_number =\u003e nil,\n      :external_conditional_target    =\u003e nil,\n      :lines_to_conditional_target    =\u003e nil\n    }\n  ]\n}\n```\n\nI'm working [on a project](https://help.github.com/enterprise/2.3/admin/guides/migrations/) at [GitHub](https://github.com) that feels pain when callback behavior changes so I decided to build this tool to help us manage change better and hopefully in the long run move away from ActiveRecord callbacks for most things.\n\nFor the first iteration I am hoping to use this tool in a set of model lint tests that break when callback behavior changes.\n\n```ruby\n  def assert_equal(expected, actual)\n    super(expected, actual, ARCA_FAILURE_MESSAGE)\n  end\n\n  def test_foo\n    report = Arca[Foo].report\n    expected = {\n      :model_name                  =\u003e \"Foo\",\n      :model_file_path             =\u003e \"app/models/foo.rb\",\n      :callbacks_count             =\u003e 30,\n      :conditionals_count          =\u003e 3,\n      :lines_between_count         =\u003e 1026,\n      :external_callbacks_count    =\u003e 12,\n      :external_targets_count      =\u003e 3,\n      :external_conditionals_count =\u003e 2,\n      :calculated_permutations     =\u003e 11\n    }\n\n    assert_equal expected, report.to_hash\n  end\n  ```\n\n  When change happens and that test fails it outputs a helpful error message.\n\n```\n---------------------------------------------\nPlease /cc @github/migration on the PR if you\nhave to update this test to make it pass.\n---------------------------------------------\n```\n\n## Contributors\n\n- [@jonmagic](https://github.com/jonmagic)\n- [@jch](https://github.com/jch)\n- [@bensheldon](https://github.com/bensheldon)\n- [@jasonkim](https://github.com/jasonkim)\n\n## License\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Jonathan Hoyt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonmagic%2Farca","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonmagic%2Farca","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonmagic%2Farca/lists"}