manufactory 0.0.1 → 0.0.2

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.
@@ -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