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.
data/README CHANGED
@@ -1,86 +1,158 @@
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
- #
1
+ = JoinFix
2
+ A reflection-based solution to the fixture join problem.
86
3
 
4
+ == Description
5
+ Making fixtures for models with complex joins can be a redundant, error-prone process. JoinFix
6
+ provides a solution to this problem by letting you reference and/or define child entries inline with
7
+ their parents.
8
+
9
+ [users.yml]
10
+ john_doe:
11
+ full_name: John Doe
12
+ groups:
13
+ - admin_group # => entry reference
14
+ - devel_group: # => inline definition
15
+ name: Developers
16
+
17
+ [groups.yml]
18
+ admin_group: # => referenced entry
19
+ name: Administrators
20
+
21
+ JoinFix uses reflection on ActiveRecord associations to determine how to perform joins so
22
+ no configuration is required. Simply require joinfix and begin writing entries.
23
+
24
+ == Info
25
+
26
+ Available at {rubyforge.org/projects/joinfix}[http://rubyforge.org/projects/joinfix]
27
+
28
+ Copyright (c) 2006-2007, Regents of the University of Colorado.
29
+ Developer:: Simon Chiang, Biomolecular Structure Program
30
+ Support:: UCHSC School of Medicine Deans Academic Enrichment Fund
31
+ Licence:: MIT-Style
32
+
33
+ == Usage
34
+ Consider the following data model:
35
+
36
+ class User < ActiveRecord::Base
37
+ has_many :user_groups
38
+ has_many :groups, :through => :user_groups
39
+ end
40
+
41
+ class Group < ActiveRecord::Base
42
+ has_many :user_groups
43
+ has_many :users, :through => :user_groups
44
+ end
45
+
46
+ class UserGroup < ActiveRecord::Base
47
+ belongs_to :user
48
+ belongs_to :group
49
+ end
50
+
51
+ Write your fixtures using the naming scheme you lay out in your models.
52
+ Entries can be referenced across multiple fixture files or defined inline:
53
+
54
+ [users.yml]
55
+ john_doe:
56
+ full_name: John Doe
57
+ groups: admin_group # => reference to the 'admin_group' entry
58
+
59
+ jane_doe:
60
+ full_name: Jane Doe
61
+ groups: # => you can specify an array of entries if needed
62
+ - admin_group
63
+ - worker_group: # => inline definition of the 'worker_group' entry
64
+ name: Workers
65
+
66
+ [groups.yml]
67
+ admin_group: # => the referenced 'admin_group' entry
68
+ id: 3 # => you can (but don't have to) specify ids
69
+ name: Administrators
70
+
71
+ Join entries implied in your definition, as in a has_and_belongs_to_many association,
72
+ will be created and named by joining together the names of the parent and child,
73
+ ordered by the '<' operator. For example, the users.yml and groups.yml fixtures
74
+ produce these entries:
75
+
76
+ [users]
77
+ john_doe:
78
+ id: 1 # => primary keys are assigned to all entries (see note)
79
+ full_name: John Doe
80
+ jane_doe:
81
+ id: 2
82
+ full_name: Jane Doe
83
+
84
+ [groups]
85
+ admin_group:
86
+ id: 3
87
+ name: Administrators
88
+ worker_group:
89
+ id: 1
90
+ name: Workers
91
+
92
+ [user_groups]
93
+ admin_group_john_doe
94
+ id: 1
95
+ user_id: 1 # => references are resolved to their foreign keys
96
+ group_id: 3 # => explicitly set primary keys are respected
97
+ admin_group_jane_doe
98
+ id: 2
99
+ user_id: 2
100
+ group_id: 3
101
+ jane_doe_worker_group # => Notice the '<' operator in action
102
+ id: 3
103
+ user_id: 2
104
+ group_id: 1
105
+
106
+ Note: Primary keys are assigned to entries based on the way the entry names are hashed, ie 'john_doe' will not necessarily have id '1'. If you need a specific id for an entry, then you must explicitly set it.
107
+
108
+ If you need to add additional fields to an implied entry, simply define them in their
109
+ fixture file. All fields across all fixtures will be merged into one entry (JoinFix raises
110
+ an error in the event of a collision).
111
+
112
+ [user_groups.yml]
113
+ admin_group_john_doe:
114
+ date_added: 2007-06-12
115
+
116
+ Nesting is allowed. This will make the same entries as above:
117
+
118
+ [users.yml]
119
+ john_doe:
120
+ full_name: John Doe
121
+ groups:
122
+ admin_group:
123
+ id: 3
124
+ name: Administrators
125
+ users:
126
+ jane_doe:
127
+ full_name: Jane Doe
128
+ groups:
129
+ worker_group:
130
+ name: Workers
131
+
132
+ In your tests, require joinfix and use the fixtures exactly as you would normally.
133
+ One gotcha -- you must be sure to name all the tables for which your fixtures create entries.
134
+ In fact this is no different than normal, but it's easy to forget if you lump joins into one file.
135
+
136
+ require 'joinfix'
137
+
138
+ class UserTest < Test::Unit::TestCase
139
+ fixtures :users, :groups, :user_groups # => got to name them all!!!
140
+
141
+ def test_joinfix
142
+ assert_equal "Administrators", users(:john_doe).groups.first.name
143
+ assert_equal 2, User.find_by_full_name("jane_doe").groups.count
144
+ assert_equal 3, UserGroup.find(user_groups(:admin_group_jane_doe).id).group.id
145
+ end
146
+ end
147
+
148
+ === Command line options
149
+
150
+ JoinFix provides some command line options through the ENV variables. Setting these
151
+ variables is easy if you're using rake[http://rake.rubyforge.org/] to run your test suite:
152
+
153
+ % rake test key=value # => sets ENV['key'] = 'value'
154
+
155
+ Available options:
156
+
157
+ format_joinfix_errors:: Unless 'false', this option causes JoinFix to simplify the console output when a JoinFixError occurs.
158
+ joinfix_dump:: Prints all entries for tables matching joinfix_dump to STDOUT upon make_join_fixtures. Prints entries for all tables if 'true'.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ # tasks
7
+ desc 'Default: Run tests.'
8
+ task :default => :test
9
+
10
+ desc 'Run tests.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.pattern = File.join('test', ENV['subset'] || '', ENV['pattern'] || '**/*_test.rb')
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Generate documentation.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = 'JoinFixtures'
21
+ rdoc.main = 'README'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README', 'MIT-LICENSE', 'TEST_README')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
26
+
27
+ #
28
+ # Gem specification
29
+ #
30
+ if File.exists?("./gemspecs/1.0.0.gemspec")
31
+
32
+ Gem::manage_gems
33
+ spec = Gem::SourceIndex.load_specification("./gemspecs/1.0.0.gemspec")
34
+ Rake::GemPackageTask.new(spec) do |pkg|
35
+ pkg.need_tar = true
36
+ end
37
+
38
+ end
data/TEST_README CHANGED
@@ -1,44 +1,45 @@
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
- #
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 test user
10
+
11
+ These commands will create the database and grand permissions as needed, assuming
12
+ you're using mysql on localhost. Log into 'mysql', then enter:
13
+
14
+ create database joinfix;
15
+ grant all on joinfix.* to 'ruby'@'localhost' identified by 'rubypass'
16
+
17
+ Now you can run the test from the command line using:
18
+
19
+ % rake test
20
+
21
+ = Rails Tests
22
+ JoinFix is intended to integrate with Ruby on Rails. I've set up a rails project for
23
+ testing, complete with models, migrations, fixtures, and the appropriate tests.
24
+
25
+ The rails tests require a '_development' and '_test' database to connect to.
26
+
27
+ To setup the databases:
28
+ * create the databases and grant permissions to a test user
29
+ * modify rails/config/database.yml to reflect the databases and test user
30
+ * migrate the table schema into the development database
31
+
32
+ These commands will create the database and grand permissions as needed, assuming
33
+ you're using mysql on localhost. Log into 'mysql', then enter:
34
+
35
+ create database joinfix_development;
36
+ grant all on joinfix_development.* to 'ruby'@'localhost' identified by 'rubypass';
37
+
38
+ create database joinfix_test;
39
+ grant all on joinfix_test.* to 'ruby'@'localhost' identified by 'rubypass';
40
+
41
+ Subsequently you need to migrate the database specification into these tables
42
+ and run the tests. From the command line, in the rails directory:
43
+
44
+ % rake db:migrate
45
+ % rake test
@@ -0,0 +1,220 @@
1
+ # JoinFix provides help for debugging errors in your fixtures through subclasses of JoinFixError.
2
+ class JoinFixError < RuntimeError
3
+ class << self
4
+ def new(*args)
5
+ @already_created ||= {}
6
+
7
+ error = super(*args)
8
+ key = error.fixtures.respond_to?(:fixture_path) ? error.fixtures.fixture_path : error.fixtures
9
+ @already_created[key] ||= error
10
+ end
11
+ end
12
+
13
+ attr_reader :fixtures, :entry_name, :msg
14
+ attr_accessor :advice
15
+
16
+ def initialize(fixtures, entry_name, msg=nil)
17
+ @fixtures = fixtures
18
+ @entry_name = entry_name
19
+ @msg = msg
20
+ end
21
+ end
22
+
23
+ class MissingFixtureError < JoinFixError # :nodoc:
24
+ attr_reader :table_name
25
+ def initialize(table_name)
26
+ @fixtures = table_name
27
+ @table_name = table_name
28
+ end
29
+
30
+ def message
31
+ "No fixture loaded for <#{table_name}>\n" +
32
+ (advice.nil? ? '' : "#{advice}\n")
33
+ end
34
+
35
+ def advice
36
+ %Q{
37
+ Be sure you've specified all the tables for which your fixtures create entries.
38
+ ---
39
+ class UserTest < Test::Unit::TestCase
40
+ fixtures :users, :groups, :user_groups # => got to name them all!!!
41
+ end}
42
+ end
43
+ end
44
+
45
+ class MakeEntryError < JoinFixError # :nodoc:
46
+ attr_reader :entry
47
+ attr_accessor :advice
48
+
49
+ def initialize(fixtures, entry_name, entry, msg=nil)
50
+ super(fixtures, entry_name, msg)
51
+ @entry = entry
52
+ end
53
+
54
+ def message
55
+ "Error making <#{fixtures.klass.table_name}(:#{entry_name})> in <#{fixtures.fixture_path}>.\n" +
56
+ {entry_name => entry}.to_yaml +
57
+ (msg.nil? ? '' : "\n#{msg}\n") +
58
+ (advice.nil? ? '' : "#{advice}\n")
59
+ end
60
+ end
61
+
62
+ class EntryCollisionError < MakeEntryError # :nodoc:
63
+ end
64
+
65
+ class NoEntryNameError < MakeEntryError # :nodoc:
66
+ def advice
67
+ %Q{
68
+ This error occurs when an entry is not named as in:
69
+ ---
70
+ john_doe:
71
+ full_name: John Doe
72
+ groups:
73
+ # an entry name like 'admin_group' is missing here
74
+ name: Administrators
75
+ ...
76
+
77
+ Or sometimes if you have an error in your YAML:
78
+ ---
79
+ john_doe:
80
+ full_name: John Doe
81
+ groups:
82
+ - admin_group:
83
+ name: Administrators
84
+ - worker_group:
85
+ name: Workers # this field is not properly indented
86
+ ...}
87
+ end
88
+ end
89
+
90
+ class MultipleChildrenError < MakeEntryError # :nodoc:
91
+ def advice
92
+ %Q{
93
+ Single entry joins should specify a single entry, not an array of entries.
94
+ Use a different association if you need multiple joined entries.}
95
+ end
96
+ end
97
+
98
+ class MissingPolymorphicTypeError < MakeEntryError # :nodoc:
99
+ def advice
100
+ %Q{
101
+ When specifying a belongs_to :polymorphic join, the type
102
+ of the joined entry must be specified because it cannot be
103
+ inferred from association itself. Use something like:
104
+ --
105
+ book_I_read:
106
+ opinion: Great!
107
+ readable_type: Book
108
+ readable:
109
+ the_jungle_books:
110
+ author: Rudyard Kipling
111
+ title: The Jungle Books
112
+
113
+ poem_I_read:
114
+ opinion: Essential!
115
+ readable_type: Poem
116
+ readable:
117
+ sea_fever:
118
+ poet: John Masefield
119
+ title: Sea-Fever}
120
+ end
121
+ end
122
+
123
+ class ResolveJoinReferenceError < JoinFixError # :nodoc:
124
+ attr_reader :join_table_name, :join_name
125
+
126
+ def initialize(fixtures, entry_name, join_table_name, join_name, msg=nil)
127
+ super(fixtures, entry_name, msg)
128
+ @join_table_name = join_table_name
129
+ @join_name = join_name
130
+ end
131
+
132
+ def message
133
+ "Cannot resolve reference to <#{join_table_name}(:#{join_name})> " +
134
+ "for <#{fixtures.klass.table_name}(:#{entry_name})> " +
135
+ "in <#{fixtures.fixture_path}>.\n" +
136
+ (msg.nil? ? '' : "\n#{msg}\n") +
137
+ (advice.nil? ? '' : "#{advice}\n")
138
+ end
139
+ end
140
+
141
+ class ForeignKeySetError < ResolveJoinReferenceError # :nodoc:
142
+ def advice
143
+ %Q{
144
+ This error occurs when you specifiy the foreign key, as well as a join entry.
145
+ ---
146
+ poem:
147
+ title: Poetry of Departures
148
+ author_id: 8
149
+ author: larkin
150
+
151
+ If you need to specify the foreign key, do so within the entry.
152
+ ---
153
+ poem:
154
+ title: Poetry of Departures
155
+ author:
156
+ larkin:
157
+ id: 8}
158
+ end
159
+ end
160
+
161
+ require 'test/unit/ui/console/testrunner'
162
+ module Test # :nodoc:
163
+ module Unit # :nodoc:
164
+ module UI # :nodoc:
165
+ module Console # :nodoc:
166
+
167
+ # TestRunner is the Test::Unit class providing a user interface for running tests in a console.
168
+ # JoinFix modifies this class to provide a nicer output when JoinFix fails. Without these
169
+ # modifications a single error causes the test output to explode into a meaningless jumble,
170
+ # once for every test in a suite.
171
+ #
172
+ # Turn off the modifications by setting ENV['format_joinfix_errors'] = 'false', for instance by using:
173
+ #
174
+ # % rake test format_joinfix_errors=false
175
+ #
176
+ class TestRunner
177
+ private
178
+
179
+ alias join_fix_original_finished finished
180
+
181
+ def finished(elapsed_time)
182
+ if ENV['format_joinfix_errors'].to_s =~ /^false$/i
183
+ join_fix_original_finished(elapsed_time)
184
+ return
185
+ end
186
+
187
+ nl
188
+ output("Finished in #{elapsed_time} seconds.")
189
+
190
+ joinfix_faults = {}
191
+ @faults.each_with_index do |fault, index|
192
+ if fault.kind_of?(Test::Unit::Error) && fault.exception.kind_of?(JoinFixError)
193
+ affected_tests = (joinfix_faults[fault.exception] ||= [])
194
+ affected_tests << [index, fault.test_name]
195
+ end
196
+ end
197
+
198
+ joinfix_faults.each_pair do |exception, affected_tests|
199
+ output("\n**************************************\n")
200
+ output("JoinFix -- #{exception.class}:\n#{exception.message}\n")
201
+ output("Caused Errors:\n")
202
+ affected_tests.each do |index, test_name|
203
+ output("%3d) Error: %s\n" % [index + 1, test_name])
204
+ end
205
+ end
206
+
207
+ @faults.each_with_index do |fault, index|
208
+ next if fault.kind_of?(Test::Unit::Error) && fault.exception.kind_of?(JoinFixError)
209
+
210
+ nl
211
+ output("%3d) %s" % [index + 1, fault.long_display])
212
+ end
213
+ nl
214
+ output(@result)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -1,7 +1,6 @@
1
- # Additional classes to make Fixture behave like a Hash. Required for resolving joins.
1
+ # Defines additional methods to make Fixture behave like a Hash. Required for resolving joins.
2
2
  #
3
- # Just off of trivial because the internal data structure for Fixture can be a Hash or
4
- # a YAML::Omap
3
+ # Accomodates both of the allowed internal data structures for Fixture: Hash and YAML::Omap
5
4
  class Fixture
6
5
  def has_key?(key)
7
6
  # should work for Hash and Omap
@@ -14,7 +13,7 @@ class Fixture
14
13
  @fixture.each_pair(&block)
15
14
  else
16
15
  # for Omap
17
- @fixture.map { |k, v| yield(k,v) }
16
+ @fixture.map { |key, value| yield(key, value) }
18
17
  end
19
18
  end
20
19