table_warnings 0.0.7 → 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,3 +1,3 @@
1
1
  module TableWarnings
2
- VERSION = "0.0.7"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,53 +1,163 @@
1
+ require 'thread'
2
+
1
3
  require 'active_support'
2
4
  require 'active_support/version'
3
- require 'active_support/core_ext' if ::ActiveSupport::VERSION::MAJOR >= 3
5
+ require 'active_support/core_ext' if ActiveSupport::VERSION::MAJOR >= 3
4
6
  require 'active_record'
5
7
 
6
- module TableWarnings
7
- autoload :Config, 'table_warnings/config'
8
- autoload :Warning, 'table_warnings/warning'
8
+ require 'table_warnings/registry'
9
+ require 'table_warnings/exclusive'
10
+ require 'table_warnings/blank'
11
+ require 'table_warnings/size'
12
+ require 'table_warnings/arbitrary'
13
+ require 'table_warnings/null'
14
+ require 'table_warnings/column'
15
+ require 'table_warnings/scout'
16
+ require 'table_warnings/nonexistent_owner'
17
+ require 'table_warnings/range'
9
18
 
10
- def self.config #:nodoc: all
11
- Config.instance
19
+ module TableWarnings
20
+ def TableWarnings.registry
21
+ @registry || Thread.exclusive do
22
+ @registry ||= Registry.new
23
+ end
12
24
  end
13
25
 
14
- # Get current warnings on the table.
26
+ # used to resolve columns to warnings
27
+ Disposition = Struct.new(:exclusives, :covers, :matches)
28
+
29
+ # Get current warning messages on the table.
15
30
  def table_warnings
16
- ::TableWarnings.config.warnings[self].map { |w| w.messages }.flatten.compact.sort
31
+ messages = []
32
+
33
+ TableWarnings.registry.nonexclusive(self).each do |warning|
34
+ messages << warning.messages
35
+ end
36
+
37
+ pool = column_names.map do |column_name|
38
+ TableWarnings::Column.new self, column_name
39
+ end
40
+ exclusive = TableWarnings.registry.exclusive(self)
41
+
42
+ assignments = {}
43
+ # pass 1 - exclusives and covers
44
+ exclusive.each do |warning|
45
+ disposition = Disposition.new
46
+ disposition.exclusives = warning.exclusives pool
47
+ disposition.covers = warning.covers pool
48
+ assignments[warning] = disposition
49
+ pool -= disposition.exclusives
50
+ end
51
+ if ENV['TABLE_WARNINGS_DEBUG'] == 'true'
52
+ $stderr.puts "pass 1"
53
+ assignments.each do |warning, disposition|
54
+ $stderr.puts " #{warning.scout.matcher} - exclusives=#{disposition.exclusives.map(&:name)} covers=#{disposition.covers.map(&:name)}"
55
+ end
56
+ end
57
+ # pass 2 - allow regexp matching, but only if somebody else didn't cover it
58
+ exclusive.each do |warning|
59
+ disposition = assignments[warning]
60
+ disposition.matches = warning.matches(pool).select do |match|
61
+ assignments.except(warning).none? { |_, other| other.covers.include?(match) }
62
+ end
63
+ pool -= disposition.matches
64
+ end
65
+ if ENV['TABLE_WARNINGS_DEBUG'] == 'true'
66
+ $stderr.puts "pass 2"
67
+ assignments.each do |warning, disposition|
68
+ $stderr.puts " #{warning.scout.matcher} - exclusives=#{disposition.exclusives.map(&:name)} covers=#{disposition.covers.map(&:name)} matches=#{disposition.matches.map(&:name)}"
69
+ end
70
+ end
71
+ if ENV['TABLE_WARNINGS_STRICT'] == 'true'
72
+ $stderr.puts "uncovered columns"
73
+ $stderr.puts pool.join("\n")
74
+ end
75
+
76
+ # now you can generate messages
77
+ assignments.each do |warning, disposition|
78
+ messages << warning.messages(disposition.exclusives+disposition.matches)
79
+ end
80
+
81
+ messages.flatten.compact
82
+ end
83
+
84
+ def warn_unless_range(*args)
85
+ options = args.extract_options!
86
+ args.flatten.each do |matcher|
87
+ TableWarnings.registry.add_warning self, TableWarnings::Range.new(self, matcher, options)
88
+ end
17
89
  end
