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