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 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
+