active_factory 0.1.0
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/MIT-LICENSE +20 -0
- data/Manifest +17 -0
- data/README.rdoc +113 -0
- data/Rakefile +41 -0
- data/active_factory.gemspec +30 -0
- data/init.rb +1 -0
- data/lib/active_factory.rb +480 -0
- data/lib/generators/active_factory/install/USAGE +2 -0
- data/lib/generators/active_factory/install/install_generator.rb +15 -0
- data/lib/generators/active_factory/install/templates/active_factory.rb +9 -0
- data/lib/generators/active_factory/install/templates/define_factories.rb +7 -0
- data/lib/hash_struct.rb +16 -0
- data/spec/active_factory_define_spec.rb +95 -0
- data/spec/active_factory_spec.rb +324 -0
- data/spec/active_record_environment_spec.rb +14 -0
- data/spec/define_lib_factories.rb +39 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/active_record_environment.rb +46 -0
- metadata +75 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
MIT-LICENSE
|
2
|
+
README.rdoc
|
3
|
+
Rakefile
|
4
|
+
init.rb
|
5
|
+
lib/active_factory.rb
|
6
|
+
lib/generators/active_factory/install/USAGE
|
7
|
+
lib/generators/active_factory/install/install_generator.rb
|
8
|
+
lib/generators/active_factory/install/templates/active_factory.rb
|
9
|
+
lib/generators/active_factory/install/templates/define_factories.rb
|
10
|
+
lib/hash_struct.rb
|
11
|
+
spec/active_factory_define_spec.rb
|
12
|
+
spec/active_factory_spec.rb
|
13
|
+
spec/active_record_environment_spec.rb
|
14
|
+
spec/define_lib_factories.rb
|
15
|
+
spec/spec_helper.rb
|
16
|
+
spec/support/active_record_environment.rb
|
17
|
+
Manifest
|
data/README.rdoc
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
== ActiveFactory
|
2
|
+
|
3
|
+
<em>A fixture replacement library. With it your specs will become more declarative, uniform and terse.</em>
|
4
|
+
|
5
|
+
<b>Feedback will be highly appreciated</b>
|
6
|
+
|
7
|
+
* {Google group}[http://groups.google.com/group/active_factory]
|
8
|
+
* {Factory Definition Reference}[https://github.com/tarasevich/active_factory/wiki/Factory-Definition-Reference]
|
9
|
+
* {Models Clause Reference}[https://github.com/tarasevich/active_factory/wiki/Models-Clause-Reference]
|
10
|
+
|
11
|
+
== Introduction
|
12
|
+
|
13
|
+
ActiveFactory allows you declaratively define
|
14
|
+
which objects you want to have in a database for your spec.
|
15
|
+
You can also define associations between the objects and
|
16
|
+
redefine default values.
|
17
|
+
Additionally ActiveFactory automatically defines accessor methods for the objects.
|
18
|
+
The scope of the methods is limited to current spec. So they will not affect you other specs.
|
19
|
+
|
20
|
+
it "Task.incomplete returns only incomplete tasks" do
|
21
|
+
models { project - tasks({:complete => 0}, {:complete => 1}) }
|
22
|
+
|
23
|
+
project.tasks.incomplete.should == [tasks[0]]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "project displays incomplete tasks" do
|
27
|
+
models { my - project - task(:complete => 0) }
|
28
|
+
|
29
|
+
visit project_path(project)
|
30
|
+
|
31
|
+
page.should have_content task.title
|
32
|
+
end
|
33
|
+
|
34
|
+
These specs require the following configuration:
|
35
|
+
|
36
|
+
class ActiveFactory::Define
|
37
|
+
|
38
|
+
factory :my, :class => User do
|
39
|
+
username "my_name"
|
40
|
+
password "my_password"
|
41
|
+
|
42
|
+
before_save do
|
43
|
+
model.save!
|
44
|
+
context.emulate_sign_in model
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
factory :project do
|
49
|
+
title { "Project #{index} title" }
|
50
|
+
due { Time.now }
|
51
|
+
end
|
52
|
+
|
53
|
+
factory :task do
|
54
|
+
title { "Task #{index} title" }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
In the configuration you specify default attribute values for an object,
|
59
|
+
and in a specific test you may reassign the values for the needs of the test.
|
60
|
+
Optional block after_build specifies actions that should be done
|
61
|
+
after object was initialized but before saving it.
|
62
|
+
If you want to create by the same factory several objects with different values,
|
63
|
+
you may use blocks.
|
64
|
+
Method index in those blocks returns index of the object being created.
|
65
|
+
|
66
|
+
== Installation
|
67
|
+
|
68
|
+
ActiveFactory requires Rails 3 and RSpec 2.
|
69
|
+
|
70
|
+
rails plugin install git@github.com:tarasevich/active_factory.git
|
71
|
+
rails g active_factory:install
|
72
|
+
|
73
|
+
Now you are ready to add you factories spec/define_factories.rb
|
74
|
+
and use models {} block in your specs.
|
75
|
+
|
76
|
+
<em>Copyright (c) 2010-2011 Alexey Tarasevich, released under the MIT license</em>
|
77
|
+
|
78
|
+
== Some Examples
|
79
|
+
|
80
|
+
Consider we have factory declarations from introduction secion.
|
81
|
+
|
82
|
+
describe ProjectController do
|
83
|
+
|
84
|
+
it "creates a new project" do
|
85
|
+
models { my ; project_ }
|
86
|
+
|
87
|
+
post :create, :project => project_
|
88
|
+
|
89
|
+
Project.all.map(&:title).should == [project_[:title]]
|
90
|
+
end
|
91
|
+
...
|
92
|
+
|
93
|
+
In previous example factory +my+ in +models+ block causes to emulate login.
|
94
|
+
+project_+ declares corresponding method inside the spec.
|
95
|
+
Syntax with uderscore indicates that it's should be just hash
|
96
|
+
with values taken from the factory declaration.
|
97
|
+
In our case it will be <code>{:title => "Project 0 title"}</code>
|
98
|
+
|
99
|
+
it "modifies a project's title" do
|
100
|
+
models { my - project }
|
101
|
+
|
102
|
+
put :update, :id => project.id, :project => project_(:title => "New Title")
|
103
|
+
|
104
|
+
project.reload.title.should == "New Title"
|
105
|
+
end
|
106
|
+
|
107
|
+
In this example we again use +my+ to create a user and log in with it.
|
108
|
+
Additionally we create +Project+ instance using +project+ factory
|
109
|
+
and define method +project+ locally for the spec.
|
110
|
+
Finally we use minus sign to create association between user +my+ and +project+.
|
111
|
+
Active factory iterates through associations of +User+ model and chooses the one that
|
112
|
+
has +Project+ on the other side.
|
113
|
+
<code>project_(h)</code> is just syntax sugar for <code>project_.merge(h)</code>
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'echoe'
|
6
|
+
|
7
|
+
desc 'Default: run specs.'
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
desc "Run specs"
|
11
|
+
RSpec::Core::RakeTask.new do |t|
|
12
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
13
|
+
# Put spec opts in a file named .rspec in root
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate code coverage"
|
17
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
18
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
19
|
+
t.rcov = true
|
20
|
+
t.rcov_opts = ['--exclude', 'spec']
|
21
|
+
end
|
22
|
+
|
23
|
+
Echoe.new('active_factory', '0.1.0') do |p|
|
24
|
+
p.description = "Fixtures replacement with sweet syntax"
|
25
|
+
p.url = "http://github.com/tarasevich/active_factory"
|
26
|
+
p.author = "Alexey Tarasevich"
|
27
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
28
|
+
p.development_dependencies = []
|
29
|
+
end
|
30
|
+
|
31
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
32
|
+
|
33
|
+
|
34
|
+
desc 'Generate documentation for the active_factory plugin.'
|
35
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
36
|
+
rdoc.rdoc_dir = 'rdoc'
|
37
|
+
rdoc.title = 'ActiveFactory'
|
38
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
39
|
+
rdoc.rdoc_files.include('README.rdoc')
|
40
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{active_factory}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Alexey Tarasevich"]
|
9
|
+
s.date = %q{2011-04-21}
|
10
|
+
s.description = %q{Fixtures replacement with sweet syntax}
|
11
|
+
s.email = %q{}
|
12
|
+
s.extra_rdoc_files = ["README.rdoc", "lib/active_factory.rb", "lib/generators/active_factory/install/USAGE", "lib/generators/active_factory/install/install_generator.rb", "lib/generators/active_factory/install/templates/active_factory.rb", "lib/generators/active_factory/install/templates/define_factories.rb", "lib/hash_struct.rb"]
|
13
|
+
s.files = ["MIT-LICENSE", "README.rdoc", "Rakefile", "init.rb", "lib/active_factory.rb", "lib/generators/active_factory/install/USAGE", "lib/generators/active_factory/install/install_generator.rb", "lib/generators/active_factory/install/templates/active_factory.rb", "lib/generators/active_factory/install/templates/define_factories.rb", "lib/hash_struct.rb", "spec/active_factory_define_spec.rb", "spec/active_factory_spec.rb", "spec/active_record_environment_spec.rb", "spec/define_lib_factories.rb", "spec/spec_helper.rb", "spec/support/active_record_environment.rb", "Manifest", "active_factory.gemspec"]
|
14
|
+
s.homepage = %q{http://github.com/tarasevich/active_factory}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Active_factory", "--main", "README.rdoc"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{active_factory}
|
18
|
+
s.rubygems_version = %q{1.3.7}
|
19
|
+
s.summary = %q{Fixtures replacement with sweet syntax}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_factory' if Rails.env == 'test'
|
@@ -0,0 +1,480 @@
|
|
1
|
+
require 'hash_struct'
|
2
|
+
|
3
|
+
module ActiveFactory
|
4
|
+
# should be included in specs
|
5
|
+
module API
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
attr_accessor :_active_factory_context_extension
|
10
|
+
|
11
|
+
after do
|
12
|
+
_active_factory_context_extension.try :undo
|
13
|
+
self._active_factory_context_extension = nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def factory_attributes model_name, index = 0
|
19
|
+
Define.
|
20
|
+
factories_hash[model_name].
|
21
|
+
attributes_for(index)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# methods available in specs
|
26
|
+
module InstanceMethods
|
27
|
+
def models &define_graph
|
28
|
+
not _active_factory_context_extension or raise "cannot use models twice in an example"
|
29
|
+
|
30
|
+
context = self
|
31
|
+
factories_hash = Define.factories_hash
|
32
|
+
containers_hash = Hash.new { |this, name|
|
33
|
+
factory = factories_hash[name]
|
34
|
+
this[name] = Container.new(name, factory, context)
|
35
|
+
}
|
36
|
+
linking_context = LinkingContext.new factories_hash.keys, containers_hash, context
|
37
|
+
self._active_factory_context_extension = ContextExtension.new
|
38
|
+
|
39
|
+
linking_context.instance_eval &define_graph
|
40
|
+
containers_hash.values.each &:before_save
|
41
|
+
containers_hash.values.each &:save
|
42
|
+
_active_factory_context_extension.extend_test_context containers_hash, context
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class FactoryDSL
|
49
|
+
def initialize
|
50
|
+
@attribute_expressions = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def prefer_associations *assoc_symbols
|
54
|
+
@prefer_associations = assoc_symbols
|
55
|
+
end
|
56
|
+
|
57
|
+
def after_build &callback
|
58
|
+
@after_build = callback
|
59
|
+
end
|
60
|
+
|
61
|
+
def before_save &callback
|
62
|
+
@before_save = callback
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing method, *args, &expression
|
66
|
+
if args.many? or args.any? and block_given?
|
67
|
+
raise "should be either block or value: #{method} #{args.inspect[1..-2]}"
|
68
|
+
end
|
69
|
+
@attribute_expressions[method.to_sym] = expression || proc { args[0] }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# the class that should be "extended" to define models
|
74
|
+
class Define
|
75
|
+
@@factories = {}
|
76
|
+
|
77
|
+
def self.factory name, options = {}, &block
|
78
|
+
model_class = options[:class]
|
79
|
+
if parent_sym = options[:parent]
|
80
|
+
parent = @@factories[parent_sym] or raise "undefined parent factory #{parent_sym}"
|
81
|
+
end
|
82
|
+
|
83
|
+
@@factories[name] = FactoryDSL.new.instance_eval {
|
84
|
+
instance_eval(&block)
|
85
|
+
Factory.new name, parent, model_class,
|
86
|
+
@prefer_associations, @attribute_expressions, @after_build, @before_save
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.factories_hash
|
91
|
+
@@factories
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# defines methods that can be used in a model definition
|
96
|
+
# model - the model under construction
|
97
|
+
# index - index of the model in the factory
|
98
|
+
# context - spec context where the models {} block was evaluated
|
99
|
+
class CreationContext < Struct.new :index, :context, :model
|
100
|
+
alias i index
|
101
|
+
end
|
102
|
+
|
103
|
+
# creates instances of the given model class
|
104
|
+
class Factory < Struct.new :name, :parent, :model_class,
|
105
|
+
:prefer_associations, :attribute_expressions, :after_build, :before_save
|
106
|
+
def initialize name, parent, *overridable
|
107
|
+
@overridable = parent ? parent.merge_overridable(overridable) : overridable
|
108
|
+
super(name, parent, *@overridable)
|
109
|
+
self.attribute_expressions =
|
110
|
+
parent.attribute_expressions.merge(self.attribute_expressions) if parent
|
111
|
+
|
112
|
+
name.is_a? Symbol or raise "factory name #{name.inspect} must be symbol"
|
113
|
+
self.model_class ||= (@overridable[0] = Kernel.const_get(name.to_s.capitalize))
|
114
|
+
end
|
115
|
+
|
116
|
+
def merge_overridable overridable
|
117
|
+
overridable.zip(@overridable).
|
118
|
+
map { |his, my| his or my }
|
119
|
+
end
|
120
|
+
|
121
|
+
def attributes_for index
|
122
|
+
context = CreationContext.new(index)
|
123
|
+
attrs = attribute_expressions.map { |a, e| [a, context.instance_eval(&e)] }
|
124
|
+
Hash[attrs]
|
125
|
+
end
|
126
|
+
|
127
|
+
def apply_after_build index, context, model
|
128
|
+
if after_build
|
129
|
+
CreationContext.new(index, context, model).
|
130
|
+
instance_eval(&after_build)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def apply_before_save index, context, model
|
135
|
+
if before_save
|
136
|
+
CreationContext.new(index, context, model).
|
137
|
+
instance_eval(&before_save)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class ContainerEntry
|
143
|
+
attr_reader :model, :attrs
|
144
|
+
|
145
|
+
def initialize index, factory, context
|
146
|
+
@index = index
|
147
|
+
@factory = factory
|
148
|
+
@attrs = HashStruct[factory.attributes_for(index)]
|
149
|
+
@context = context
|
150
|
+
end
|
151
|
+
|
152
|
+
def merge hash
|
153
|
+
@attrs = @attrs.merge hash
|
154
|
+
end
|
155
|
+
|
156
|
+
def build
|
157
|
+
unless @model
|
158
|
+
@model = @factory.model_class.new
|
159
|
+
@attrs.each_pair { |k,v| @model.send "#{k}=", v }
|
160
|
+
|
161
|
+
@factory.apply_after_build @index, @context, @model
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def before_save
|
166
|
+
if @model and not @saved
|
167
|
+
@factory.apply_before_save @index, @context, @model
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def save
|
172
|
+
if @model and not @saved
|
173
|
+
@model.save!
|
174
|
+
@saved = true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# keeps collection of created instances of the given model class
|
180
|
+
class Container
|
181
|
+
attr_accessor :entries
|
182
|
+
attr_reader :name, :factory
|
183
|
+
|
184
|
+
def initialize name, factory, context
|
185
|
+
@name = name
|
186
|
+
@factory = factory
|
187
|
+
@context = context
|
188
|
+
@entries = [].freeze
|
189
|
+
end
|
190
|
+
|
191
|
+
def create count
|
192
|
+
dup_with add_entries count
|
193
|
+
end
|
194
|
+
|
195
|
+
def singleton
|
196
|
+
if @entries.none?
|
197
|
+
add_entries 1
|
198
|
+
elsif @entries.many?
|
199
|
+
raise "Multiple instances were declared for model :#{@name}."+
|
200
|
+
"Use <#{@name.to_s.pluralize}> to access them"
|
201
|
+
end
|
202
|
+
self
|
203
|
+
end
|
204
|
+
|
205
|
+
def zip_merge *hashes
|
206
|
+
@entries.size == hashes.size or raise
|
207
|
+
|
208
|
+
@entries.zip(hashes) { |entry, hash|
|
209
|
+
entry.merge hash
|
210
|
+
}
|
211
|
+
self
|
212
|
+
end
|
213
|
+
|
214
|
+
def build
|
215
|
+
@entries.each &:build
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
219
|
+
def make_linker
|
220
|
+
Linker.new self
|
221
|
+
end
|
222
|
+
|
223
|
+
def before_save
|
224
|
+
@entries.each &:before_save
|
225
|
+
end
|
226
|
+
|
227
|
+
def save
|
228
|
+
@entries.each &:save
|
229
|
+
end
|
230
|
+
|
231
|
+
def attrs
|
232
|
+
@entries.map &:attrs
|
233
|
+
end
|
234
|
+
|
235
|
+
def objects
|
236
|
+
@entries.map &:model
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def dup_with entries
|
242
|
+
that = clone
|
243
|
+
that.entries = entries
|
244
|
+
that
|
245
|
+
end
|
246
|
+
|
247
|
+
def add_entries count
|
248
|
+
size = @entries.size
|
249
|
+
added = (size...size+count).
|
250
|
+
map { |i| ContainerEntry.new i, factory, @context }
|
251
|
+
@entries = (@entries + added).freeze
|
252
|
+
added
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# provides syntax to create associations between models
|
257
|
+
class Linker
|
258
|
+
def initialize container, use_association = nil
|
259
|
+
@container = container
|
260
|
+
@use_association = use_association
|
261
|
+
|
262
|
+
@entries = container.entries
|
263
|
+
@model_class = container.factory.model_class
|
264
|
+
@prefer_associations = container.factory.prefer_associations
|
265
|
+
end
|
266
|
+
|
267
|
+
attr_accessor :entries, :model_class
|
268
|
+
|
269
|
+
def - that
|
270
|
+
case that
|
271
|
+
when Linker
|
272
|
+
associate that
|
273
|
+
that
|
274
|
+
when Symbol
|
275
|
+
Linker.new @container, that
|
276
|
+
else
|
277
|
+
raise "cannot associate with #{that.inspect}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
private
|
282
|
+
|
283
|
+
def associate linker
|
284
|
+
ar = get_association linker.model_class
|
285
|
+
|
286
|
+
case ar.macro
|
287
|
+
when :has_many, :has_and_belongs_to_many
|
288
|
+
assoc_entries = proc { |e, e2|
|
289
|
+
e.model.send(ar.name) << e2.model
|
290
|
+
}
|
291
|
+
|
292
|
+
if entries.one? or linker.entries.one?
|
293
|
+
entries.each { |e|
|
294
|
+
linker.entries.each { |e2|
|
295
|
+
assoc_entries[e, e2]
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
elsif entries.size == linker.entries.size
|
300
|
+
entries.zip(linker.entries) { |e, e2|
|
301
|
+
assoc_entries[e, e2]
|
302
|
+
}
|
303
|
+
|
304
|
+
else
|
305
|
+
raise "when linking models, they should be one of this: 1-n, n-1, n-n (e.i. equal number)"
|
306
|
+
end
|
307
|
+
|
308
|
+
when :belongs_to, :has_one
|
309
|
+
assoc_entries = proc { |e, e2|
|
310
|
+
e.model.send :"#{ar.name}=", e2.model
|
311
|
+
}
|
312
|
+
|
313
|
+
if linker.entries.one?
|
314
|
+
entries.each { |e|
|
315
|
+
assoc_entries[e, linker.entries.first]
|
316
|
+
}
|
317
|
+
|
318
|
+
elsif entries.size == linker.entries.size
|
319
|
+
entries.zip(linker.entries) { |e, e2|
|
320
|
+
assoc_entries[e, e2]
|
321
|
+
}
|
322
|
+
|
323
|
+
else
|
324
|
+
raise "exactly one instance of an object should be assigned to belongs_to association: #{@container.name} - #{linker.instance_variable_get(:@container).try :name}"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def get_association with_class
|
330
|
+
if @use_association
|
331
|
+
@model_class.reflect_on_association(@use_association) or
|
332
|
+
raise "No association #{@use_association.inspect} found for #{@model_class}"
|
333
|
+
else
|
334
|
+
find_association with_class
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def find_association with_class
|
339
|
+
assocs = @model_class.reflect_on_all_associations.find_all { |assoc|
|
340
|
+
assoc.class_name == with_class.name
|
341
|
+
}
|
342
|
+
|
343
|
+
if assocs.none?
|
344
|
+
raise "Trying to link, but no association found from #{@model_class} to #{with_class}"
|
345
|
+
|
346
|
+
elsif assocs.one?
|
347
|
+
assocs.first
|
348
|
+
|
349
|
+
elsif assocs.many?
|
350
|
+
resolved = assocs.select { |assoc| @prefer_associations.member? assoc.name }
|
351
|
+
resolved.one? or
|
352
|
+
raise "Ambiguous associations: #{assocs.map(&:name).inspect} of #{@model_class} to #{with_class}. prefer_associations=#{@prefer_associations.inspect}"
|
353
|
+
|
354
|
+
resolved.first
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
# provides methods that refer models in models {} block
|
361
|
+
class LinkingContext
|
362
|
+
def initialize model_names, containers_hash, context
|
363
|
+
@context = context
|
364
|
+
h = containers_hash
|
365
|
+
|
366
|
+
obj_class_eval do
|
367
|
+
model_names.each { |name|
|
368
|
+
|
369
|
+
define_method name do |*args|
|
370
|
+
not args.many? or raise "0 or 1 arguments expected, got: #{args.inspect}"
|
371
|
+
|
372
|
+
if args.none?
|
373
|
+
h[name].singleton
|
374
|
+
else
|
375
|
+
h[name].singleton.zip_merge(args[0])
|
376
|
+
|
377
|
+
end.build.make_linker
|
378
|
+
end
|
379
|
+
|
380
|
+
define_method :"#{name.to_s.pluralize}" do |*args|
|
381
|
+
|
382
|
+
if args.none?
|
383
|
+
h[name]
|
384
|
+
|
385
|
+
elsif args[0].is_a? Fixnum and args.one?
|
386
|
+
h[name].create(args[0])
|
387
|
+
|
388
|
+
elsif args.all? { |arg| arg.is_a? Hash }
|
389
|
+
h[name].create(args.size).zip_merge(*args)
|
390
|
+
|
391
|
+
else
|
392
|
+
raise "expected no args, or single integer, or several hashes, got: #{args.inspect}"
|
393
|
+
|
394
|
+
end.build.make_linker
|
395
|
+
end
|
396
|
+
|
397
|
+
define_method :"#{name}_" do
|
398
|
+
h[name].singleton.make_linker
|
399
|
+
end
|
400
|
+
|
401
|
+
define_method :"#{name.to_s.pluralize}_" do |count|
|
402
|
+
h[name].create(count).make_linker
|
403
|
+
end
|
404
|
+
}
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
private
|
409
|
+
|
410
|
+
def obj_class_eval &block
|
411
|
+
class << self
|
412
|
+
self
|
413
|
+
end.class_eval &block
|
414
|
+
end
|
415
|
+
|
416
|
+
def method_missing *args, &block
|
417
|
+
if block
|
418
|
+
@context.send *args, &block
|
419
|
+
else
|
420
|
+
@context.send *args
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# introduces models' names in a spec's context
|
426
|
+
class ContextExtension
|
427
|
+
def undo
|
428
|
+
@undo_define_methods[] if @undo_define_methods
|
429
|
+
@undo_define_methods = nil
|
430
|
+
end
|
431
|
+
|
432
|
+
def extend_test_context containers_hash, context
|
433
|
+
mrg = proc {|args, hash|
|
434
|
+
if args.none?
|
435
|
+
hash
|
436
|
+
elsif args.one? and args[0].is_a? Hash
|
437
|
+
hash.merge args[0]
|
438
|
+
else
|
439
|
+
raise "Only has is valid argument, but *args=#{args.inspect}"
|
440
|
+
end
|
441
|
+
}
|
442
|
+
|
443
|
+
method_defs =
|
444
|
+
containers_hash.map { |name, container| [
|
445
|
+
name, proc { container.singleton.objects[0] },
|
446
|
+
:"#{name}_", proc { |*args| mrg[args, container.singleton.attrs[0]] },
|
447
|
+
:"#{name.to_s.pluralize}", proc { container.objects },
|
448
|
+
:"#{name.to_s.pluralize}_", proc { container.attrs }
|
449
|
+
] }.
|
450
|
+
flatten.each_slice(2)
|
451
|
+
|
452
|
+
@undo_define_methods =
|
453
|
+
define_methods_with_undo context, method_defs
|
454
|
+
end
|
455
|
+
|
456
|
+
private
|
457
|
+
|
458
|
+
def define_methods_with_undo model, method_defs
|
459
|
+
old_methods = model.methods.map &:to_sym
|
460
|
+
|
461
|
+
overridden_methods, new_methods =
|
462
|
+
method_defs.
|
463
|
+
map(&:first).
|
464
|
+
partition { |name| old_methods.include? name.to_sym }
|
465
|
+
|
466
|
+
overridden_methods.map! { |name| [name, model.method(name)] }
|
467
|
+
|
468
|
+
define_method, undef_method = class << model
|
469
|
+
[method(:define_method), method(:undef_method)]
|
470
|
+
end
|
471
|
+
|
472
|
+
method_defs.each{ |n,b| define_method[n,b] }
|
473
|
+
|
474
|
+
lambda {
|
475
|
+
overridden_methods.each{ |n,b| define_method[n,b] }
|
476
|
+
new_methods.each &undef_method
|
477
|
+
}
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|