From ede14cbd99729249a4c2c210b22b60a7da2fc402 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Wed, 6 Dec 2023 13:43:19 +0000 Subject: [PATCH] Added underscore magic methods as a preferred way to define bang and predicate methods. --- README.md | 27 ++++++++++++++++++++++----- fixtures/user.rb | 20 ++++++++++++++++++++ lib/spaced.rb | 34 ++++++++++++++++++++++++++-------- test/spaced/namespace.rb | 40 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 019f441..28dfd8c 100644 --- a/README.md +++ b/README.md @@ -38,28 +38,28 @@ In the example above, `namespace` creates and initializes a new class `Twitter` ## Magic bang and predicate methods -If you define a `call` method in your namespaced class, you can then conveniently call that with a bang method: +If you define a `#_!` or `#call` method in your namespaced class, you can then conveniently call that with a bang method: ```ruby class User include Spaced namespace :tweet do - def call(content) + def _!(content) create_tweet content end end end user = User.new -user.tweet!('my new tweet') # Will call the `#call` method with whatever arguments you give it. +user.tweet!('my new tweet') # Will call the `#_!` method with whatever arguments you give it. ``` -There is also an equivalent `predicate` method: +There is also an equivalent predicate method: ```ruby namespace :tweet do - def predicate + def _? false end end @@ -67,6 +67,23 @@ user = User.new user.tweet? # Will call the `#predicate` method. ``` +Both bang and predicate methods can be defined using identically named methods: + +```ruby +namespace :tweet do + def tweet? + false + end + + def tweet! + # ... + end +end +user = User.new +user.tweet? # Will call the `#tweet?` method on `:tweet` namespace. +user.tweet! # Will call the `#tweet!` method on `:tweet` namespace. +``` + ## Installation Add this line to your application's Gemfile: diff --git a/fixtures/user.rb b/fixtures/user.rb index 94aef56..8e576d3 100644 --- a/fixtures/user.rb +++ b/fixtures/user.rb @@ -19,4 +19,24 @@ def predicate true end end + + namespace :sister do + def sister!(append = nil) + "Alex Moss#{append}" + end + + def sister? + true + end + end + + namespace :mother do + def _!(append = nil) + "Lesley Moss#{append}" + end + + def _? + true + end + end end diff --git a/lib/spaced.rb b/lib/spaced.rb index e2441dd..022bc8f 100644 --- a/lib/spaced.rb +++ b/lib/spaced.rb @@ -14,8 +14,7 @@ class Base end module ClassMethods - # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity - def namespace(name, klass = nil, &) + def namespace(name, klass = nil, &) # rubocop:disable Metrics/* if klass raise "#{klass} must be a subclass of Spaced::Base" unless klass < Spaced::Base else @@ -41,25 +40,44 @@ def namespace(name, klass = nil, &) # Define the bang and predicate methods. methods = klass.instance_methods(false) - if methods.include?(:call) + if methods.include?(:_!) + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}!(...); #{name}._!(...); end # def user!; user._!; end + RUBY + elsif methods.include?(:"#{name}!") + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}!(...); #{name}.#{name}!(...); end # def user!; user.user!; end + RUBY + elsif methods.include?(:call) # DEPRECATED module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name}!(...); #{name}.call(...); end # def user!(...); user.call(...); end RUBY else define_method :"#{name}!" do - raise NoMethodError, "undefined method `#{name}!' for #<#{klass}>. Have you defined `#{klass}#call`?", caller + raise NoMethodError, + "undefined method `#{name}!' for #<#{klass}>. Have you defined `#{klass}#_!`?", caller end end - if methods.include?(:predicate) - define_method(:"#{name}?") { send(name).predicate } + if methods.include?(:_?) + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}?; #{name}._?; end # def user?; user._?; end + RUBY + elsif methods.include?(:"#{name}?") + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}?; #{name}.#{name}?; end # def user?; user.user?; end + RUBY + elsif methods.include?(:predicate) # DEPRECATED + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}?; #{name}.predicate; end # def user?; user.predicate; end + RUBY else define_method :"#{name}?" do - raise NoMethodError, "undefined method `#{name}?' for #<#{klass}>. Have you defined `#{klass}#predicate`?", + raise NoMethodError, + "undefined method `#{name}?' for #<#{klass}>. Have you defined `#{klass}#_?`?", caller end end end - # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity end end diff --git a/test/spaced/namespace.rb b/test/spaced/namespace.rb index 026a70d..8a1afb5 100644 --- a/test/spaced/namespace.rb +++ b/test/spaced/namespace.rb @@ -25,7 +25,7 @@ expect(user.mum.full_name).to be == "Lesley Moss" end - with "bang method" do + with "'call' bang method" do it "aliases to #call" do expect(user.brother!).to be == "Andy Moss" end @@ -37,9 +37,45 @@ end end - with "predicate method" do + with "bang named method" do + it "aliases to #sister!" do + expect(user.sister!).to be == "Alex Moss" + end + + with "arguments" do + it "passes through arguments" do + expect(user.sister!("?")).to be == "Alex Moss?" + end + end + end + + with "bang underscore method" do + it "aliases to #_!" do + expect(user.mother!).to be == "Lesley Moss" + end + + with "arguments" do + it "passes through arguments" do + expect(user.mother!("?")).to be == "Lesley Moss?" + end + end + end + + with "'predicate' method" do it "aliases to #predicate" do expect(user.brother?).to be == true end end + + with "predicate named method" do + it "aliases to #sister?" do + expect(user.sister?).to be == true + end + end + + with "predicate underscore method" do + it "aliases to #_?" do + expect(user.mother?).to be == true + end + end end