workbench 0.1
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/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
|
+
|