{"id":18605316,"url":"https://github.com/fny/interrobang","last_synced_at":"2025-04-10T20:30:57.709Z","repository":{"id":28124431,"uuid":"31623486","full_name":"fny/interrobang","owner":"fny","description":"Convert your predicate_methods? into bang_methods! without abusing method_missing","archived":false,"fork":false,"pushed_at":"2018-09-21T01:07:15.000Z","size":39,"stargazers_count":16,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-06T00:02:22.249Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fny.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-03-03T21:53:59.000Z","updated_at":"2019-08-18T16:40:05.000Z","dependencies_parsed_at":"2022-09-02T19:02:25.720Z","dependency_job_id":null,"html_url":"https://github.com/fny/interrobang","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fny%2Finterrobang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fny%2Finterrobang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fny%2Finterrobang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fny%2Finterrobang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fny","download_url":"https://codeload.github.com/fny/interrobang/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247959797,"owners_count":21024842,"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":[],"created_at":"2024-11-07T02:20:52.855Z","updated_at":"2025-04-10T20:30:57.386Z","avatar_url":"https://github.com/fny.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Interrobang :interrobang:\n\n[![Gem Version](https://badge.fury.io/rb/interrobang.svg)](http://badge.fury.io/rb/interrobang)\n[![Build Status](https://travis-ci.org/fny/interrobang.svg?branch=master)](https://travis-ci.org/fny/interrobang)\n[![Inline docs](http://inch-ci.org/github/fny/interrobang.svg?branch=master)](http://inch-ci.org/github/fny/interrobang)\n\nConvert your `predicate_methods?` into `bang_methods!` without abusing `method_missing`.\n\n`Interrobang` currently only works with Ruby versions that support keyword arguments.\n\n## Overview\n\nSay we have the following class:\n\n```ruby\nclass Answer\n  # Say these return a boolean.\n  def correct?; end\n  def is_correct; end\n  def is_factual; end\n  def is_right; end\nend\n```\n\n`Interrobang` automagically adds corresponding bang methods for any predicate methods that end in a `?`. The bang methods explode when the predicate method returns a falsey value and otherwise return true.\n\n```ruby\n# Pick your poison...\nInterrobang(Answer) # =\u003e [:correct!]\nInterrobang.bangify(Answer) # =\u003e [:correct!]\nInterrobang.bangify_class(Answer) # =\u003e [:correct!]\n\nanswer = Answer.new \nanswer.respond_to?(:correct!) # =\u003e true (no method missing shenanigans!)\nanswer.correct! # =\u003e Raises Interrobang::FalsePredicate if `#correct?` is false\n```\n\nYou can add prefixes and suffixes to the generated bang method.\n\n```ruby\nInterrobang(Answer, prefix: 'ensure_', suffix: '_or_else')\n# =\u003e [:ensure_correct_or_else!]\nAnswer.new.ensure_correct_or_else!\n# =\u003e Raises Interrobang::FalsePredicate if `#correct?` is false\n```\n\nProvide your own blocks to execute on failure. You can optionally access the symbol of the predicate method as an argument.\n\n```ruby\nInterrobang(Answer, prefix: 'ensure_') do |predicate_method|\n  raise StandardError, predicate_method\nend # =\u003e [:ensure_correct!]\nAnswer.new.ensure_correct! # =\u003e Raises StandardError if `#correct?` is false\n```\n\nNeed to convert a single method? No problem.\n\n```ruby\n# Pick your poison...\nInterrobang(Answer, :correct?) # =\u003e :correct!\nInterrobang.bangify(Answer, :correct?) # =\u003e :correct!\nInterrobang.bangify_method(Answer, :correct?) # =\u003e :correct!\n\nInterrobang(Answer, :correct?, prefix: 'ensure_', suffix: '_on_saturday') do\n  if Time.now.saturday?\n    raise WeekendLaziness\n  else\n    true\n  end\nend # =\u003e :ensure_correct_on_saturday!\n```\n\nBeware! `Interrobang` will bangify undefined methods too. This allows for classes driven by `method_missing` to be converted.\n\n```ruby\nclass NaySayer\n  def method_missing(method, *args, \u0026block)\n    false\n  end\nend\nInterrobang(NaySayer, :correct?) # =\u003e :correct!\nNaySayer.new.correct! # =\u003e Raises Interrobang::FalsePredicate\n```\n\n`Interrobang` will not convert `bang_methods!` or `assignment_methods=` and instead returns `nil`.\n\n### Filters\n\nPerhaps you'd like to convert methods that match a different pattern?\n\n```ruby\nInterrobang(Answer, matching: %r{\\Ais_.*\\z})\n# =\u003e [:is_correct!, :is_factual!, :is_right!]\n```\n\nYou can exclude methods that match the pattern with `except`.\n\n```ruby\nInterrobang(Answer, matching: %r{\\Ais_.*\\z}, except: [:is_factual,  :is_right])\n# =\u003e [:is_correct!]\n```\n\nMaybe you'd like to state the methods to convert explicitly? Use `only`. This will override the pattern or any exclusions.\n\n```ruby\nInterrobang(Answer, only: :is_correct) # =\u003e [:is_correct!]\n```\n\nYou can opt to include methods from parent classes, but proceed with caution...\n\n```ruby\nInterrobang(Answer, include_super: true,  prefix: 'ensure_')\n# =\u003e [:ensure_correct!, :ensure_nil!, :ensure_eql!, :ensure_tainted!, :ensure_untrusted!, :ensure_frozen!, :ensure_instance_variable_defined!, :ensure_instance_of!, :ensure_kind_of!, :ensure_is_a!, :ensure_respond_to!, :ensure_equal!]\nAnswer.new.ensure_nil! # =\u003e Raises Interrobang::FalsePredicate\n```\n\nToo lazy to type `Interrobang` a few times? Just `extend` it. It's methods are `module_function`s.\n\n```ruby\nclass Answer\n  extend Interrobang\n  bangify self, :is_correct\n  bangify self, :is_correct, prefix: 'ensure_'\nend\n```\n\n### Details\n\nSee `lib/interrobang.rb` for complete documentation and the tests for details.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'interrobang'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install interrobang\n\n## Example Use Case with Rails\n\n`Interrobang` works wonderfully with permission-related objects. Say we have a bangified `Protector` class that defines user permissions in our application:\n\n```ruby\nclass Protector\n  NotSignedIn = Class.new(Exception)\n  Unauthorized = Class.new(Exception)\n\n  def initialize(user)\n    @user = user\n  end\n\n  def signed_in?\n    @user.is_a?(User)\n  end\n\n  def admin?\n    @user \u0026\u0026 @user.is_admin\n  end\n\n  def can_edit_user?(other_user)\n    @user \u0026\u0026 (@user.is_admin || @user.id == other_user.id)\n  end\n\n  Interrobang(self, prefix: 'ensure_') do |predicate_method|\n    raise Unauthorized, \"#{predicate_method} failed\"\n  end\n\n  Interrobang(self, :signed_in?, prefix: 'ensure_') do |predicate_method|\n    raise NotSignedIn, \"#{predicate_method} failed\"\n  end\nend\n```\n\nIn our controller, we can then define rescue handlers for the those exceptions, and add a method to access a `Protector` instance.\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  def protector\n    @protector ||= Protector.new(current_user)\n  end\n\n  rescue_from Protector::NotSignedIn do\n    redirect_to sign_in_path, alert: \"Please sign in to continue.\"\n  end\n\n  rescue_from Protector::Unauthorized do\n    # Handle as you will\n  end\nend\n```\n\nNow we can call `protector.ensure_signed_in!`, `protector.ensure_admin!`, `protector.ensure_can_edit!(other_user)!` from any controller and trigger the errors defined with `Interrobang`.\n\n### Aside: Testing Tricks with Rescue Handlers\n\nFor tests, we can stub the rescue handlers with methods that expose the original errors so we can check for them directly.\n\n```ruby\n# spec/support/helpers.rb\ndef raise_handled_rescues(controller = ApplicationController)\n  stubbed_handlers = controller.rescue_handlers.map { |rescue_handler|\n    name, proc = rescue_handler\n    [ name, -\u003e { raise Kernel.const_get(name) } ]\n  }\n  allow(controller).to receive(:rescue_handlers).and_return(stubbed_handlers)\nend\n```\n\nThis allows us to test that proper errors are being raised independently from testing each error's particular handling.\n\n```ruby\n# spec/controllers/users_controller_spec.rb\nRSpec.describe UsersController, type: :controller do\n  before { raise_handled_rescues }\n  after { reset_handled_rescues }\n  describe \"GET index\" do\n    context \"unauthenticated user\" do\n      it \"raises Protector::NotSignedIn\" do\n        expect { get :index }.to raise_error(Protector::NotSignedIn)\n      end\n    end\n  end\nend\n\n# spec/controllers/application_controller_spec.rb\nRSpec.describe ApplicationController, type: :controller do\n  describe \"Protector::NotSignedIn rescue handler\" do\n    controller { def index; raise Protector::NotSignedIn; end }\n    it \"redirects to the sign in page\" do\n      get :index\n      expect(response).to redirect_to sign_in_path\n    end\n  end\nend\n```\n\n## What are these predicate methods and bang methods?\n\n**Predicate methods** return a Boolean. By Ruby convention, these methods typically end in a `?`. Other languages like [Scheme][scheme-conventions], [C#][csharp-predicates], [Java][java-predicates], support this interface too.\n\n**Bang methods** are \"dangerous\" or modify the receiver. By convention, these methods typically end with a `!`. In the case of `Interrobang`, these methods are considered \"dangerous\" because they may raise an exception.\n\n### Fun Fact\n\nThe Ruby conventions for `?` and `!`  are borrowed from [Scheme][scheme-conventions]:\n\n\u003e 1.3.5  Naming conventions\n\u003e\n\u003e\n\u003e By convention, the names of procedures that always return a boolean value usually end in ``?''. Such procedures are called predicates.\n\u003e\n\u003e By convention, the names of procedures that store values into previously\n\u003e allocated locations (see section 3.4) usually end in ``!''. Such procedures\n\u003e are called mutation procedures. By convention, the value returned by a \n\u003e mutation procedure is unspecified.\n\n## Development\n\nBe sure to test all the things. Just `rake test`. You can use `bundle console` to play with things in an IRB session.\n\n## Contributing\n\n1. Fork it (https://github.com/fny/interrobang/fork)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Special Thanks To...\n\n - [Discourse][discourse] for inspiring me to find a `method_missing`-free alternative to its [EnsureMagic][discourse-ensure]\n - [Michael Josephson][josephson] for [pointing me in the right direction][so-question]\n - To all [contributers][github-contributers]! :beers:\n\n[csharp-predicates]: https://msdn.microsoft.com/en-us/library/bfcke1bz%28v=vs.110%29.aspx \"Predicate\u003cT\u003e Delegate\"\n[java-predicates]: https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html \"Interface Predicate\u003cT\u003e\"\n[scheme-conventions]: http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-4.html#%_sec_1.3.5 \"Scheme Naming Conventions\"\n[discourse]: http://discourse.org \"Discourse\"\n[discourse-ensure]: https://github.com/discourse/discourse/blob/ba0084edee8ace004855b987e1661a7eaff60122/lib/guardian/ensure_magic.rb \"module EnsureMagic\"\n[josephson]: http://www.josephson.org/ \"Michael Josephson\"\n[so-question]: http://stackoverflow.com/questions/28818193/define-method-based-on-existing-method-in-ruby \"Define Method Based on Existing Method in Ruby - Stack Overflow\"\n[github-contributers]: https://github.com/fny/interrobang/graphs/contributors \"Predicate Bang Contributers - GitHub\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffny%2Finterrobang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffny%2Finterrobang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffny%2Finterrobang/lists"}