workbench 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +130 -30
- data/lib/workbench.rb +48 -19
- data/lib/workbench/string_helpers.rb +2 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/workbench/string_helpers_spec.rb +24 -0
- data/spec/workbench_spec.rb +77 -23
- metadata +4 -2
data/README.rdoc
CHANGED
@@ -8,35 +8,32 @@ understand solution without the tangled mess of meta-magic.
|
|
8
8
|
|
9
9
|
Highlights:
|
10
10
|
|
11
|
-
* Does not use method_missing,
|
12
|
-
|
13
|
-
used to generate modules, but it ends there).
|
11
|
+
* Does not use method_missing. Builder modules, in the end, contain no
|
12
|
+
magic (meta-programming only used to generate modules).
|
14
13
|
* No DSL. Builders are defined by declaring ruby methods (can your
|
15
14
|
editor jump DSL declarations in a file?). Easily call other builder
|
16
15
|
methods to achieve inheritance (see example below) or whatever you
|
17
16
|
please.
|
18
|
-
* Operate on actual instances of the objects.
|
19
|
-
|
20
|
-
*
|
21
|
-
|
22
|
-
If your Object implements #save! as method to raise if model not
|
23
|
-
valid, it uses it.
|
17
|
+
* Operate on actual instances of the objects. Wield ruby.
|
18
|
+
* No dependencies (not even ActiveSupport).
|
19
|
+
* Works with any Object that defines .new (with no params) and #save.
|
20
|
+
Will use #valid? and #save! if the methods exist.
|
24
21
|
|
25
22
|
= Requirements
|
26
23
|
|
27
24
|
Builder works with Ruby 1.8.7 and above.
|
28
25
|
|
29
|
-
= Why?
|
26
|
+
= Why another factory system?
|
30
27
|
|
31
|
-
There are a lot of factory frameworks, why one more?
|
32
|
-
|
33
|
-
|
28
|
+
There are a lot of factory frameworks, why one more? I felt I had an
|
29
|
+
idea to maximize simplicity and minimize the experience of the factory
|
30
|
+
system getting in the way by following a few conventions that should
|
31
|
+
cover most use cases, while keeping it robust by getting out of the
|
32
|
+
way.
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
should have no trouble understanding the code, and there is not that
|
39
|
-
much to parse.
|
34
|
+
Workbench is less than 70 lines, and every line counts. Moderately
|
35
|
+
experienced rubyists should have no trouble understanding the code,
|
36
|
+
and there is not that much to parse.
|
40
37
|
|
41
38
|
= Usage
|
42
39
|
|
@@ -72,11 +69,12 @@ include it in their global test config or RSpec config (like so):
|
|
72
69
|
# Activate Workbench for this module. MUST go at the top.
|
73
70
|
extend Workbench
|
74
71
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
72
|
+
# Declare builder for model User (class name is infered by the
|
73
|
+
# method name).
|
74
|
+
#
|
75
|
+
# A builder method may receive between 1 and 3 arguments. (due to
|
76
|
+
# shortcoming in ruby 1.8.7, if you specify any optional
|
77
|
+
# arguments, Workbench will assume it can send all 3 arguments).
|
80
78
|
#
|
81
79
|
# The following methods will be automatically added in this module:
|
82
80
|
#
|
@@ -88,9 +86,9 @@ include it in their global test config or RSpec config (like so):
|
|
88
86
|
# attributes provided to new_user et al will be sent to the User
|
89
87
|
# instance via user#send("#{attribute_name}=", value)
|
90
88
|
|
91
|
-
def user_defaults(u)
|
92
|
-
u.name ||=
|
93
|
-
u.code ||=
|
89
|
+
def user_defaults(u, n)
|
90
|
+
u.name ||= "Name #{n}"
|
91
|
+
u.code ||= "U#{n}"
|
94
92
|
u.role ||= "user"
|
95
93
|
end
|
96
94
|
|
@@ -104,12 +102,14 @@ include it in their global test config or RSpec config (like so):
|
|
104
102
|
# * create_admin_user(attributes)
|
105
103
|
# * find_or_create_admin_user(attributes)
|
106
104
|
#
|
107
|
-
# Caveat: find_or_create_admin_user is not aware of the role, so
|
105
|
+
# Caveat: find_or_create_admin_user is not aware of the role, so
|
106
|
+
# it will return a non-admin user just the same that matches the
|
107
|
+
# attributes you provide.
|
108
108
|
|
109
|
-
|
110
|
-
def admin_user_defaults(u)
|
109
|
+
use_class :User
|
110
|
+
def admin_user_defaults(u, n)
|
111
111
|
u.role ||= "admin"
|
112
|
-
user_defaults(u)
|
112
|
+
user_defaults(u, n)
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
@@ -117,3 +117,103 @@ See:
|
|
117
117
|
|
118
118
|
* Workbench
|
119
119
|
* Workbench#sequence
|
120
|
+
|
121
|
+
= Counters
|
122
|
+
|
123
|
+
Counters are scoped to the class name. In the following example, user
|
124
|
+
and admin_user will count together:
|
125
|
+
|
126
|
+
module Builders
|
127
|
+
extend Workbench
|
128
|
+
def user_defaults(u, n)
|
129
|
+
...
|
130
|
+
end
|
131
|
+
|
132
|
+
use_class :User
|
133
|
+
def admin_user_defaults(u, n)
|
134
|
+
...
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
new_admin_user # n = 1
|
139
|
+
new_user # n = 2
|
140
|
+
new_admin_user # n = 3
|
141
|
+
|
142
|
+
In cases where have two builders that use different classes but the
|
143
|
+
same table and require them to count together, use #count_with:
|
144
|
+
|
145
|
+
module Builders
|
146
|
+
extend Workbench
|
147
|
+
|
148
|
+
count_with :Publication
|
149
|
+
def book_defaults(u, n)
|
150
|
+
...
|
151
|
+
end
|
152
|
+
|
153
|
+
count_with :Publication
|
154
|
+
def article_defaults(u, n)
|
155
|
+
...
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
new_book # n = 1
|
160
|
+
new_publication # n = 2
|
161
|
+
new_book # n = 3
|
162
|
+
|
163
|
+
== Resetting counters
|
164
|
+
|
165
|
+
If you do clear your database for every test, run the following before
|
166
|
+
or after each test is run. (a la config.before(:each)...)
|
167
|
+
|
168
|
+
Workbench.reset_counters!
|
169
|
+
|
170
|
+
Otherwise you may have tests that fail due to an increment getting so
|
171
|
+
big that it breaks a length validation, or something of the sort. A
|
172
|
+
failure that would probably not fail if the test were run
|
173
|
+
individually.
|
174
|
+
|
175
|
+
= A note about false / nil
|
176
|
+
|
177
|
+
Workbench builders contain a lot of conditionals, testing for nil or
|
178
|
+
false to see if a value is populated. This is problematic, naturally,
|
179
|
+
if you need to provide nil or false as an override value.
|
180
|
+
|
181
|
+
IE:
|
182
|
+
|
183
|
+
def user_defaults(u)
|
184
|
+
u.name ||= "Bob"
|
185
|
+
end
|
186
|
+
|
187
|
+
new_user(:name => nil)
|
188
|
+
|
189
|
+
Workbench is opinionated as follows:
|
190
|
+
|
191
|
+
* Only set the bare minimum: If a model accepts false or nil as a
|
192
|
+
valid value, you should consider leaving it false or nil by the
|
193
|
+
builder.
|
194
|
+
|
195
|
+
* When designing data models, Nil / False should represent the
|
196
|
+
neutral, non-exceptional case: IE: prefer User#has_admin_priviledges
|
197
|
+
over User#deny_admin_privileges.
|
198
|
+
|
199
|
+
* You should never provide attributes to new_<model> or create_<model>
|
200
|
+
that produce an invalid object. When testing validation, instantiate
|
201
|
+
a valid instance with new_<model> and then modify it outside of the
|
202
|
+
builder as follows:
|
203
|
+
|
204
|
+
context "validation"
|
205
|
+
it "requires a name field" do
|
206
|
+
u = new_user
|
207
|
+
u.name = nil
|
208
|
+
u.should have(1).errors_on(:name)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
These conventions should work for 99% of scenarios. In the case you
|
213
|
+
find a good reason to override an attribute with nil or false, defined
|
214
|
+
your builder method to receive 3 arguments to receive the overrides
|
215
|
+
hash:
|
216
|
+
|
217
|
+
def user_defaults(u, n, overrides = {})
|
218
|
+
u.name = "Bob" unless overrides.has_key?(:name)
|
219
|
+
end
|
data/lib/workbench.rb
CHANGED
@@ -1,43 +1,72 @@
|
|
1
1
|
require File.expand_path('../workbench/string_helpers', __FILE__)
|
2
2
|
module Workbench
|
3
|
-
|
4
|
-
# Defines a sequence method named next_#{name}.
|
5
|
-
#
|
6
|
-
# [value] - Optional. An incrementable value (any object that responds to #succ, Fixnum, Time, String, Symbol, etc.)
|
7
|
-
# [block] - Optional. A formatter block. Successive values with be passed to block, and sequence method will return result of block.
|
8
|
-
def sequence(name, value = 1, &block)
|
9
|
-
define_method("next_#{name}") do
|
10
|
-
(block ? block.call(value) : value).tap do
|
11
|
-
value = value.succ
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
3
|
+
COUNTERS = Hash.new(0)
|
15
4
|
|
16
5
|
# Declare that the next builder method is to use the said class
|
17
6
|
# (Symbol such as :User or string such as "Models::User" are
|
18
7
|
# acceptable)
|
19
|
-
def
|
20
|
-
@
|
8
|
+
def use_class(name)
|
9
|
+
@next_class = name
|
10
|
+
end
|
11
|
+
|
12
|
+
# Declare that the next builder method is to count scoped to the following class,
|
13
|
+
#
|
14
|
+
# module Builders
|
15
|
+
# extend Workbench
|
16
|
+
#
|
17
|
+
# def book_defaults(u, n)
|
18
|
+
# ...
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# count_with :Book
|
22
|
+
# def article_defaults(u, n)
|
23
|
+
# ...
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# new_book # n = 1
|
28
|
+
# new_publication # n = 2
|
29
|
+
# new_book # n = 3
|
30
|
+
#
|
31
|
+
# expects a Symbol such as :User or string such as "Models::User" that maps to a valid class name.
|
32
|
+
def count_with(name)
|
33
|
+
@next_count_with = name
|
34
|
+
end
|
35
|
+
|
36
|
+
# The counter routine. Provide a key, get back an incrementer.
|
37
|
+
def self.counter(key)
|
38
|
+
COUNTERS[key] += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
# Reset all counters. It doesn't happen automatically, you'll likley
|
42
|
+
# want to call this method before or after each test is run
|
43
|
+
def self.reset_counters!
|
44
|
+
COUNTERS.clear
|
21
45
|
end
|
22
46
|
|
23
47
|
private
|
24
48
|
def method_added(name)
|
25
49
|
if builder_name = inferred_builder_class_name(name)
|
26
|
-
klass
|
27
|
-
|
28
|
-
|
50
|
+
klass = StringHelpers.constantize(@next_class || builder_name)
|
51
|
+
counter_klass = StringHelpers.constantize(@next_count_with || klass)
|
52
|
+
define_builder_methods(builder_name, klass, counter_klass)
|
53
|
+
@next_class, @next_count_with = nil
|
29
54
|
end
|
30
55
|
super
|
31
56
|
end
|
32
57
|
|
33
|
-
def define_builder_methods(name, klass)
|
58
|
+
def define_builder_methods(name, klass, counter_klass)
|
34
59
|
define_method("new_#{name}") do |*args|
|
35
60
|
attributes = args[0] || { }
|
36
61
|
klass.new.tap do |model|
|
37
62
|
attributes.each do |k, v|
|
38
63
|
model.send("#{k}=", v)
|
39
64
|
end
|
40
|
-
|
65
|
+
build_method = method("#{name}_defaults")
|
66
|
+
p = [model]
|
67
|
+
p << Workbench.counter(counter_klass) if build_method.arity >= 2 || build_method.arity <= -1
|
68
|
+
p << attributes if build_method.arity >= 3 || build_method.arity <= -1
|
69
|
+
build_method.call *p
|
41
70
|
end
|
42
71
|
end
|
43
72
|
|
@@ -3,8 +3,10 @@ module Workbench
|
|
3
3
|
module StringHelpers
|
4
4
|
# takes :User, "user", or :user and returns User
|
5
5
|
def self.constantize(value)
|
6
|
+
return value if value.is_a?(Module)
|
6
7
|
value = value.to_s
|
7
8
|
value = classify(value) unless value =~ /^[A-Z]/
|
9
|
+
value = "::#{value}" unless value =~ /^:/
|
8
10
|
eval(value.to_s)
|
9
11
|
end
|
10
12
|
|
data/spec/spec_helper.rb
CHANGED
@@ -2,7 +2,7 @@ require 'rspec'
|
|
2
2
|
require File.expand_path("./lib/workbench.rb")
|
3
3
|
|
4
4
|
class User
|
5
|
-
attr_accessor :name, :phone
|
5
|
+
attr_accessor :name, :phone, :active
|
6
6
|
|
7
7
|
def save
|
8
8
|
@saved = true
|
@@ -11,6 +11,8 @@ class User
|
|
11
11
|
def new_record?
|
12
12
|
! @saved
|
13
13
|
end
|
14
|
+
|
15
|
+
alias active? active
|
14
16
|
end
|
15
17
|
|
16
18
|
Rspec.configure do
|
@@ -2,6 +2,30 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Workbench
|
4
4
|
describe StringHelpers do
|
5
|
+
describe "#constantize" do
|
6
|
+
it "returns a constant several levels deep, beginning from object" do
|
7
|
+
StringHelpers.constantize("::Workbench::StringHelpers").should == Workbench::StringHelpers
|
8
|
+
end
|
9
|
+
|
10
|
+
it "defaults to root if no preceeding colon is provided" do
|
11
|
+
lambda {
|
12
|
+
StringHelpers.constantize("StringHelpers")
|
13
|
+
}.should raise_error(NameError, /uninitialized constant StringHelpers/)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "automatically 'classifies' underscored class names" do
|
17
|
+
StringHelpers.constantize("workbench/string_helpers").should == Workbench::StringHelpers
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns the provided modules" do
|
21
|
+
StringHelpers.constantize(Workbench).should == Workbench
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns the provided classes " do
|
25
|
+
StringHelpers.constantize(Class).should == Class
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
5
29
|
describe "#classify" do
|
6
30
|
it "transforms 'namespace/model_name' to 'Namespace::ModelName'" do
|
7
31
|
StringHelpers.classify('namespace/model_name').should == 'Namespace::ModelName'
|
data/spec/workbench_spec.rb
CHANGED
@@ -10,7 +10,11 @@ describe Workbench do
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
before(:each) do
|
14
|
+
Workbench.reset_counters!
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "defining builders" do
|
14
18
|
it "creates a module with create_, new_, and find_or_create_" do
|
15
19
|
builder_methods = Module.new do
|
16
20
|
extend Workbench
|
@@ -26,61 +30,111 @@ describe Workbench do
|
|
26
30
|
methods.should include(:create_user)
|
27
31
|
methods.should include(:find_or_create_user)
|
28
32
|
end
|
29
|
-
end
|
30
33
|
|
31
|
-
|
32
|
-
it "over-rides the class to use for the next defined builder" do
|
34
|
+
it "passes the next counter value to the builder method when arity = 2" do
|
33
35
|
builder_methods = Module.new do
|
34
36
|
extend Workbench
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
u.name = "Bill"
|
39
|
-
u.phone = "999-999-9999"
|
38
|
+
def user_defaults(u, n)
|
39
|
+
u.name ||= "Bill #{n}"
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
extend builder_methods
|
44
|
-
|
44
|
+
|
45
|
+
new_user.name.should == "Bill 1"
|
46
|
+
new_user.name.should == "Bill 2"
|
45
47
|
end
|
46
48
|
|
47
|
-
|
49
|
+
it "passes the overrides hash when arity = 3" do
|
50
|
+
builder_methods = Module.new do
|
51
|
+
extend Workbench
|
52
|
+
|
53
|
+
def user_defaults(u, n, overrides)
|
54
|
+
u.active = true unless overrides.has_key?(:active)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
extend builder_methods
|
59
|
+
new_user(:active => false).should_not be_active
|
60
|
+
end
|
48
61
|
|
49
|
-
|
50
|
-
it "defines a sequence which, when used, yields an incrementing number and returns the value from the block" do
|
62
|
+
it "passes the overrides hash if arity is < -1 (unfortunately, the value when a splat or default value used)" do
|
51
63
|
builder_methods = Module.new do
|
52
64
|
extend Workbench
|
53
65
|
|
54
|
-
|
66
|
+
def user_defaults(u, n = -1, overrides = { })
|
67
|
+
u.active = true unless overrides.has_key?(:active)
|
68
|
+
end
|
55
69
|
end
|
56
70
|
|
57
71
|
extend builder_methods
|
58
|
-
|
59
|
-
next_name.should == "Name 2"
|
72
|
+
new_user(:active => false).should_not be_active
|
60
73
|
end
|
74
|
+
end
|
61
75
|
|
62
|
-
|
76
|
+
describe "counting" do
|
77
|
+
it "scopes counting to builders with the same class" do
|
63
78
|
builder_methods = Module.new do
|
64
79
|
extend Workbench
|
65
80
|
|
66
|
-
|
81
|
+
def user_defaults(u, n)
|
82
|
+
u.name ||= "User #{n}"
|
83
|
+
end
|
84
|
+
|
85
|
+
use_class :user
|
86
|
+
def admin_user_defaults(u, n)
|
87
|
+
u.name ||= "Admin #{n}"
|
88
|
+
user_defaults(u, n)
|
89
|
+
end
|
67
90
|
end
|
68
91
|
|
69
92
|
extend builder_methods
|
70
|
-
|
71
|
-
|
93
|
+
new_user .name.should == "User 1"
|
94
|
+
new_admin_user .name.should == "Admin 2"
|
95
|
+
new_user .name.should == "User 3"
|
72
96
|
end
|
73
97
|
|
74
|
-
|
98
|
+
end
|
99
|
+
|
100
|
+
describe ".count_with" do
|
101
|
+
it "causes the counter to count with the provided model" do
|
102
|
+
admin_user = Class.new(User)
|
75
103
|
builder_methods = Module.new do
|
76
104
|
extend Workbench
|
77
105
|
|
78
|
-
|
106
|
+
def user_defaults(u, n)
|
107
|
+
u.name ||= "User #{n}"
|
108
|
+
end
|
109
|
+
|
110
|
+
use_class admin_user
|
111
|
+
count_with :user
|
112
|
+
def admin_user_defaults(u, n)
|
113
|
+
u.name ||= "Admin #{n}"
|
114
|
+
user_defaults(u, n)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
extend builder_methods
|
118
|
+
new_user .name.should == "User 1"
|
119
|
+
new_admin_user .name.should == "Admin 2"
|
120
|
+
new_user .name.should == "User 3"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe ".use_class" do
|
125
|
+
it "over-rides the class to use for the next defined builder" do
|
126
|
+
builder_methods = Module.new do
|
127
|
+
extend Workbench
|
128
|
+
|
129
|
+
use_class :User
|
130
|
+
def admin_user_defaults(u)
|
131
|
+
u.name = "Bill"
|
132
|
+
u.phone = "999-999-9999"
|
133
|
+
end
|
79
134
|
end
|
80
135
|
|
81
136
|
extend builder_methods
|
82
|
-
|
83
|
-
next_code.should == "AAB"
|
137
|
+
new_admin_user.class.should == User
|
84
138
|
end
|
85
139
|
|
86
140
|
end
|
metadata
CHANGED
@@ -2,15 +2,17 @@
|
|
2
2
|
name: workbench
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: "0.
|
5
|
+
version: "0.2"
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Tim Harper
|
9
|
+
- Eric Wollesen
|
10
|
+
- Ben Mabey
|
9
11
|
autorequire:
|
10
12
|
bindir: bin
|
11
13
|
cert_chain: []
|
12
14
|
|
13
|
-
date: 2011-08-
|
15
|
+
date: 2011-08-19 00:00:00 -06:00
|
14
16
|
default_executable:
|
15
17
|
dependencies:
|
16
18
|
- !ruby/object:Gem::Dependency
|