manufactory 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,90 @@
1
+ require 'manufactory'
2
+ require 'manufactory/blueprints'
3
+ require 'active_record'
4
+
5
+ module Manufactory
6
+ class ActiveRecordAdapter
7
+ def self.has_association?(object, attribute)
8
+ object.class.reflect_on_association(attribute)
9
+ end
10
+
11
+ def self.class_for_association(object, attribute)
12
+ association = object.class.reflect_on_association(attribute)
13
+ association && association.klass
14
+ end
15
+
16
+ # This method takes care of converting any associated objects,
17
+ # in the hash returned by Lathe#assigned_attributes, into their
18
+ # object ids.
19
+ #
20
+ # For example, let's say we have blueprints like this:
21
+ #
22
+ # Post.blueprint { }
23
+ # Comment.blueprint { post }
24
+ #
25
+ # Lathe#assigned_attributes will return { :post => ... }, but
26
+ # we want to pass { :post_id => 1 } to a controller.
27
+ #
28
+ # This method takes care of cleaning this up.
29
+ def self.assigned_attributes_without_associations(lathe)
30
+ attributes = {}
31
+ lathe.assigned_attributes.each_pair do |attribute, value|
32
+ association = lathe.object.class.reflect_on_association(attribute)
33
+ if association && association.macro == :belongs_to && !value.nil?
34
+ attributes[association.primary_key_name.to_sym] = value.id
35
+ else
36
+ attributes[attribute] = value
37
+ end
38
+ end
39
+ attributes
40
+ end
41
+ end
42
+
43
+ module ActiveRecordExtensions
44
+ def make(*args, &block)
45
+ lathe = Lathe.run(Manufactory::ActiveRecordAdapter, self.new, *args)
46
+ unless Manufactory.nerfed?
47
+ lathe.object.save!
48
+ lathe.object.reload
49
+ end
50
+ lathe.object(&block)
51
+ end
52
+
53
+ def make_unsaved(*args)
54
+ object = Manufactory.with_save_nerfed { make(*args) }
55
+ yield object if block_given?
56
+ object
57
+ end
58
+
59
+ def plan(*args)
60
+ lathe = Lathe.run(Manufactory::ActiveRecordAdapter, self.new, *args)
61
+ Manufactory::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
62
+ end
63
+ end
64
+
65
+ module ActiveRecordHasManyExtensions
66
+ def make(*args, &block)
67
+ lathe = Lathe.run(Manufactory::ActiveRecordAdapter, self.build, *args)
68
+ unless Manufactory.nerfed?
69
+ lathe.object.save!
70
+ lathe.object.reload
71
+ end
72
+ lathe.object(&block)
73
+ end
74
+
75
+ def plan(*args)
76
+ lathe = Lathe.run(Manufactory::ActiveRecordAdapter, self.build, *args)
77
+ Manufactory::ActiveRecordAdapter.assigned_attributes_without_associations(lathe)
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ class ActiveRecord::Base
84
+ extend Manufactory::Blueprints
85
+ extend Manufactory::ActiveRecordExtensions
86
+ end
87
+
88
+ class ActiveRecord::Associations::HasManyAssociation
89
+ include Manufactory::ActiveRecordHasManyExtensions
90
+ end
@@ -1,3 +1,92 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "manufactory"
4
+ require "dm-core"
5
+ require "dm-validations"
6
+
7
+ module Manufactory
8
+ class DataMapperAdapter < GenericModelAdapter
9
+ def has_association?(object, attribute)
10
+ object.class.relationships.has_key?(attribute)
11
+ end
12
+
13
+ def class_for_association(object, attribute)
14
+ association = object.class.relationships[attribute]
15
+ association && association.parent_model
16
+ end
17
+
18
+ def association_is_many_to_one?(association)
19
+ if defined?(DataMapper::Associations::ManyToOne::Relationship)
20
+ # We're using the next branch of DM
21
+ association.class == DataMapper::Associations::ManyToOne::Relationship
22
+ else
23
+ # We're using the 0.9 or less branch.
24
+ association.options[:max].nil?
25
+ end
26
+ end
27
+
28
+ # This method takes care of converting any associated objects,
29
+ # in the hash returned by Lathe#assigned_attributes, into their
30
+ # object ids.
31
+ #
32
+ # For example, let's say we have blueprints like this:
33
+ #
34
+ # Post.blueprint { }
35
+ # Comment.blueprint { post }
36
+ #
37
+ # Lathe#assigned_attributes will return { :post => ... }, but
38
+ # we want to pass { :post_id => 1 } to a controller.
39
+ #
40
+ # This method takes care of cleaning this up.
41
+ def assigned_attributes_without_associations(dsl)
42
+ attributes = {}
43
+ dsl.assigned_attributes.each_pair do |attribute, value|
44
+ association = dsl.object.class.relationships[attribute]
45
+ if association && association_is_many_to_one?(association)
46
+ # DataMapper child_key can have more than one property, but I'm not
47
+ # sure in what circumstances this would be the case. I'm assuming
48
+ # here that there's only one property.
49
+ key = association.child_key.map(&:field).first.to_sym
50
+ attributes[key] = value.id
51
+ else
52
+ attributes[attribute] = value
53
+ end
54
+ end
55
+ attributes
56
+ end
57
+ end
58
+
59
+ module DataMapperExtensions
60
+ include ManufactoryMixin
61
+ # Post.make
62
+ # Post.make(:named)
63
+ # Post.make(first_name: "Jakub")
64
+ # Post.make(named, first_name: "Jakub")
65
+ # Post.make(named, first_name: "Jakub") do
66
+ # self.whatever = whatever
67
+ # end
68
+ def make(name = :default, attributes = Hash.new, &block)
69
+ instance = super(DataMapperAdapter, name, attributes, &block)
70
+ unless Manufactory.nerfed?
71
+ instance.save || raise("Save failed: #{instance.errors.full_messages.join(", ")}")
72
+ instance.reload
73
+ end
74
+ return instance
75
+ end
76
+
77
+ def make_unsaved(*args)
78
+ object = Manufactory.with_save_nerfed { make(*args) }
79
+ yield object if block_given?
80
+ object
81
+ end
82
+
83
+ def plan(*args)
84
+ adapter = Manufactory::DataMapperAdapter.new(self, *args)
85
+ dsl = DSL.new(adapter, object, blueprint).run
86
+ Manufactory::DataMapperAdapter.assigned_attributes_without_associations(dsl)
87
+ end
88
+ end
89
+ end
90
+
91
+ DataMapper::Validate::ClassMethods.send(:include, Manufactory::DataMapperExtensions)
92
+ # DataMapper::Model.append_extensions(Manufactory::DataMapperExtensions)
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ # This is adapter for generic models,
4
+ # so it call ModelClass.new(attributes) rather than
5
+ # instance = ModelClass.new, attributes.each { |attribute, value| instance.send("#{attribute}="), value }
6
+ # like it is in object adapter
7
+
8
+ # Since this interface is generic, you have to register it before usage like:
9
+ # HashStruct.extend(Manufactory::GenericObjectMixin)
10
+
11
+ require "manufactory"
12
+
13
+ module Manufactory
14
+ module GenericModelMixin
15
+ include ManufactoryMixin
16
+ # Post.make
17
+ # Post.make(:named)
18
+ # Post.make(first_name: "Jakub")
19
+ # Post.make(named, first_name: "Jakub")
20
+ # Post.make(named, first_name: "Jakub") do
21
+ # self.whatever = whatever
22
+ # end
23
+ def make(name = :default, attributes = Hash.new, &block)
24
+ super(GenericModelAdapter, name, attributes, &block)
25
+ end
26
+ end
27
+
28
+ # ObjectAdapter.new(Object.new, *args)
29
+ class GenericModelAdapter < Adapter
30
+ def initialize_object(klass, default_attributes, attributes)
31
+ klass.new(default_attributes.merge(attributes))
32
+ end
33
+
34
+ def has_association?(object, attribute) # tohle nechceme zdedit!
35
+ false
36
+ end
37
+ end
38
+ end
@@ -4,43 +4,16 @@ require "manufactory"
4
4
 