18
90
 
19
91
  # Warn if there are blanks in a certain column.
20
92
  #
21
93
  # Blank includes both NULL and "" (empty string)
22
- def warn_if_blanks_in(column_name)
23
- warning = ::TableWarnings::Warning::Blank.new :table => self, :column_name => column_name
24
- ::TableWarnings.config.warnings[self].add warning
94
+ def warn_if_blanks(*args)
95
+ options = args.extract_options!
96
+ args.flatten.each do |matcher|
97
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, matcher, options)
98
+ end
25
99
  end
26
100
 
101
+ # Warn if there are NULLs in a certain column.
102
+ def warn_if_nulls(*args)
103
+ options = args.extract_options!
104
+ args.flatten.each do |matcher|
105
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, matcher, options)
106
+ end
107
+ end
108
+
27
109
  # Warn if there are blanks in ANY column.
28
- #
29
- # Blank includes both NULL and "" (empty string)
30
110
  def warn_if_any_blanks
31
- warning = ::TableWarnings::Warning::Blank.new :table => self
32
- ::TableWarnings.config.warnings[self].add warning
111
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, /.*/)
112
+ end
113
+
114
+ # Warn if there are nulls in ANY column.
115
+ def warn_if_any_nulls
116
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, /.*/)
33
117
  end
34
118
 
35
- # Warn if the number of records falls out of an (approximate) range.
119
+ # Warn if the number of records falls out of an (approximate) size.
36
120
  #
37
121
  # Approximations: :few, :tens, :dozens, :hundreds, :thousands, :hundreds_of_thousands, :millions
38
122
  # Exact: pass a Range or a Numeric
39
- def warn_unless_size_is(approximate_size)
40
- warning = ::TableWarnings::Warning::Size.new :table => self, :approximate_size => approximate_size
41
- ::TableWarnings.config.warnings[self].add warning
123
+ def warn_unless_size(approximate_size, options = {})
124
+ TableWarnings.registry.add_warning self, TableWarnings::Size.new(self, approximate_size, options)
42
125
  end
43
126
 
44
127
  # An arbitrary warning.
45
- def warn(&blk)
46
- warning = ::TableWarnings::Warning::Arbitrary.new :table => self, :blk => blk
47
- ::TableWarnings.config.warnings[self].add warning
128
+ def warn_if(&blk)
129
+ TableWarnings.registry.add_warning self, TableWarnings::Arbitrary.new(self, blk)
130
+ end
131
+
132
+ def warn_if_nulls_except(*args)
133
+ options = args.extract_options!
134
+ args.flatten.each do |matcher|
135
+ TableWarnings.registry.add_warning self, TableWarnings::Null.new(self, matcher, options.merge(:negative => true))
136
+ end
137
+ end
138
+
139
+ def warn_if_blanks_except(*args)
140
+ options = args.extract_options!
141
+ args.flatten.each do |matcher|
142
+ TableWarnings.registry.add_warning self, TableWarnings::Blank.new(self, matcher, options.merge(:negative => true))
143
+ end
144
+ end
145
+
146
+ def warn_if_nonexistent_owner(*args)
147
+ options = args.extract_options!
148
+ args.flatten.each do |belongs_to_association_name|
149
+ TableWarnings.registry.add_warning self, TableWarnings::NonexistentOwner.new(self, belongs_to_association_name, options)
150
+ end
151
+ end
152
+
153
+ def warn_if_nonexistent_owner_except(*args)
154
+ options = args.extract_options!
155
+ args.flatten.each do |belongs_to_association_name|
156
+ TableWarnings.registry.add_warning self, TableWarnings::NonexistentOwner.new(self, belongs_to_association_name, options.merge(:negative => true))
157
+ end
48
158
  end
