joinfix 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/README +86 -0
- data/TEST_README +44 -0
- data/lib/joinfix/fixture.rb +29 -0
- data/lib/joinfix/fixtures.rb +271 -0
- data/lib/joinfix/fixtures_class.rb +102 -0
- data/lib/joinfix.rb +208 -0
- data/rails/README +182 -0
- data/rails/Rakefile +10 -0
- data/rails/app/controllers/application.rb +7 -0
- data/rails/app/helpers/application_helper.rb +3 -0
- data/rails/app/models/group.rb +4 -0
- data/rails/app/models/inner_child.rb +4 -0
- data/rails/app/models/nested_child.rb +4 -0
- data/rails/app/models/nested_parent.rb +4 -0
- data/rails/app/models/user.rb +4 -0
- data/rails/app/models/user_group.rb +5 -0
- data/rails/config/boot.rb +45 -0
- data/rails/config/database.yml +36 -0
- data/rails/config/environment.rb +60 -0
- data/rails/config/environments/development.rb +21 -0
- data/rails/config/environments/production.rb +18 -0
- data/rails/config/environments/test.rb +19 -0
- data/rails/config/routes.rb +23 -0
- data/rails/db/migrate/001_create_nested_parents.rb +12 -0
- data/rails/db/migrate/002_create_nested_children.rb +12 -0
- data/rails/db/migrate/003_create_inner_children.rb +12 -0
- data/rails/db/migrate/004_create_users.rb +11 -0
- data/rails/db/migrate/005_create_groups.rb +11 -0
- data/rails/db/migrate/006_create_user_groups.rb +12 -0
- data/rails/db/schema.rb +35 -0
- data/rails/doc/README_FOR_APP +2 -0
- data/rails/public/404.html +30 -0
- data/rails/public/500.html +30 -0
- data/rails/public/dispatch.cgi +10 -0
- data/rails/public/dispatch.fcgi +24 -0
- data/rails/public/dispatch.rb +10 -0
- data/rails/public/favicon.ico +0 -0
- data/rails/public/images/rails.png +0 -0
- data/rails/public/index.html +277 -0
- data/rails/public/javascripts/application.js +2 -0
- data/rails/public/javascripts/controls.js +833 -0
- data/rails/public/javascripts/dragdrop.js +942 -0
- data/rails/public/javascripts/effects.js +1088 -0
- data/rails/public/javascripts/prototype.js +2515 -0
- data/rails/public/robots.txt +1 -0
- data/rails/script/about +3 -0
- data/rails/script/breakpointer +3 -0
- data/rails/script/console +3 -0
- data/rails/script/destroy +3 -0
- data/rails/script/generate +3 -0
- data/rails/script/performance/benchmarker +3 -0
- data/rails/script/performance/profiler +3 -0
- data/rails/script/plugin +3 -0
- data/rails/script/process/inspector +3 -0
- data/rails/script/process/reaper +3 -0
- data/rails/script/process/spawner +3 -0
- data/rails/script/runner +3 -0
- data/rails/script/server +3 -0
- data/rails/test/fixtures/groups.yml +3 -0
- data/rails/test/fixtures/inner_children.yml +2 -0
- data/rails/test/fixtures/nested_children.yml +2 -0
- data/rails/test/fixtures/nested_parents.yml +33 -0
- data/rails/test/fixtures/user_groups.yml +0 -0
- data/rails/test/fixtures/users.yml +20 -0
- data/rails/test/test_helper.rb +29 -0
- data/rails/test/unit/group_test.rb +10 -0
- data/rails/test/unit/inner_child_test.rb +10 -0
- data/rails/test/unit/nested_child_test.rb +10 -0
- data/rails/test/unit/nested_parent_test.rb +24 -0
- data/rails/test/unit/user_group_test.rb +10 -0
- data/rails/test/unit/user_test.rb +12 -0
- data/test/belongs_to_test.rb +77 -0
- data/test/config.yml +5 -0
- data/test/fixtures/as_children.yml +0 -0
- data/test/fixtures/bt_children.yml +0 -0
- data/test/fixtures/bt_parents.yml +30 -0
- data/test/fixtures/habtm_children.yml +0 -0
- data/test/fixtures/habtm_children_habtm_parents.yml +0 -0
- data/test/fixtures/habtm_joins.yml +0 -0
- data/test/fixtures/habtm_parents.yml +18 -0
- data/test/fixtures/hm_children.yml +0 -0
- data/test/fixtures/hm_joins.yml +0 -0
- data/test/fixtures/hm_parents.yml +34 -0
- data/test/fixtures/ho_children.yml +0 -0
- data/test/fixtures/ho_parents.yml +14 -0
- data/test/fixtures/inner_children.yml +2 -0
- data/test/fixtures/nested_children.yml +0 -0
- data/test/fixtures/nested_parents.yml +33 -0
- data/test/fixtures/no_join_fixes.yml +4 -0
- data/test/fixtures/omap_no_join_fixes.yml +7 -0
- data/test/fixtures/polymorphic_children.yml +0 -0
- data/test/has_and_belongs_to_many_test.rb +72 -0
- data/test/has_many_test.rb +97 -0
- data/test/has_one_test.rb +56 -0
- data/test/joinfix_test.rb +287 -0
- data/test/joinfix_test_helper.rb +54 -0
- data/test/joinfix_test_suite.rb +10 -0
- data/test/nested_test.rb +70 -0
- metadata +189 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2006-2007, Regents of the University of Colorado.
|
2
|
+
Developer:: Simon Chiang, Biomolecular Structure Program
|
3
|
+
Support:: UCHSC School of Medicine Deans Academic Enrichment Fund
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
8
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
9
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
12
|
+
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
|
18
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
19
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
21
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# = JoinFix
|
2
|
+
# A reflection-based solution to the fixture join problem.
|
3
|
+
#
|
4
|
+
# == Info
|
5
|
+
#
|
6
|
+
# Copyright (c) 2006-2007, Regents of the University of Colorado.
|
7
|
+
# Developer:: Simon Chiang, Biomolecular Structure Program
|
8
|
+
# Support:: UCHSC School of Medicine Deans Academic Enrichment Fund
|
9
|
+
# Licence:: MIT-Style
|
10
|
+
#
|
11
|
+
# == Usage
|
12
|
+
# Consider the following data model:
|
13
|
+
#
|
14
|
+
# class User < ActiveRecord::Base
|
15
|
+
# has_many :user_groups,
|
16
|
+
# has_many :groups, :through => :user_groups
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# class Group < ActiveRecord::Base
|
20
|
+
# has_many :group_users, :class_name => 'UserGroup'
|
21
|
+
# has_many :users, :through => :group_users
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class UserGroup < ActiveRecord::Base
|
25
|
+
# belongs_to :user
|
26
|
+
# belongs_to :group
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Write your fixtures using the naming scheme you lay out in your models.
|
30
|
+
# Entries can be spread across multiple fixture files, or not. Joins between
|
31
|
+
# entries are written inline, either by referening the name of another entry
|
32
|
+
# or by defining the joined entry:
|
33
|
+
#
|
34
|
+
# [users.yml]
|
35
|
+
# bob:
|
36
|
+
# login: bob
|
37
|
+
# groups: admin_group # => reference to the 'admin_group' entry
|
38
|
+
#
|
39
|
+
# jane:
|
40
|
+
# id: 3 # => you can specify ids if you want
|
41
|
+
# login: jane
|
42
|
+
# groups: # => an array of joins
|
43
|
+
# - admin_group
|
44
|
+
# - workers:
|
45
|
+
# name: worker bees
|
46
|
+
#
|
47
|
+
# samantha:
|
48
|
+
# login: sam
|
49
|
+
# groups: # => inline definition of joined entries
|
50
|
+
# movers:
|
51
|
+
# name: movers
|
52
|
+
# shakers:
|
53
|
+
# name: shakers
|
54
|
+
#
|
55
|
+
# [groups.yml]
|
56
|
+
# admin_group: # => references can span files
|
57
|
+
# name: administrators
|
58
|
+
#
|
59
|
+
# Join entries implied in your definition, as in a has_and_belongs_to_many association, will
|
60
|
+
# be created and named by joining together the names of the parent and child, ordered
|
61
|
+
# by the '<' operator. For example, the users.yml and groups.yml fixtures will also produce:
|
62
|
+
#
|
63
|
+
# admin_group_bob # => join for bob and admin_group
|
64
|
+
# jane_workers # => join for jane and workers
|
65
|
+
# ...
|
66
|
+
#
|
67
|
+
# In your tests, require joinfix and use the fixtures exactly as you would normally.
|
68
|
+
# One gotcha (which really isn't a gotcha) -- you must be sure to name all the tables
|
69
|
+
# for which your fixtures create entries. In fact this is no different than normal,
|
70
|
+
# but it's easy to forget if you lump joins into one file.
|
71
|
+
#
|
72
|
+
# require 'joinfix'
|
73
|
+
#
|
74
|
+
# class UserTest < Test::Unit::TestCase
|
75
|
+
# fixtures :users, :groups, :user_groups # => got to name them all!
|
76
|
+
#
|
77
|
+
# def test_joinfix
|
78
|
+
# assert_equal "administrators", users(:bob).groups.first.name
|
79
|
+
# assert_equal 2, User.find_by_login("jane").groups.count
|
80
|
+
# assert_equal 3, UserGroup.find(user_groups(:jane_workers).id).user.id
|
81
|
+
# assert_equal users(:samantha), User.find_by_login("sam").groups.find_by_name("movers").users.first
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
#
|
86
|
+
|
data/TEST_README
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# = JoinFix Tests
|
2
|
+
# JoinFix does not have an active test suite that can be run when installed via RubyGems,
|
3
|
+
# because as you may expect, running the tests for joinfix requires that ActiveRecord can
|
4
|
+
# connect to a test database. Additionally, the tests depend on the 'gemdev' gem.
|
5
|
+
#
|
6
|
+
# If you want to run the tests, you need to do the following:
|
7
|
+
# * install gemdev ('gem install gemdev')
|
8
|
+
# * create the database and grant permissions to a test user
|
9
|
+
# * modify config.yml to reflect the database and user
|
10
|
+
#
|
11
|
+
# Execute these commands when logged into 'mysql' (assuming you're using mysql on localhost)
|
12
|
+
#
|
13
|
+
# create database joinfix;
|
14
|
+
# grant all on joinfix.* to 'ruby'@'localhost' identified by 'rubypass'
|
15
|
+
#
|
16
|
+
# Now you can run the test from the command line using:
|
17
|
+
#
|
18
|
+
# % rake test
|
19
|
+
#
|
20
|
+
# = Rails Tests
|
21
|
+
# JoinFix is intended to be used in Ruby on Rails. I've set up a rails project for
|
22
|
+
# testing, complete with models, migrations, fixtures, and the appropriate tests.
|
23
|
+
#
|
24
|
+
# The rails tests require a '_development' and '_test' database to connect to.
|
25
|
+
#
|
26
|
+
# To setup the databases:
|
27
|
+
# * create the databases and grant permissions to a test user
|
28
|
+
# * modify rails/config/database.yml to reflect the databases and user
|
29
|
+
# * migrate the table schema into the development database
|
30
|
+
#
|
31
|
+
# Execute these commands when logged into 'mysql' (assuming you're using mysql on localhost)
|
32
|
+
#
|
33
|
+
# create database joinfix_development;
|
34
|
+
# grant all on joinfix_development.* to 'ruby'@'localhost' identified by 'rubypass';
|
35
|
+
#
|
36
|
+
# create database joinfix_test;
|
37
|
+
# grant all on joinfix_test.* to 'ruby'@'localhost' identified by 'rubypass';
|
38
|
+
#
|
39
|
+
# Subsequently you need to migrate the database specification into these tables
|
40
|
+
# and run the tests. From the command line, in the rails directory:
|
41
|
+
#
|
42
|
+
# % rake db:migrate
|
43
|
+
# % rake test
|
44
|
+
#
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Additional classes to make Fixture behave like a Hash. Required for resolving joins.
|
2
|
+
#
|
3
|
+
# Just off of trivial because the internal data structure for Fixture can be a Hash or
|
4
|
+
# a YAML::Omap
|
5
|
+
class Fixture
|
6
|
+
def has_key?(key)
|
7
|
+
# should work for Hash and Omap
|
8
|
+
@fixture.keys.include?(key)
|
9
|
+
end
|
10
|
+
|
11
|
+
def each_pair(&block)
|
12
|
+
if @fixture.respond_to?(:each_pair)
|
13
|
+
# for Hash
|
14
|
+
@fixture.each_pair(&block)
|
15
|
+
else
|
16
|
+
# for Omap
|
17
|
+
@fixture.map { |k, v| yield(k,v) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete_if(&block)
|
22
|
+
# should work for Hash and Omap
|
23
|
+
@fixture.delete_if(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, value)
|
27
|
+
@fixture[key] = value
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
class Fixtures
|
2
|
+
attr_reader :join_configs, :templates
|
3
|
+
|
4
|
+
alias join_fix_original_insert_fixtures insert_fixtures
|
5
|
+
def insert_fixtures
|
6
|
+
Fixtures.template_all_loaded_fixtures if templates.nil?
|
7
|
+
join_fix_original_insert_fixtures
|
8
|
+
end
|
9
|
+
|
10
|
+
@@entry_stack = []
|
11
|
+
|
12
|
+
def template_fixtures
|
13
|
+
return unless templates.nil?
|
14
|
+
|
15
|
+
begin
|
16
|
+
configure
|
17
|
+
rescue
|
18
|
+
raise "\nError configuring fixtures for: #{@class_name}\n#{$!.message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
extract_templates
|
22
|
+
|
23
|
+
begin
|
24
|
+
templates.each_pair do |entry_name, entry|
|
25
|
+
make_entry(entry_name, entry, true)
|
26
|
+
end
|
27
|
+
rescue
|
28
|
+
entry_str = "Entry:\n" + @@entry_stack.last.to_yaml
|
29
|
+
raise "Error making entry for: '#{@class_name}'\n#{entry_str}#{$!.message}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def offset
|
34
|
+
0
|
35
|
+
# FUTURE!: is this even needed? I don't think so given that all rows are
|
36
|
+
# deleted by delete_existing_fixtures... not sure.
|
37
|
+
# @connection. QUERY FOR NEXT ID
|
38
|
+
end
|
39
|
+
|
40
|
+
def each_pair(&block)
|
41
|
+
map { |entry_name, fixture| yield(entry_name, fixture) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def config
|
45
|
+
configure unless @config
|
46
|
+
@config
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def configure
|
52
|
+
klass = @class_name.constantize
|
53
|
+
file_base = @class_name.underscore.singularize
|
54
|
+
|
55
|
+
# default attributes, constructed from reflecting on the active record table
|
56
|
+
attributes = {}
|
57
|
+
klass.columns.each do |column|
|
58
|
+
next if JoinFix.preserves?(column.name)
|
59
|
+
attributes[column.name] = column.default
|
60
|
+
end
|
61
|
+
|
62
|
+
# default associations, constructed from reflecting on the active record class
|
63
|
+
associations = {}
|
64
|
+
klass.reflect_on_all_associations.each do |association|
|
65
|
+
begin
|
66
|
+
config = {:macro => association.macro}.merge(association.options)
|
67
|
+
|
68
|
+
# get the child table name either by reflecting on the association class
|
69
|
+
config[:class_name] ||= association.class_name
|
70
|
+
config[:table_name] = config[:class_name].constantize.table_name unless config[:polymorphic]
|
71
|
+
|
72
|
+
associations[association.name] = config
|
73
|
+
rescue
|
74
|
+
# known failure retrieving class name for has_many :through if the join model doesn't
|
75
|
+
# have a 'belongs_to' pointing to the source model
|
76
|
+
raise "Could not reflect on association: #{association.name}\n" +
|
77
|
+
"Check that your join models are properly configured.\n" +
|
78
|
+
$!.message
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# merge the defaults with the file configurations
|
83
|
+
@config = {
|
84
|
+
'class_name' => klass.class_name,
|
85
|
+
'table_name' => klass.table_name,
|
86
|
+
'attributes' => attributes,
|
87
|
+
'associations' => associations
|
88
|
+
# FUTURE! bring back if you start running methods
|
89
|
+
#'modules' => [JoinFix, "#{@class_name}Template"]
|
90
|
+
}.with_indifferent_access
|
91
|
+
|
92
|
+
@join_configs = {}.with_indifferent_access
|
93
|
+
parent_config = @config
|
94
|
+
associations.each_pair do |association, child_config|
|
95
|
+
# define shared parameters
|
96
|
+
join_config = {
|
97
|
+
:parent_table => parent_config[:table_name],
|
98
|
+
:child_table => child_config[:table_name],
|
99
|
+
:macro => child_config[:macro]
|
100
|
+
}.with_indifferent_access
|
101
|
+
|
102
|
+
case join_config[:macro].to_sym
|
103
|
+
when :belongs_to
|
104
|
+
if child_config[:polymorphic]
|
105
|
+
join_config[:polymorphic] = true
|
106
|
+
join_config[:associable] = association.to_s
|
107
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{association}_id"
|
108
|
+
join_config.delete(:child_table) # not necessary, but reflects the fact that the polymorphic entry must specify this directly
|
109
|
+
else
|
110
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{join_config[:child_table].singularize}_id"
|
111
|
+
end
|
112
|
+
when :has_one
|
113
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{join_config[:parent_table].singularize}_id"
|
114
|
+
when :has_many
|
115
|
+
if child_config[:as]
|
116
|
+
join_config[:as] = child_config[:as]
|
117
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{child_config[:as]}_id"
|
118
|
+
join_config[:parent_class] = parent_config[:class_name]
|
119
|
+
elsif child_config[:through]
|
120
|
+
join_config[:through] = child_config[:through]
|
121
|
+
join_config[:join_table] = associations[child_config[:through]][:table_name]
|
122
|
+
join_config[:foreign_key] = "#{join_config[:parent_table].singularize}_id"
|
123
|
+
join_config[:association_foreign_key] = "#{join_config[:child_table].singularize}_id"
|
124
|
+
else
|
125
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{join_config[:parent_table].singularize}_id"
|
126
|
+
end
|
127
|
+
when :has_and_belongs_to_many
|
128
|
+
join_config[:join_table] = child_config[:join_table] || (join_config[:parent_table] < join_config[:child_table] ? "#{join_config[:parent_table]}_#{join_config[:child_table]}" : "#{join_config[:child_table]}_#{join_config[:parent_table]}") # echos the way join tables are guessed by ActiveRecord
|
129
|
+
join_config[:foreign_key] = child_config[:foreign_key] || "#{join_config[:parent_table].singularize}_id"
|
130
|
+
join_config[:association_foreign_key] = child_config[:association_foreign_key] || "#{join_config[:child_table].singularize}_id"
|
131
|
+
else
|
132
|
+
raise ArgumentError, "Unknown join type '#{join_config[:macro]}'."
|
133
|
+
end
|
134
|
+
|
135
|
+
join_config[:attributes] = Fixtures.fixture(join_config[:join_table]).config[:attributes] if join_config.has_key?(:join_table)
|
136
|
+
@join_configs[association] = join_config
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def extract_templates
|
141
|
+
# fixture is a template if it contains an association
|
142
|
+
@templates = {}
|
143
|
+
associations = config[:associations].keys
|
144
|
+
each do |entry_name, fixture|
|
145
|
+
#next if key.to_s !~ /[=|!]$/ # FUTURE! bring back if you start running methods
|
146
|
+
entry = fixture.to_hash
|
147
|
+
next if (entry.keys & associations).empty?
|
148
|
+
@templates[entry_name] = entry
|
149
|
+
end
|
150
|
+
delete_if do |entry_name, fixture|
|
151
|
+
@templates.has_key?(entry_name)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def make_entry(entry_name, entry, add_entry_on_complete)
|
156
|
+
@@entry_stack << entry.dup
|
157
|
+
|
158
|
+
attributes = config[:attributes]
|
159
|
+
#modules = config[:modules] # FUTURE! bring back if you start running methods
|
160
|
+
|
161
|
+
entry = attributes.merge(entry)
|
162
|
+
entry.extend JoinFix
|
163
|
+
entry.entry_name = entry_name
|
164
|
+
|
165
|
+
# FUTURE! bring back if you start running methods
|
166
|
+
#modules.each do |module_name|
|
167
|
+
# mod = get_module(module_name, module_options)
|
168
|
+
# template.extend(mod) if mod
|
169
|
+
#end
|
170
|
+
|
171
|
+
# extract templates for entries that will be joined to the current entry
|
172
|
+
# Associated entries are indicated by any key undeclared in attributes, that also does not
|
173
|
+
# match one of the reseved key patterns... ie 'id' or keys ending in '_id'. Additionally includes
|
174
|
+
# join references, which will all be arrays.
|
175
|
+
associated_entries = entry.extract_unless(attributes.keys) do |key, value|
|
176
|
+
JoinFix.preserves?(key) || key.kind_of?(Array)
|
177
|
+
end
|
178
|
+
|
179
|
+
# also extract templates for entries that have an array value. These entries indicate a set of
|
180
|
+
# associated entries that should each be joined to the current entry
|
181
|
+
associated_entries.merge( entry.extract_if { |key, value| value.kind_of?(Array) } )
|
182
|
+
associated_entries.each_pair do |association, association_templates|
|
183
|
+
# ensure that association_templates is an array of template
|
184
|
+
association_templates = [association_templates] unless association_templates.kind_of?(Array)
|
185
|
+
|
186
|
+
# translate the association configuration into a join configuration
|
187
|
+
join_config = join_configs[association]
|
188
|
+
unless join_config
|
189
|
+
raise ArgumentError, "Unknown association '#{association}' for '#{table_name}'."
|
190
|
+
end
|
191
|
+
|
192
|
+
join_table = join_config[:join_table]
|
193
|
+
macro = join_config[:macro]
|
194
|
+
|
195
|
+
# pull the polymorphic association class out of the entry -- polymorphic joins cannot determine
|
196
|
+
# beforehand what table they will join to. The associable_type MUST be specified in the fixture.
|
197
|
+
if join_config[:polymorphic] == true
|
198
|
+
join_config = join_config.dup
|
199
|
+
|
200
|
+
associable_type = "#{join_config[:associable]}_type"
|
201
|
+
unless entry.has_key?(associable_type)
|
202
|
+
raise ArgumentError, "Polymorphic type '#{associable_type}' missing in entry '#{entry_name}' for '#{table_name}'."
|
203
|
+
end
|
204
|
+
|
205
|
+
associable_class = entry[associable_type]
|
206
|
+
join_config[:child_class] = associable_class
|
207
|
+
join_config[:child_table] = associable_class.constantize.table_name
|
208
|
+
end
|
209
|
+
|
210
|
+
# raise an error if the macro doesn't allow for multiple joins, but multiple associated entries are specified
|
211
|
+
unless association_templates.length == 1 || JoinFix.macro_allows_multiple(macro)
|
212
|
+
raise ArgumentError, "Multiple associated entries specified for the single-association macro '#{macro}' in table '#{table_name}'."
|
213
|
+
end
|
214
|
+
|
215
|
+
association_templates.each do |template|
|
216
|
+
# template is a reference to another entry
|
217
|
+
template = {template => JoinFix.new(template)} if template.kind_of?(String)
|
218
|
+
|
219
|
+
# template is a fixture
|
220
|
+
template.each_pair do |child_name, child|
|
221
|
+
# generate the child entry
|
222
|
+
child_fixture = Fixtures.fixture(join_config[:child_table])
|
223
|
+
child = child_fixture.make_entry(child_name, child, false) unless child.kind_of?(JoinFix)
|
224
|
+
|
225
|
+
# generate the join entry
|
226
|
+
join = entry.send("join_#{macro}", child, join_config)
|
227
|
+
|
228
|
+
# add entries
|
229
|
+
# Note: join is sent through the make_entry machinery before adding... this ensures that the
|
230
|
+
# join entry will be processed according to the wizard modules specified for the join table.
|
231
|
+
child_fixture.add_entry(child_name, child)
|
232
|
+
Fixtures.fixture(join_table).make_entry(join_name(entry_name, child_name), join, true) if join
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# add the entry if flagged and return the entry
|
238
|
+
add_entry(entry_name, entry) if add_entry_on_complete
|
239
|
+
@@entry_stack.pop
|
240
|
+
entry
|
241
|
+
end
|
242
|
+
|
243
|
+
def add_entry(entry_name, entry)
|
244
|
+
# FUTURE! bring back if you start running methods
|
245
|
+
#return unless entry.addable?
|
246
|
+
|
247
|
+
# remove any attributes that are equal to their default
|
248
|
+
attributes = config[:attributes]
|
249
|
+
entry.delete_if do |attribute, value|
|
250
|
+
default = attributes[attribute]
|
251
|
+
value == default
|
252
|
+
end
|
253
|
+
|
254
|
+
# create a new fixture if one by the entry_name doesn't exist
|
255
|
+
existing = self[entry_name] ||= Fixture.new({}, config[:class_name])
|
256
|
+
|
257
|
+
# merge new data, checking for data collissions
|
258
|
+
entry.each_pair do |attribute, value|
|
259
|
+
if existing.has_key?(attribute) && existing[attribute] != value
|
260
|
+
raise ArgumentError,
|
261
|
+
"Data collision: #{@table_name}(:#{entry_name}).#{attribute}\n" +
|
262
|
+
"<#{existing[attribute]}> is not equal to\n<#{value}>"
|
263
|
+
end
|
264
|
+
existing[attribute] = value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def join_name(entry_name, child_name)
|
269
|
+
entry_name < child_name ? "#{entry_name}_#{child_name}" : "#{child_name}_#{entry_name}"
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
class Fixtures
|
2
|
+
# Class methods to tie fixture joining into the standard process for generating fixtures.
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Called by individual Fixtures instances just before they insert fixtures.
|
6
|
+
# This allows templating to occur at the correct time (ie after all fixtures
|
7
|
+
# have been loaded and configured) -- resolution of references naturally requires
|
8
|
+
# that all join tables are available.
|
9
|
+
def template_all_loaded_fixtures
|
10
|
+
all_loaded_fixtures.each_pair do |table_name, fixtures|
|
11
|
+
fixtures.template_fixtures
|
12
|
+
end
|
13
|
+
# note indexing and reference resolution must execute after all
|
14
|
+
# fixtures have been templated because you never know which fixture(s)
|
15
|
+
# will recieve new entries. Additionally, indexing and resolution must
|
16
|
+
# run separately, because reference resolution requires the target ids
|
17
|
+
# to be set.
|
18
|
+
all_loaded_fixtures.values.each {|fixtures| index_fixtures(fixtures) }
|
19
|
+
all_loaded_fixtures.values.each {|fixtures| resolve_references(fixtures)}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retreives the fixture by the given table name. Raises an error if the fixture has
|
23
|
+
# not yet been loaded.
|
24
|
+
def fixture(table_name)
|
25
|
+
fixture = all_loaded_fixtures[table_name.to_s]
|
26
|
+
raise ArgumentError, "No fixture loaded for: #{table_name}" unless fixture
|
27
|
+
fixture
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def index_fixtures(fixtures)
|
33
|
+
# first find entries with an id and record the ids so that they will be skipped
|
34
|
+
skip_indicies = []
|
35
|
+
fixtures.each_pair do |name, fixture|
|
36
|
+
skip_indicies << fixture["id"].to_i if fixture.has_key?("id")
|
37
|
+
end
|
38
|
+
|
39
|
+
# next find and index entries that do not have an id defined
|
40
|
+
index = fixtures.offset
|
41
|
+
fixtures.each_pair do |name, fixture|
|
42
|
+
# skip entries that already have an id defined
|
43
|
+
next if fixture.has_key?("id")
|
44
|
+
|
45
|
+
# find the next available index
|
46
|
+
# note this must happen before the id assignment,
|
47
|
+
# in case index 1 is marked for skipping
|
48
|
+
while true
|
49
|
+
index += 1
|
50
|
+
break unless skip_indicies.include?(index)
|
51
|
+
end
|
52
|
+
|
53
|
+
fixture["id"] = index
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def resolve_references(fixtures)
|
58
|
+
fixtures.each_pair do |name, fixture|
|
59
|
+
# search the fixture for join references
|
60
|
+
fixture.each_pair do |join_ref, join_name|
|
61
|
+
# next if the key isn't a join reference
|
62
|
+
next unless join_ref.kind_of?(Array)
|
63
|
+
|
64
|
+
foreign_key = join_ref.first
|
65
|
+
join_table_name = join_ref.last
|
66
|
+
|
67
|
+
# next if the foreign key is already defined
|
68
|
+
next if fixture.has_key?(foreign_key)
|
69
|
+
|
70
|
+
begin
|
71
|
+
# raise an error if the join table isn't loaded; the reference cannot be resolved
|
72
|
+
unless Fixtures.all_loaded_fixtures.has_key?(join_table_name)
|
73
|
+
raise ArgumentError, "The join table '#{join_table_name}' has not been loaded."
|
74
|
+
end
|
75
|
+
|
76
|
+
join_fixtures = Fixtures.all_loaded_fixtures[join_table_name]
|
77
|
+
|
78
|
+
# raise an error if the join entry isn't in the join table; the reference cannot be resolved
|
79
|
+
unless join_fixtures.has_key?(join_name)
|
80
|
+
raise ArgumentError, "The join entry '#{join_name}' doesn't exist in '#{join_table_name}'."
|
81
|
+
end
|
82
|
+
|
83
|
+
join_entry = join_fixtures[join_name]
|
84
|
+
|
85
|
+
# raise an exception if a join_id was not found
|
86
|
+
unless join_entry.has_key?("id")
|
87
|
+
raise ArgumentError, "No id present in join entry '#{join_name}'."
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
raise ArgumentError, "Cannot resolve reference '#{join_reference} => #{join_name}' in '#{@table_name}.#{name}'. #{$!}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# set the join id
|
94
|
+
fixture[foreign_key] = join_entry["id"]
|
95
|
+
end
|
96
|
+
|
97
|
+
# delete the join references
|
98
|
+
fixture.delete_if {|key,value| key.kind_of?(Array) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|