## better-definers.rb --- better attribute and method definers # Copyright (C) 2005 Daniel Brockman # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; # either version 2 of the License, or (at your option) any # later version. # This file is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # You should have received a copy of the GNU General Public # License along with this program; if not, write to the Free # Software Foundation, 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. class Symbol def predicate? to_s.include? "?" end def imperative? to_s.include? "!" end def writer? to_s.include? "=" end def punctuated? predicate? or imperative? or writer? end def without_punctuation to_s.delete("?!=").to_sym end def predicate without_punctuation.to_s + "?" end def imperative without_punctuation.to_s + "!" end def writer without_punctuation.to_s + "=" end end class Hash def collect! (&block) replace Hash[*collect(&block).flatten] end def flatten to_a.flatten end end module Kernel def returning (value) yield value ; value end end class Module def define_hard_aliases (name_pairs) for new_aliases, existing_name in name_pairs do new_aliases.kind_of? Array or new_aliases = [new_aliases] for new_alias in new_aliases do alias_method(new_alias, existing_name) end end end def define_soft_aliases (name_pairs) for new_aliases, existing_name in name_pairs do new_aliases.kind_of? Array or new_aliases = [new_aliases] for new_alias in new_aliases do class_eval %{def #{new_alias}(*args, &block) #{existing_name}(*args, &block) end} end end end define_soft_aliases \ :define_hard_alias => :define_hard_aliases, :define_soft_alias => :define_soft_aliases # This method lets you define predicates like :foo?, # which will be defined to return the value of @foo. def define_readers (*names) for name in names.map { |x| x.to_sym } do if name.punctuated? # There's no way to define an efficient reader whose # name is different from the instance variable. class_eval %{def #{name} ; @#{name.without_punctuation} end} else # Use `attr_reader' to define an efficient method. attr_reader(name) end end end def writer_defined? (name) method_defined? name.to_sym.writer end # If you pass a predicate symbol :foo? to this method, it'll first # define a regular writer method :foo, without a question mark. # Then it'll define an imperative writer method :foo! as a shorthand # for setting the property to true. def define_writers (*names, &body) for name in names.map { |x| x.to_sym } do if block_given? define_method(name.writer, &body) else attr_writer(name.without_punctuation) end if name.predicate? class_eval %{def #{name.imperative} self.#{name.writer} true end} end end end define_soft_aliases \ :define_reader => :define_readers, :define_writer => :define_writers # We don't need a singular alias for `define_accessors', # because it always defines at least two methods. def define_accessors (*names) define_readers(*names) define_writers(*names) end def define_opposite_readers (name_pairs) name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } for opposite_name, name in name_pairs do define_reader(name) unless method_defined? name class_eval %{def #{opposite_name} ; not #{name} end} end end def define_opposite_writers (name_pairs) name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } for opposite_name, name in name_pairs do define_writer(name) unless writer_defined? name class_eval %{def #{opposite_name.writer} x self.#{name.writer} !x end} class_eval %{def #{opposite_name.imperative} self.#{name.writer} false end} end end define_soft_aliases \ :define_opposite_reader => :define_opposite_readers, :define_opposite_writer => :define_opposite_writers def define_opposite_accessors (name_pairs) define_opposite_readers name_pairs define_opposite_writers name_pairs end def define_reader_with_opposite (name_pair, &body) name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } define_method(name, &body) define_opposite_reader(opposite_name => name) end def define_writer_with_opposite (name_pair, &body) name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } define_writer(name, &body) define_opposite_writer(opposite_name => name) end public :define_method def define_methods (*names, &body) names.each { |name| define_method(name, &body) } end def define_private_methods (*names, &body) define_methods(*names, &body) names.each { |name| private name } end def define_protected_methods (*names, &body) define_methods(*names, &body) names.each { |name| protected name } end def define_private_method (name, &body) define_method(name, &body) private name end def define_protected_method (name, &body) define_method(name, &body) protected name end end class ImmutableAttributeError < StandardError def initialize (attribute=nil, message=nil) super message @attribute = attribute end define_accessors :attribute def to_s if @attribute and @message "cannot change the value of `#@attribute': #@message" elsif @attribute "cannot change the value of `#@attribute'" elsif @message "cannot change the value of attribute: #@message" else "cannot change the value of attribute" end end end class Module # Guard each of the specified attributes by replacing the writer # method with a proxy that asks the supplied block before proceeding # with the change. # # If it's okay to change the attribute, the block should return # either nil or the symbol :mutable. If it isn't okay, the block # should return a string saying why the attribute can't be changed. # If you don't want to provide a reason, you can have the block # return just the symbol :immutable. def guard_writers(*names, &predicate) for name in names.map { |x| x.to_sym } do define_hard_alias("__unguarded_#{name.writer}" => name.writer) define_method(name.writer) do |new_value| case result = predicate.call when :mutable, nil __send__("__unguarded_#{name.writer}", new_value) when :immutable raise ImmutableAttributeError.new(name) else raise ImmutableAttributeError.new(name, result) end end end end def define_guarded_writers (*names, &block) define_writers(*names) guard_writers(*names, &block) end define_soft_alias :guard_writer => :guard_writers define_soft_alias :define_guarded_writer => :define_guarded_writers end if __FILE__ == $0 require "test/unit" class DefineAccessorsTest < Test::Unit::TestCase def setup @X = Class.new @Y = Class.new @X @x = @X.new @y = @Y.new end def test_define_hard_aliases @X.define_method(:foo) { 123 } @X.define_method(:baz) { 321 } @X.define_hard_aliases :bar => :foo, :quux => :baz assert_equal @x.foo, 123 assert_equal @x.bar, 123 assert_equal @y.foo, 123 assert_equal @y.bar, 123 assert_equal @x.baz, 321 assert_equal @x.quux, 321 assert_equal @y.baz, 321 assert_equal @y.quux, 321 @Y.define_method(:foo) { 456 } assert_equal @y.foo, 456 assert_equal @y.bar, 123 @Y.define_method(:quux) { 654 } assert_equal @y.baz, 321 assert_equal @y.quux, 654 end def test_define_soft_aliases @X.define_method(:foo) { 123 } @X.define_method(:baz) { 321 } @X.define_soft_aliases :bar => :foo, :quux => :baz assert_equal @x.foo, 123 assert_equal @x.bar, 123 assert_equal @y.foo, 123 assert_equal @y.bar, 123 assert_equal @x.baz, 321 assert_equal @x.quux, 321 assert_equal @y.baz, 321 assert_equal @y.quux, 321 @Y.define_method(:foo) { 456 } assert_equal @y.foo, @y.bar, 456 @Y.define_method(:quux) { 654 } assert_equal @y.baz, 321 assert_equal @y.quux, 654 end def test_define_readers @X.define_readers :foo, :bar assert !@x.respond_to?(:foo=) assert !@x.respond_to?(:bar=) @x.instance_eval { @foo = 123 ; @bar = 456 } assert_equal @x.foo, 123 assert_equal @x.bar, 456 @X.define_readers :baz?, :quux? assert !@x.respond_to?(:baz=) assert !@x.respond_to?(:quux=) @x.instance_eval { @baz = false ; @quux = true } assert !@x.baz? assert @x.quux? end def test_define_writers assert !@X.writer_defined?(:foo) assert !@X.writer_defined?(:bar) @X.define_writers :foo, :bar assert @X.writer_defined?(:foo) assert @X.writer_defined?(:bar) assert @X.writer_defined?(:foo=) assert @X.writer_defined?(:bar=) assert @X.writer_defined?(:foo?) assert @X.writer_defined?(:bar?) assert !@x.respond_to?(:foo) assert !@x.respond_to?(:bar) @x.foo = 123 @x.bar = 456 assert_equal @x.instance_eval { @foo }, 123 assert_equal @x.instance_eval { @bar }, 456 @X.define_writers :baz?, :quux? assert !@x.respond_to?(:baz?) assert !@x.respond_to?(:quux?) @x.baz = true @x.quux = false assert_equal @x.instance_eval { @baz }, true assert_equal @x.instance_eval { @quux }, false end def test_define_accessors @X.define_accessors :foo, :bar @x.foo = 123 ; @x.bar = 456 assert_equal @x.foo, 123 assert_equal @x.bar, 456 end def test_define_opposite_readers @X.define_opposite_readers :foo? => :bar?, :baz? => :quux? assert !@x.respond_to?(:foo=) assert !@x.respond_to?(:bar=) assert !@x.respond_to?(:baz=) assert !@x.respond_to?(:quux=) @x.instance_eval { @bar = true ; @quux = false } assert !@x.foo? assert @x.bar? assert @x.baz? assert !@x.quux? end def test_define_opposite_writers @X.define_opposite_writers :foo? => :bar?, :baz => :quux end end end