sigterm_extensions 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE.md +0 -0
  5. data/README.md +0 -0
  6. data/bin/ctxirb +156 -0
  7. data/lib/git.rb +166 -0
  8. data/lib/git/LICENSE +21 -0
  9. data/lib/git/author.rb +14 -0
  10. data/lib/git/base.rb +551 -0
  11. data/lib/git/base/factory.rb +75 -0
  12. data/lib/git/branch.rb +126 -0
  13. data/lib/git/branches.rb +71 -0
  14. data/lib/git/config.rb +22 -0
  15. data/lib/git/diff.rb +159 -0
  16. data/lib/git/index.rb +5 -0
  17. data/lib/git/lib.rb +1041 -0
  18. data/lib/git/log.rb +128 -0
  19. data/lib/git/object.rb +312 -0
  20. data/lib/git/path.rb +31 -0
  21. data/lib/git/remote.rb +36 -0
  22. data/lib/git/repository.rb +6 -0
  23. data/lib/git/stash.rb +27 -0
  24. data/lib/git/stashes.rb +55 -0
  25. data/lib/git/status.rb +199 -0
  26. data/lib/git/version.rb +5 -0
  27. data/lib/git/working_directory.rb +4 -0
  28. data/lib/sigterm_extensions.rb +75 -0
  29. data/lib/sigterm_extensions/all.rb +12 -0
  30. data/lib/sigterm_extensions/backtrace_cleaner.rb +129 -0
  31. data/lib/sigterm_extensions/callbacks.rb +847 -0
  32. data/lib/sigterm_extensions/concern.rb +169 -0
  33. data/lib/sigterm_extensions/configurable.rb +38 -0
  34. data/lib/sigterm_extensions/core_ext.rb +4 -0
  35. data/lib/sigterm_extensions/core_ext/array.rb +3 -0
  36. data/lib/sigterm_extensions/core_ext/array/extract.rb +19 -0
  37. data/lib/sigterm_extensions/core_ext/array/extract_options.rb +29 -0
  38. data/lib/sigterm_extensions/core_ext/class.rb +3 -0
  39. data/lib/sigterm_extensions/core_ext/class/attribute.rb +139 -0
  40. data/lib/sigterm_extensions/core_ext/class/attribute_accessors.rb +4 -0
  41. data/lib/sigterm_extensions/core_ext/class/subclasses.rb +52 -0
  42. data/lib/sigterm_extensions/core_ext/custom.rb +12 -0
  43. data/lib/sigterm_extensions/core_ext/digest.rb +3 -0
  44. data/lib/sigterm_extensions/core_ext/digest/uuid.rb +51 -0
  45. data/lib/sigterm_extensions/core_ext/enumerable.rb +232 -0
  46. data/lib/sigterm_extensions/core_ext/file.rb +3 -0
  47. data/lib/sigterm_extensions/core_ext/file/atomic.rb +68 -0
  48. data/lib/sigterm_extensions/core_ext/hash.rb +3 -0
  49. data/lib/sigterm_extensions/core_ext/hash/deep_merge.rb +41 -0
  50. data/lib/sigterm_extensions/core_ext/hash/deep_transform_values.rb +44 -0
  51. data/lib/sigterm_extensions/core_ext/hash/except.rb +22 -0
  52. data/lib/sigterm_extensions/core_ext/hash/keys.rb +141 -0
  53. data/lib/sigterm_extensions/core_ext/hash/reverse_merge.rb +23 -0
  54. data/lib/sigterm_extensions/core_ext/hash/slice.rb +24 -0
  55. data/lib/sigterm_extensions/core_ext/kernel.rb +3 -0
  56. data/lib/sigterm_extensions/core_ext/kernel/concern.rb +12 -0
  57. data/lib/sigterm_extensions/core_ext/kernel/reporting.rb +43 -0
  58. data/lib/sigterm_extensions/core_ext/kernel/singleton_class.rb +6 -0
  59. data/lib/sigterm_extensions/core_ext/load_error.rb +7 -0
  60. data/lib/sigterm_extensions/core_ext/module.rb +3 -0
  61. data/lib/sigterm_extensions/core_ext/module/aliasing.rb +29 -0
  62. data/lib/sigterm_extensions/core_ext/module/anonymous.rb +28 -0
  63. data/lib/sigterm_extensions/core_ext/module/attr_internal.rb +36 -0
  64. data/lib/sigterm_extensions/core_ext/module/attribute_accessors.rb +208 -0
  65. data/lib/sigterm_extensions/core_ext/module/attribute_accessors_per_thread.rb +146 -0
  66. data/lib/sigterm_extensions/core_ext/module/concerning.rb +132 -0
  67. data/lib/sigterm_extensions/core_ext/module/delegation.rb +319 -0
  68. data/lib/sigterm_extensions/core_ext/module/redefine_method.rb +38 -0
  69. data/lib/sigterm_extensions/core_ext/module/remove_method.rb +15 -0
  70. data/lib/sigterm_extensions/core_ext/name_error.rb +36 -0
  71. data/lib/sigterm_extensions/core_ext/object.rb +3 -0
  72. data/lib/sigterm_extensions/core_ext/object/blank.rb +153 -0
  73. data/lib/sigterm_extensions/core_ext/object/colors.rb +39 -0
  74. data/lib/sigterm_extensions/core_ext/object/duplicable.rb +47 -0
  75. data/lib/sigterm_extensions/core_ext/object/inclusion.rb +27 -0
  76. data/lib/sigterm_extensions/core_ext/object/instance_variables.rb +28 -0
  77. data/lib/sigterm_extensions/core_ext/object/methods.rb +61 -0
  78. data/lib/sigterm_extensions/core_ext/object/with_options.rb +80 -0
  79. data/lib/sigterm_extensions/core_ext/range.rb +3 -0
  80. data/lib/sigterm_extensions/core_ext/range/compare_range.rb +74 -0
  81. data/lib/sigterm_extensions/core_ext/range/conversions.rb +39 -0
  82. data/lib/sigterm_extensions/core_ext/range/overlaps.rb +8 -0
  83. data/lib/sigterm_extensions/core_ext/securerandom.rb +43 -0
  84. data/lib/sigterm_extensions/core_ext/string.rb +3 -0
  85. data/lib/sigterm_extensions/core_ext/string/access.rb +93 -0
  86. data/lib/sigterm_extensions/core_ext/string/filters.rb +143 -0
  87. data/lib/sigterm_extensions/core_ext/string/starts_ends_with.rb +4 -0
  88. data/lib/sigterm_extensions/core_ext/string/strip.rb +25 -0
  89. data/lib/sigterm_extensions/core_ext/tryable.rb +132 -0
  90. data/lib/sigterm_extensions/descendants_tracker.rb +108 -0
  91. data/lib/sigterm_extensions/gem_methods.rb +47 -0
  92. data/lib/sigterm_extensions/hash_binding.rb +16 -0
  93. data/lib/sigterm_extensions/inflector.rb +339 -0
  94. data/lib/sigterm_extensions/inflector/acronyms.rb +42 -0
  95. data/lib/sigterm_extensions/inflector/inflections.rb +249 -0
  96. data/lib/sigterm_extensions/inflector/inflections/defaults.rb +117 -0
  97. data/lib/sigterm_extensions/inflector/rules.rb +37 -0
  98. data/lib/sigterm_extensions/inflector/version.rb +8 -0
  99. data/lib/sigterm_extensions/interactive_editor.rb +120 -0
  100. data/lib/sigterm_extensions/lazy.rb +34 -0
  101. data/lib/sigterm_extensions/lazy_load_hooks.rb +79 -0
  102. data/lib/sigterm_extensions/option_merger.rb +32 -0
  103. data/lib/sigterm_extensions/ordered_hash.rb +48 -0
  104. data/lib/sigterm_extensions/ordered_options.rb +83 -0
  105. data/lib/sigterm_extensions/paths.rb +235 -0
  106. data/lib/sigterm_extensions/per_thread_registry.rb +58 -0
  107. data/lib/sigterm_extensions/proxy_object.rb +14 -0
  108. data/lib/sigterm_extensions/staging/boot.rb +31 -0
  109. data/lib/sigterm_extensions/staging/boot/bundler_patch.rb +24 -0
  110. data/lib/sigterm_extensions/staging/boot/command.rb +26 -0
  111. data/lib/sigterm_extensions/staging/boot/gemfile_next_auto_sync.rb +79 -0
  112. data/lib/sigterm_extensions/version.rb +4 -0
  113. data/lib/sigterm_extensions/wrappable.rb +16 -0
  114. data/sigterm_extensions.gemspec +42 -0
  115. data/templates/dotpryrc.rb.erb +124 -0
  116. metadata +315 -0
