workbench 0.1 → 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|