joinfix 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +86 -0
  3. data/TEST_README +44 -0
  4. data/lib/joinfix/fixture.rb +29 -0
  5. data/lib/joinfix/fixtures.rb +271 -0
  6. data/lib/joinfix/fixtures_class.rb +102 -0
  7. data/lib/joinfix.rb +208 -0
  8. data/rails/README +182 -0
  9. data/rails/Rakefile +10 -0
  10. data/rails/app/controllers/application.rb +7 -0
  11. data/rails/app/helpers/application_helper.rb +3 -0
  12. data/rails/app/models/group.rb +4 -0
  13. data/rails/app/models/inner_child.rb +4 -0
  14. data/rails/app/models/nested_child.rb +4 -0
  15. data/rails/app/models/nested_parent.rb +4 -0
  16. data/rails/app/models/user.rb +4 -0
  17. data/rails/app/models/user_group.rb +5 -0
  18. data/rails/config/boot.rb +45 -0
  19. data/rails/config/database.yml +36 -0
  20. data/rails/config/environment.rb +60 -0
  21. data/rails/config/environments/development.rb +21 -0
  22. data/rails/config/environments/production.rb +18 -0
  23. data/rails/config/environments/test.rb +19 -0
  24. data/rails/config/routes.rb +23 -0
  25. data/rails/db/migrate/001_create_nested_parents.rb +12 -0
  26. data/rails/db/migrate/002_create_nested_children.rb +12 -0
  27. data/rails/db/migrate/003_create_inner_children.rb +12 -0
  28. data/rails/db/migrate/004_create_users.rb +11 -0
  29. data/rails/db/migrate/005_create_groups.rb +11 -0
  30. data/rails/db/migrate/006_create_user_groups.rb +12 -0
  31. data/rails/db/schema.rb +35 -0
  32. data/rails/doc/README_FOR_APP +2 -0
  33. data/rails/public/404.html +30 -0
  34. data/rails/public/500.html +30 -0
  35. data/rails/public/dispatch.cgi +10 -0
  36. data/rails/public/dispatch.fcgi +24 -0
  37. data/rails/public/dispatch.rb +10 -0
  38. data/rails/public/favicon.ico +0 -0
  39. data/rails/public/images/rails.png +0 -0
  40. data/rails/public/index.html +277 -0
  41. data/rails/public/javascripts/application.js +2 -0
  42. data/rails/public/javascripts/controls.js +833 -0
  43. data/rails/public/javascripts/dragdrop.js +942 -0
  44. data/rails/public/javascripts/effects.js +1088 -0
  45. data/rails/public/javascripts/prototype.js +2515 -0
  46. data/rails/public/robots.txt +1 -0
  47. data/rails/script/about +3 -0
  48. data/rails/script/breakpointer +3 -0
  49. data/rails/script/console +3 -0
  50. data/rails/script/destroy +3 -0
  51. data/rails/script/generate +3 -0
  52. data/rails/script/performance/benchmarker +3 -0
  53. data/rails/script/performance/profiler +3 -0
  54. data/rails/script/plugin +3 -0
  55. data/rails/script/process/inspector +3 -0
  56. data/rails/script/process/reaper +3 -0
  57. data/rails/script/process/spawner +3 -0
  58. data/rails/script/runner +3 -0
  59. data/rails/script/server +3 -0
  60. data/rails/test/fixtures/groups.yml +3 -0
  61. data/rails/test/fixtures/inner_children.yml +2 -0
  62. data/rails/test/fixtures/nested_children.yml +2 -0
  63. data/rails/test/fixtures/nested_parents.yml +33 -0
  64. data/rails/test/fixtures/user_groups.yml +0 -0
  65. data/rails/test/fixtures/users.yml +20 -0
  66. data/rails/test/test_helper.rb +29 -0
  67. data/rails/test/unit/group_test.rb +10 -0
  68. data/rails/test/unit/inner_child_test.rb +10 -0
  69. data/rails/test/unit/nested_child_test.rb +10 -0
  70. data/rails/test/unit/nested_parent_test.rb +24 -0
  71. data/rails/test/unit/user_group_test.rb +10 -0
  72. data/rails/test/unit/user_test.rb +12 -0
  73. data/test/belongs_to_test.rb +77 -0
  74. data/test/config.yml +5 -0
  75. data/test/fixtures/as_children.yml +0 -0
  76. data/test/fixtures/bt_children.yml +0 -0
  77. data/test/fixtures/bt_parents.yml +30 -0
  78. data/test/fixtures/habtm_children.yml +0 -0
  79. data/test/fixtures/habtm_children_habtm_parents.yml +0 -0
  80. data/test/fixtures/habtm_joins.yml +0 -0
  81. data/test/fixtures/habtm_parents.yml +18 -0
  82. data/test/fixtures/hm_children.yml +0 -0
  83. data/test/fixtures/hm_joins.yml +0 -0
  84. data/test/fixtures/hm_parents.yml +34 -0
  85. data/test/fixtures/ho_children.yml +0 -0
  86. data/test/fixtures/ho_parents.yml +14 -0
  87. data/test/fixtures/inner_children.yml +2 -0
  88. data/test/fixtures/nested_children.yml +0 -0
  89. data/test/fixtures/nested_parents.yml +33 -0
  90. data/test/fixtures/no_join_fixes.yml +4 -0
  91. data/test/fixtures/omap_no_join_fixes.yml +7 -0
  92. data/test/fixtures/polymorphic_children.yml +0 -0
  93. data/test/has_and_belongs_to_many_test.rb +72 -0
  94. data/test/has_many_test.rb +97 -0
  95. data/test/has_one_test.rb +56 -0
  96. data/test/joinfix_test.rb +287 -0
  97. data/test/joinfix_test_helper.rb +54 -0
  98. data/test/joinfix_test_suite.rb +10 -0
  99. data/test/nested_test.rb +70 -0
  100. 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