@@ -0,0 +1,146 @@
1
+ # Extends the module object with class/module and instance accessors for
2
+ # class/module attributes, just like the native attr* accessors for instance
3
+ # attributes, but does so on a per-thread basis.
4
+ #
5
+ # So the values are scoped within the Thread.current space under the class name
6
+ # of the module.
7
+ class Module
8
+ # Defines a per-thread class attribute and creates class and instance reader methods.
9
+ # The underlying per-thread class variable is set to +nil+, if it is not previously defined.
10
+ #
11
+ # module Current
12
+ # thread_mattr_reader :user
13
+ # end
14
+ #
15
+ # Current.user # => nil
16
+ # Thread.current[:attr_Current_user] = "DHH"
17
+ # Current.user # => "DHH"
18
+ #
19
+ # The attribute name must be a valid method name in Ruby.
20
+ #
21
+ # module Foo
22
+ # thread_mattr_reader :"1_Badname"
23
+ # end
24
+ # # => NameError: invalid attribute name: 1_Badname
25
+ #
26
+ # To omit the instance reader method, pass
27
+ # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
28
+ #
29
+ # class Current
30
+ # thread_mattr_reader :user, instance_reader: false
31
+ # end
32
+ #
33
+ # Current.new.user # => NoMethodError
34
+ def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
35
+ syms.each do |sym|
36
+ raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
37
+
38
+ # The following generated method concatenates `name` because we want it
39
+ # to work with inheritance via polymorphism.
40
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
41
+ def self.#{sym}
42
+ Thread.current["attr_" + name + "_#{sym}"]
43
+ end
44
+ EOS
45
+
46
+ if instance_reader && instance_accessor
47
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
48
+ def #{sym}
49
+ self.class.#{sym}
50
+ end
51
+ EOS
52
+ end
53
+
54
+ Thread.current["attr_" + name + "_#{sym}"] = default unless default.nil?
55
+ end
56
+ end
57
+ alias :thread_cattr_reader :thread_mattr_reader
58
+
59
+ # Defines a per-thread class attribute and creates a class and instance writer methods to
60
+ # allow assignment to the attribute.
61
+ #
62
+ # module Current
63
+ # thread_mattr_writer :user
64
+ # end
65
+ #
66
+ # Current.user = "DHH"
67
+ # Thread.current[:attr_Current_user] # => "DHH"
68
+ #
69
+ # To omit the instance writer method, pass
70
+ # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
71
+ #
72
+ # class Current
73
+ # thread_mattr_writer :user, instance_writer: false
74
+ # end
75
+ #
76
+ # Current.new.user = "DHH" # => NoMethodError
77
+ def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
78
+ syms.each do |sym|
79
+ raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
80
+
81
+ # The following generated method concatenates `name` because we want it
82
+ # to work with inheritance via polymorphism.
83
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
84
+ def self.#{sym}=(obj)
85
+ Thread.current["attr_" + name + "_#{sym}"] = obj
86
+ end
87
+ EOS
88
+
89
+ if instance_writer && instance_accessor
90
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
91
+ def #{sym}=(obj)
92
+ self.class.#{sym} = obj
93
+ end
94
+ EOS
95
+ end
96
+
97
+ public_send("#{sym}=", default) unless default.nil?
98
+ end
99
+ end
100
+ alias :thread_cattr_writer :thread_mattr_writer
101
+
102
+ # Defines both class and instance accessors for class attributes.
103
+ #
104
+ # class Account
105
+ # thread_mattr_accessor :user
106
+ # end
107
+ #
108
+ # Account.user = "DHH"
109
+ # Account.user # => "DHH"
110
+ # Account.new.user # => "DHH"
111
+ #
112
+ # If a subclass changes the value, the parent class' value is not changed.
113
+ # Similarly, if the parent class changes the value, the value of subclasses
114
+ # is not changed.
115
+ #
116
+ # class Customer < Account
117
+ # end
118
+ #
119
+ # Customer.user = "Rafael"
120
+ # Customer.user # => "Rafael"
121
+ # Account.user # => "DHH"
122
+ #
123
+ # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
124
+ # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
125
+ #
126
+ # class Current
127
+ # thread_mattr_accessor :user, instance_writer: false, instance_reader: false
128
+ # end
129
+ #
130
+ # Current.new.user = "DHH" # => NoMethodError
131
+ # Current.new.user # => NoMethodError
132
+ #
133
+ # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
134
+ #
135
+ # class Current
136
+ # thread_mattr_accessor :user, instance_accessor: false
137
+ # end
138
+ #
139
+ # Current.new.user = "DHH" # => NoMethodError
140
+ # Current.new.user # => NoMethodError
141
+ def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
142
+ thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
143
+ thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
144
+ end
145
+ alias :thread_cattr_accessor :thread_mattr_accessor
146
+ end
@@ -0,0 +1,132 @@
1
+ require "sigterm_extensions/concern"
2
+
3
+ class Module
4
+ # = Bite-sized separation of concerns
5
+ #
6
+ # We often find ourselves with a medium-sized chunk of behavior that we'd
7
+ # like to extract, but only mix in to a single class.
8
+ #
9
+ # Extracting a plain old Ruby object to encapsulate it and collaborate or
10
+ # delegate to the original object is often a good choice, but when there's
11
+ # no additional state to encapsulate or we're making DSL-style declarations
12
+ # about the parent class, introducing new collaborators can obfuscate rather
13
+ # than simplify.
14
+ #
15
+ # The typical route is to just dump everything in a monolithic class, perhaps
16
+ # with a comment, as a least-bad alternative. Using modules in separate files
17
+ # means tedious sifting to get a big-picture view.
18
+ #
19
+ # = Dissatisfying ways to separate small concerns
20
+ #
21
+ # == Using comments:
22
+ #
23
+ # class Todo < ApplicationRecord
24
+ # # Other todo implementation
25
+ # # ...
26
+ #
27
+ # ## Event tracking
28
+ # has_many :events
29
+ #
30
+ # before_create :track_creation
31
+ #
32
+ # private
33
+ # def track_creation
34
+ # # ...
35
+ # end
36
+ # end
37
+ #
38
+ # == With an inline module:
39
+ #
40
+ # Noisy syntax.
41
+ #
42
+ # class Todo < ApplicationRecord
43
+ # # Other todo implementation
44
+ # # ...
45
+ #
46
+ # module EventTracking
47
+ # extend ActiveSupport::Concern
48
+ #
49
+ # included do
50
+ # has_many :events
51
+ # before_create :track_creation
52
+ # end
53
+ #
54
+ # private
55
+ # def track_creation
56
+ # # ...
57
+ # end
58
+ # end
59
+ # include EventTracking
60
+ # end
61
+ #
62
+ # == Mix-in noise exiled to its own file:
63
+ #
64
+ # Once our chunk of behavior starts pushing the scroll-to-understand-it
65
+ # boundary, we give in and move it to a separate file. At this size, the
66
+ # increased overhead can be a reasonable tradeoff even if it reduces our
67
+ # at-a-glance perception of how things work.
68
+ #
69
+ # class Todo < ApplicationRecord
70
+ # # Other todo implementation
71
+ # # ...
72
+ #
73
+ # include TodoEventTracking
74
+ # end
75
+ #
76
+ # = Introducing Module#concerning
77
+ #
78
+ # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
79
+ # separate bite-sized concerns.
80
+ #
81
+ # class Todo < ApplicationRecord
82
+ # # Other todo implementation
83
+ # # ...
84
+ #
85
+ # concerning :EventTracking do
86
+ # included do
87
+ # has_many :events
88
+ # before_create :track_creation
89
+ # end
90
+ #
91
+ # private
92
+ # def track_creation
93
+ # # ...
94
+ # end
95
+ # end
96
+ # end
97
+ #
98
+ # Todo.ancestors
99
+ # # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
100
+ #
101
+ # This small step has some wonderful ripple effects. We can
102
+ # * grok the behavior of our class in one glance,
103
+ # * clean up monolithic junk-drawer classes by separating their concerns, and
104
+ # * stop leaning on protected/private for crude "this is internal stuff" modularity.
105
+ module Concerning
106
+ # Define a new concern and mix it in.
107
+ def concerning(topic, &block)
108
+ include concern(topic, &block)
109
+ end
110
+
111
+ # A low-cruft shortcut to define a concern.
112
+ #
113
+ # concern :EventTracking do
114
+ # ...
115
+ # end
116
+ #
117
+ # is equivalent to
118
+ #
119
+ # module EventTracking
120
+ # extend ActiveSupport::Concern
121
+ #
122
+ # ...
123
+ # end
124
+ def concern(topic, &module_definition)
125
+ const_set topic, Module.new {
126
+ extend ::ActiveSupport::Concern
127
+ module_eval(&module_definition)
128
+ }
129
+ end
130
+ end
131
+ include Concerning
132
+ end
@@ -0,0 +1,319 @@
1
+ require "set"
2
+
3
+ class Module
4
+ # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
5
+ # option is not used.
6
+ class DelegationError < NoMethodError; end
7
+
8
+ RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
9
+ else elsif END end ensure false for if in module next nil not or redo rescue retry
10
+ return self super then true undef unless until when while yield)
11
+ DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
12
+ DELEGATION_RESERVED_METHOD_NAMES = Set.new(
13
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
14
+ ).freeze
15
+
16
+ # Provides a +delegate+ class method to easily expose contained objects'
17
+ # public methods as your own.
18
+ #
19
+ # ==== Options
20
+ # * <tt>:to</tt> - Specifies the target object name as a symbol or string
21
+ # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
22
+ # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
23
+ # from being raised
24
+ # * <tt>:private</tt> - If set to true, changes method visibility to private
25
+ #
26
+ # The macro receives one or more method names (specified as symbols or
27
+ # strings) and the name of the target object via the <tt>:to</tt> option
28
+ # (also a symbol or string).
29
+ #
30
+ # Delegation is particularly useful with Active Record associations:
31
+ #
32
+ # class Greeter < ActiveRecord::Base
33
+ # def hello
34
+ # 'hello'
35
+ # end
36
+ #
37
+ # def goodbye
38
+ # 'goodbye'
39
+ # end
40
+ # end
41
+ #
42
+ # class Foo < ActiveRecord::Base
43
+ # belongs_to :greeter
44
+ # delegate :hello, to: :greeter
45
+ # end
46
+ #
47
+ # Foo.new.hello # => "hello"
48
+ # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
49
+ #
50
+ # Multiple delegates to the same target are allowed:
51
+ #
52
+ # class Foo < ActiveRecord::Base
53
+ # belongs_to :greeter
54
+ # delegate :hello, :goodbye, to: :greeter
55
+ # end
56
+ #
57
+ # Foo.new.goodbye # => "goodbye"
58
+ #
59
+ # Methods can be delegated to instance variables, class variables, or constants
60
+ # by providing them as a symbols:
61
+ #
62
+ # class Foo
63
+ # CONSTANT_ARRAY = [0,1,2,3]
64
+ # @@class_array = [4,5,6,7]
65
+ #
66
+ # def initialize
67
+ # @instance_array = [8,9,10,11]
68
+ # end
69
+ # delegate :sum, to: :CONSTANT_ARRAY
70
+ # delegate :min, to: :@@class_array
71
+ # delegate :max, to: :@instance_array
72
+ # end
73
+ #
74
+ # Foo.new.sum # => 6
75
+ # Foo.new.min # => 4
76
+ # Foo.new.max # => 11
77
+ #
78
+ # It's also possible to delegate a method to the class by using +:class+:
79
+ #
80
+ # class Foo
81
+ # def self.hello
82
+ # "world"
83
+ # end
84
+ #
85
+ # delegate :hello, to: :class
86
+ # end
87
+ #
88
+ # Foo.new.hello # => "world"
89
+ #
90
+ # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
91
+ # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
92
+ # delegated to.
93
+ #
94
+ # Person = Struct.new(:name, :address)
95
+ #
96
+ # class Invoice < Struct.new(:client)
97
+ # delegate :name, :address, to: :client, prefix: true
98
+ # end
99
+ #
100
+ # john_doe = Person.new('John Doe', 'Vimmersvej 13')
101
+ # invoice = Invoice.new(john_doe)
102
+ # invoice.client_name # => "John Doe"
103
+ # invoice.client_address # => "Vimmersvej 13"
104
+ #
105
+ # It is also possible to supply a custom prefix.
106
+ #
107
+ # class Invoice < Struct.new(:client)
108
+ # delegate :name, :address, to: :client, prefix: :customer
109
+ # end
110
+ #
111
+ # invoice = Invoice.new(john_doe)
112
+ # invoice.customer_name # => 'John Doe'
113
+ # invoice.customer_address # => 'Vimmersvej 13'
114
+ #
115
+ # The delegated methods are public by default.
116
+ # Pass <tt>private: true</tt> to change that.
117
+ #
118
+ # class User < ActiveRecord::Base
119
+ # has_one :profile
120
+ # delegate :first_name, to: :profile
121
+ # delegate :date_of_birth, to: :profile, private: true
122
+ #
123
+ # def age
124
+ # Date.today.year - date_of_birth.year
125
+ # end
126
+ # end
127
+ #
128
+ # User.new.first_name # => "Tomas"
129
+ # User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
130
+ # User.new.age # => 2
131
+ #
132
+ # If the target is +nil+ and does not respond to the delegated method a
133
+ # +Module::DelegationError+ is raised. If you wish to instead return +nil+,
134
+ # use the <tt>:allow_nil</tt> option.
135
+ #
136
+ # class User < ActiveRecord::Base
137
+ # has_one :profile
138
+ # delegate :age, to: :profile
139
+ # end
140
+ #
141
+ # User.new.age
142
+ # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
143
+ #
144
+ # But if not having a profile yet is fine and should not be an error
145
+ # condition:
146
+ #
147
+ # class User < ActiveRecord::Base
148
+ # has_one :profile
149
+ # delegate :age, to: :profile, allow_nil: true
150
+ # end
151
+ #
152
+ # User.new.age # nil
153
+ #
154
+ # Note that if the target is not +nil+ then the call is attempted regardless of the
155
+ # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
156
+ # does not respond to the method:
157
+ #
158
+ # class Foo
159
+ # def initialize(bar)
160
+ # @bar = bar
161
+ # end
162
+ #
163
+ # delegate :name, to: :@bar, allow_nil: true
164
+ # end
165
+ #
166
+ # Foo.new("Bar").name # raises NoMethodError: undefined method `name'
167
+ #
168
+ # The target method must be public, otherwise it will raise +NoMethodError+.
169
+ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
170
+ unless to
171
+ raise ArgumentError, "Delegation needs a target. Supply a keyword argument 'to' (e.g. delegate :hello, to: :greeter)."
172
+ end
173
+
174
+ if prefix == true && /^[^a-z_]/.match?(to)
175
+ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
176
+ end
177
+
178
+ method_prefix = \
179
+ if prefix
180
+ "#{prefix == true ? to : prefix}_"
181
+ else
182
+ ""
183
+ end
184
+
185
+ location = caller_locations(1, 1).first
186
+ file, line = location.path, location.lineno
187
+
188
+ to = to.to_s
189
+ to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
190
+
191
+ method_def = []
192
+ method_names = []
193
+
194
+ methods.map do |method|
195
+ method_name = prefix ? "#{method_prefix}#{method}" : method
196
+ method_names << method_name.to_sym
197
+
198
+ # Attribute writer methods only accept one argument. Makes sure []=
199
+ # methods still accept two arguments.
200
+ definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
201
+
202
+ # The following generated method calls the target exactly once, storing
203
+ # the returned value in a dummy variable.
204
+ #
205
+ # Reason is twofold: On one hand doing less calls is in general better.
206
+ # On the other hand it could be that the target has side-effects,
207
+ # whereas conceptually, from the user point of view, the delegator should
208
+ # be doing one call.
209
+ if allow_nil
210
+ method = method.to_s
211
+
212
+ method_def <<
213
+ "def #{method_name}(#{definition})" <<
214
+ " _ = #{to}" <<
215
+ " if !_.nil? || nil.respond_to?(:#{method})" <<
216
+ " _.#{method}(#{definition})" <<
217
+ " end" <<
218
+ "end"
219
+ else
220
+ method = method.to_s
221
+ method_name = method_name.to_s
222
+
223
+ method_def <<
224
+ "def #{method_name}(#{definition})" <<
225
+ " _ = #{to}" <<
226
+ " _.#{method}(#{definition})" <<
227
+ "rescue NoMethodError => e" <<
228
+ " if _.nil? && e.name == :#{method}" <<
229
+ %( raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") <<
230
+ " else" <<
231
+ " raise" <<
232
+ " end" <<
233
+ "end"
234
+ end
235
+ end
236
+ module_eval(method_def.join(";"), file, line)
237
+ private(*method_names) if private
238
+ method_names
239
+ end
240
+
241
+ # When building decorators, a common pattern may emerge:
242
+ #
243
+ # class Partition
244
+ # def initialize(event)
245
+ # @event = event
246
+ # end
247
+ #
248
+ # def person
249
+ # detail.person || creator
250
+ # end
251
+ #
252
+ # private
253
+ # def respond_to_missing?(name, include_private = false)
254
+ # @event.respond_to?(name, include_private)
255
+ # end
256
+ #
257
+ # def method_missing(method, *args, &block)
258
+ # @event.send(method, *args, &block)
259
+ # end
260
+ # end
261
+ #
262
+ # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
263
+ #
264
+ # class Partition
265
+ # delegate_missing_to :@event
266
+ #
267
+ # def initialize(event)
268
+ # @event = event
269
+ # end
270
+ #
271
+ # def person
272
+ # detail.person || creator
273
+ # end
274
+ # end
275
+ #
276
+ # The target can be anything callable within the object, e.g. instance
277
+ # variables, methods, constants, etc.
278
+ #
279
+ # The delegated method must be public on the target, otherwise it will
280
+ # raise +DelegationError+. If you wish to instead return +nil+,
281
+ # use the <tt>:allow_nil</tt> option.
282
+ #
283
+ # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
284
+ # delegation due to possible interference when calling
285
+ # <tt>Marshal.dump(object)</tt>, should the delegation target method
286
+ # of <tt>object</tt> add or remove instance variables.
287
+ def delegate_missing_to(target, allow_nil: nil)
288
+ target = target.to_s
289
+ target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
290
+
291
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
292
+ def respond_to_missing?(name, include_private = false)
293
+ # It may look like an oversight, but we deliberately do not pass
294
+ # +include_private+, because they do not get delegated.
295
+ return false if name == :marshal_dump || name == :_dump
296
+ #{target}.respond_to?(name) || super
297
+ end
298
+ def method_missing(method, *args, &block)
299
+ if #{target}.respond_to?(method)
300
+ #{target}.public_send(method, *args, &block)
301
+ else
302
+ begin
303
+ super
304
+ rescue NoMethodError
305
+ if #{target}.nil?
306
+ if #{allow_nil == true}
307
+ nil
308
+ else
309
+ raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
310
+ end
311
+ else
312
+ raise
313
+ end
314
+ end
315
+ end
316
+ end
317
+ RUBY
318
+ end
319
+ end