machinist 1.0.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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