5
5
  module Manufactory
6
6
  module ObjectMixin
7
- # Post.make
8
- # Post.make(:named)
9
- # Post.make(first_name: "Jakub")
10
- # Post.make(named, first_name: "Jakub")
11
- # Post.make(named, first_name: "Jakub") do
12
- # self.whatever = whatever
13
- # end
7
+ include ManufactoryMixin
14
8
  def make(name = :default, attributes = Hash.new, &block)
15
- name, attributes = :default, name if name.is_a?(Hash) && attributes.empty?
16
- callables = self.blueprints[name]
17
- adapter = ObjectAdapter.new(self, name, callables)
18
- instance = adapter.run(attributes)
19
- instance.instance_eval(&block) if block_given?
20
- return instance
9
+ super(ObjectAdapter, name, attributes, &block)
21
10
  end
22
11
  end
23
12
 
24
- # ObjectAdapter.new(Object.new, *args)
25
- # NOTE: first argument is an object, not a class!
26
13
  class ObjectAdapter < Adapter
27
- def has_association?(object, attribute)
28
- false
29
- end
30
-
31
- protected
32
- def initialize_object(klass, default_attributes, attributes)
33
- attributes = default_attributes.merge(attributes)
34
- instance = klass.new
35
- attributes.each do |attribute, value|
36
- instance.send("#{attribute}=", value)
37
- end
38
- return instance
39
- end
40
14
  end