49
159
  end
50
160
 
51
- unless ::ActiveRecord::Base.method_defined? :table_warnings
52
- ::ActiveRecord::Base.extend ::TableWarnings
161
+ unless ActiveRecord::Base.method_defined? :table_warnings
162
+ ActiveRecord::Base.extend TableWarnings
53
163
  end
@@ -1,11 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path('../lib', __FILE__)
3
- require 'table_warnings/version'
2
+ require File.expand_path('../lib/table_warnings/version', __FILE__)
4
3
 
5
4
  Gem::Specification.new do |s|
6
5
  s.name = 'table_warnings'
7
6
  s.version = TableWarnings::VERSION
8
- s.platform = Gem::Platform::RUBY
9
7
  s.authors = ['Seamus Abshere']
10
8
  s.email = ['seamus@abshere.net']
11
9
  s.homepage = 'https://github.com/seamusabshere/table_warnings'
@@ -23,7 +21,10 @@ Gem::Specification.new do |s|
23
21
  s.add_runtime_dependency 'activesupport'
24
22
 
25
23
  s.add_development_dependency 'fastercsv'
26
- s.add_development_dependency 'rake'
27
- s.add_development_dependency 'mini_record-compat'
24
+ s.add_development_dependency 'active_record_inline_schema'
28
25
  s.add_development_dependency 'sqlite3'
26
+ s.add_development_dependency 'minitest'
27
+ s.add_development_dependency 'minitest-reporters'
28
+ s.add_development_dependency 'yard'
29
+ # s.add_development_dependency 'debugger'
29
30
  end
data/test/helper.rb CHANGED
@@ -1,25 +1,103 @@
1
1
  require 'rubygems'
2
- require 'bundler'
3
- Bundler.setup
4
- require 'test/unit'
5
- require 'active_support/all'
6
- require 'active_record'
7
- require 'mini_record'
8
- # thanks authlogic!
9
- ActiveRecord::Schema.verbose = false
10
- begin
11
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
12
- rescue ArgumentError
13
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
2
+ require 'bundler/setup'
3
+
4
+ if ::Bundler.definition.specs['debugger'].first
5
+ require 'debugger'
6
+ elsif ::Bundler.definition.specs['ruby-debug'].first
7
+ require 'ruby-debug'
14
8
  end
15
9
 
10
+ require 'minitest/spec'
11
+ require 'minitest/autorun'
12
+ require 'minitest/reporters'
13
+ MiniTest::Unit.runner = MiniTest::SuiteRunner.new
14
+ MiniTest::Unit.runner.reporters << MiniTest::Reporters::SpecReporter.new
15
+
16
+ require 'active_record'
17
+ require 'active_record_inline_schema'
18
+
19
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
20
+
16
21
  # require 'logger'
17
22
  # logger = Logger.new $stdout
18
23
  # logger.level = Logger::DEBUG
19
24
  # ActiveRecord::Base.logger = logger
20
25
 
21
- $LOAD_PATH.unshift(File.dirname(__FILE__))
22
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
23
- require 'table_warnings'
24
- class Test::Unit::TestCase
26
+ class ActiveRecord::Base
27
+ class << self
28
+ # ignores protected attrs
29
+ def force_create!(attrs = {})
30
+ record = new
31
+ attrs.each do |k, v|
32
+ record.send "#{k}=", v
33
+ end
34
+ record.save!
35
+ record
36
+ end
37
+ end
25
38
  end
