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.
@@ -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