workbench 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/MIT_LICENSE +20 -0
- data/README.rdoc +119 -0
- data/lib/workbench.rb +66 -0
- data/lib/workbench/string_helpers.rb +16 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/workbench/string_helpers_spec.rb +11 -0
- data/spec/workbench_spec.rb +126 -0
- metadata +100 -0
data/Gemfile
ADDED
data/MIT_LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 LeadTune
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
= About
|
2
|
+
|
3
|
+
Workbench is tiny, lean, mean, intuitive factory system that exploits
|
4
|
+
some lesser known features of ruby to deliver a robust, easy to
|
5
|
+
understand solution without the tangled mess of meta-magic.
|
6
|
+
|
7
|
+
(disclaimer: some meta-magic involved, but it is minimal)
|
8
|
+
|
9
|
+
Highlights:
|
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).
|
14
|
+
* No DSL. Builders are defined by declaring ruby methods (can your
|
15
|
+
editor jump DSL declarations in a file?). Easily call other builder
|
16
|
+
methods to achieve inheritance (see example below) or whatever you
|
17
|
+
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.
|
24
|
+
|
25
|
+
= Requirements
|
26
|
+
|
27
|
+
Builder works with Ruby 1.8.7 and above.
|
28
|
+
|
29
|
+
= Why?
|
30
|
+
|
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.
|
34
|
+
|
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.
|
40
|
+
|
41
|
+
= Usage
|
42
|
+
|
43
|
+
Workbench is very simple to use, but does require a small amount of
|
44
|
+
knowledge for othings to be immediately clear. Grok the following:
|
45
|
+
|
46
|
+
Add to your Gemspec:
|
47
|
+
|
48
|
+
gem "workbench"
|
49
|
+
|
50
|
+
Create a module in any place of your choosing:
|
51
|
+
|
52
|
+
<code>spec/support/workbench_builders.rb</code>
|
53
|
+
|
54
|
+
module WorkbenchBuilders
|
55
|
+
extend Workbench
|
56
|
+
|
57
|
+
... your code here. See sample below ...
|
58
|
+
end
|
59
|
+
|
60
|
+
Now, include the module anywhere you want (yes, you can even include
|
61
|
+
it in your ERB views, you sick freak). Most will probably want to
|
62
|
+
include it in their global test config or RSpec config (like so):
|
63
|
+
|
64
|
+
RSpec.configure do |config|
|
65
|
+
include WorkbenchBuilders
|
66
|
+
...
|
67
|
+
end
|
68
|
+
|
69
|
+
= Sample Builders
|
70
|
+
|
71
|
+
module WorkbenchBuilders
|
72
|
+
# Activate Workbench for this module. MUST go at the top.
|
73
|
+
extend Workbench
|
74
|
+
|
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).
|
80
|
+
#
|
81
|
+
# The following methods will be automatically added in this module:
|
82
|
+
#
|
83
|
+
# * new_user(attributes)
|
84
|
+
# * create_user(attributes)
|
85
|
+
# * find_or_create_user(attributes)
|
86
|
+
#
|
87
|
+
# user_defaults will be called with a new instance of User. Any
|
88
|
+
# attributes provided to new_user et al will be sent to the User
|
89
|
+
# instance via user#send("#{attribute_name}=", value)
|
90
|
+
|
91
|
+
def user_defaults(u)
|
92
|
+
u.name ||= next_name
|
93
|
+
u.code ||= next_code
|
94
|
+
u.role ||= "user"
|
95
|
+
end
|
96
|
+
|
97
|
+
# An admin user. Since we intend to build a User and not an
|
98
|
+
# AdminUser (the would-be inferred class name), we need to declare
|
99
|
+
# the class name just before the builder method definition.
|
100
|
+
#
|
101
|
+
# The following methods will be automatically added in this module:
|
102
|
+
#
|
103
|
+
# * new_admin_user(attributes)
|
104
|
+
# * create_admin_user(attributes)
|
105
|
+
# * find_or_create_admin_user(attributes)
|
106
|
+
#
|
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.
|
108
|
+
|
109
|
+
class_name :User
|
110
|
+
def admin_user_defaults(u)
|
111
|
+
u.role ||= "admin"
|
112
|
+
user_defaults(u)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
See:
|
117
|
+
|
118
|
+
* Workbench
|
119
|
+
* Workbench#sequence
|
data/lib/workbench.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.expand_path('../workbench/string_helpers', __FILE__)
|
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
|
15
|
+
|
16
|
+
# Declare that the next builder method is to use the said class
|
17
|
+
# (Symbol such as :User or string such as "Models::User" are
|
18
|
+
# acceptable)
|
19
|
+
def class_name(name)
|
20
|
+
@next_class_name = name
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def method_added(name)
|
25
|
+
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
|
29
|
+
end
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_builder_methods(name, klass)
|
34
|
+
define_method("new_#{name}") do |*args|
|
35
|
+
attributes = args[0] || { }
|
36
|
+
klass.new.tap do |model|
|
37
|
+
attributes.each do |k, v|
|
38
|
+
model.send("#{k}=", v)
|
39
|
+
end
|
40
|
+
send("#{name}_defaults", model)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
define_method("create_#{name}") do |*args|
|
45
|
+
send("new_#{name}", *args).tap do |model|
|
46
|
+
if model.respond_to?(:save!) # save should raise if unsuccessful
|
47
|
+
model.save!
|
48
|
+
else
|
49
|
+
model.save
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
define_method("find_or_create_#{name}") do |*args|
|
55
|
+
attrs = args[0] || { }
|
56
|
+
klass.where(attrs).first || send("create_#{name}", attrs)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def inferred_builder_class_name(name)
|
61
|
+
if name.to_s.match(/^(.+)_defaults$/)
|
62
|
+
$1.to_sym
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Workbench
|
2
|
+
# A collection of needed String methods. They are here to remove a dependency on ActiveSupport. You can use them if you want... but you probably should use ActiveSupport or copy them into your project.
|
3
|
+
module StringHelpers
|
4
|
+
# takes :User, "user", or :user and returns User
|
5
|
+
def self.constantize(value)
|
6
|
+
value = value.to_s
|
7
|
+
value = classify(value) unless value =~ /^[A-Z]/
|
8
|
+
eval(value.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
# "transforms 'namespace/model_name' to 'Namespace::ModelName'"
|
12
|
+
def self.classify(string)
|
13
|
+
string.to_s.gsub("/", "::").gsub(/(_|\b)(.)/) { |r| r[-1..-1].upcase }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Workbench
|
4
|
+
describe StringHelpers do
|
5
|
+
describe "#classify" do
|
6
|
+
it "transforms 'namespace/model_name' to 'Namespace::ModelName'" do
|
7
|
+
StringHelpers.classify('namespace/model_name').should == 'Namespace::ModelName'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Workbench do
|
4
|
+
USER_BUILDER = Module.new do
|
5
|
+
extend Workbench
|
6
|
+
|
7
|
+
def user_defaults(u)
|
8
|
+
u.name ||= "Bill"
|
9
|
+
u.phone ||= "999-999-9999"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "defining" do
|
14
|
+
it "creates a module with create_, new_, and find_or_create_" do
|
15
|
+
builder_methods = Module.new do
|
16
|
+
extend Workbench
|
17
|
+
|
18
|
+
def user_defaults(u)
|
19
|
+
u.name ||= "Bill"
|
20
|
+
u.phone ||= "999-999-9999"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
methods = builder_methods.instance_methods.map(&:to_sym)
|
25
|
+
methods.should include(:new_user)
|
26
|
+
methods.should include(:create_user)
|
27
|
+
methods.should include(:find_or_create_user)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe ".class_name" do
|
32
|
+
it "over-rides the class to use for the next defined builder" do
|
33
|
+
builder_methods = Module.new do
|
34
|
+
extend Workbench
|
35
|
+
|
36
|
+
class_name :User
|
37
|
+
def admin_user_defaults(u)
|
38
|
+
u.name = "Bill"
|
39
|
+
u.phone = "999-999-9999"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
extend builder_methods
|
44
|
+
new_admin_user.class.should == User
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe ".sequence" do
|
50
|
+
it "defines a sequence which, when used, yields an incrementing number and returns the value from the block" do
|
51
|
+
builder_methods = Module.new do
|
52
|
+
extend Workbench
|
53
|
+
|
54
|
+
sequence(:name) { |n| "Name #{n}" }
|
55
|
+
end
|
56
|
+
|
57
|
+
extend builder_methods
|
58
|
+
next_name.should == "Name 1"
|
59
|
+
next_name.should == "Name 2"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can define a sequence that starts with any value that responds to #succ" do
|
63
|
+
builder_methods = Module.new do
|
64
|
+
extend Workbench
|
65
|
+
|
66
|
+
sequence(:code, "A") { |n| "ABC-#{n}" }
|
67
|
+
end
|
68
|
+
|
69
|
+
extend builder_methods
|
70
|
+
next_code.should == "ABC-A"
|
71
|
+
next_code.should == "ABC-B"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "defines sequences without a proc" do
|
75
|
+
builder_methods = Module.new do
|
76
|
+
extend Workbench
|
77
|
+
|
78
|
+
sequence(:code, "AAA")
|
79
|
+
end
|
80
|
+
|
81
|
+
extend builder_methods
|
82
|
+
next_code.should == "AAA"
|
83
|
+
next_code.should == "AAB"
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "#new_(builder_name)" do
|
89
|
+
before(:each) do
|
90
|
+
extend USER_BUILDER
|
91
|
+
end
|
92
|
+
|
93
|
+
it "initializes a model with .new, calls user_defaults, but doesn't call save" do
|
94
|
+
user = new_user
|
95
|
+
user.name.should == "Bill"
|
96
|
+
user.phone.should == "999-999-9999"
|
97
|
+
user.should be_a_new_record
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'sets attributes passed as an argument by calling Model#attribute=' do
|
101
|
+
user = new_user(:name => "Bob")
|
102
|
+
user.name.should == "Bob"
|
103
|
+
user.phone.should == "999-999-9999"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "#create_(builder_name)" do
|
108
|
+
it "behaves like #new_(builder_name), but calls save at the end" do
|
109
|
+
extend USER_BUILDER
|
110
|
+
user = create_user(:name => "Bob")
|
111
|
+
user.name.should == "Bob"
|
112
|
+
user.phone.should == "999-999-9999"
|
113
|
+
user.should_not be_a_new_record
|
114
|
+
user.name.should == "Bob"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#find_of_create_(builder_name)" do
|
119
|
+
it "calls Model.where(attributes).first. If a model is returned, use that, otherwise, create it" do
|
120
|
+
extend USER_BUILDER
|
121
|
+
bob_user = create_user(:name => "Bob")
|
122
|
+
User.should_receive(:where).with({ :name => "Bob" }).and_return([bob_user])
|
123
|
+
find_or_create_user(:name => "Bob").should == bob_user
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: workbench
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: "0.1"
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tim Harper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-11 00:00:00 -06:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rspec
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "2.6"
|
25
|
+
type: :development
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-debug19
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
description: |
|
39
|
+
There are a lot of factory frameworks, why one more? In my experience
|
40
|
+
the otherones use fancy DSLs that were intended to increase
|
41
|
+
readability but instead increased confusion.
|
42
|
+
|
43
|
+
Workbench doesn't use a fancy DSL. It uses ruby. It doesn't use any
|
44
|
+
fancy tricks like providing proxy objects for modification, workbench
|
45
|
+
builders operate on the actual model. Since it uses very little
|
46
|
+
tricks, it is also ORM agnostic. If your models respond to '.new' and
|
47
|
+
you set attributes by calling #attribute=, then Workbench will work
|
48
|
+
with your model.
|
49
|
+
|
50
|
+
I have a hard time understanding why all the complexity around factory
|
51
|
+
builders has grown, but often I look at the resulting DSL and say
|
52
|
+
"That's more work than just defining a method and calling Model.new!"
|
53
|
+
|
54
|
+
email:
|
55
|
+
- tim@leadtune.com
|
56
|
+
executables: []
|
57
|
+
|
58
|
+
extensions: []
|
59
|
+
|
60
|
+
extra_rdoc_files: []
|
61
|
+
|
62
|
+
files:
|
63
|
+
- spec/spec_helper.rb
|
64
|
+
- spec/workbench/string_helpers_spec.rb
|
65
|
+
- spec/workbench_spec.rb
|
66
|
+
- lib/workbench/string_helpers.rb
|
67
|
+
- lib/workbench.rb
|
68
|
+
- MIT_LICENSE
|
69
|
+
- README.rdoc
|
70
|
+
- Gemfile
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: http://github.com/leadtune/workbench
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 1.3.6
|
92
|
+
requirements: []
|
93
|
+
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.6.2
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: A small, simple ORM agnostic factory library that does the job without any crazy tricks.
|
99
|
+
test_files: []
|
100
|
+
|