laminate 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e77157349328090e7c00245481ec1d53804a835f
4
+ data.tar.gz: c181e76a479b851250cb11de2536af6c3476bd68
5
+ SHA512:
6
+ metadata.gz: 60b365e0eb3cf0c11d8db3fbfbab76cc65be39c366507f5dd6b204669bfc838ca10f6783896ea549e748469321ae7f7e03b617bfe5ec222dda0d21bf18219c9c
7
+ data.tar.gz: bbdea1d7aa0aa019e5f3d51ece65efd2e509d4a9d8edcceb8d0ada5fd6c69086db0d872c5fd9c24437e672d58b93335e683113c4ec57eddf530f73b419d0a9c6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Cameron C. Dutro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,130 @@
1
+ ## laminate
2
+
3
+ Turn any Ruby module into a composable decorator.
4
+
5
+ ## Installation
6
+
7
+ `gem install laminate`
8
+
9
+ or put it in your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'laminate'
13
+ ```
14
+
15
+ ### Background
16
+
17
+ If you've ever worked on a large Ruby application, you've probably seen a few classes get too big. A typical example in a Rails application is the `User` model, which is a convenient place to put all the functionality for the site's logged-in experience. If allowed to grow unchecked, these bloated classes can become increasingly difficult to hold in your head at once, meaning they're more difficult to change, meaning you change them less often out of fear you'll break a fundamental part of your application.
18
+
19
+ Enter modules.
20
+
21
+ At their core, Ruby modules are just bags of methods. They're commonly used to encapsulate shared functionality, but they can also be used to separate shared groups of methods from classes that are getting too big. What this means in practical terms is a `User` model that looks something like this:
22
+
23
+ ```ruby
24
+ class User < ActiveRecord::Base
25
+ include LoginMethods
26
+ include EmailMethods
27
+ include RoleMethods
28
+ ...
29
+ end
30
+ ```
31
+
32
+ Each of the modules represents a cohesive group of methods that we're basically injecting into `User` when the application starts up. The upside is that now `User` contains a lot less code.
33
+
34
+ Or does it? What have we really done here? As it turns out, nothing. `User` still contains the same methods it did before. The only difference is that now the methods are spread out in different files. Logically the `User` class hasn't changed at all and we're still stuck with the same problem - it's still too large to reason about and still too scary to change.
35
+
36
+ But that's not all. The separation has now made it even more difficult for the programmer to track down potential bugs. The programmer can still call all the same methods on instances of `User` but those methods aren't actually defined in user.rb.
37
+
38
+ But that's also not all. When you include a module, you're actually adding that module to the host class or module's inheritance chain. Any _public or private_ methods in the host class will take precedence over included methods. For example, consider the following class and included module:
39
+
40
+ ```ruby
41
+ module HarvestHelpers
42
+ def harvest
43
+ :harvest_from_helper
44
+ end
45
+ end
46
+
47
+ class VegetableGarden
48
+ include HarvestHelpers
49
+
50
+ def harvest
51
+ :harvest_from_garden
52
+ end
53
+ end
54
+ ```
55
+
56
+ What gets returned if I run `VegetableGarden.new.harvest`? Perhaps a bit counterintuitively, you'll get `:harvest_from_garden`. Now imagine `VegetableGarden` is a huge class with 15 included modules, any of which may define the `#harvest` method. If you define the `#harvest` method in `VegetableGarden` without realizing it's already defined, you could end up breaking your application in subtle, difficult-to-debug ways. Sure, you could `prepend` the `HarvestHelpers` module instead of `include`-ing it, but that could mean stepping on the toes of another prepended or included module. What's worse, remember that both public _and_ private methods are affected, even though your private methods are probably only designed to be used in the module in which they're defined.
57
+
58
+ ### Ok, so how can this gem help?
59
+
60
+ Laminate tries to address the downsides of module inclusion by converting modules into composable decorators called layers. Layers are composable because they can be progressively applied, or laid on top of one another. For example, you could add `HarvestHelpers` progressively to an instance of `VegetableGarden`. First, we'll need to turn `HarvestHelper` and `VegetableGarden` into a layers:
61
+
62
+ ```ruby
63
+ module HarvestHelpers
64
+ include Laminate::Layer
65
+
66
+ def harvest
67
+ :harvest_from_helper
68
+ end
69
+ end
70
+
71
+ class VegetableGarden
72
+ include Laminate::Layer
73
+ end
74
+ ```
75
+
76
+ Once that's done, we can layer `HarvestHelpers` onto instances of `VegetableGarden` whenever we want harvest functionality:
77
+
78
+ ```ruby
79
+ garden = VegetableGarden.new.with_layer(HarvestHelpers)
80
+ garden.harvest
81
+ ```
82
+
83
+ What's more, you can create a layer out of more than one module at a time:
84
+
85
+ ```ruby
86
+ module PlantHelpers
87
+ def dig_hole
88
+ # dig dig
89
+ end
90
+ end
91
+
92
+ garden = VegetableGarden.new.with_layers([HarvestHelpers, PlantHelpers])
93
+ # returns #<VegetableGarden::WithHarvestHelpersAndPlantHelpers:0x007f7f9c836da0>
94
+
95
+ garden.dig_hole
96
+ garden.harvest
97
+ ```
98
+
99
+ ### Sweet! How does it work?
100
+
101
+ The `garden` variable is an instance of `VegetableGarden::WithHarvestHelpers`, a class laminate dynamically created and cached for you (these dynamically created classes will be created once and reused the next time `#with_layer` is called).
102
+
103
+ `VegetableGarden::WithHarvestHelpers` forwards all _public_ methods already defined in `VegetableGarden` but none of the _private_ methods, meaning layers can define private methods without fearing those methods will be inadvertently overridden by other modules.
104
+
105
+ ### What about already defined methods?
106
+
107
+ Glad you asked. If `#harvest` is already defined in `VegetableGarden`, laminate will raise a helpful error:
108
+
109
+ ```ruby
110
+ VegetableGarden.new.with_layer(HarvestHelpers)
111
+
112
+ # Laminate::MethodAlreadyDefinedError: Unable to add layer:
113
+ # `#harvest' is already defined by VegetableGarden
114
+ ```
115
+
116
+ If you want the layer's method to override its ancestor's method, pass `allow_overrides: true`:
117
+
118
+ ```ruby
119
+ VegetableGarden.new.with_layer(HarvestHelpers, allow_overrides: true)
120
+ ```
121
+
122
+ If overrides are allowed, any method defined in `HarvestHelpers` will override the corresponding method defined in `VegetableGarden`. You should consider your use case carefully before using this rather large hammer.
123
+
124
+ ## License
125
+
126
+ Licensed under the MIT license. See LICENSE for details.
127
+
128
+ ## Authors
129
+
130
+ * Cameron C. Dutro: http://github.com/camertron
@@ -0,0 +1,18 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'laminate/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'laminate'
6
+ s.version = ::Laminate::VERSION
7
+ s.authors = ['Cameron Dutro']
8
+ s.email = ['camertron@gmail.com']
9
+ s.homepage = 'https://github.com/camertron/laminate'
10
+
11
+ s.description = s.summary = 'Turn any Ruby module into a composable decorator.'
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+ s.has_rdoc = true
15
+
16
+ s.require_path = 'lib'
17
+ s.files = Dir['{lib,spec}/**/*', 'README.md', 'laminate.gemspec', 'LICENSE']
18
+ end
@@ -0,0 +1,5 @@
1
+ module Laminate
2
+ autoload :MethodAlreadyDefinedError, 'laminate/errors'
3
+ autoload :UnsupportedStrategyError, 'laminate/errors'
4
+ autoload :Layer, 'laminate/layer'
5
+ end
@@ -0,0 +1,4 @@
1
+ module Laminate
2
+ class MethodAlreadyDefinedError < StandardError; end
3
+ class UnsupportedStrategyError < StandardError; end
4
+ end
@@ -0,0 +1,121 @@
1
+ require 'forwardable'
2
+
3
+ module Laminate
4
+ module Layer
5
+ def with_layer(layer_module, options = {})
6
+ with_layers(Array(layer_module), options)
7
+ end
8
+
9
+ def with_layers(layer_modules, options = {})
10
+ # If layer_module isn't a ruby module, blow up.
11
+ unless layer_modules.all? { |mod| mod.is_a?(Module) }
12
+ raise ArgumentError, 'layers must all be modules'
13
+ end
14
+
15
+ # Grab the cache from the super class's singleton. It's not correct to
16
+ # simply reference the @@__laminate_layer_cache variable because of
17
+ # lexical scoping. If we were to reference the variable directly, Ruby
18
+ # would think we wanted to associate it with the Layer module, where in
19
+ # reality we want to associate it with each module Layer is mixed into.
20
+ cache = self.class.class_variable_get(:@@__laminate_layer_cache)
21
+
22
+ # We don't want to generate each wrapper class more than once, so keep
23
+ # track of the modules => dynamic wrapper mapping and avoid re-creating
24
+ # them on every call to with_layer.
25
+ cache[layer_modules] ||= begin
26
+ layer_module_methods = layer_modules.flat_map do |mod|
27
+ mod.instance_methods(false)
28
+ end
29
+
30
+ unless options.fetch(:allow_overrides, false)
31
+ # Identify method collisions. In order to minimize accidental
32
+ # monkeypatching, Celophane will error if you try to wrap an object
33
+ # with a layer that defines any method with the same name as a method
34
+ # the object already responds to. Starts with self, or more accurately,
35
+ # the methods defined on self. The loop walks the ancestor chain an
36
+ # checks each ancestor for previously defined methods.
37
+ ancestor = self
38
+
39
+ while ancestor
40
+ # Filter out Object's methods, which are common to all objects and not
41
+ # ones we should be forwarding. Also filter out Layer's methods for
42
+ # the same reason.
43
+ ancestor_methods = ancestor.class.instance_methods - (
44
+ Object.methods + Layer.instance_methods(false)
45
+ )
46
+
47
+ # Calculate the intersection between the layer's methods and the
48
+ # methods defined by the current ancestor.
49
+ already_defined = ancestor_methods & layer_module_methods
50
+
51
+ unless already_defined.empty?
52
+ ancestor_modules = [self.class] + (ancestor.instance_variable_get(:@__laminate_modules) || [])
53
+
54
+ error_messages = already_defined.map do |method_name|
55
+ ancestor_module = ancestor_modules.find do |mod|
56
+ mod.method_defined?(method_name)
57
+ end
58
+
59
+ " `##{method_name}' is already defined in #{ancestor_module.name}"
60
+ end
61
+
62
+ layer_plural = error_messages.size == 1 ? 'layer' : 'layers'
63
+
64
+ # @TODO: fix the English here
65
+ raise MethodAlreadyDefinedError,
66
+ "Unable to add #{layer_plural} (pass `allow_overrides: true` if "\
67
+ "intentional):\n#{error_messages.join("\n")}"
68
+ end
69
+
70
+ # Grab the next ancestor and keep going. The loop exits when ancestor
71
+ # is nil, which happens whenever the end of the ancestor chain has
72
+ # been reached (i.e. when iteration reaches the base object).
73
+ ancestor = ancestor.instance_variable_get(:@__laminate_ancestor)
74
+ end
75
+ end
76
+
77
+ ancestor_methods = self.class.instance_methods - (
78
+ Object.methods + Layer.instance_methods(false) + layer_module_methods
79
+ )
80
+
81
+ # Dynamically define a new class and mix in the layer modules. Forward
82
+ # all the ancestor's methods to the ancestor. Dynamic layer classes keep
83
+ # track of both the ancestor itself as well as the modules it was
84
+ # constructed from.
85
+ klass = Class.new do
86
+ layer_modules.each do |layer_module|
87
+ include layer_module
88
+ end
89
+
90
+ extend Forwardable
91
+
92
+ # Forward all the ancestor's methods to the ancestor.
93
+ def_delegators :@__laminate_ancestor, *ancestor_methods
94
+
95
+ def initialize(ancestor, layer_modules)
96
+ # Use absurd variable names to avoid re-defining instance variables
97
+ # introduced by the layer module.
98
+ @__laminate_ancestor = ancestor
99
+ @__laminate_modules = layer_modules
100
+ end
101
+ end
102
+
103
+ module_names = layer_modules.map { |mod| mod.name.split('::').last }
104
+ wrapper_name = 'With' + module_names.join('And')
105
+
106
+ # Assign the new wrapper class to a constant inside self, with 'With'
107
+ # prepended. For example, if the module is called Engine the wrapper
108
+ # class will be assigned to a constant named WithEngine.
109
+ self.class.const_set(wrapper_name, klass)
110
+ klass
111
+ end
112
+
113
+ # Wrap self in a new instance of the wrapper class.
114
+ cache[layer_modules].new(self, layer_modules)
115
+ end
116
+
117
+ def self.included(base)
118
+ base.class_variable_set(:@@__laminate_layer_cache, {})
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,3 @@
1
+ module Laminate
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ include TestLayers
4
+
5
+ describe Laminate::Layer do
6
+ shared_examples 'a base class' do
7
+ it 'allows calling base methods' do
8
+ expect(instance.base_method).to eq(:base)
9
+ end
10
+
11
+ it 'allows calling layer methods' do
12
+ expect(instance.jog).to eq(:jogging)
13
+ end
14
+ end
15
+
16
+ shared_examples 'a 2nd level layer class' do
17
+ it 'allows calling 2nd level layer methods' do
18
+ expect(instance.run).to eq(:running)
19
+ end
20
+ end
21
+
22
+ shared_examples 'a 3rd level layer class' do
23
+ it 'allows calling 3rd level layer methods' do
24
+ expect(instance.sprint).to eq(:sprinting)
25
+ end
26
+ end
27
+
28
+ context 'with an instance of the base class' do
29
+ let(:instance) { Person.new }
30
+
31
+ it "doesn't do any nesting for the base layer" do
32
+ expect(instance).to be_a(Person)
33
+ end
34
+
35
+ it_behaves_like 'a base class'
36
+ end
37
+
38
+ context 'with a single layer' do
39
+ let(:instance) { Person.new.with_layer(Runner) }
40
+
41
+ it 'nests the new module names' do
42
+ expect(instance).to be_a(Person::WithRunner)
43
+ end
44
+
45
+ it_behaves_like 'a base class'
46
+ it_behaves_like 'a 2nd level layer class'
47
+ end
48
+
49
+ context 'with a second layer' do
50
+ let(:instance) { Person.new.with_layer(Runner).with_layer(Sprinter) }
51
+
52
+ it 'nests the new module names' do
53
+ expect(instance).to be_a(Person::WithRunner::WithSprinter)
54
+ end
55
+
56
+ it_behaves_like 'a base class'
57
+ it_behaves_like 'a 2nd level layer class'
58
+ it_behaves_like 'a 3rd level layer class'
59
+ end
60
+
61
+ context 'with a combined layer' do
62
+ let(:instance) { Person.new.with_layers([Runner, Sprinter]) }
63
+
64
+ it 'combines the module names for the new constant name' do
65
+ expect(instance).to be_a(Person::WithRunnerAndSprinter)
66
+ end
67
+
68
+ it_behaves_like 'a base class'
69
+ it_behaves_like 'a 2nd level layer class'
70
+ it_behaves_like 'a 3rd level layer class'
71
+ end
72
+
73
+ describe '#with_layer' do
74
+ it 'raises an error if passed something other than a module' do
75
+ expect { Person.new.with_layer('foo') }.to(
76
+ raise_error(ArgumentError, 'layers must all be modules')
77
+ )
78
+ end
79
+
80
+ it 'raises an error if a method is already defined' do
81
+ expect { Person.new.with_layer(JogMethodCollision) }.to(
82
+ raise_error(Laminate::MethodAlreadyDefinedError)
83
+ )
84
+ end
85
+
86
+ it 'ensures overriding methods does not raise errors if explicitly asked' do
87
+ instance = -> { Person.new.with_layer(JogMethodCollision, allow_overrides: true) }
88
+ expect(&instance).to_not raise_error
89
+ end
90
+
91
+ it 'ensures overridden methods take precedence' do
92
+ person = Person.new.with_layer(JogMethodCollision, allow_overrides: true)
93
+ expect(person.jog).to eq(:jogging_collision)
94
+ end
95
+ end
96
+
97
+ describe '#with_layers' do
98
+ it 'includes modules in order' do
99
+ layers = [JogMethodCollision, JogMethodCollision2]
100
+ person = Person.new.with_layers(layers, allow_overrides: true)
101
+ expect(person.jog).to eq(:jogging_collision2)
102
+
103
+ layers = [JogMethodCollision2, JogMethodCollision]
104
+ person = Person.new.with_layers(layers, allow_overrides: true)
105
+ expect(person.jog).to eq(:jogging_collision)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,56 @@
1
+ require 'pry-byebug'
2
+ require 'laminate'
3
+ require 'rspec'
4
+
5
+ RSpec.configure do |config|
6
+ config.filter_run(focus: true)
7
+ config.run_all_when_everything_filtered = true
8
+ end
9
+
10
+ module TestLayers
11
+ class Base
12
+ def base_method
13
+ :base
14
+ end
15
+ end
16
+
17
+ class Person < Base
18
+ include Laminate::Layer
19
+
20
+ def jog
21
+ :jogging
22
+ end
23
+ end
24
+
25
+ module JogMethodCollision
26
+ include Laminate::Layer
27
+
28
+ def jog
29
+ :jogging_collision
30
+ end
31
+ end
32
+
33
+ module JogMethodCollision2
34
+ include Laminate::Layer
35
+
36
+ def jog
37
+ :jogging_collision2
38
+ end
39
+ end
40
+
41
+ module Runner
42
+ include Laminate::Layer
43
+
44
+ def run
45
+ :running
46
+ end
47
+ end
48
+
49
+ module Sprinter
50
+ include Laminate::Layer
51
+
52
+ def sprint
53
+ :sprinting
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: laminate
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Turn any Ruby module into a composable decorator.
14
+ email:
15
+ - camertron@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - laminate.gemspec
23
+ - lib/laminate.rb
24
+ - lib/laminate/errors.rb
25
+ - lib/laminate/layer.rb
26
+ - lib/laminate/version.rb
27
+ - spec/layer_spec.rb
28
+ - spec/spec_helper.rb
29
+ homepage: https://github.com/camertron/laminate
30
+ licenses: []
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 2.5.2
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Turn any Ruby module into a composable decorator.
52
+ test_files: []