laminate 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +130 -0
- data/laminate.gemspec +18 -0
- data/lib/laminate.rb +5 -0
- data/lib/laminate/errors.rb +4 -0
- data/lib/laminate/layer.rb +121 -0
- data/lib/laminate/version.rb +3 -0
- data/spec/layer_spec.rb +108 -0
- data/spec/spec_helper.rb +56 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/laminate.gemspec
ADDED
@@ -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
|
data/lib/laminate.rb
ADDED
@@ -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
|
data/spec/layer_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|