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.
Files changed (47) hide show
  1. data/.gitignore +3 -2
  2. data/Gemfile +8 -0
  3. data/MIT-LICENSE +2 -1
  4. data/README.markdown +39 -271
  5. data/Rakefile +22 -14
  6. data/lib/generators/machinist/install/USAGE +2 -0
  7. data/lib/generators/machinist/install/install_generator.rb +48 -0
  8. data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
  9. data/lib/generators/machinist/install/templates/machinist.rb.erb +10 -0
  10. data/lib/generators/machinist/model/model_generator.rb +13 -0
  11. data/lib/machinist.rb +11 -105
  12. data/lib/machinist/active_record.rb +8 -93
  13. data/lib/machinist/active_record/blueprint.rb +41 -0
  14. data/lib/machinist/active_record/lathe.rb +24 -0
  15. data/lib/machinist/blueprint.rb +89 -0
  16. data/lib/machinist/exceptions.rb +32 -0
  17. data/lib/machinist/lathe.rb +69 -0
  18. data/lib/machinist/machinable.rb +97 -0
  19. data/lib/machinist/shop.rb +52 -0
  20. data/lib/machinist/warehouse.rb +36 -0
  21. data/spec/active_record_spec.rb +100 -169
  22. data/spec/blueprint_spec.rb +74 -0
  23. data/spec/exceptions_spec.rb +20 -0
  24. data/spec/inheritance_spec.rb +104 -0
  25. data/spec/machinable_spec.rb +101 -0
  26. data/spec/shop_spec.rb +94 -0
  27. data/spec/spec_helper.rb +4 -6
  28. data/spec/support/active_record_environment.rb +65 -0
  29. data/spec/warehouse_spec.rb +24 -0
  30. metadata +52 -40
  31. data/.autotest +0 -7
  32. data/FAQ.markdown +0 -18
  33. data/VERSION +0 -1
  34. data/init.rb +0 -2
  35. data/lib/machinist/blueprints.rb +0 -25
  36. data/lib/machinist/data_mapper.rb +0 -83
  37. data/lib/machinist/object.rb +0 -30
  38. data/lib/machinist/sequel.rb +0 -62
  39. data/lib/sham.rb +0 -77
  40. data/machinist.gemspec +0 -72
  41. data/spec/data_mapper_spec.rb +0 -134
  42. data/spec/db/.gitignore +0 -1
  43. data/spec/db/schema.rb +0 -20
  44. data/spec/log/.gitignore +0 -1
  45. data/spec/machinist_spec.rb +0 -190
  46. data/spec/sequel_spec.rb +0 -146
  47. 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
@@ -1,194 +1,125 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
- require 'machinist/active_record'
3
- require 'active_support/whiny_nil'
2
+ require 'support/active_record_environment'
4
3
 
5
- module MachinistActiveRecordSpecs
6
-
7
- class Person < ActiveRecord::Base
8
- attr_protected :password
9
- end
4
+ describe Machinist::ActiveRecord do
5
+ include ActiveRecordEnvironment
10
6
 
11
- class Admin < Person
7
+ before(:each) do
8
+ Machinist::Shop.instance.reset!
9
+ empty_database!
12
10
  end
13
11
 
14
- class Post < ActiveRecord::Base
15
- has_many :comments
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
- class Comment < ActiveRecord::Base
19
- belongs_to :post
20
- belongs_to :author, :class_name => "Person"
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
- describe Machinist, "ActiveRecord adapter" do
24
- before(:suite) do
25
- ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/log/test.log")
26
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
27
- load(File.dirname(__FILE__) + "/db/schema.rb")
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
- before(:each) do
31
- [Person, Admin, Post, Comment].each(&:clear_blueprints!)
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
- it "should save the constructed object" do
44
- Person.blueprint { }
45
- person = Person.make
46
- person.should_not be_new_record
47
- end
48
-
49
- it "should create an object through belongs_to association" do
50
- Post.blueprint { }
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
- it "should create an object through belongs_to association using a named blueprint" do
62
- Post.blueprint { }
63
- Post.blueprint(:dummy) { title 'Dummy Post' }
64
- Comment.blueprint { post(:dummy) }
65
- Comment.make.post.title.should == 'Dummy Post'
66
- end
67
-
68
- it "should allow creating an object through a has_many association" do
69
- Post.blueprint do
70
- comments { [Comment.make] }
71
- end
72
- Comment.blueprint { }
73
- Post.make.comments.should have(1).instance_of(Comment)
74
- end
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
- describe "on a has_many association" do
101
- before do
102
- Post.blueprint { }
103
- Comment.blueprint { post }
104
- @post = Post.make
105
- @comment = @post.comments.make
106
- end
107
-
108
- it "should save the created object" do
109
- @comment.should_not be_new_record
110
- end
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
- describe "plan method" do
119
- it "should not save the constructed object" do
120
- person_count = Person.count
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
- it "should create an object through a belongs_to association, and return its id" do
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
- describe "on a belongs_to association" do
137
- it "should allow explicitly setting the association to nil" do
138
- Comment.blueprint { post }
139
- Comment.blueprint(:no_post) { post { nil } }
140
- lambda {
141
- @comment = Comment.plan(:no_post)
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
- describe "make_unsaved method" do
170
- it "should not save the constructed object" do
171
- Person.blueprint { }
172
- person = Person.make_unsaved
173
- person.should be_new_record
174
- end
175
-
176
- it "should not save associated objects" do
177
- Post.blueprint { }
178
- Comment.blueprint { post }
179
- comment = Comment.make_unsaved
180
- comment.post.should be_new_record
181
- end
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