joinfix 0.1.1 → 1.0.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.
@@ -1,27 +1,67 @@
1
+ # Fixtures objects are essentially nested hashes:
2
+ #
3
+ # # A Fixtures is a hash of entry name keys and Fixture values
4
+ # entry_name:
5
+ # # A Fixture is a hash of entry fields and values
6
+ # field: value
7
+ #
8
+ # Fixtures also tracks data required for loading entries into the database,
9
+ # such as table_name and the connection to use. See 'active_record/fixtures.rb'
10
+ # for more details.
11
+ #
12
+ # JoinFix extends Fixtures to track additional information like the ActiveRecord
13
+ # class modeling the entry data. Using reflection, Fixtures can figure out which fields
14
+ # in an entry correspond to an association. Entries with association fields are
15
+ # extracted and turned into join entries. Consider and ActiveRecord class User, with
16
+ # 'name' as an attribute and a 'groups' association...
17
+ #
18
+ # [users.yml]
19
+ # # This entry will not be extracted as a template because it defines no 'groups'
20
+ # john:
21
+ # name: John
22
+ #
23
+ # # This entry will be extracted because it defines the 'groups' association
24
+ # jane:
25
+ # name: Jane
26
+ # groups:
27
+ # - admin_group
28
+ # - workers
29
+ #
30
+ # If you loaded users.yml into a Fixtures...
31
+ # users = Fixtures.new( ...arguments to load users.yml...)
32
+ # users.to_hash # => {'john' => ..., 'jane' => ...}
33
+ # users.templates # => nil
34
+ #
35
+ # users.extract_templates
36
+ # users.to_hash # => {'john' => ...}
37
+ # users.templates # => {'jane' => ...}
38
+ #
39
+ # The entry templates are extended with JoinFix and used to create the join entries.
1
40
  class Fixtures
2
- attr_reader :klass, :attributes, :templates, :fixture_path
41
+ attr_reader :klass, :association_names, :templates, :fixture_path
3
42
 
4
43
  alias join_fix_original_initialize initialize
5
- def initialize(*args)
44
+ def initialize(*args) # :nodoc:
6
45
  join_fix_original_initialize(*args)
7
46
  @klass = Object.const_get(@class_name)
8
-
9
- # default attributes, constructed from reflecting on the active record table
10
- @attributes = {}
11
- @klass.columns.each do |column|
12
- @attributes[column.name] = column.default
13
- end
14
-
47
+ @association_names = klass.reflect_on_all_associations.collect {|assoc| assoc.name.to_s}
15
48
  @join_configs = {}
16
49
  end
17
50
 
18
51
  alias join_fix_original_insert_fixtures insert_fixtures
52
+
53
+ # Extends the default method to provide a hook for making join fixtures.
54
+ # Now insert_fixtures calls Fixtures.make_join_fixtures before calling
55
+ # the original insert_fixtures method.
19
56
  def insert_fixtures
20
- Fixtures.template_all_loaded_fixtures if templates.nil?
57
+ Fixtures.make_join_fixtures if templates.nil?
21
58
  join_fix_original_insert_fixtures
22
59
  end
23
60
 
24
- def template_fixtures
61
+ # Extracts the child entry templates then makes the entries specified
62
+ # by each template. Does not execute if templates have already been
63
+ # extracted.
64
+ def make_child_entries
25
65
  return unless templates.nil?
26
66
 
27
67
  extract_templates
@@ -30,83 +70,88 @@ class Fixtures
30
70
  end
31
71
  end
32
72
 
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
-
73
+ # Returns the join configuration (as per JoinFix.configure) for the given association.
40
74
  def join_config(assoc_name)
41
75
  @join_configs[assoc_name] ||= JoinFix.send("configure", klass, assoc_name)
42
76
  end
43
77
 
44
- def each_pair(&block)
78
+ # Iterates through each entry
79
+ def each_pair(&block)
45
80
  map { |entry_name, fixture| yield(entry_name, fixture) }
46
81
  end
47
82
 
83
+ # Returns all entries as a hash of {entry_name => entry} pairs
48
84
  def to_hash
49
85
  hash = {}
50
- each_pair do |name, fixture|
51
- hash[name] = fixture.to_hash
86
+ each_pair do |entry_name, entry|
87
+ hash[entry_name] = entry.to_hash
52
88
  end
53
89
  hash
54
90
  end
91
+
92
+ # The initial offset for indexing (ie 0 so that indexing starts at 1).
93
+ # This method is provided as a hook for overrides if necessary.
94
+ def primary_key_offset
95
+ 0
96
+ end
55
97
 
56
98
  protected
57
99
 
