Module: Laminate::Layer
- Defined in:
- lib/laminate/layer.rb
Class Method Summary collapse
Instance Method Summary collapse
Class Method Details
.included(base) ⇒ Object
117 118 119 |
# File 'lib/laminate/layer.rb', line 117 def self.included(base) base.class_variable_set(:@@__laminate_layer_cache, {}) end |
Instance Method Details
#with_layer(layer_module, options = {}) ⇒ Object
5 6 7 |
# File 'lib/laminate/layer.rb', line 5 def with_layer(layer_module, = {}) with_layers(Array(layer_module), ) end |
#with_layers(layer_modules, options = {}) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/laminate/layer.rb', line 9 def with_layers(layer_modules, = {}) # If layer_module isn't a ruby module, blow up. unless layer_modules.all? { |mod| mod.is_a?(Module) } raise ArgumentError, 'layers must all be modules' end # Grab the cache from the super class's singleton. It's not correct to # simply reference the @@__laminate_layer_cache variable because of # lexical scoping. If we were to reference the variable directly, Ruby # would think we wanted to associate it with the Layer module, where in # reality we want to associate it with each module Layer is mixed into. cache = self.class.class_variable_get(:@@__laminate_layer_cache) # We don't want to generate each wrapper class more than once, so keep # track of the modules => dynamic wrapper mapping and avoid re-creating # them on every call to with_layer. cache[layer_modules] ||= begin layer_module_methods = layer_modules.flat_map do |mod| mod.instance_methods(false) end unless .fetch(:allow_overrides, false) # Identify method collisions. In order to minimize accidental # monkeypatching, Celophane will error if you try to wrap an object # with a layer that defines any method with the same name as a method # the object already responds to. Starts with self, or more accurately, # the methods defined on self. The loop walks the ancestor chain an # checks each ancestor for previously defined methods. ancestor = self while ancestor # Filter out Object's methods, which are common to all objects and not # ones we should be forwarding. Also filter out Layer's methods for # the same reason. ancestor_methods = ancestor.class.instance_methods - ( Object.methods + Layer.instance_methods(false) ) # Calculate the intersection between the layer's methods and the # methods defined by the current ancestor. already_defined = ancestor_methods & layer_module_methods unless already_defined.empty? ancestor_modules = [self.class] + (ancestor.instance_variable_get(:@__laminate_modules) || []) = already_defined.map do |method_name| ancestor_module = ancestor_modules.find do |mod| mod.method_defined?(method_name) end " `##{method_name}' is already defined in #{ancestor_module.name}" end layer_plural = .size == 1 ? 'layer' : 'layers' # @TODO: fix the English here raise MethodAlreadyDefinedError, "Unable to add #{layer_plural} (pass `allow_overrides: true` if "\ "intentional):\n#{.join("\n")}" end # Grab the next ancestor and keep going. The loop exits when ancestor # is nil, which happens whenever the end of the ancestor chain has # been reached (i.e. when iteration reaches the base object). ancestor = ancestor.instance_variable_get(:@__laminate_ancestor) end end ancestor_methods = self.class.instance_methods - ( Object.methods + Layer.instance_methods(false) + layer_module_methods ) # Dynamically define a new class and mix in the layer modules. Forward # all the ancestor's methods to the ancestor. Dynamic layer classes keep # track of both the ancestor itself as well as the modules it was # constructed from. klass = Class.new do layer_modules.each do |layer_module| include layer_module end extend Forwardable # Forward all the ancestor's methods to the ancestor. def_delegators :@__laminate_ancestor, *ancestor_methods def initialize(ancestor, layer_modules) # Use absurd variable names to avoid re-defining instance variables # introduced by the layer module. @__laminate_ancestor = ancestor @__laminate_modules = layer_modules end end module_names = layer_modules.map { |mod| mod.name.split('::').last } wrapper_name = 'With' + module_names.join('And') # Assign the new wrapper class to a constant inside self, with 'With' # prepended. For example, if the module is called Engine the wrapper # class will be assigned to a constant named WithEngine. self.class.const_set(wrapper_name, klass) klass end # Wrap self in a new instance of the wrapper class. cache[layer_modules].new(self, layer_modules) end |