decor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -0
- data/Gemfile.lock +21 -0
- data/Rakefile +17 -0
- data/Readme.textile +159 -0
- data/decor.gemspec +52 -0
- data/lib/decor.rb +317 -0
- data/spec/decor_spec.rb +139 -0
- data/spec/models/bare.rb +2 -0
- data/spec/models/resource.rb +37 -0
- data/spec/spec_helper.rb +6 -0
- metadata +126 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.2)
|
5
|
+
rake (0.8.7)
|
6
|
+
rspec (2.1.0)
|
7
|
+
rspec-core (~> 2.1.0)
|
8
|
+
rspec-expectations (~> 2.1.0)
|
9
|
+
rspec-mocks (~> 2.1.0)
|
10
|
+
rspec-core (2.1.0)
|
11
|
+
rspec-expectations (2.1.0)
|
12
|
+
diff-lcs (~> 1.1.2)
|
13
|
+
rspec-mocks (2.1.0)
|
14
|
+
|
15
|
+
PLATFORMS
|
16
|
+
ruby
|
17
|
+
|
18
|
+
DEPENDENCIES
|
19
|
+
bundler (~> 1.0.0)
|
20
|
+
rake (~> 0.8.7)
|
21
|
+
rspec (= 2.1.0)
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
RSpec::Core::RakeTask.new do |t|
|
13
|
+
t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
14
|
+
t.pattern = 'spec/**/*_spec.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :spec
|
data/Readme.textile
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
h1. Decor
|
2
|
+
|
3
|
+
Define multiple representations of an object.
|
4
|
+
|
5
|
+
"Read the documentation":http://empl.us/decor/ or see an example scenario and usage below.
|
6
|
+
|
7
|
+
h2. Scenario
|
8
|
+
|
9
|
+
You have an API. It returns provides access to a User resource. This resource:
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
create_table do |t|
|
13
|
+
t.string :screen_name
|
14
|
+
t.string :state, :default => "active"
|
15
|
+
t.string :role, :default => "user"
|
16
|
+
end
|
17
|
+
|
18
|
+
class User < ActiveRecord::Base
|
19
|
+
STATES = ["active", "inactive"]
|
20
|
+
ROLES = ["user", "admin"]
|
21
|
+
end
|
22
|
+
</pre>
|
23
|
+
|
24
|
+
At one point, you had just defined booleans for @active@ and @admin@, but you have plans to expand the states and roles of a User. However, you already provide an API that you do not wish to break.
|
25
|
+
|
26
|
+
How do you transition your code and your data over and not break backwards compatibility? How do you do this modularly and in an easily testable way without making your API endpoints (controllers et al) heavy?
|
27
|
+
|
28
|
+
How about:
|
29
|
+
|
30
|
+
<pre>
|
31
|
+
class User < ActiveRecord::Base
|
32
|
+
include Decor
|
33
|
+
|
34
|
+
STATES = ["active", "inactive"]
|
35
|
+
ROLES = ["user", "admin"]
|
36
|
+
|
37
|
+
version "v1" do
|
38
|
+
def active
|
39
|
+
state == "active"
|
40
|
+
end
|
41
|
+
def admin
|
42
|
+
role == "admin"
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json(options = {})
|
46
|
+
super(options.merge(:only => [:screen_name],
|
47
|
+
:methods => [:active, :admin]))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
version "v2" do
|
52
|
+
def as_json(*args)
|
53
|
+
super(options.merge(:only => [:screen_name, :state, :role]))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
</pre>
|
59
|
+
|
60
|
+
Then, our endpoint:
|
61
|
+
|
62
|
+
<pre>
|
63
|
+
get '/api/:version/users/:id' do
|
64
|
+
@user = User.find(params[:id])
|
65
|
+
@user.for(version).to_json
|
66
|
+
end
|
67
|
+
</pre>
|
68
|
+
|
69
|
+
You can also include external resources providing some contextual influence over how your resources choose to represent themselves:
|
70
|
+
|
71
|
+
<pre>
|
72
|
+
class User < ActiveRecord::Base
|
73
|
+
include Decor
|
74
|
+
|
75
|
+
version "v3" do
|
76
|
+
def screen_name
|
77
|
+
"%s (%s)" % [super, organization.name]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
get '/api/:version/organizations/:org_id/users/:id' do
|
84
|
+
@organization = Organization.find(params[:org_id])
|
85
|
+
@user = User.find(params[:id]).for(version, :organization => @organization)
|
86
|
+
@user.to_json
|
87
|
+
end
|
88
|
+
</pre>
|
89
|
+
|
90
|
+
h2. Example Usage
|
91
|
+
|
92
|
+
Defining your class and versions:
|
93
|
+
|
94
|
+
<pre>
|
95
|
+
class Resource < Struct.new(:name, :value)
|
96
|
+
include Decor
|
97
|
+
|
98
|
+
version "v1" do
|
99
|
+
# a computed value specific to this version
|
100
|
+
def computed
|
101
|
+
value * 10
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
version "v2" do
|
106
|
+
# overloaded name
|
107
|
+
def name
|
108
|
+
super.reverse
|
109
|
+
end
|
110
|
+
|
111
|
+
# different computed value for this version
|
112
|
+
def computed
|
113
|
+
value * 100
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def unversioned
|
118
|
+
"yes"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
</pre>
|
122
|
+
|
123
|
+
Using the versioned instance:
|
124
|
+
|
125
|
+
<pre>
|
126
|
+
resource = resource.for(version)
|
127
|
+
</pre>
|
128
|
+
|
129
|
+
h2. Contributing
|
130
|
+
|
131
|
+
Feel free to open "Issues":https://github.com/mtodd/decor/issues or fork and create "Pull Requests":https://github.com/mtodd/decor/pulls.
|
132
|
+
|
133
|
+
The minimum for contribution is a failing spec. If you can at least convey what the problem is or the feature you'd like to add in a failing spec, I will be much more motivated to fix the issue or add the feature.
|
134
|
+
|
135
|
+
If you'd like to fix the bug or implement the feature yourself, feel free to! Please document and test your code thoroughly and in similar fashion to what already exists.
|
136
|
+
|
137
|
+
h2. Copyright and License
|
138
|
+
|
139
|
+
The MIT License
|
140
|
+
|
141
|
+
Copyright (c) 2010 Matt Todd.
|
142
|
+
|
143
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
144
|
+
of this software and associated documentation files (the "Software"), to deal
|
145
|
+
in the Software without restriction, including without limitation the rights
|
146
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
147
|
+
copies of the Software, and to permit persons to whom the Software is
|
148
|
+
furnished to do so, subject to the following conditions:
|
149
|
+
|
150
|
+
The above copyright notice and this permission notice shall be included in
|
151
|
+
all copies or substantial portions of the Software.
|
152
|
+
|
153
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
154
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
155
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
156
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
157
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
158
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
159
|
+
THE SOFTWARE.
|
data/decor.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{decor}
|
3
|
+
s.version = "0.1.0"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Matt Todd"]
|
7
|
+
s.date = %q{2010-12-13}
|
8
|
+
s.description = %q{Provides a simple way to define multiple representations of an object}
|
9
|
+
s.email = %q{chiology@gmail.com}
|
10
|
+
s.files = [
|
11
|
+
"decor.gemspec",
|
12
|
+
"Gemfile",
|
13
|
+
"Gemfile.lock",
|
14
|
+
"Rakefile",
|
15
|
+
"Readme.textile",
|
16
|
+
"lib/decor.rb",
|
17
|
+
"spec/decor_spec.rb",
|
18
|
+
"spec/models/bare.rb",
|
19
|
+
"spec/models/resource.rb",
|
20
|
+
"spec/spec_helper.rb"
|
21
|
+
]
|
22
|
+
s.homepage = %q{http://empl.us/decor/}
|
23
|
+
s.licenses = ["MIT"]
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
s.rubygems_version = %q{1.3.7}
|
26
|
+
s.summary = %q{Defines multiple representations of objects}
|
27
|
+
s.test_files = [
|
28
|
+
"spec/spec_helper.rb",
|
29
|
+
"spec/decor_spec.rb",
|
30
|
+
"spec/models/bare.rb",
|
31
|
+
"spec/models/resource.rb"
|
32
|
+
]
|
33
|
+
|
34
|
+
if s.respond_to? :specification_version then
|
35
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
36
|
+
s.specification_version = 3
|
37
|
+
|
38
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
39
|
+
s.add_development_dependency("bundler", ["~> 1.0.0"])
|
40
|
+
s.add_development_dependency("rake", ["~> 0.8.7"])
|
41
|
+
s.add_development_dependency("rspec", ["= 2.1.0"])
|
42
|
+
else
|
43
|
+
s.add_dependency("bundler", ["~> 1.0.0"])
|
44
|
+
s.add_dependency("rake", ["~> 0.8.7"])
|
45
|
+
s.add_dependency("rspec", ["= 2.1.0"])
|
46
|
+
end
|
47
|
+
else
|
48
|
+
s.add_dependency("bundler", ["~> 1.0.0"])
|
49
|
+
s.add_dependency("rake", ["~> 0.8.7"])
|
50
|
+
s.add_dependency("rspec", ["= 2.1.0"])
|
51
|
+
end
|
52
|
+
end
|
data/lib/decor.rb
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
# Decor provides a simple way to define multiple representations of an object.
|
2
|
+
#
|
3
|
+
# This is useful for when you want to retain multiple versions of your objects
|
4
|
+
# while providing a consistent interface between versions.
|
5
|
+
#
|
6
|
+
# For example:
|
7
|
+
#
|
8
|
+
# class Company < ActiveRecord::Base
|
9
|
+
# include Decor
|
10
|
+
#
|
11
|
+
# # the first version, customers are dependent on certain artifacts
|
12
|
+
# # like industry_id and industry
|
13
|
+
# version "v1" do
|
14
|
+
# INDUSTRIES = {1 => "Farm",
|
15
|
+
# 2 => "Software"}
|
16
|
+
#
|
17
|
+
# def industry
|
18
|
+
# INDUSTRIES[industry_id]
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def as_json(*_)
|
22
|
+
# super(:only => [:id, :name, :industry_id],
|
23
|
+
# :methods => [:industry])
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # switch to using industry standard codes, cleans up the name
|
28
|
+
# version "v2" do
|
29
|
+
# SIC_CODES = {...}
|
30
|
+
#
|
31
|
+
# # use the cleaned name for the name instead
|
32
|
+
# def name
|
33
|
+
# name_cleaned
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# def industry
|
37
|
+
# SIC_DOES[sic_code]
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# def as_json(*_)
|
41
|
+
# super(:only => [:id, :name, :sic_code],
|
42
|
+
# :methods => [:industry])
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# In our API, for instance, we can then define a single endpoint for both
|
49
|
+
# versions:
|
50
|
+
#
|
51
|
+
# get "/api/:version/companies/:id.json" do
|
52
|
+
# Company.find(params[:id]).for(version).to_json
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# This helps us keep our models fat and our "controllers" skinny. This also
|
56
|
+
# helps unit testing the versions of your API.
|
57
|
+
#
|
58
|
+
# Further details can be found on [Github](https://github.com/mtodd/decor/).
|
59
|
+
#
|
60
|
+
module Decor
|
61
|
+
|
62
|
+
# Decor is a mixin. Include it into your class and then use the `version`
|
63
|
+
# class methods in the class body to define your versions, then use the
|
64
|
+
# `for` instance method on your objects to use a specific version.
|
65
|
+
#
|
66
|
+
# For example:
|
67
|
+
#
|
68
|
+
# class Model
|
69
|
+
# include Decor
|
70
|
+
#
|
71
|
+
# version "v1" do
|
72
|
+
# # implement specifics for this version here
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# model = Model.new.for("v1")
|
77
|
+
#
|
78
|
+
def self.included(target)
|
79
|
+
target.send(:extend, ClassMethods)
|
80
|
+
class << target; attr_accessor :versions; end
|
81
|
+
target.versions = {}
|
82
|
+
end
|
83
|
+
|
84
|
+
module ClassMethods
|
85
|
+
# Defines versions of the model's representation.
|
86
|
+
#
|
87
|
+
# The `version` supplied is a string representation of the version.
|
88
|
+
# For example, `"v1"` represents version 1 of this model's representation.
|
89
|
+
#
|
90
|
+
# The `version` is just a key, however, and any value works. Strings are
|
91
|
+
# convenient, especially in the form of `v1` or `v2010-12-09`.
|
92
|
+
#
|
93
|
+
# If a block is provided, the block is treated as the body of the version's
|
94
|
+
# definition.
|
95
|
+
#
|
96
|
+
# If a hash of `version => version_module` is passed in, we use your module
|
97
|
+
# instead of creating our own.
|
98
|
+
#
|
99
|
+
# For example:
|
100
|
+
#
|
101
|
+
# class Model
|
102
|
+
# include Decor
|
103
|
+
#
|
104
|
+
# version "v1" do
|
105
|
+
# # ...
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# module V20101209
|
109
|
+
# # ...
|
110
|
+
# end
|
111
|
+
# version "v2010-12-09" => V20101209
|
112
|
+
#
|
113
|
+
# # or use classes (for example) as version keys and alias to other
|
114
|
+
# # versions
|
115
|
+
# version AnotherModel => "v1"
|
116
|
+
# version OtherModel => V20101209
|
117
|
+
#
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
def version(version, &block)
|
121
|
+
case
|
122
|
+
# Look up version module if no version block or module specified.
|
123
|
+
# version "v1" #=> #<Module>
|
124
|
+
when self.versions.key?(version)
|
125
|
+
return self.versions[version]
|
126
|
+
|
127
|
+
# Define a new version from the block.
|
128
|
+
# version "v1" { ... }
|
129
|
+
when block_given?
|
130
|
+
constant = Module.new(&block)
|
131
|
+
self.versions[version] = constant
|
132
|
+
self
|
133
|
+
|
134
|
+
# Set versions from a module, supports as many versions as passed in.
|
135
|
+
# version "v1" => Version1,
|
136
|
+
# "v2" => Version2,
|
137
|
+
# "v20101209" => "v2" # supports aliases
|
138
|
+
else
|
139
|
+
version.each do |(new_version, module_or_version)|
|
140
|
+
self.versions[new_version] =
|
141
|
+
if module_or_version.is_a?(Module)
|
142
|
+
module_or_version
|
143
|
+
else
|
144
|
+
self.versions[module_or_version]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
self
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# An object will be told to behave like the version specified.
|
154
|
+
#
|
155
|
+
# Options provide additional values in the context of the verions.
|
156
|
+
#
|
157
|
+
# class Model
|
158
|
+
# include Decor
|
159
|
+
#
|
160
|
+
# version "v1" do
|
161
|
+
# def versioned?
|
162
|
+
# true
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# def versioned?
|
167
|
+
# false
|
168
|
+
# end
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# model = Model.new
|
172
|
+
# model. versioned? #=> false
|
173
|
+
# model.for("v1").versioned? #=> true
|
174
|
+
#
|
175
|
+
# An optional context can be supplied which will make external resources
|
176
|
+
# available for specific functions in your versions. For example:
|
177
|
+
#
|
178
|
+
# class User
|
179
|
+
# include Decor
|
180
|
+
#
|
181
|
+
# version "v1" do
|
182
|
+
# def display_name
|
183
|
+
# "%s (%s)" % [name, band.display_name]
|
184
|
+
# end
|
185
|
+
# end
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# user = User.find(id).for("v1", :band => external_band)
|
189
|
+
# user.display_name #=> "Dan Auerbach (The Black Keys)"
|
190
|
+
#
|
191
|
+
# Lastly, it's possible to pass in a `:module` option which will override the
|
192
|
+
# module already defined for the version specified (making the `version`
|
193
|
+
# passed in almost meaningless).
|
194
|
+
#
|
195
|
+
# module Specialized
|
196
|
+
# # special considerations here
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# Model.new.for("v1", :module => Specialized)
|
200
|
+
#
|
201
|
+
def for(version, options = {})
|
202
|
+
version_module = self.version_module_for(version, options)
|
203
|
+
decorator = Class.new(Base).new(self, version, options)
|
204
|
+
decorator.send(:extend, version_module)
|
205
|
+
decorator
|
206
|
+
end
|
207
|
+
|
208
|
+
# Handles finding the module defined for the `version` specified, or
|
209
|
+
# overriding with the `:module` option.
|
210
|
+
#
|
211
|
+
# See `for` for details.
|
212
|
+
#
|
213
|
+
def version_module_for(version, options = {})
|
214
|
+
return options.delete(:module) if options.key?(:module)
|
215
|
+
return self.class.versions[version] if self.class.versions.key?(version)
|
216
|
+
self.class.const_get(version.upcase)
|
217
|
+
end
|
218
|
+
|
219
|
+
# The basis of our version wrapper.
|
220
|
+
#
|
221
|
+
# Whenever a version is specified using `Decor#for`, it is wrapped in a
|
222
|
+
# decoration that makes it behave like the specified version.
|
223
|
+
#
|
224
|
+
# This decoration proxies all calls it doesn't define itself to the original
|
225
|
+
# object (the `target`).
|
226
|
+
#
|
227
|
+
# `options` provides context, which are treated as instance methods for the
|
228
|
+
# versions. See `for` for details.
|
229
|
+
#
|
230
|
+
class Base < Struct.new(:target, :version, :options)
|
231
|
+
|
232
|
+
# This proxies calls to the target if we don't define it explicitly.
|
233
|
+
#
|
234
|
+
# However, if the `options` context defines a key that matches the `method`
|
235
|
+
# name, we return that value instead.
|
236
|
+
#
|
237
|
+
def method_missing(method, *args, &block)
|
238
|
+
return options[method] if options.key?(method)
|
239
|
+
return target.send(method, *args, &block) if respond_to?(method)
|
240
|
+
super
|
241
|
+
end
|
242
|
+
|
243
|
+
# We can handle the method call if we have a match in the `options` context
|
244
|
+
# or if our `target` object responds to the method.
|
245
|
+
#
|
246
|
+
# For example:
|
247
|
+
#
|
248
|
+
# class Model
|
249
|
+
# include Decor
|
250
|
+
#
|
251
|
+
# version "v1" do
|
252
|
+
# def foo
|
253
|
+
# end
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# def baz
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# model = Model.new.for("v1", :bar => true)
|
262
|
+
# model.respond_to?(:foo) #=> true # in version
|
263
|
+
# model.respond_to?(:bar) #=> true # in context
|
264
|
+
# model.respond_to?(:baz) #=> true # on target
|
265
|
+
# model.respond_to?(:quux) #=> false
|
266
|
+
#
|
267
|
+
def respond_to?(method)
|
268
|
+
super or options.key?(method) or target.respond_to?(method)
|
269
|
+
end
|
270
|
+
|
271
|
+
# If `for` is called on an object that is already wrapped by a version,
|
272
|
+
# we return the `target` with a new version wrapper. This allows for an
|
273
|
+
# object to change its version representation at will.
|
274
|
+
#
|
275
|
+
# For example:
|
276
|
+
#
|
277
|
+
# class Model
|
278
|
+
# include Decor
|
279
|
+
#
|
280
|
+
# version "v1" do
|
281
|
+
# def original?
|
282
|
+
# true
|
283
|
+
# end
|
284
|
+
# end
|
285
|
+
#
|
286
|
+
# version "v2" do
|
287
|
+
# def original?
|
288
|
+
# false
|
289
|
+
# end
|
290
|
+
# end
|
291
|
+
# end
|
292
|
+
#
|
293
|
+
# model = Model.new.for("v1")
|
294
|
+
# model.original? #=> true
|
295
|
+
#
|
296
|
+
# model = model.for("v2")
|
297
|
+
# model.original? #=> false
|
298
|
+
#
|
299
|
+
def for(*args)
|
300
|
+
target.for(*args)
|
301
|
+
end
|
302
|
+
|
303
|
+
# This provides additional context to the versions specified. Useful for
|
304
|
+
# making external resources accessible, overriding accessors, etc.
|
305
|
+
#
|
306
|
+
# Ensures `options` is an empty Hash even when not set.
|
307
|
+
#
|
308
|
+
def options
|
309
|
+
super or begin
|
310
|
+
self.options = {}
|
311
|
+
redo
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
data/spec/decor_spec.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'models/bare'
|
4
|
+
require 'models/resource'
|
5
|
+
|
6
|
+
describe Decor do
|
7
|
+
|
8
|
+
describe "when included in a class" do
|
9
|
+
|
10
|
+
describe ".version" do
|
11
|
+
it "should be available" do
|
12
|
+
Bare.should_not respond_to(:version)
|
13
|
+
Bare.send(:include, Decor)
|
14
|
+
Bare.should respond_to(:version)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should define a collection of versions" do
|
18
|
+
Bare.versions.should respond_to(:to_hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should allow new versions to be defined with a block" do
|
22
|
+
Bare.should respond_to(:version)
|
23
|
+
Bare.version("v1"){}
|
24
|
+
Bare.versions.key?("v1").should be_true
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should allow new versions to be defined with a module" do
|
28
|
+
v2 = Module.new
|
29
|
+
Bare.version "v2" => v2
|
30
|
+
Bare.versions.key?("v2").should be_true
|
31
|
+
Bare.versions["v2"].should == v2
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should return versions by name when no block or module is given" do
|
35
|
+
v3 = Module.new
|
36
|
+
Bare.version "v3" => v3
|
37
|
+
Bare.version("v3").should == v3
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should turn a block into a module" do
|
41
|
+
Bare.version("v4"){}
|
42
|
+
Bare.version("v4").should be_a(Module)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#for" do
|
47
|
+
subject{ Bare.new }
|
48
|
+
|
49
|
+
it "should decorate the object with the version specified" do
|
50
|
+
subject.for("v1").version.should == "v1"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should target the instance" do
|
54
|
+
subject.for("v1").target.should be_a(Bare)
|
55
|
+
subject.for("v1").target.should == subject
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should decorate with an instance of Decor::Base" do
|
59
|
+
subject.for("v1").should be_a(Decor::Base)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should decorate by including the version module" do
|
63
|
+
subject.for("v1").should be_a(Bare.version("v1"))
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
describe Decor::Base do
|
71
|
+
before{ @version = "v1"; @name = "foo"; @value = 2; @multi = 2; @options = {} }
|
72
|
+
subject{ Resource.new(@name, @value, @multi).for(@version, @options) }
|
73
|
+
|
74
|
+
it "should delegate to the object" do
|
75
|
+
subject.name.should == @name
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should allow version switching" do
|
79
|
+
resource = subject
|
80
|
+
resource.version.should == "v1"
|
81
|
+
resource = resource.for("v2")
|
82
|
+
resource.version.should == "v2"
|
83
|
+
resource.name.should == @name.upcase
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should allow overriding the module for the version" do
|
87
|
+
v2 = Module.new
|
88
|
+
resource = subject.for("v1", :module => v2)
|
89
|
+
resource.should be_a(v2)
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "v1" do
|
93
|
+
before{ @version = "v1" }
|
94
|
+
|
95
|
+
it "should delegate to the target for original values" do
|
96
|
+
subject.name.should == @name
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should allow computed values" do
|
100
|
+
subject.computed.should == @value * @multi
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "v2" do
|
105
|
+
before{ @version = "v2" }
|
106
|
+
|
107
|
+
it "should allow modified versions of methods in the target" do
|
108
|
+
subject.name.should == @name.upcase
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should allow constants in the context of the version" do
|
112
|
+
subject.computed.should == @value * @multi * Resource::Version2::MUTLI
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "v2010-12-08" do
|
118
|
+
before{ @version = "v2010-12-08" }
|
119
|
+
|
120
|
+
it "should keep the name of the alias as the version" do
|
121
|
+
subject.version.should == "v2010-12-08"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should be the same as its alias" do
|
125
|
+
subject.should be_a(subject.target.class.version("v2"))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "v3" do
|
130
|
+
before{ @version = "v3"; @options = {:foo => "bar"} }
|
131
|
+
|
132
|
+
it "should use values in the context hash as methods" do
|
133
|
+
subject.name.should == "%s (%s)" % [@name, @options[:foo]]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
data/spec/models/bare.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
class Resource < Struct.new(:name, :value, :multi)
|
2
|
+
include Decor
|
3
|
+
|
4
|
+
# Module method
|
5
|
+
module V1
|
6
|
+
def computed
|
7
|
+
value * multi
|
8
|
+
end
|
9
|
+
end
|
10
|
+
version "v1" => V1
|
11
|
+
|
12
|
+
# Module method, non-standard
|
13
|
+
module Version2
|
14
|
+
MUTLI = 10
|
15
|
+
def name
|
16
|
+
super.upcase
|
17
|
+
end
|
18
|
+
def computed
|
19
|
+
value * multi * MUTLI
|
20
|
+
end
|
21
|
+
end
|
22
|
+
# mutliple aliases
|
23
|
+
version "v2" => Version2,
|
24
|
+
"v2010-12-08" => Version2
|
25
|
+
|
26
|
+
# Block method
|
27
|
+
version "v3" do
|
28
|
+
# utilizes options hash
|
29
|
+
def name
|
30
|
+
"%s (%s)" % [super, foo]
|
31
|
+
end
|
32
|
+
def computed
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: decor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matt Todd
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-12-13 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: bundler
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
version: 1.0.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rake
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 49
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 8
|
49
|
+
- 7
|
50
|
+
version: 0.8.7
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rspec
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - "="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 11
|
62
|
+
segments:
|
63
|
+
- 2
|
64
|
+
- 1
|
65
|
+
- 0
|
66
|
+
version: 2.1.0
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Provides a simple way to define multiple representations of an object
|
70
|
+
email: chiology@gmail.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files: []
|
76
|
+
|
77
|
+
files:
|
78
|
+
- decor.gemspec
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
81
|
+
- Rakefile
|
82
|
+
- Readme.textile
|
83
|
+
- lib/decor.rb
|
84
|
+
- spec/decor_spec.rb
|
85
|
+
- spec/models/bare.rb
|
86
|
+
- spec/models/resource.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
has_rdoc: true
|
89
|
+
homepage: http://empl.us/decor/
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
hash: 3
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
hash: 3
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
requirements: []
|
116
|
+
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 1.3.7
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: Defines multiple representations of objects
|
122
|
+
test_files:
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
- spec/decor_spec.rb
|
125
|
+
- spec/models/bare.rb
|
126
|
+
- spec/models/resource.rb
|