machinist 1.0.6 → 2.0.0.beta1
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/.gitignore +3 -2
- data/Gemfile +8 -0
- data/MIT-LICENSE +2 -1
- data/README.markdown +39 -271
- data/Rakefile +22 -14
- data/lib/generators/machinist/install/USAGE +2 -0
- data/lib/generators/machinist/install/install_generator.rb +48 -0
- data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
- data/lib/generators/machinist/install/templates/machinist.rb.erb +10 -0
- data/lib/generators/machinist/model/model_generator.rb +13 -0
- data/lib/machinist.rb +11 -105
- data/lib/machinist/active_record.rb +8 -93
- data/lib/machinist/active_record/blueprint.rb +41 -0
- data/lib/machinist/active_record/lathe.rb +24 -0
- data/lib/machinist/blueprint.rb +89 -0
- data/lib/machinist/exceptions.rb +32 -0
- data/lib/machinist/lathe.rb +69 -0
- data/lib/machinist/machinable.rb +97 -0
- data/lib/machinist/shop.rb +52 -0
- data/lib/machinist/warehouse.rb +36 -0
- data/spec/active_record_spec.rb +100 -169
- data/spec/blueprint_spec.rb +74 -0
- data/spec/exceptions_spec.rb +20 -0
- data/spec/inheritance_spec.rb +104 -0
- data/spec/machinable_spec.rb +101 -0
- data/spec/shop_spec.rb +94 -0
- data/spec/spec_helper.rb +4 -6
- data/spec/support/active_record_environment.rb +65 -0
- data/spec/warehouse_spec.rb +24 -0
- metadata +52 -40
- data/.autotest +0 -7
- data/FAQ.markdown +0 -18
- data/VERSION +0 -1
- data/init.rb +0 -2
- data/lib/machinist/blueprints.rb +0 -25
- data/lib/machinist/data_mapper.rb +0 -83
- data/lib/machinist/object.rb +0 -30
- data/lib/machinist/sequel.rb +0 -62
- data/lib/sham.rb +0 -77
- data/machinist.gemspec +0 -72
- data/spec/data_mapper_spec.rb +0 -134
- data/spec/db/.gitignore +0 -1
- data/spec/db/schema.rb +0 -20
- data/spec/log/.gitignore +0 -1
- data/spec/machinist_spec.rb +0 -190
- data/spec/sequel_spec.rb +0 -146
- data/spec/sham_spec.rb +0 -95
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module Machinist
|
4
|
+
|
5
|
+
# When you make an object, the blueprint for that object is instance-evaled
|
6
|
+
# against a Lathe.
|
7
|
+
#
|
8
|
+
# The Lathe implements all the methods that are available to the blueprint,
|
9
|
+
# including method_missing to let the blueprint define attributes.
|
10
|
+
class Lathe
|
11
|
+
|
12
|
+
def initialize(klass, serial_number, attributes = {})
|
13
|
+
@klass = klass
|
14
|
+
@serial_number = serial_number
|
15
|
+
@assigned_attributes = {}
|
16
|
+
|
17
|
+
@object = @klass.new
|
18
|
+
attributes.each {|key, value| assign_attribute(key, value) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a unique serial number for the object under construction.
|
22
|
+
attr_reader :serial_number
|
23
|
+
alias_method :sn, :serial_number
|
24
|
+
|
25
|
+
# Returns the object under construction.
|
26
|
+
attr_reader :object
|
27
|
+
|
28
|
+
def method_missing(attribute, *args, &block) #:nodoc:
|
29
|
+
unless attribute_assigned?(attribute)
|
30
|
+
assign_attribute(attribute, make_attribute(attribute, args, &block))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Undef a couple of methods that are common ActiveRecord attributes.
|
35
|
+
# (Both of these are deprecated in Ruby 1.8 anyway.)
|
36
|
+
undef_method :id if respond_to?(:id)
|
37
|
+
undef_method :type if respond_to?(:type)
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def make_attribute(attribute, args, &block) #:nodoc:
|
42
|
+
count = args.shift if args.first.is_a?(Fixnum)
|
43
|
+
if count
|
44
|
+
Array.new(count) { make_one_value(attribute, args, &block) }
|
45
|
+
else
|
46
|
+
make_one_value(attribute, args, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def make_one_value(attribute, args) #:nodoc:
|
51
|
+
raise_argument_error(attribute) unless args.empty?
|
52
|
+
yield
|
53
|
+
end
|
54
|
+
|
55
|
+
def assign_attribute(key, value) #:nodoc:
|
56
|
+
@assigned_attributes[key.to_sym] = value
|
57
|
+
@object.send("#{key}=", value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def attribute_assigned?(key) #:nodoc:
|
61
|
+
@assigned_attributes.has_key?(key.to_sym)
|
62
|
+
end
|
63
|
+
|
64
|
+
def raise_argument_error(attribute) #:nodoc:
|
65
|
+
raise ArgumentError.new("Invalid arguments to attribute #{attribute} in blueprint")
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Machinist
|
2
|
+
|
3
|
+
# Extend classes with this module to define the blueprint and make methods.
|
4
|
+
module Machinable
|
5
|
+
# Define a blueprint with the given name for this class.
|
6
|
+
#
|
7
|
+
# e.g.
|
8
|
+
# Post.blueprint do
|
9
|
+
# title { "A Post" }
|
10
|
+
# body { "Lorem ipsum..." }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# If you provide the +name+ argument, a named blueprint will be created.
|
14
|
+
# See the +blueprint_name+ argument to the make method.
|
15
|
+
def blueprint(name = :master, &block)
|
16
|
+
@blueprints ||= {}
|
17
|
+
if block_given?
|
18
|
+
parent = (name == :master ? superclass : self) # Where should we look for the parent blueprint?
|
19
|
+
@blueprints[name] = blueprint_class.new(self, :parent => parent, &block)
|
20
|
+
end
|
21
|
+
@blueprints[name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Construct an object from a blueprint.
|
25
|
+
#
|
26
|
+
# :call-seq:
|
27
|
+
# make([count], [blueprint_name], [attributes = {}])
|
28
|
+
#
|
29
|
+
# [+count+]
|
30
|
+
# The number of objects to construct. If +count+ is provided, make
|
31
|
+
# returns an array of objects rather than a single object.
|
32
|
+
# [+blueprint_name+]
|
33
|
+
# Construct the object from the named blueprint, rather than the master
|
34
|
+
# blueprint.
|
35
|
+
# [+attributes+]
|
36
|
+
# Override the attributes from the blueprint with values from this hash.
|
37
|
+
def make(*args)
|
38
|
+
decode_args_to_make(*args) do |blueprint, attributes|
|
39
|
+
blueprint.make(attributes)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Construct and save an object from a blueprint, if the class allows saving.
|
44
|
+
#
|
45
|
+
# :call-seq:
|
46
|
+
# make!([count], [blueprint_name], [attributes = {}])
|
47
|
+
#
|
48
|
+
# A cached object will be returned from the shop if possible. See
|
49
|
+
# Machinist::Shop.
|
50
|
+
#
|
51
|
+
# Arguments are the same as for make.
|
52
|
+
def make!(*args)
|
53
|
+
decode_args_to_make(*args) do |blueprint, attributes|
|
54
|
+
Shop.instance.buy(blueprint, attributes)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Remove all blueprints defined on this class.
|
59
|
+
def clear_blueprints!
|
60
|
+
@blueprints = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Classes that include Machinable can override this method if they want to
|
64
|
+
# use a custom blueprint class when constructing blueprints.
|
65
|
+
#
|
66
|
+
# The default is Machinist::Blueprint.
|
67
|
+
def blueprint_class
|
68
|
+
Machinist::Blueprint
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Parses the arguments to make.
|
74
|
+
#
|
75
|
+
# Yields a blueprint and an attributes hash to the block, which should
|
76
|
+
# construct an object from them. The block may be called multiple times to
|
77
|
+
# construct multiple objects.
|
78
|
+
def decode_args_to_make(*args) #:nodoc:
|
79
|
+
shift_arg = lambda {|klass| args.shift if args.first.is_a?(klass) }
|
80
|
+
count = shift_arg[Fixnum]
|
81
|
+
name = shift_arg[Symbol] || :master
|
82
|
+
attributes = shift_arg[Hash] || {}
|
83
|
+
raise ArgumentError.new("Couldn't understand arguments") unless args.empty?
|
84
|
+
|
85
|
+
@blueprints ||= {}
|
86
|
+
blueprint = @blueprints[name]
|
87
|
+
raise NoBlueprintError.new(self, name) unless blueprint
|
88
|
+
|
89
|
+
if count.nil?
|
90
|
+
yield(blueprint, attributes)
|
91
|
+
else
|
92
|
+
Array.new(count) { yield(blueprint, attributes) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Machinist
|
2
|
+
|
3
|
+
# The shop takes care of caching database objects.
|
4
|
+
#
|
5
|
+
# Calling make! on a class requests objects from the shop; you don't
|
6
|
+
# normally access the shop directly.
|
7
|
+
#
|
8
|
+
# Read more about object caching on the
|
9
|
+
# wiki[http://wiki.github.com/notahat/machinist/object-caching].
|
10
|
+
class Shop
|
11
|
+
|
12
|
+
# Return the singleton Shop instance.
|
13
|
+
def self.instance
|
14
|
+
@instance ||= Shop.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize #:nodoc:
|
18
|
+
reset!
|
19
|
+
end
|
20
|
+
|
21
|
+
# Throw out the entire collection of cached objects.
|
22
|
+
def reset!
|
23
|
+
@warehouse = Warehouse.new
|
24
|
+
restock
|
25
|
+
end
|
26
|
+
|
27
|
+
# Restock the shop with all the cached objects we've got.
|
28
|
+
#
|
29
|
+
# This should be called before each test.
|
30
|
+
def restock
|
31
|
+
@back_room = @warehouse.clone
|
32
|
+
end
|
33
|
+
|
34
|
+
# Buy a (possibly cached) object from the shop.
|
35
|
+
#
|
36
|
+
# This is just like constructing an object by calling Blueprint#make!,
|
37
|
+
# but it will return a previously cached object if one is available.
|
38
|
+
def buy(blueprint, attributes = {})
|
39
|
+
raise BlueprintCantSaveError.new(blueprint) unless blueprint.respond_to?(:make!)
|
40
|
+
|
41
|
+
shelf = @back_room[blueprint, attributes]
|
42
|
+
if shelf.empty?
|
43
|
+
object = blueprint.outside_transaction { blueprint.make!(attributes) }
|
44
|
+
@warehouse[blueprint, attributes] << blueprint.box(object)
|
45
|
+
object
|
46
|
+
else
|
47
|
+
blueprint.unbox(shelf.shift)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Machinist
|
2
|
+
|
3
|
+
# A Warehouse is a hash supports lists as keys.
|
4
|
+
#
|
5
|
+
# It's used for storing cached objects created by Machinist::Shop.
|
6
|
+
#
|
7
|
+
# warehouse[1, 2] = "Hello, world!"
|
8
|
+
# warehouse[1, 2] # => "Hello, world!"
|
9
|
+
class Warehouse < Hash
|
10
|
+
|
11
|
+
# Assign a value for the given list of keys.
|
12
|
+
def []=(*keys)
|
13
|
+
value = keys.pop
|
14
|
+
super(keys, value)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return the value for the given list of keys.
|
18
|
+
#
|
19
|
+
# If the list of keys doesn't exist in the hash, this assigns a new empty
|
20
|
+
# array to that list of keys.
|
21
|
+
def [](*keys)
|
22
|
+
self[*keys] = [] if !has_key?(keys)
|
23
|
+
super(keys)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return a new warehouse with the same keys, and dups of all the values.
|
27
|
+
def clone
|
28
|
+
clone = Warehouse.new
|
29
|
+
each_pair do |key, value|
|
30
|
+
clone[*key] = value.dup
|
31
|
+
end
|
32
|
+
clone
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/spec/active_record_spec.rb
CHANGED
@@ -1,194 +1,125 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
-
require '
|
3
|
-
require 'active_support/whiny_nil'
|
2
|
+
require 'support/active_record_environment'
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
class Person < ActiveRecord::Base
|
8
|
-
attr_protected :password
|
9
|
-
end
|
4
|
+
describe Machinist::ActiveRecord do
|
5
|
+
include ActiveRecordEnvironment
|
10
6
|
|
11
|
-
|
7
|
+
before(:each) do
|
8
|
+
Machinist::Shop.instance.reset!
|
9
|
+
empty_database!
|
12
10
|
end
|
13
11
|
|
14
|
-
|
15
|
-
|
12
|
+
def fake_a_test
|
13
|
+
ActiveRecord::Base.transaction do
|
14
|
+
Machinist.reset_before_test
|
15
|
+
yield
|
16
|
+
raise ActiveRecord::Rollback
|
17
|
+
end
|
16
18
|
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
context "make" do
|
21
|
+
it "should return an unsaved object" do
|
22
|
+
Post.blueprint { }
|
23
|
+
post = Post.make
|
24
|
+
post.should be_a(Post)
|
25
|
+
post.should be_new_record
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
context "make!" do
|
30
|
+
it "should make and save objects" do
|
31
|
+
Post.blueprint { }
|
32
|
+
post = Post.make!
|
33
|
+
post.should be_a(Post)
|
34
|
+
post.should_not be_new_record
|
28
35
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
36
|
+
|
37
|
+
it "should raise an exception for an invalid object" do
|
38
|
+
User.blueprint { }
|
39
|
+
lambda {
|
40
|
+
User.make!(:username => "")
|
41
|
+
}.should raise_error(ActiveRecord::RecordInvalid)
|
32
42
|
end
|
33
|
-
|
34
|
-
describe "make method" do
|
35
|
-
it "should support single-table inheritance" do
|
36
|
-
Person.blueprint { }
|
37
|
-
Admin.blueprint { }
|
38
|
-
admin = Admin.make
|
39
|
-
admin.should_not be_new_record
|
40
|
-
admin.type.should == "Admin"
|
41
|
-
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
Comment.blueprint { post }
|
52
|
-
Comment.make.post.class.should == Post
|
53
|
-
end
|
54
|
-
|
55
|
-
it "should create an object through belongs_to association with a class_name attribute" do
|
56
|
-
Person.blueprint { }
|
57
|
-
Comment.blueprint { author }
|
58
|
-
Comment.make.author.class.should == Person
|
59
|
-
end
|
44
|
+
it "should buy objects from the shop" do
|
45
|
+
Post.blueprint { }
|
46
|
+
post_a, post_b = nil, nil
|
47
|
+
fake_a_test { post_a = Post.make! }
|
48
|
+
fake_a_test { post_b = Post.make! }
|
49
|
+
post_a.should == post_b
|
50
|
+
end
|
51
|
+
end
|
60
52
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
it "should allow setting a protected attribute in the blueprint" do
|
77
|
-
Person.blueprint do
|
78
|
-
password "Test"
|
79
|
-
end
|
80
|
-
Person.make.password.should == "Test"
|
81
|
-
end
|
82
|
-
|
83
|
-
it "should allow overriding a protected attribute" do
|
84
|
-
Person.blueprint do
|
85
|
-
password "Test"
|
86
|
-
end
|
87
|
-
Person.make(:password => "New").password.should == "New"
|
88
|
-
end
|
89
|
-
|
90
|
-
it "should allow setting the id attribute in a blueprint" do
|
91
|
-
Person.blueprint { id 12345 }
|
92
|
-
Person.make.id.should == 12345
|
93
|
-
end
|
94
|
-
|
95
|
-
it "should allow setting the type attribute in a blueprint" do
|
96
|
-
Person.blueprint { type "Person" }
|
97
|
-
Person.make.type.should == "Person"
|
98
|
-
end
|
53
|
+
context "associations support" do
|
54
|
+
it "should handle belongs_to associations" do
|
55
|
+
User.blueprint do
|
56
|
+
username { "user_#{sn}" }
|
57
|
+
end
|
58
|
+
Post.blueprint do
|
59
|
+
author
|
60
|
+
end
|
61
|
+
post = Post.make!
|
62
|
+
post.should be_a(Post)
|
63
|
+
post.should_not be_new_record
|
64
|
+
post.author.should be_a(User)
|
65
|
+
post.author.should_not be_new_record
|
66
|
+
end
|
99
67
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
it "should set the parent association on the created object" do
|
113
|
-
@comment.post.should == @post
|
114
|
-
end
|
68
|
+
it "should handle has_many associations" do
|
69
|
+
Post.blueprint do
|
70
|
+
comments(3)
|
71
|
+
end
|
72
|
+
Comment.blueprint { }
|
73
|
+
post = Post.make!
|
74
|
+
post.should be_a(Post)
|
75
|
+
post.should_not be_new_record
|
76
|
+
post.should have(3).comments
|
77
|
+
post.comments.each do |comment|
|
78
|
+
comment.should be_a(Comment)
|
79
|
+
comment.should_not be_new_record
|
115
80
|
end
|
116
81
|
end
|
117
82
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
Person.blueprint { }
|
122
|
-
person = Person.plan
|
123
|
-
Person.count.should == person_count
|
83
|
+
it "should handle habtm associations" do
|
84
|
+
Post.blueprint do
|
85
|
+
tags(3)
|
124
86
|
end
|
125
|
-
|
126
|
-
|
127
|
-
Post.blueprint { }
|
128
|
-
Comment.blueprint { post }
|
129
|
-
post_count = Post.count
|
130
|
-
comment = Comment.plan
|
131
|
-
Post.count.should == post_count + 1
|
132
|
-
comment[:post].should be_nil
|
133
|
-
comment[:post_id].should_not be_nil
|
87
|
+
Tag.blueprint do
|
88
|
+
name { "tag_#{sn}" }
|
134
89
|
end
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
}.should_not raise_error
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
describe "on a has_many association" do
|
147
|
-
before do
|
148
|
-
Post.blueprint { }
|
149
|
-
Comment.blueprint do
|
150
|
-
post
|
151
|
-
body { "Test" }
|
152
|
-
end
|
153
|
-
@post = Post.make
|
154
|
-
@post_count = Post.count
|
155
|
-
@comment = @post.comments.plan
|
156
|
-
end
|
157
|
-
|
158
|
-
it "should not include the parent in the returned hash" do
|
159
|
-
@comment[:post].should be_nil
|
160
|
-
@comment[:post_id].should be_nil
|
161
|
-
end
|
162
|
-
|
163
|
-
it "should not create an extra parent object" do
|
164
|
-
Post.count.should == @post_count
|
165
|
-
end
|
90
|
+
post = Post.make!
|
91
|
+
post.should be_a(Post)
|
92
|
+
post.should_not be_new_record
|
93
|
+
post.should have(3).tags
|
94
|
+
post.tags.each do |tag|
|
95
|
+
tag.should be_a(Tag)
|
96
|
+
tag.should_not be_new_record
|
166
97
|
end
|
167
98
|
end
|
168
99
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
it "should save objects made within a passed-in block" do
|
184
|
-
Post.blueprint { }
|
185
|
-
Comment.blueprint { }
|
186
|
-
comment = nil
|
187
|
-
post = Post.make_unsaved { comment = Comment.make }
|
188
|
-
post.should be_new_record
|
189
|
-
comment.should_not be_new_record
|
190
|
-
end
|
100
|
+
it "should handle overriding associations" do
|
101
|
+
User.blueprint do
|
102
|
+
username { "user_#{sn}" }
|
103
|
+
end
|
104
|
+
Post.blueprint do
|
105
|
+
author { User.make!(:username => "post_author_#{sn}") }
|
106
|
+
end
|
107
|
+
post = Post.make!
|
108
|
+
post.should be_a(Post)
|
109
|
+
post.should_not be_new_record
|
110
|
+
post.author.should be_a(User)
|
111
|
+
post.author.should_not be_new_record
|
112
|
+
post.author.username.should =~ /^post_author_\d+$/
|
191
113
|
end
|
192
|
-
|
193
114
|
end
|
115
|
+
|
116
|
+
context "error handling" do
|
117
|
+
it "should raise an exception for an attribute with no value" do
|
118
|
+
User.blueprint { username }
|
119
|
+
lambda {
|
120
|
+
User.make
|
121
|
+
}.should raise_error(ArgumentError)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
194
125
|
end
|