sigterm_extensions 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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