gigantron 0.1.2 → 0.1.3

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 (66) hide show
  1. data/History.txt +22 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +65 -0
  4. data/PostInstall.txt +4 -0
  5. data/README.txt +77 -0
  6. data/Rakefile +4 -0
  7. data/app_generators/gigantron/USAGE +7 -0
  8. data/app_generators/gigantron/gigantron_generator.rb +87 -0
  9. data/app_generators/gigantron/templates/Rakefile +12 -0
  10. data/app_generators/gigantron/templates/database.yml.example +9 -0
  11. data/app_generators/gigantron/templates/initialize.rb +34 -0
  12. data/app_generators/gigantron/templates/lib/shoulda/active_record_helpers.rb +604 -0
  13. data/app_generators/gigantron/templates/lib/shoulda/general.rb +118 -0
  14. data/app_generators/gigantron/templates/lib/shoulda/private_helpers.rb +22 -0
  15. data/app_generators/gigantron/templates/tasks/import.rake +10 -0
  16. data/app_generators/gigantron/templates/test/tasks/test_import.rb +23 -0
  17. data/app_generators/gigantron/templates/test/test_helper.rb +22 -0
  18. data/bin/gigantron +15 -0
  19. data/config/hoe.rb +82 -0
  20. data/config/requirements.rb +15 -0
  21. data/gigantron_generators/mapreduce_task/USAGE +5 -0
  22. data/gigantron_generators/mapreduce_task/mapreduce_task_generator.rb +54 -0
  23. data/gigantron_generators/mapreduce_task/templates/mapreduce/mr_task.rb +22 -0
  24. data/gigantron_generators/mapreduce_task/templates/tasks/task.rake +5 -0
  25. data/gigantron_generators/mapreduce_task/templates/test/tasks/test_task.rb +22 -0
  26. data/gigantron_generators/migration/USAGE +5 -0
  27. data/gigantron_generators/migration/migration_generator.rb +61 -0
  28. data/gigantron_generators/migration/templates/db/migrate/migration.rb +7 -0
  29. data/gigantron_generators/model/USAGE +11 -0
  30. data/gigantron_generators/model/model_generator.rb +54 -0
  31. data/gigantron_generators/model/templates/models/model.rb +3 -0
  32. data/gigantron_generators/model/templates/test/models/test_model.rb +13 -0
  33. data/gigantron_generators/task/USAGE +10 -0
  34. data/gigantron_generators/task/task_generator.rb +51 -0
  35. data/gigantron_generators/task/templates/tasks/task.rake +4 -0
  36. data/gigantron_generators/task/templates/test/tasks/test_task.rb +22 -0
  37. data/lib/gigantron.rb +0 -0
  38. data/lib/gigantron/migrator.rb +10 -0
  39. data/lib/gigantron/tasks/db.rb +11 -0
  40. data/lib/gigantron/tasks/test.rb +30 -0
  41. data/lib/gigantron/version.rb +9 -0
  42. data/script/console +10 -0
  43. data/script/destroy +14 -0
  44. data/script/generate +14 -0
  45. data/script/txt2html +82 -0
  46. data/setup.rb +1585 -0
  47. data/tasks/deployment.rake +34 -0
  48. data/tasks/environment.rake +7 -0
  49. data/tasks/website.rake +17 -0
  50. data/test/template_database.yml +3 -0
  51. data/test/template_database.yml.example +9 -0
  52. data/test/template_migration.rb +16 -0
  53. data/test/test_generator_helper.rb +29 -29
  54. data/test/test_gigantron.rb +11 -11
  55. data/test/test_gigantron_generator.rb +118 -118
  56. data/test/test_helper.rb +4 -4
  57. data/test/test_mapreduce_task_generator.rb +50 -50
  58. data/test/test_migration_generator.rb +49 -49
  59. data/test/test_model_generator.rb +53 -53
  60. data/test/test_task_generator.rb +48 -48
  61. data/website/index.html +224 -0
  62. data/website/index.txt +154 -0
  63. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  64. data/website/stylesheets/screen.css +138 -0
  65. data/website/template.html.erb +48 -0
  66. metadata +152 -46
