workbench 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, Module.included, Module.extended magic.
12
- Builder modules, in the end, contain no magic (some meta-programming
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. An abstraction layer is
19
- unnecessary. If your Object supports it, you can do it. Wield ruby.
20
- * No dependencies (not even ActiveSupport). If your Object defines
21
- .new (with no params), #save, and #valid? then Workbench can use it.
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? In my experience
32
- the other ones use fancy DSLs that were intended to increase
33
- readability but instead increased confusion and get in the way.
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
- Then the code base. Some have hundreds of lines (for a framework to
36
- help you create instances of your objects? Really?). Workbench is less
37
- than 70 lines, and every line counts. Moderately experienced rubyists
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
- # Will define #next_name and #next_code. See Workbench#sequence rdoc for more info.
76
- sequence(:name) { |n| "Name #{n}" }
77
- sequence(:code, "AAA")
78
-
79
- # Declare builder for model User (class name is infered by the method name).
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 ||= next_name
93
- u.code ||= next_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 it will return a non-admin user just the same that matches the attributes you provide.
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
- class_name :User
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
@@ -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 class_name(name)
20
- @next_class_name = name
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 = StringHelpers.constantize(@next_class_name || builder_name)
27
- define_builder_methods(builder_name, klass)
28
- @next_class_name = nil
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
- send("#{name}_defaults", model)
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
 
@@ -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'
@@ -10,7 +10,11 @@ describe Workbench do
10
10
  end
11
11
  end
12
12
 
13
- describe "defining" do
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
- describe ".class_name" do
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
- class_name :User
37
- def admin_user_defaults(u)
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
- new_admin_user.class.should == User
44
+
45
+ new_user.name.should == "Bill 1"
46
+ new_user.name.should == "Bill 2"
45
47
  end
46
48
 
47
- end
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
- describe ".sequence" do
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
- sequence(:name) { |n| "Name #{n}" }
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
- next_name.should == "Name 1"
59
- next_name.should == "Name 2"
72
+ new_user(:active => false).should_not be_active
60
73
  end
74
+ end
61
75
 
62
- it "can define a sequence that starts with any value that responds to #succ" do
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
- sequence(:code, "A") { |n| "ABC-#{n}" }
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
- next_code.should == "ABC-A"
71
- next_code.should == "ABC-B"
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
- it "defines sequences without a proc" do
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
- sequence(:code, "AAA")
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
- next_code.should == "AAA"
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.1"
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-11 00:00:00 -06:00
15
+ date: 2011-08-19 00:00:00 -06:00
14
16
  default_executable:
15
17
  dependencies:
16
18
  - !ruby/object:Gem::Dependency