39
+
40
+ class MiniTest::Spec
41
+ # start transaction
42
+ before do
43
+ # activerecord-3.2.3/lib/active_record/fixtures.rb
44
+ @fixture_connections = ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
45
+ @fixture_connections.each do |connection|
46
+ connection.increment_open_transactions
47
+ connection.transaction_joinable = false
48
+ connection.begin_db_transaction
49
+ end
50
+ end
51
+
52
+ # rollback
53
+ after do
54
+ @fixture_connections.each do |connection|
55
+ if connection.open_transactions != 0
56
+ connection.rollback_db_transaction
57
+ connection.decrement_open_transactions
58
+ end
59
+ end
60
+ @fixture_connections.clear
61
+ ActiveRecord::Base.clear_active_connections!
62
+ end
63
+
64
+ def assert_warning(model, expected_warning)
65
+ hits = model.table_warnings.select { |warning| warning =~ expected_warning }
66
+ refute hits.none?, "#{model.name} unexpectedly lacked warning #{expected_warning.inspect}"
67
+ refute hits.many?, "#{model.name} had MULTIPLE warnings like #{expected_warning.inspect}: #{hits.inspect}"
68
+ end
69
+
70
+ def assert_no_warning(model, specific_unexpected_warning = nil)
71
+ warnings = model.table_warnings
72
+ if specific_unexpected_warning
73
+ refute(warnings.any? { |warning| warning =~ specific_unexpected_warning }, "#{model.name} unexpectedly had warning #{specific_unexpected_warning.inspect}")
74
+ else
75
+ refute warnings.any?, "#{model.name} unexpectedly had some warnings (#{warnings.inspect})"
76
+ end
77
+ end
78
+
79
+ def assert_causes_warning(model, expected_warnings)
80
+ expected_warnings = [expected_warnings].flatten
81
+ expected_warnings.each do |expected_warning|
82
+ assert_no_warning model, expected_warning
83
+ end
84
+ warnings_before = model.table_warnings
85
+ yield
86
+ expected_warnings.each do |expected_warning|
87
+ assert_warning model, expected_warning
88
+ end
89
+ unexpected_warnings = (model.table_warnings - warnings_before).reject do |warning|
90
+ expected_warnings.any? { |expected_warning| warning =~ expected_warning }
91
+ end
92
+ refute unexpected_warnings.any?, "#{model.name} unexpectedly ALSO got warnings #{unexpected_warnings.inspect}"
93
+ end
94
+
95
+ def assert_does_not_cause_warning(model)
96
+ assert_no_warning model
97
+ yield
98
+ assert_no_warning model
99
+ end
100
+
101
+ end
102
+
103
+ require 'table_warnings'
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ class PetAlpha < ActiveRecord::Base
4
+ col :birthday, :type => :datetime
5
+ col :gender
6
+ col :sire
7
+ warn_if_nulls :birthday
8
+ warn_if_nulls_except :gender
9
+ end
10
+ PetAlpha.auto_upgrade!
11
+
12
+ class PetBeta < ActiveRecord::Base
13
+ col :birthday, :type => :datetime
14
+ col :gender
15
+ col :sire
16
+ warn_if_nulls :birthday
17
+ warn_if_nulls_except /ende/
18
+ end
19
+ PetBeta.auto_upgrade!
20
+
21
+ class PetGamma < ActiveRecord::Base
22
+ col :birthday, :type => :datetime
23
+ col :gender
24
+ col :sire
25
+ col :certified, :type => :boolean
26
+ warn_if_nulls /irthd/, :conditions => { :certified => true }
27
+ warn_if_nulls_except /ende/
28
+ end
29
+ PetGamma.auto_upgrade!
30
+
31
+ class PetDelta < ActiveRecord::Base
32
+ col :birthday, :type => :datetime
33
+ col :gender
34
+ col :sire
35
+ col :certified, :type => :boolean
36
+ warn_if_nulls /irthd/, :conditions => { :certified => true }
37
+ warn_if_nulls_except /ende/, :conditions => { :certified => true }
38
+ end
39
+ PetDelta.auto_upgrade!
40
+
41
+ describe TableWarnings do
42
+ describe "combinations of positive ('in') and negative ('except') rules" do
43
+ it "combines a positive column and a negative column" do
44
+ assert_causes_warning PetAlpha, [/null.*birthday/i, /null.*sire/i] do
45
+ PetAlpha.force_create!
46
+ end
47
+ end
48
+ it "combines a positive column and a negative regexp" do
49
+ assert_causes_warning PetBeta, [/null.*birthday/i, /null.*sire/i] do
50
+ PetBeta.force_create!
51
+ end
52
+ end
53
+ it "combines a positive regexp with conditions and a negative regexp" do
54
+ assert_causes_warning PetGamma, [/null.*sire/i, /null.*certified/i] do
55
+ PetGamma.force_create!
56
+ end
57
+ PetGamma.delete_all # !
58
+ assert_causes_warning PetGamma, [/null.*sire/i, /null.*birthday/i] do
59
+ PetGamma.force_create! :certified => true
60
+ end
61
+ end
62
+ it "combines a positive regexp with conditions and a negative regexp with conditions" do
63
+ assert_does_not_cause_warning PetDelta do
64
+ PetDelta.force_create!
65
+ end
66
+ PetDelta.delete_all # !
67
+ assert_causes_warning PetDelta, [/null.*sire/i, /null.*birthday/i] do
68
+ PetDelta.force_create! :certified => true
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ =begin
75
+ warn_if_nulls_except(
76
+ :alt_fuel_code,
77
+ :carline_mfr_code,
78
+ :vi_mfr_code,
79
+ :carline_code,
80
+ :carline_class_code,
81
+ :carline_class_name,
82
+ )
83
+ warn_if_nulls /alt_fuel_efficiency/, :conditions => 'alt_fuel_code IS NOT NULL'
84
+ warn_if_nulls :carline_class, :conditions => 'year < 1998'
85
+ =end
@@ -0,0 +1,60 @@
1
+ require 'helper'
2
+
3
+ class Person < ActiveRecord::Base
4
+ col :llc_name
5
+ end
6
+ Person.auto_upgrade!
7
+
8
+ class PetRed < ActiveRecord::Base
9
+ col :handler_id, :type => :integer
10
+ belongs_to :handler, :class_name => 'Person'
11
+ warn_if_nonexistent_owner :handler
12
+ end
13
+ PetRed.auto_upgrade!
14
+
15
+ class PetBlue < ActiveRecord::Base
16
+ col :trainer_id
17
+ belongs_to :trainer, :class_name => 'Person', :primary_key => :llc_name
18
+ warn_if_nonexistent_owner :trainer
19
+ end
20
+ PetBlue.auto_upgrade!
21
+
22
+ class PetGreen < ActiveRecord::Base
23
+ col :trainer_id
24
+ belongs_to :trainer, :class_name => 'Person', :primary_key => :llc_name
25
+ warn_if_nonexistent_owner :trainer, :allow_null => true
26
+ end
27
+ PetGreen.auto_upgrade!
28
+
29
+ describe TableWarnings do
30
+ describe :warn_if_nonexistent_owner do
31
+ before do
32
+ Person.force_create! :llc_name => 'My Small Business, LLC'
33
+ end
34
+ it "takes a single belongs-to association name" do
35
+ assert_causes_warning PetRed, /nonexistent.*handler/i do
36
+ PetRed.force_create!
37
+ end
38
+ end
39
+ it "checks the value of foreign keys not just their presence" do
40
+ assert_causes_warning PetRed, /nonexistent.*handler/i do
41
+ PetRed.force_create! :handler_id => 999999
42
+ end
43
+ end
44
+ it "doesn't raise false warnings" do
45
+ assert_does_not_cause_warning PetRed do
46
+ PetRed.force_create! :handler_id => Person.first.id
47
+ end
48
+ end
49
+ it "regards nulls as nonexistent even if the association primary key column contains nulls" do
50
+ assert_causes_warning PetBlue, /nonexistent.*trainer/i do
51
+ PetBlue.force_create!
52
+ end
53
+ end
54
+ it "allows nulls if explicitly requested" do
55
+ assert_does_not_cause_warning PetGreen do
56
+ PetGreen.force_create!
57
+ end
58
+ end
59
+ end
60
+ end