data/History.txt ADDED
@@ -0,0 +1,22 @@
1
+ == 0.1.3 2008-06-24
2
+ * 2 major bugfixes:
3
+ * fix broken gem
4
+ * make dependencies part of the hoe configuration
5
+ == 0.1.2 2008-06-22
6
+ * 1 major enhancement
7
+ * Shoulda activerecord helpers are now bundled
8
+ == 0.1.1 2008-06-19
9
+ * 1 minor enhancement:
10
+ * Migrations are now created with model
11
+ * 1 major bugfix:
12
+ * use camelcase instead of capitalize in code generators
13
+ == 0.1.0 2008-06-18
14
+
15
+ * Millions of major enhancements:
16
+ * Now uses ActiveRecord
17
+ * Better tests
18
+ == 0.0.1 2008-05-31
19
+
20
+ * 1 major enhancement:
21
+ * Initial release
22
+ * It works somewhat, will use in practice to see where it is headed
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Ben Hughes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or 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 HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,65 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ PostInstall.txt
5
+ README.txt
6
+ Rakefile
7
+ app_generators/gigantron/USAGE
8
+ app_generators/gigantron/gigantron_generator.rb
9
+ app_generators/gigantron/templates/Rakefile
10
+ app_generators/gigantron/templates/database.yml.example
11
+ app_generators/gigantron/templates/initialize.rb
12
+ app_generators/gigantron/templates/lib/shoulda/active_record_helpers.rb
13
+ app_generators/gigantron/templates/lib/shoulda/general.rb
14
+ app_generators/gigantron/templates/lib/shoulda/private_helpers.rb
15
+ app_generators/gigantron/templates/tasks/import.rake
16
+ app_generators/gigantron/templates/test/tasks/test_import.rb
17
+ app_generators/gigantron/templates/test/test_helper.rb
18
+ bin/gigantron
19
+ config/hoe.rb
20
+ config/requirements.rb
21
+ gigantron_generators/mapreduce_task/USAGE
22
+ gigantron_generators/mapreduce_task/mapreduce_task_generator.rb
23
+ gigantron_generators/mapreduce_task/templates/mapreduce/mr_task.rb
24
+ gigantron_generators/mapreduce_task/templates/tasks/task.rake
25
+ gigantron_generators/mapreduce_task/templates/test/tasks/test_task.rb
26
+ gigantron_generators/migration/USAGE
27
+ gigantron_generators/migration/migration_generator.rb
28
+ gigantron_generators/migration/templates/db/migrate/migration.rb
29
+ gigantron_generators/model/USAGE
30
+ gigantron_generators/model/model_generator.rb
31
+ gigantron_generators/model/templates/models/model.rb
32
+ gigantron_generators/model/templates/test/models/test_model.rb
33
+ gigantron_generators/task/USAGE
34
+ gigantron_generators/task/task_generator.rb
35
+ gigantron_generators/task/templates/tasks/task.rake
36
+ gigantron_generators/task/templates/test/tasks/test_task.rb
37
+ lib/gigantron.rb
38
+ lib/gigantron/migrator.rb
39
+ lib/gigantron/tasks/db.rb
40
+ lib/gigantron/tasks/test.rb
41
+ lib/gigantron/version.rb
42
+ script/console
43
+ script/destroy
44
+ script/generate
45
+ script/txt2html
46
+ setup.rb
47
+ tasks/deployment.rake
48
+ tasks/environment.rake
49
+ tasks/website.rake
50
+ test/template_database.yml
51
+ test/template_database.yml.example
52
+ test/template_migration.rb
53
+ test/test_generator_helper.rb
54
+ test/test_gigantron.rb
55
+ test/test_gigantron_generator.rb
56
+ test/test_helper.rb
57
+ test/test_mapreduce_task_generator.rb
58
+ test/test_migration_generator.rb
59
+ test/test_model_generator.rb
60
+ test/test_task_generator.rb
61
+ website/index.html
62
+ website/index.txt
63
+ website/javascripts/rounded_corners_lite.inc.js
64
+ website/stylesheets/screen.css
65
+ website/template.html.erb
data/PostInstall.txt ADDED
@@ -0,0 +1,4 @@
1
+
2
+ For more information on gigantron, see http://gigantron.rubyforge.org
3
+
4
+ Do you enjoy the tangy zip of miracle whip?
data/README.txt ADDED
@@ -0,0 +1,77 @@
1
+ = Gigantron: Processor of Data
2
+
3
+ http://gigantron.rubyforge.org
4
+ http://github.com/schleyfox/gigantron
5
+
6
+ == DESCRIPTION:
7
+
8
+ Gigantron is a simple framework for the creation and organization of
9
+ data processing projects. Data-processing transforms are created as Rake tasks
10
+ and data is handled through ActiveRecord* models.
11
+
12
+ * Will switch back to DataMapper once it plays nice with JRuby
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ Features:
17
+ * Generates folder/file structure for new DP projects
18
+ * Contains generators for both models and tasks
19
+
20
+ == SYNOPSIS:
21
+
22
+ Use:
23
+
24
+ shell> $ gigantron projectname
25
+
26
+ to generate the project folder and then
27
+
28
+ shell> $ script/generate model modelname
29
+
30
+ OR
31
+
32
+ shell> $ script/generate task taskname
33
+
34
+ to add code.
35
+
36
+ == REQUIREMENTS:
37
+
38
+ * RubyGems
39
+ * RubiGen
40
+ * Rake
41
+ * ActiveRecord
42
+ * ActiveSupport
43
+ * Shoulda
44
+
45
+ == INSTALL:
46
+
47
+ sudo gem install gigantron
48
+
49
+ == HACKING:
50
+
51
+ Check out the website for a quick overview of how to fiddle with the generators
52
+ behind Gigantron. http://gigantron.rubyforge.org.
53
+
54
+ == LICENSE:
55
+
56
+ (The MIT License)
57
+
58
+ Copyright (c) 2008 Ben Hughes
59
+
60
+ Permission is hereby granted, free of charge, to any person obtaining
61
+ a copy of this software and associated documentation files (the
62
+ 'Software'), to deal in the Software without restriction, including
63
+ without limitation the rights to use, copy, modify, merge, publish,
64
+ distribute, sublicense, and/or sell copies of the Software, and to
65
+ permit persons to whom the Software is furnished to do so, subject to
66
+ the following conditions:
67
+
68
+ The above copyright notice and this permission notice shall be
69
+ included in all copies or substantial portions of the Software.
70
+
71
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
72
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
73
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
74
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
75
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
76
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
77
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Generate the directory and file structure for a gigantron project.
3
+
4
+
5
+ Usage:
6
+ shell> $ gigantron projectname
7
+
@@ -0,0 +1,87 @@
1
+ class GigantronGenerator < RubiGen::Base
2
+
3
+ DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'],
4
+ Config::CONFIG['ruby_install_name'])
5
+
6
+ default_options :author => nil
7
+
8
+ attr_reader :name
9
+
10
+ def initialize(runtime_args, runtime_options = {})
11
+ super
12
+ usage if args.empty?
13
+ @destination_root = File.expand_path(args.shift)
14
+ @name = base_name
15
+ extract_options
16
+ end
17
+
18
+ def manifest
19
+ record do |m|
20
+ # Ensure appropriate folder(s) exists
21
+ m.directory ''
22
+ BASEDIRS.each { |path| m.directory path }
23
+
24
+ # Create stubs
25
+ m.file "Rakefile", "Rakefile"
26
+ m.file "database.yml.example", "database.yml.example"
27
+ m.file "initialize.rb", "initialize.rb"
28
+
29
+ m.file "tasks/import.rake", "tasks/import.rake"
30
+
31
+ m.file "test/test_helper.rb", "test/test_helper.rb"
32
+
33
+ m.directory "test/models"
34
+ m.directory "test/tasks"
35
+
36
+ m.file "test/tasks/test_import.rb", "test/tasks/test_import.rb"
37
+
38
+ m.directory "lib/shoulda"
39
+ m.file "lib/shoulda/general.rb", "lib/shoulda/general.rb"
40
+ m.file "lib/shoulda/private_helpers.rb", "lib/shoulda/private_helpers.rb"
41
+ m.file "lib/shoulda/active_record_helpers.rb",
42
+ "lib/shoulda/active_record_helpers.rb"
43
+
44
+
45
+ m.dependency "install_rubigen_scripts", [destination_root, 'gigantron'],
46
+ :shebang => options[:shebang], :collision => :force
47
+ end
48
+ end
49
+
50
+ protected
51
+ def banner
52
+ <<-EOS
53
+ Creates a ...
54
+
55
+ USAGE: #{spec.name} name
56
+ EOS
57
+ end
58
+
59
+ def add_options!(opts)
60
+ opts.separator ''
61
+ opts.separator 'Options:'
62
+ # For each option below, place the default
63
+ # at the top of the file next to "default_options"
64
+ # opts.on("-a", "--author=\"Your Name\"", String,
65
+ # "Some comment about this option",
66
+ # "Default: none") { |options[:author]| }
67
+ opts.on("-v", "--version", "Show the #{File.basename($0)} version number and quit.")
68
+ end
69
+
70
+ def extract_options
71
+ # for each option, extract it into a local variable (and create an "attr_reader :author" at the top)
72
+ # Templates can access these value via the attr_reader-generated methods, but not the
73
+ # raw instance variable value.
74
+ # @author = options[:author]
75
+ end
76
+
77
+ # Installation skeleton. Intermediate directories are automatically
78
+ # created so don't sweat their absence here.
79
+ BASEDIRS = %w(
80
+ tasks
81
+ db
82
+ models
83
+ lib
84
+ test
85
+ log
86
+ )
87
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'fileutils'
4
+
5
+ GTRON_ENV = :real
6
+ require 'initialize'
7
+
8
+ require 'gigantron/tasks/test'
9
+ require 'gigantron/tasks/db'
10
+
11
+ Dir['tasks/**/*.rake'].each {|r| load r }
12
+
@@ -0,0 +1,9 @@
1
+ # JRuby
2
+ #:test:
3
+ # :adapter: jdbcsqlite3
4
+ # :url: jdbc:sqlite:db/test.sqlite3
5
+
6
+ # Ruby 1.8
7
+ #:test:
8
+ # :adapter: sqlite3
9
+ # :database: db/test.sqlite3
@@ -0,0 +1,34 @@
1
+ # This file handles all the background initialization work that the programmer
2
+ # shouldn't have to worry about.
3
+ # This includes database startup, common requires, activesupport, and other
4
+ # magic. I'm not sure if this is a good idea or not.
5
+
6
+ # ENV works like in rails, except is :real or :test
7
+ GTRON_ENV rescue GTRON_ENV = :real
8
+
9
+ GTRON_ROOT = File.dirname(__FILE__)
10
+
11
+ require 'rubygems'
12
+ require 'rake'
13
+
14
+ require 'activesupport'
15
+ #set up autoload paths
16
+ Dependencies.load_paths << "#{GTRON_ROOT}/lib/"
17
+
18
+ require 'active_record'
19
+
20
+
21
+ def get_db_conn(env)
22
+ env = env.to_sym
23
+ #set up logging
24
+ ActiveRecord::Base.logger = Logger.new("#{GTRON_ROOT}/log/#{env}.log")
25
+
26
+ #load in dbs from database.yml
27
+ ActiveRecord::Base.establish_connection(
28
+ YAML::load(File.read("#{GTRON_ROOT}/database.yml"))[env])
29
+
30
+ #load all models
31
+ Dir["#{GTRON_ROOT}/models/**/*.rb"].each {|r| load r }
32
+
33
+ nil
34
+ end
@@ -0,0 +1,604 @@
1
+ module ThoughtBot # :nodoc:
2
+ module Shoulda # :nodoc:
3
+ # = Macro test helpers for your active record models
4
+ #
5
+ # These helpers will test most of the validations and associations for your ActiveRecord models.
6
+ #
7
+ # class UserTest < Test::Unit::TestCase
8
+ # should_require_attributes :name, :phone_number
9
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
10
+ # should_allow_values_for :phone_number, "(123) 456-7890"
11
+ #
12
+ # should_protect_attributes :password
13
+ #
14
+ # should_have_one :profile
15
+ # should_have_many :dogs
16
+ # should_have_many :messes, :through => :dogs
17
+ # should_belong_to :lover
18
+ # end
19
+ #
20
+ # For all of these helpers, the last parameter may be a hash of options.
21
+ #
22
+ module ActiveRecord
23
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
24
+ #
25
+ # Options:
26
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
27
+ # Regexp or string. Default = <tt>/blank/</tt>
28
+ #
29
+ # Example:
30
+ # should_require_attributes :name, :phone_number
31
+ #
32
+ def should_require_attributes(*attributes)
33
+ message = get_options!(attributes, :message)
34
+ message ||= /blank/
35
+ klass = model_class
36
+
37
+ attributes.each do |attribute|
38
+ should "require #{attribute} to be set" do
39
+ object = klass.new
40
+ object.send("#{attribute}=", nil)
41
+ assert !object.valid?, "#{klass.name} does not require #{attribute}."
42
+ assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}."
43
+ assert_contains(object.errors.on(attribute), message)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
49
+ # Requires an existing record
50
+ #
51
+ # Options:
52
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
53
+ # Regexp or string. Default = <tt>/taken/</tt>
54
+ # * <tt>:scoped_to</tt> - field(s) to scope the uniqueness to.
55
+ #
56
+ # Examples:
57
+ # should_require_unique_attributes :keyword, :username
58
+ # should_require_unique_attributes :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
59
+ # should_require_unique_attributes :email, :scoped_to => :name
60
+ # should_require_unique_attributes :address, :scoped_to => [:first_name, :last_name]
61
+ #
62
+ def should_require_unique_attributes(*attributes)
63
+ message, scope = get_options!(attributes, :message, :scoped_to)
64
+ scope = [*scope].compact
65
+ message ||= /taken/
66
+
67
+ klass = model_class
68
+ attributes.each do |attribute|
69
+ attribute = attribute.to_sym
70
+ should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" if scope}" do
71
+ assert existing = klass.find(:first), "Can't find first #{klass}"
72
+ object = klass.new
73
+
74
+ object.send(:"#{attribute}=", existing.send(attribute))
75
+ if !scope.blank?
76
+ scope.each do |s|
77
+ assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute."
78
+ object.send(:"#{s}=", existing.send(s))
79
+ end
80
+ end
81
+
82
+ assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}."
83
+ assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}."
84
+
85
+ assert_contains(object.errors.on(attribute), message)
86
+
87
+ # Now test that the object is valid when changing the scoped attribute
88
+ # TODO: There is a chance that we could change the scoped field
89
+ # to a value that's already taken. An alternative implementation
90
+ # could actually find all values for scope and create a unique
91
+ # one.
92
+ if !scope.blank?
93
+ scope.each do |s|
94
+ # Assume the scope is a foreign key if the field is nil
95
+ object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next)
96
+ end
97
+
98
+ object.errors.clear
99
+ object.valid?
100
+ scope.each do |s|
101
+ assert_does_not_contain(object.errors.on(attribute), message,
102
+ "after :#{s} set to #{object.send(s.to_sym)}")
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Ensures that the attribute cannot be set on mass update.
110
+ # Requires an existing record.
111
+ #
112
+ # should_protect_attributes :password, :admin_flag
113
+ #
114
+ def should_protect_attributes(*attributes)
115
+ get_options!(attributes)
116
+ klass = model_class
117
+
118
+ attributes.each do |attribute|
119
+ attribute = attribute.to_sym
120
+ should "protect #{attribute} from mass updates" do
121
+ protected = klass.protected_attributes || []
122
+ accessible = klass.accessible_attributes || []
123
+
124
+ assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s),
125
+ (accessible.empty? ?
126
+ "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." :
127
+ "#{klass} has made #{attribute} accessible")
128
+ end
129
+ end
130
+ end
131
+
132
+ # Ensures that the attribute cannot be set to the given values
133
+ # Requires an existing record
134
+ #
135
+ # Options:
136
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
137
+ # Regexp or string. Default = <tt>/invalid/</tt>
138
+ #
139
+ # Example:
140
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
141
+ #
142
+ def should_not_allow_values_for(attribute, *bad_values)
143
+ message = get_options!(bad_values, :message)
144
+ message ||= /invalid/
145
+ klass = model_class
146
+ bad_values.each do |v|
147
+ should "not allow #{attribute} to be set to #{v.inspect}" do
148
+ assert object = klass.find(:first), "Can't find first #{klass}"
149
+ object.send("#{attribute}=", v)
150
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
151
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
152
+ assert_contains(object.errors.on(attribute), message, "when set to \"#{v}\"")
153
+ end
154
+ end
155
+ end
156
+
157
+ # Ensures that the attribute can be set to the given values.
158
+ # Requires an existing record
159
+ #
160
+ # Example:
161
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
162
+ #
163
+ def should_allow_values_for(attribute, *good_values)
164
+ get_options!(good_values)
165
+ klass = model_class
166
+ good_values.each do |v|
167
+ should "allow #{attribute} to be set to #{v.inspect}" do
168
+ assert object = klass.find(:first), "Can't find first #{klass}"
169
+ object.send("#{attribute}=", v)
170
+ object.save
171
+ assert_nil object.errors.on(attribute)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Ensures that the length of the attribute is in the given range
177
+ # Requires an existing record
178
+ #
179
+ # Options:
180
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
181
+ # Regexp or string. Default = <tt>/short/</tt>
182
+ # * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
183
+ # Regexp or string. Default = <tt>/long/</tt>
184
+ #
185
+ # Example:
186
+ # should_ensure_length_in_range :password, (6..20)
187
+ #
188
+ def should_ensure_length_in_range(attribute, range, opts = {})
189
+ short_message, long_message = get_options!([opts], :short_message, :long_message)
190
+ short_message ||= /short/
191
+ long_message ||= /long/
192
+
193
+ klass = model_class
194
+ min_length = range.first
195
+ max_length = range.last
196
+ same_length = (min_length == max_length)
197
+
198
+ if min_length > 0
199
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
200
+ min_value = "x" * (min_length - 1)
201
+ assert object = klass.find(:first), "Can't find first #{klass}"
202
+ object.send("#{attribute}=", min_value)
203
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
204
+ assert object.errors.on(attribute),
205
+ "There are no errors set on #{attribute} after being set to \"#{min_value}\""
206
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
207
+ end
208
+ end
209
+
210
+ if min_length > 0
211
+ should "allow #{attribute} to be exactly #{min_length} chars long" do
212
+ min_value = "x" * min_length
213
+ assert object = klass.find(:first), "Can't find first #{klass}"
214
+ object.send("#{attribute}=", min_value)
215
+ object.save
216
+ assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
217
+ end
218
+ end
219
+
220
+ should "not allow #{attribute} to be more than #{max_length} chars long" do
221
+ max_value = "x" * (max_length + 1)
222
+ assert object = klass.find(:first), "Can't find first #{klass}"
223
+ object.send("#{attribute}=", max_value)
224
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
225
+ assert object.errors.on(attribute),
226
+ "There are no errors set on #{attribute} after being set to \"#{max_value}\""
227
+ assert_contains(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
228
+ end
229
+
230
+ unless same_length
231
+ should "allow #{attribute} to be exactly #{max_length} chars long" do
232
+ max_value = "x" * max_length
233
+ assert object = klass.find(:first), "Can't find first #{klass}"
234
+ object.send("#{attribute}=", max_value)
235
+ object.save
236
+ assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
237
+ end
238
+ end
239
+ end
240
+
241
+ # Ensures that the length of the attribute is at least a certain length
242
+ # Requires an existing record
243
+ #
244
+ # Options:
245
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
246
+ # Regexp or string. Default = <tt>/short/</tt>
247
+ #
248
+ # Example:
249
+ # should_ensure_length_at_least :name, 3
250
+ #
251
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
252
+ short_message = get_options!([opts], :short_message)
253
+ short_message ||= /short/
254
+
255
+ klass = model_class
256
+
257
+ if min_length > 0
258
+ min_value = "x" * (min_length - 1)
259
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
260
+ assert object = klass.find(:first), "Can't find first #{klass}"
261
+ object.send("#{attribute}=", min_value)
262
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
263
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
264
+ assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
265
+ end
266
+ end
267
+ should "allow #{attribute} to be at least #{min_length} chars long" do
268
+ valid_value = "x" * (min_length)
269
+ assert object = klass.find(:first), "Can't find first #{klass}"
270
+ object.send("#{attribute}=", valid_value)
271
+ assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\""
272
+ end
273
+ end
274
+
275
+ # Ensure that the attribute is in the range specified
276
+ # Requires an existing record
277
+ #
278
+ # Options:
279
+ # * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
280
+ # Regexp or string. Default = <tt>/included/</tt>
281
+ # * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
282
+ # Regexp or string. Default = <tt>/included/</tt>
283
+ #
284
+ # Example:
285
+ # should_ensure_value_in_range :age, (0..100)
286
+ #
287
+ def should_ensure_value_in_range(attribute, range, opts = {})
288
+ low_message, high_message = get_options!([opts], :low_message, :high_message)
289
+ low_message ||= /included/
290
+ high_message ||= /included/
291
+
292
+ klass = model_class
293
+ min = range.first
294
+ max = range.last
295
+
296
+ should "not allow #{attribute} to be less than #{min}" do
297
+ v = min - 1
298
+ assert object = klass.find(:first), "Can't find first #{klass}"
299
+ object.send("#{attribute}=", v)
300
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
301
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
302
+ assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
303
+ end
304
+
305
+ should "allow #{attribute} to be #{min}" do
306
+ v = min
307
+ assert object = klass.find(:first), "Can't find first #{klass}"
308
+ object.send("#{attribute}=", v)
309
+ object.save
310
+ assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
311
+ end
312
+
313
+ should "not allow #{attribute} to be more than #{max}" do
314
+ v = max + 1
315
+ assert object = klass.find(:first), "Can't find first #{klass}"
316
+ object.send("#{attribute}=", v)
317
+ assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
318
+ assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
319
+ assert_contains(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
320
+ end
321
+
322
+ should "allow #{attribute} to be #{max}" do
323
+ v = max
324
+ assert object = klass.find(:first), "Can't find first #{klass}"
325
+ object.send("#{attribute}=", v)
326
+ object.save
327
+ assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
328
+ end
329
+ end
330
+
331
+ # Ensure that the attribute is numeric
332
+ # Requires an existing record
333
+ #
334
+ # Options:
335
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
336
+ # Regexp or string. Default = <tt>/number/</tt>
337
+ #
338
+ # Example:
339
+ # should_only_allow_numeric_values_for :age
340
+ #
341
+ def should_only_allow_numeric_values_for(*attributes)
342
+ message = get_options!(attributes, :message)
343
+ message ||= /number/
344
+ klass = model_class
345
+ attributes.each do |attribute|
346
+ attribute = attribute.to_sym
347
+ should "only allow numeric values for #{attribute}" do
348
+ assert object = klass.find(:first), "Can't find first #{klass}"
349
+ object.send(:"#{attribute}=", "abcd")
350
+ assert !object.valid?, "Instance is still valid"
351
+ assert_contains(object.errors.on(attribute), message)
352
+ end
353
+ end
354
+ end
355
+
356
+ # Ensures that the has_many relationship exists. Will also test that the
357
+ # associated table has the required columns. Works with polymorphic
358
+ # associations.
359
+ #
360
+ # Options:
361
+ # * <tt>:through</tt> - association name for <tt>has_many :through</tt>
362
+ # * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
363
+ #
364
+ # Example:
365
+ # should_have_many :friends
366
+ # should_have_many :enemies, :through => :friends
367
+ # should_have_many :enemies, :dependent => :destroy
368
+ #
369
+ def should_have_many(*associations)
370
+ through, dependent = get_options!(associations, :through, :dependent)
371
+ klass = model_class
372
+ associations.each do |association|
373
+ name = "have many #{association}"
374
+ name += " through #{through}" if through
375
+ name += " dependent => #{dependent}" if dependent
376
+ should name do
377
+ reflection = klass.reflect_on_association(association)
378
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
379
+ assert_equal :has_many, reflection.macro
380
+
381
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
382
+
383
+ if through
384
+ through_reflection = klass.reflect_on_association(through)
385
+ assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
386
+ assert_equal(through, reflection.options[:through])
387
+ end
388
+
389
+ if dependent
390
+ assert_equal dependent.to_s,
391
+ reflection.options[:dependent].to_s,
392
+ "#{associated_klass.name} should have #{dependent} dependency"
393
+ end
394
+
395
+ # Check for the existence of the foreign key on the other table
396
+ unless reflection.options[:through]
397
+ if reflection.options[:foreign_key]
398
+ fk = reflection.options[:foreign_key]
399
+ elsif reflection.options[:as]
400
+ fk = reflection.options[:as].to_s.foreign_key
401
+ else
402
+ fk = reflection.primary_key_name
403
+ end
404
+
405
+ assert associated_klass.column_names.include?(fk.to_s),
406
+ "#{associated_klass.name} does not have a #{fk} foreign key."
407
+ end
408
+ end
409
+ end
410
+ end
411
+
412
+ # Ensure that the has_one relationship exists. Will also test that the
413
+ # associated table has the required columns. Works with polymorphic
414
+ # associations.
415
+ #
416
+ # Example:
417
+ # should_have_one :god # unless hindu
418
+ #
419
+ def should_have_one(*associations)
420
+ get_options!(associations)
421
+ klass = model_class
422
+ associations.each do |association|
423
+ should "have one #{association}" do
424
+ reflection = klass.reflect_on_association(association)
425
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
426
+ assert_equal :has_one, reflection.macro
427
+
428
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
429
+
430
+ if reflection.options[:foreign_key]
431
+ fk = reflection.options[:foreign_key]
432
+ elsif reflection.options[:as]
433
+ fk = reflection.options[:as].to_s.foreign_key
434
+ fk_type = fk.gsub(/_id$/, '_type')
435
+ assert associated_klass.column_names.include?(fk_type),
436
+ "#{associated_klass.name} does not have a #{fk_type} column."
437
+ else
438
+ fk = klass.name.foreign_key
439
+ end
440
+ assert associated_klass.column_names.include?(fk.to_s),
441
+ "#{associated_klass.name} does not have a #{fk} foreign key."
442
+ end
443
+ end
444
+ end
445
+
446
+ # Ensures that the has_and_belongs_to_many relationship exists, and that the join
447
+ # table is in place.
448
+ #
449
+ # should_have_and_belong_to_many :posts, :cars
450
+ #
451
+ def should_have_and_belong_to_many(*associations)
452
+ get_options!(associations)
453
+ klass = model_class
454
+
455
+ associations.each do |association|
456
+ should "should have and belong to many #{association}" do
457
+ reflection = klass.reflect_on_association(association)
458
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
459
+ assert_equal :has_and_belongs_to_many, reflection.macro
460
+ table = reflection.options[:join_table]
461
+ assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist"
462
+ end
463
+ end
464
+ end
465
+
466
+ # Ensure that the belongs_to relationship exists.
467
+ #
468
+ # should_belong_to :parent
469
+ #
470
+ def should_belong_to(*associations)
471
+ get_options!(associations)
472
+ klass = model_class
473
+ associations.each do |association|
474
+ should "belong_to #{association}" do
475
+ reflection = klass.reflect_on_association(association)
476
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
477
+ assert_equal :belongs_to, reflection.macro
478
+
479
+ unless reflection.options[:polymorphic]
480
+ associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
481
+ fk = reflection.options[:foreign_key] || reflection.primary_key_name
482
+ assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
483
+ end
484
+ end
485
+ end
486
+ end
487
+
488
+ # Ensure that the given class methods are defined on the model.
489
+ #
490
+ # should_have_class_methods :find, :destroy
491
+ #
492
+ def should_have_class_methods(*methods)
493
+ get_options!(methods)
494
+ klass = model_class
495
+ methods.each do |method|
496
+ should "respond to class method ##{method}" do
497
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
498
+ end
499
+ end
500
+ end
501
+
502
+ # Ensure that the given instance methods are defined on the model.
503
+ #
504
+ # should_have_instance_methods :email, :name, :name=
505
+ #
506
+ def should_have_instance_methods(*methods)
507
+ get_options!(methods)
508
+ klass = model_class
509
+ methods.each do |method|
510
+ should "respond to instance method ##{method}" do
511
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
512
+ end
513
+ end
514
+ end
515
+
516
+ # Ensure that the given columns are defined on the models backing SQL table.
517
+ #
518
+ # should_have_db_columns :id, :email, :name, :created_at
519
+ #
520
+ def should_have_db_columns(*columns)
521
+ column_type = get_options!(columns, :type)
522
+ klass = model_class
523
+ columns.each do |name|
524
+ test_name = "have column #{name}"
525
+ test_name += " of type #{column_type}" if column_type
526
+ should test_name do
527
+ column = klass.columns.detect {|c| c.name == name.to_s }
528
+ assert column, "#{klass.name} does not have column #{name}"
529
+ end
530
+ end
531
+ end
532
+
533
+ # Ensure that the given column is defined on the models backing SQL table. The options are the same as
534
+ # the instance variables defined on the column definition: :precision, :limit, :default, :null,
535
+ # :primary, :type, :scale, and :sql_type.
536
+ #
537
+ # should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
538
+ # :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
539
+ #
540
+ def should_have_db_column(name, opts = {})
541
+ klass = model_class
542
+ test_name = "have column named :#{name}"
543
+ test_name += " with options " + opts.inspect unless opts.empty?
544
+ should test_name do
545
+ column = klass.columns.detect {|c| c.name == name.to_s }
546
+ assert column, "#{klass.name} does not have column #{name}"
547
+ opts.each do |k, v|
548
+ assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
549
+ end
550
+ end
551
+ end
552
+
553
+ # Ensures that there are DB indices on the given columns or tuples of columns.
554
+ # Also aliased to should_have_index for readability
555
+ #
556
+ # should_have_indices :email, :name, [:commentable_type, :commentable_id]
557
+ # should_have_index :age
558
+ #
559
+ def should_have_indices(*columns)
560
+ table = model_class.name.tableize
561
+ indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
562
+
563
+ columns.each do |column|
564
+ should "have index on #{table} for #{column.inspect}" do
565
+ columns = [column].flatten.map(&:to_s)
566
+ assert_contains(indices, columns)
567
+ end
568
+ end
569
+ end
570
+
571
+ alias_method :should_have_index, :should_have_indices
572
+
573
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
574
+ #
575
+ # Options:
576
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
577
+ # Regexp or string. Default = <tt>/must be accepted/</tt>
578
+ #
579
+ # Example:
580
+ # should_require_acceptance_of :eula
581
+ #
582
+ def should_require_acceptance_of(*attributes)
583
+ message = get_options!(attributes, :message)
584
+ message ||= /must be accepted/
585
+ klass = model_class
586
+
587
+ attributes.each do |attribute|
588
+ should "require #{attribute} to be accepted" do
589
+ object = klass.new
590
+ object.send("#{attribute}=", false)
591
+
592
+ assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}."
593
+ assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}."
594
+ assert_contains(object.errors.on(attribute), message)
595
+ end
596
+ end
597
+ end
598
+
599
+ private
600
+
601
+ include ThoughtBot::Shoulda::Private
602
+ end
603
+ end
604
+ end