41
15
  end
42
16
 
43
17
  class Object
44
- extend Manufactory::Blueprints
45
18
  extend Manufactory::ObjectMixin
46
19
  end
@@ -0,0 +1,62 @@
1
+ require 'manufactory'
2
+ require 'manufactory/blueprints'
3
+ require 'sequel'
4
+
5
+ module Manufactory
6
+ class SequelAdapter
7
+ def self.has_association?(object, attribute)
8
+ object.class.associations.include?(attribute)
9
+ end
10
+
11
+ def self.class_for_association(object, attribute)
12
+ object.class.association_reflection(attribute).associated_class
13
+ end
14
+
15
+ def self.assigned_attributes_without_associations(lathe)
16
+ attributes = {}
17
+ lathe.assigned_attributes.each_pair do |attribute, value|
18
+ association = lathe.object.class.association_reflection(attribute)
19
+ if association && association[:type] == :many_to_one
20
+ key = association[:key] || association.default_key
21
+ attributes[key] = value.send(association.primary_key)
22
+ else
23
+ attributes[attribute] = value
24
+ end
25
+ end
26
+ attributes
27
+ end
28
+ end
29
+
30
+ module SequelExtensions
31
+ def self.included(base)
32
+ base.extend(ClassMethods)
33
+ end
34
+
35
+ module ClassMethods
36
+ def make(*args, &block)
37
+ lathe = Lathe.run(Manufactory::SequelAdapter, self.new, *args)
38
+ unless Manufactory.nerfed?
39
+ lathe.object.save
40
+ lathe.object.refresh
41
+ end
42
+ lathe.object(&block)
43
+ end
44
+
45
+ def make_unsaved(*args)
46
+ returning(Manufactory.with_save_nerfed { make(*args) }) do |object|
47
+ yield object if block_given?
48
+ end
49
+ end
50
+
51
+ def plan(*args)
52
+ lathe = Lathe.run(Manufactory::SequelAdapter, self.new, *args)
53
+ Manufactory::SequelAdapter.assigned_attributes_without_associations(lathe)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ class Sequel::Model
60
+ include Manufactory::Blueprints
61
+ include Manufactory::SequelExtensions
62
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ module Manufactory
4
+ # DSL is used to execute the blueprint and construct an object.
5
+ # The blueprint is instance_eval'd against the Lathe.
6
+ class DSL
7
+ attr_reader :adapter, :object, :blueprint
8
+ def initialize(adapter, object, blueprint)
9
+ @adapter = adapter
10
+ @blueprint = blueprint
11
+ @object = object
12
+ end
13
+
14
+ def run
15
+ self.instance_eval(&self.blueprint)
16
+ assigned_attributes
17
+ end
18
+
19
+ def object
20
+ yield @object if block_given?
21
+ @object
22
+ end
23
+
24
+ def method_missing(symbol, *args, &block)
25
+ if attribute_assigned?(symbol)
26
+ # If we've already assigned the attribute, return that.
27
+ self.object.send(symbol)
28
+ elsif @adapter.has_association?(self.object, symbol) && !nil_or_empty?(self.object.send(symbol))
29
+ # If the attribute is an association and is already assigned, return that.
30
+ self.object.send(symbol)
31
+ else
32
+ # Otherwise generate a value and assign it.
33
+ assigned_attributes[symbol] = generate_attribute_value(symbol, *args, &block)
34
+ end
35
+ end
36
+
37
+ def assigned_attributes
38
+ @assigned_attributes ||= {}
39
+ end
40
+
41
+ # Undef a couple of methods that are common ActiveRecord attributes.
42
+ # (Both of these are deprecated in Ruby 1.8 anyway.)
43
+ undef_method :id if respond_to?(:id)
44
+ undef_method :type if respond_to?(:type)
45
+
46
+ private
47
+ def nil_or_empty?(object)
48
+ object.respond_to?(:empty?) ? object.empty? : object.nil?
49
+ end
50
+
51
+ def attribute_assigned?(key)
52
+ assigned_attributes.has_key?(key.to_sym)
53
+ end
54
+
55
+ def generate_attribute_value(attribute, *args, &block)
56
+ if block_given?
57
+ # If we've got a block, use that to generate the value.
58
+ yield
59
+ else
60
+ # Otherwise, look for an association or a sham.
61
+ if @adapter.has_association?(object, attribute)
62
+ @adapter.class_for_association(object, attribute).make(args.first || {})
63
+ elsif args.empty?
64
+ Sham.send(attribute)
65
+ else
66
+ # If we've got a constant, just use that.
67
+ args.first
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ # This sets a flag that stops make from saving objects, so
74
+ # that calls to make from within a blueprint don't create
75
+ # anything inside make_unsaved.
76
+ def self.with_save_nerfed
77
+ begin
78
+ @@nerfed = true
79
+ yield
80
+ ensure
81
+ @@nerfed = false
82
+ end
83
+ end
84
+
85
+ @@nerfed = false
86
+ def self.nerfed?
87
+ @@nerfed
88
+ end
89
+ end
@@ -0,0 +1,196 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require 'manufactory/active_record'
3
+ require 'active_support/whiny_nil'
4
+
5
+ module ManufactoryActiveRecordSpecs
6
+
7
+ class Person < ActiveRecord::Base
8
+ attr_protected :password
9
+ end
10
+
11
+ class Admin < Person
12
+ end
13
+
14
+ class Post < ActiveRecord::Base
15
+ has_many :comments
16
+ end
17
+
18
+ class Comment < ActiveRecord::Base
19
+ belongs_to :post
20
+ belongs_to :author, :class_name => "Person"
21
+ end
22
+
23
+ describe Manufactory, "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")
28
+ end
29
+
30
+ before(:each) do
31
+ [Person, Admin, Post, Comment].each do |model|
32
+ model.blueprints.clear
33
+ end
34
+ end
35
+
36
+ describe "make method" do
37
+ it "should support single-table inheritance" do
38
+ Person.blueprint { }
39
+ Admin.blueprint { }
40
+ admin = Admin.make
41
+ admin.should_not be_new_record
42
+ admin.type.should == "Admin"
43
+ end
44
+
45
+ it "should save the constructed object" do
46
+ Person.blueprint { }
47
+ person = Person.make
48
+ person.should_not be_new_record
49
+ end
50
+
51
+ it "should create an object through belongs_to association" do
52
+ Post.blueprint { }
53
+ Comment.blueprint { post }
54
+ Comment.make.post.class.should == Post
55
+ end
56
+
57
+ it "should create an object through belongs_to association with a class_name attribute" do
58
+ Person.blueprint { }
59
+ Comment.blueprint { author }
60
+ Comment.make.author.class.should == Person
61
+ end
62
+
63
+ it "should create an object through belongs_to association using a named blueprint" do
64
+ Post.blueprint { }
65
+ Post.blueprint(:dummy) { title 'Dummy Post' }
66
+ Comment.blueprint { post(:dummy) }
67
+ Comment.make.post.title.should == 'Dummy Post'
68
+ end
69
+
70
+ it "should allow creating an object through a has_many association" do
71
+ Post.blueprint do
72
+ comments { [Comment.make] }
73
+ end
74
+ Comment.blueprint { }
75
+ Post.make.comments.should have(1).instance_of(Comment)
76
+ end
77
+
78
+ it "should allow setting a protected attribute in the blueprint" do
79
+ Person.blueprint do
80
+ password "Test"
81
+ end
82
+ Person.make.password.should == "Test"
83
+ end
84
+
85
+ it "should allow overriding a protected attribute" do
86
+ Person.blueprint do
87
+ password "Test"
88
+ end
89
+ Person.make(:password => "New").password.should == "New"
90
+ end
91
+
92
+ it "should allow setting the id attribute in a blueprint" do
93
+ Person.blueprint { id 12345 }
94
+ Person.make.id.should == 12345
95
+ end
96
+
97
+ it "should allow setting the type attribute in a blueprint" do
98
+ Person.blueprint { type "Person" }
99
+ Person.make.type.should == "Person"
100
+ end
101
+
102
+ describe "on a has_many association" do
103
+ before do
104
+ Post.blueprint { }
105
+ Comment.blueprint { post }
106
+ @post = Post.make
107
+ @comment = @post.comments.make
108
+ end
109
+
110
+ it "should save the created object" do
111
+ @comment.should_not be_new_record
112
+ end
113
+
114
+ it "should set the parent association on the created object" do
115
+ @comment.post.should == @post
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "plan method" do
121
+ it "should not save the constructed object" do
122
+ person_count = Person.count
123
+ Person.blueprint { }
124
+ person = Person.plan
125
+ Person.count.should == person_count
126
+ end
127
+
128
+ it "should create an object through a belongs_to association, and return its id" do
129
+ Post.blueprint { }
130
+ Comment.blueprint { post }
131
+ post_count = Post.count
132
+ comment = Comment.plan
133
+ Post.count.should == post_count + 1
134
+ comment[:post].should be_nil
135
+ comment[:post_id].should_not be_nil
136
+ end
137
+
138
+ describe "on a belongs_to association" do
139
+ it "should allow explicitly setting the association to nil" do
140
+ Comment.blueprint { post }
141
+ Comment.blueprint(:no_post) { post { nil } }
142
+ lambda {
143
+ @comment = Comment.plan(:no_post)
144
+ }.should_not raise_error
145
+ end
146
+ end
147
+
148
+ describe "on a has_many association" do
149
+ before do
150
+ Post.blueprint { }
151
+ Comment.blueprint do
152
+ post
153
+ body { "Test" }
154
+ end
155
+ @post = Post.make
156
+ @post_count = Post.count
157
+ @comment = @post.comments.plan
158
+ end
159
+
160
+ it "should not include the parent in the returned hash" do
161
+ @comment[:post].should be_nil
162
+ @comment[:post_id].should be_nil
163
+ end
164
+
165
+ it "should not create an extra parent object" do
166
+ Post.count.should == @post_count
167
+ end
168
+ end
169
+ end
170
+
171
+ describe "make_unsaved method" do
172
+ it "should not save the constructed object" do
173
+ Person.blueprint { }
174
+ person = Person.make_unsaved
175
+ person.should be_new_record
176
+ end
177
+
178
+ it "should not save associated objects" do
179
+ Post.blueprint { }
180
+ Comment.blueprint { post }
181
+ comment = Comment.make_unsaved
182
+ comment.post.should be_new_record
183
+ end
184
+
185
+ it "should save objects made within a passed-in block" do
186
+ Post.blueprint { }
187
+ Comment.blueprint { }
188
+ comment = nil
189
+ post = Post.make_unsaved { comment = Comment.make }
190
+ post.should be_new_record
191
+ comment.should_not be_new_record
192
+ end
193
+ end
194
+
195
+ end
196
+ end