joinfix 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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