workbench 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require File.expand_path("./lib/workbench.rb")
3
+
4
+ class User
5
+ attr_accessor :name, :phone
6
+
7
+ def save
8
+ @saved = true
9
+ end
10
+
11
+ def new_record?
12
+ ! @saved
13
+ end
14
+ end
15
+
16
+ Rspec.configure do
17
+ end
@@ -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
+