58
- def extract_templates
59
- # fixture is a template if it contains an association
100
+ # Extracts template entries. Entries are Fixture objects, essentially a hash of {attribute => value} pairs.
101
+ # An entry is considered a template if it contains an association attribute. If this is true, then the entry
102
+ # is removed from self and stored as a template.
103
+ def extract_templates # :nodoc:
60
104
  @templates = {}
61
- assoc_names = klass.reflect_on_all_associations.collect {|assoc| assoc.name.to_s}
62
- each do |entry_name, fixture|
105
+
106
+ # iterate over all entries looking for templates
107
+ each do |entry_name, fixture|
63
108
  entry = fixture.to_hash
64
- next if (entry.keys & assoc_names).empty?
109
+
110
+ # fixture is a template if it contains an association
111
+ next if (entry.keys & association_names).empty?
65
112
  @templates[entry_name] = entry
66
113
  end
114
+
115
+ # remove template entries
67
116
  delete_if do |entry_name, fixture|
68
117
  @templates.has_key?(entry_name)
69
118
  end
70
119
  end
71
120
 
72
- def make_entry(entry_name, entry_template, add_entry_on_complete)
73
- raise NoEntryNameError.new(self, entry_name, entry_template) unless entry_template.kind_of?(Hash)
121
+ # Makes the specified entry and adds the entry to self if add_entry_on_complete is true.
122
+ def make_entry(entry_name, entry, add_entry_on_complete) # :nodoc:
123
+
124
+ # If the entry is not a hash, then no entry name was specified in the fixture file
125
+ unless entry.kind_of?(Hash)
126
+ raise NoEntryNameError.new(self, entry_name, entry)
127
+ end
74
128
 
75
- entry = attributes.merge(entry_template)
76
129
  entry.extend JoinFix
77
130
  entry.entry_name = entry_name
78
131
 
79
- # extract templates for entries that will be joined to the current entry
80
- # Associated entries are indicated by any key undeclared in attributes, that also does not
81
- # match one of the reseved key patterns... ie 'id' or keys ending in '_id'. Additionally includes
82
- # join references, which will all be arrays.
83
- assoc_entries = entry.extract_unless(attributes.keys) do |key, value|
84
- key == "id" || key.kind_of?(Array)
85
- end
86
-
87
- # also extract templates for entries that have an array value. These entries indicate a set of
88
- # associated entries that should each be joined to the current entry
89
- assoc_entries.merge( entry.extract_if { |key, value| value.kind_of?(Array) } )
90
- assoc_entries.each_pair do |assoc_name, assoc_array|
91
- # ensure that assoc_array is an array of template
92
- assoc_array = [assoc_array] unless assoc_array.kind_of?(Array)
132
+ # extract and construct associated entries
133
+ associated_entries = entry.extract_if(association_names)
134
+ associated_entries.each_pair do |assoc_name, associations|
135
+ # ensure that associations is an array
136
+ associations = [associations] unless associations.kind_of?(Array)
93
137
 
94
- # translate the association configuration into a join configuration
138
+ # get the join configuration for the association
95
139
  join_config = join_config(assoc_name)
96
140
  join_table = join_config[:join_table]
97
141
  macro = join_config[:macro]
98
142
 
99
- # pull the polymorphic association class out of the entry -- polymorphic joins cannot determine
100
- # beforehand what table they will join to. The associable_type MUST be specified in the fixture.
143
+ # pull the polymorphic join information out of the entry -- polymorphic joins cannot determine
144
+ # beforehand what table they will join to; the associable_type MUST be specified in the entry.
101
145
  if join_config[:polymorphic] == true
102
- join_config = join_config.dup
146
+ # be sure to preserve the base configuration...
147
+ join_config = join_config.dup
103
148
 
104
149
  associable_type = join_config[:associable_type]
105
150
  associable_class = entry[associable_type]
106
151
 
107
152
  unless associable_class
108
- raise MissingPolymorphicTypeError.new(self, entry_name, entry_template,
109
- "No <#{associable_type}> was specified.")
153
+ raise MissingPolymorphicTypeError.new(self, entry_name, entry.merge(associated_entries),
154
+ "No <#{associable_type}> was specified.")
110
155
  end
111
156
 
112
157
  join_config[:child_class] = associable_class
@@ -114,16 +159,16 @@ class Fixtures
114
159
  end
115
160
 
116
161
  # raise an error if the macro doesn't allow for multiple joins, but multiple associated entries are specified
117
- unless assoc_array.length == 1 || JoinFix.macro_allows_multiple(macro)
118
- raise MultipleChildrenError.new(self, entry_name, entry_template,
162
+ unless associations.length == 1 || JoinFix.macro_allows_multiple(macro)
163
+ raise MultipleChildrenError.new(self, entry_name, entry.merge(associated_entries),
119
164
  "Multiple joined entries specified for the single-join macro '#{macro}'.")
120
165
  end
121
166
 
122
- assoc_array.each do |template|
123
- # template is a reference to another entry
167
+ associations.each do |template|
168
+ # String templates are references to another entry. Make a dummy template with a dummy JoinFix.
124
169
  template = {template => JoinFix.new(template)} if template.kind_of?(String)
125
170
 
126
- # template is a fixture
171
+ # Make the entries specified in the template
127
172
  template.each_pair do |child_name, child|
128
173
  # generate the child entry
129
174
  child_fixture = Fixtures.fixture(join_config[:child_table])
@@ -131,12 +176,10 @@ class Fixtures
131
176
 
132
177
  # generate the join entry
133
178
  join = entry.send("join_#{macro}", child, join_config)
134
-
135
- # add entries
136
- # Note: join is sent through the make_entry machinery before adding... this ensures that the
137
- # join entry will be processed according to the wizard modules specified for the join table.
179
+ Fixtures.fixture(join_table).make_entry(join.entry_name, join, true) if join
180
+
181
+ # add the child entry
138
182
  child_fixture.add_entry(child_name, child)
139
- Fixtures.fixture(join_table).make_entry(join.entry_name, join, true) if join
140
183
  end
141
184
  end
142
185
  end
@@ -145,17 +188,11 @@ class Fixtures
145
188
  add_entry(entry_name, entry) if add_entry_on_complete
146
189
  entry
147
190
  end
148
-
149
- def add_entry(entry_name, entry)
150
- # FUTURE! bring back if you start running methods
151
- #return unless entry.addable?
152
-
153
- # remove any attributes that are equal to their default
154
- entry.delete_if do |attribute, value|
155
- default = attributes[attribute]
156
- value == default
157
- end
158
-
191
+
192
+ # Adds the specified entry to self, or merges entry attributes into the
193
+ # existing entry. Raises an error in the case of an entry attribute collision
194
+ # (ie an existing attribute does not equal the new attribute).
195
+ def add_entry(entry_name, entry) # :nodoc:
159
196
  # create a new fixture if one by the entry_name doesn't exist
160
197
  existing = self[entry_name] ||= Fixture.new({}, klass.class_name)
161
198
 
@@ -169,78 +206,3 @@ class Fixtures
169
206
  end
170
207
  end
171
208
  end
172
-
173
- class MakeEntryError < RuntimeError # :nodoc:
174
- attr_reader :fixtures, :entry_name, :entry, :msg
175
- attr_accessor :advice
176
-
177
- def initialize(fixtures, entry_name, entry, msg=nil)
178
- @fixtures = fixtures
179
- @entry_name = entry_name
180
- @entry = entry
181
- @msg =msg
182
- end
183
-
184
- def message
185
- "Error making <#{fixtures.klass.table_name}(:#{entry_name})> in <#{fixtures.fixture_path}>.\n" +
186
- {entry_name => entry}.to_yaml +
187
- (msg.nil? ? '' : "\n#{msg}\n") +
188
- (advice.nil? ? '' : "#{advice}\n")
189
- end
190
- end
191
-
192
- class EntryCollisionError < MakeEntryError # :nodoc:
193
- end
194
-
195
- class NoEntryNameError < MakeEntryError # :nodoc:
196
- def advice
197
- %Q{
198
- This error occurs when an entry is not named as in:
199
- ---
200
- dog:
201
- title: Dog
202
- author:
203
- # <an entry name like 'ferlinghetti' is missing here>
204
- full_name: Lawrence Ferlinghetti
205
- ...}
206
- end
207
- end
208
-
209
- class MultipleChildrenError < MakeEntryError # :nodoc:
210
- def advice
211
- %Q{
212
- Single entry joins should specify a single entry, not an array of entries.
213
- Use a different association if you need multiple joined entries.
214
- ---
215
- poem:
216
- title: Slave's Dream
217
- author:
218
- longfellow:
219
- full_name: Henry Wadsworth Longfellow}
220
- end
221
- end
222
-
223
- class MissingPolymorphicTypeError < MakeEntryError # :nodoc:
224
- def advice
225
- %Q{
226
- When specifying a belongs_to :polymorphic join, the type
227
- of the joined entry must be specified because it cannot be
228
- inferred from association itself. Use something like:
229
- --
230
- book_I_read:
231
- opinion: Great!
232
- readable_type: Book
233
- readable:
234
- the_jungle_books:
235
- author: Rudyard Kipling
236
- title: The Jungle Books
237
-
238
- poem_I_read:
239
- opinion: Essential!
240
- readable_type: Poem
241
- readable:
242
- sea_fever:
243
- poet: John Masefield
244
- title: Sea-Fever}
245
- end
246
- end
@@ -1,26 +1,45 @@
1
1
  class Fixtures
2
- # Class methods to tie fixture joining into the standard process for generating fixtures.
3
2
  class << self
4
3
 
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
4
+ # Makes join fixtures for all loaded fixtures. Called just prior to inserting
5
+ # fixtures, after all fixtures have been loaded.
6
+ #
7
+ # After make_join_fixtures completes, entries for tables matching ENV['joinfix_dump']
8
+ # are printed to STDOUT. You can print entries for each loaded fixture by setting
9
+ # ENV['joinfix_dump'] = 'true'. By default, no entries are printed.
10
+ #
11
+ # Note: printing does not affect the creation or loading of fixtures.
12
+ def make_join_fixtures
10
13
  all_loaded_fixtures.each_pair do |table_name, fixtures|
11
- fixtures.template_fixtures
14
+ fixtures.make_child_entries
12
15
  end
16
+
13
17
  # note indexing and reference resolution must execute after all
14
18
  # fixtures have been templated because you never know which fixture(s)
15
19
  # will recieve new entries. Additionally, indexing and resolution must
16
- # run separately, because reference resolution requires the target ids
17
- # to be set.
20
+ # run separately, because reference resolution requires the primary key
21
+ # be set for the referenced entry.
18
22
  all_loaded_fixtures.values.each {|fixtures| index_fixtures(fixtures) }
19
23
  all_loaded_fixtures.values.each {|fixtures| resolve_references(fixtures)}
24
+
25
+ # Print entries to STDOUT for review, if applicable
26
+ joinfix_dump = ENV['joinfix_dump']
27
+ if joinfix_dump
28
+ print_all = (joinfix_dump.to_s =~ /^true$/i)
29
+ regexp = Regexp.new(joinfix_dump.to_s)
30
+
31
+ all_loaded_fixtures.values.each do |fixtures|
32
+ table_name = fixtures.klass.table_name
33
+ next unless print_all || table_name =~ regexp
34
+
35
+ puts "------- #{fixtures.klass.table_name} --------"
36
+ puts fixtures.to_hash.to_yaml.gsub(/^---/, '') + "\n"
37
+ end
38
+ end
20
39
  end
21
40
 
22
- # Retreives the fixture by the given table name. Raises an error if the fixture has
23
- # not yet been loaded.
41
+ # Retreives the Fixtures for the given table name. Raises an error if this Fixtures
42
+ # has not yet been loaded.
24
43
  def fixture(table_name)
25
44
  fixture = all_loaded_fixtures[table_name.to_s]
26
45
  raise MissingFixtureError.new(table_name) unless fixture
@@ -29,26 +48,25 @@ class Fixtures
29
48
 
30
49
  protected
31
50
 
32
- # Sets the primary key in each of the input fixtures, beginning one-past fixtures.offset
33
- # (generally this is 1). If a fixture already has an id assigned, it will be skipped.
34
- def index_fixtures(fixtures)
51
+ # Sets the primary key for entries in each of the input Fixtures, beginning
52
+ # one past the Fixtures primary key offset (ie 1). Skips entries that already
53
+ # have a primary key assigned.
54
+ def index_fixtures(fixtures) # :nodoc:
35
55
  id = fixtures.klass.primary_key
36
56
 
37
- # first find entries with an id and record the ids so that they will be skipped
57
+ # find entries with the primary key and record these so that they will be skipped
38
58
  skip_indicies = []
39
59
  fixtures.each_pair do |name, fixture|
40
60
  skip_indicies << fixture[id].to_i if fixture.has_key?(id)
41
61
  end
42
62
 
43
- # next find and index entries that do not have an id defined
44
- index = fixtures.offset
63
+ # find and index entries that do not have a primary key defined
64
+ index = fixtures.primary_key_offset
45
65
  fixtures.each_pair do |name, fixture|
46
- # skip entries that already have an id defined
47
66
  next if fixture.has_key?(id)
48
67
 
49
- # find the next available index
50
- # note this must happen before the id assignment,
51
- # in case index 1 is marked for skipping
68
+ # find the next available index. Note this must happen before
69
+ # the assignment, in case '1' is marked for skipping
52
70
  while true
53
71
  index += 1
54
72
  break unless skip_indicies.include?(index)
@@ -58,24 +76,17 @@ class Fixtures
58
76
  end
59
77
  end
60
78
 
61
- # Resolves each join reference in the input fixtures
62
- def resolve_references(fixtures)
63
- fixtures.each_pair do |entry_name, fixture|
64
- # search the fixture for join references
65
- fixture.each_pair do |join_ref, join_name|
79
+ # Resolves join references in the input Fixtures.
80
+ def resolve_references(fixtures) # :nodoc:
81
+ fixtures.each_pair do |entry_name, entry|
82
+ entry.each_pair do |join_ref, join_name|
66
83
  # next if the key isn't a join reference
67
84
  next unless join_ref.kind_of?(Array)
68
85
 
69
86
  foreign_key = join_ref.first
70
87
  join_table_name = join_ref.last
71
-
72
- # raise an error if the join table isn't loaded; the reference cannot be resolved
73
- unless Fixtures.all_loaded_fixtures.has_key?(join_table_name)
74
- raise ResolveJoinReferenceError.new(fixtures, entry_name, join_table_name, join_name,
75
- "The join table '#{join_table_name}' has not been loaded.")
76
- end
77
-
78
- join_fixtures = Fixtures.all_loaded_fixtures[join_table_name]
88
+
89
+ join_fixtures = Fixtures.fixture(join_table_name)
79
90
  id = join_fixtures.klass.primary_key
80
91
 
81
92
  # raise an error if the join entry isn't in the join table; the reference cannot be resolved
@@ -86,82 +97,25 @@ class Fixtures
86
97
 
87
98
  join_entry = join_fixtures[join_name]
88
99
 
89
- # raise an exception if a join_id was not found
100
+ # raise an exception if the primary key for the join entry is not set
90
101
  unless join_entry.has_key?(id)
91
102
  raise ResolveJoinReferenceError.new(fixtures, entry_name, join_table_name, join_name,
92
103
  "No #{id} present in join entry '#{join_name}'.")
93
104
  end
94
105
 
95
106
  # raise an exception if the foreign key is already set
96
- unless fixture[foreign_key].nil?
107
+ unless entry[foreign_key].nil?
97
108
  raise ForeignKeySetError.new(fixtures, entry_name, join_table_name, join_name,
98
109
  "Foreign key <#{foreign_key}> is already set!")
99
110
  end
100
111
 
101
- # set the join id
102
- fixture[foreign_key] = join_entry[id]
112
+ # set the foreign key to the joined entry primary key
113
+ entry[foreign_key] = join_entry[id]
103
114
  end
104
115
 
105
116
  # delete the join references
106
- fixture.delete_if {|key,value| key.kind_of?(Array) }
117
+ entry.delete_if {|key,value| key.kind_of?(Array) }
107
118
  end
108
119
  end
109
120
  end
110
- end
111
-
112
- class MissingFixtureError < RuntimeError # :nodoc:
113
- attr_reader :table_name
114
- def initialize(table_name)
115
- @table_name = table_name
116
- end
117
-
118
- def message
119
- "No fixture loaded for <#{table_name}>\n" +
120
- (advice.nil? ? '' : "#{advice}\n")
121
- end
122
-
123
- def advice
124
- %Q{}
125
- end
126
- end
127
-
128
- class ResolveJoinReferenceError < RuntimeError # :nodoc:
129
- attr_reader :fixtures, :entry_name, :join_table_name, :join_name, :msg
130
- attr_accessor :advice
131
-
132
- def initialize(fixtures, entry_name, join_table_name, join_name, msg=nil)
133
- @fixtures = fixtures
134
- @entry_name = entry_name
135
- @join_table_name = join_table_name
136
- @join_name = join_name
137
- @msg =msg
138
- end
139
-
140
- def message
141
- "Cannot resolve reference to <#{join_table_name}(:#{join_name})> " +
142
- "for <#{fixtures.klass.table_name}(:#{entry_name})> " +
143
- "in <#{fixtures.fixture_path}>.\n" +
144
- (msg.nil? ? '' : "\n#{msg}\n") +
145
- (advice.nil? ? '' : "#{advice}\n")
146
- end
147
- end
148
-
149
- class ForeignKeySetError < ResolveJoinReferenceError # :nodoc:
150
- def advice
151
- %Q{
152
- This error occurs when you specifiy the foreign key, as well as a join entry.
153
- ---
154
- poem:
155
- title: Poetry of Departures
156
- author_id: 8
157
- author: larkin
158
-
159
- If you need to specify the foreign key, do so within the entry.
160
- ---
161
- poem:
162
- title: Poetry of Departures
163
- author:
164
- larkin:
165
- id: 8}
166
- end
167
- end
121
+ end