database_consistency 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef796f91309cb17dcba40b0b2af43569e1f938ba1c8ddb4be230a2f470d4a291
4
- data.tar.gz: 122b3e9154b464def4ee5533b9e4401bc25057ffac9d51cec2cf214bc1ef9534
3
+ metadata.gz: 0cb421a3fbd1c94df1816ecaf52487578c2a68c2aa13f384d1f80a9c68b35613
4
+ data.tar.gz: 5a38ad0d44e448e08e7474e74ecd0f934f0b9bf86e2fe43a80c2bb4540b154e8
5
5
  SHA512:
6
- metadata.gz: 9711216a1cbd3a46ca8cef1dd142eef6c0ba590431faed0a5bf74f1fd83121317fec341265867c85b93e5b18112dbebd1673e982cb7d641edf8cf5bfa0b2af20
7
- data.tar.gz: c1ca40e919cc496157999965b5edc3bbc51c67ab3e11fc55bc06ceb5a3503db66d7197ab420bc0dc38c09fb05be7323e26fc2d85301b32987e05fb3786a5e5a8
6
+ metadata.gz: a6195f162dac4165dd70ce10fddc1dde43527f2d3dd845f9a4f2f4b9f0def589ed125fbe5cce3a5e61eb80c91ccefb609de4bc42ea0dcbe4ff26edd556522884
7
+ data.tar.gz: 47ebc91829df9bc38a2f518b0b5d4b938a51d79d441984c257c9fa7429d12cc22d3d6184029396d29c0e422ac890c9311391ae9cbc6e2d052fae33e22694f11c
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Checkers
5
+ # The base class for checkers
6
+ class BaseChecker
7
+ def initialize(table_or_model, column_or_attribute, opts = {})
8
+ @table_or_model = table_or_model
9
+ @column_or_attribute = column_or_attribute
10
+ @opts = opts
11
+ end
12
+
13
+ # @return [Hash]
14
+ def report
15
+ @report ||= check
16
+ end
17
+
18
+ # @param [DatabaseConsistency::Configuration] configuration
19
+ def enabled?(configuration)
20
+ configuration.enabled?(checker_name, table_or_model_name, column_or_attribute_name)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :table_or_model, :column_or_attribute, :opts
26
+
27
+ # @return [String]
28
+ def checker_name
29
+ @checker_name ||= self.class.name.split('::').last
30
+ end
31
+
32
+ def table_or_model_name
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def column_or_attribute_name
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # @return [Hash]
41
+ def report_template(status, message = nil)
42
+ OpenStruct.new(
43
+ checker_name: checker_name,
44
+ table_or_model_name: table_or_model_name,
45
+ column_or_attribute_name: column_or_attribute_name,
46
+ status: status,
47
+ message: message
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Checkers
5
+ # This class checks missing presence validator
6
+ class NullConstraintChecker < BaseChecker
7
+ # Message templates
8
+ VALIDATOR_MISSING = 'is required but do not have presence validator'
9
+
10
+ private
11
+
12
+ # Table of possible statuses
13
+ # | validation | database | status |
14
+ # | ---------- | -------- | ------ |
15
+ # | missed | required | fail |
16
+ #
17
+ # We skip check when:
18
+ # - column hasn't null constraint
19
+ # - column has default value
20
+ # - column is a primary key
21
+ # - column is a timestamp
22
+ # - presence validation exists
23
+ # - inclusion validation exists
24
+ # - belongs_to reflection exists with given column as foreign key or foreign type
25
+ def check
26
+ return if skip? ||
27
+ validator?(ActiveModel::Validations::PresenceValidator) ||
28
+ validator?(ActiveModel::Validations::InclusionValidator) ||
29
+ belongs_to_reflection?
30
+
31
+ report_template(:fail, VALIDATOR_MISSING)
32
+ end
33
+
34
+ def column_or_attribute_name
35
+ column_or_attribute.name.to_s
36
+ end
37
+
38
+ def table_or_model_name
39
+ table_or_model.name.to_s
40
+ end
41
+
42
+ def skip?
43
+ column_or_attribute.null ||
44
+ !column_or_attribute.default.nil? ||
45
+ column_or_attribute.name == table_or_model.primary_key ||
46
+ timestamp_field?
47
+ end
48
+
49
+ def timestamp_field?
50
+ table_or_model.record_timestamps? && %w[created_at updated_at].include?(column_or_attribute.name)
51
+ end
52
+
53
+ def validator?(validator_class)
54
+ table_or_model.validators.grep(validator_class).any? do |validator|
55
+ Helper.check_inclusion?(validator.attributes, column_or_attribute.name)
56
+ end
57
+ end
58
+
59
+ def belongs_to_reflection?
60
+ table_or_model.reflect_on_all_associations.grep(ActiveRecord::Reflection::BelongsToReflection).any? do |r|
61
+ Helper.check_inclusion?([r.foreign_key, r.foreign_type], column_or_attribute.name)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DatabaseConsistency
2
- module Comparators
3
- # The comparator class for {{ActiveModel::Validations::PresenceValidator}}
4
- class PresenceComparator < BaseComparator
4
+ module Checkers
5
+ # This class checks PresenceValidator
6
+ class PresenceValidationChecker < BaseChecker
5
7
  WEAK_OPTIONS = %i[allow_nil allow_blank if unless].freeze
6
8
  # Message templates
7
- CONSTRAINT_MISSING = 'should be required in the database'.freeze
8
- POSSIBLE_NULL = 'is required but possible null value insert'.freeze
9
+ CONSTRAINT_MISSING = 'should be required in the database'
10
+ POSSIBLE_NULL = 'is required but possible null value insert'
11
+
12
+ private
9
13
 
10
14
  # Table of possible statuses
11
15
  # | allow_nil/allow_blank/if/unless | database | status |
@@ -14,23 +18,38 @@ module DatabaseConsistency
14
18
  # | at least one provided | optional | ok |
15
19
  # | all missed | required | ok |
16
20
  # | all missed | optional | fail |
17
- def compare
21
+ #
22
+ # We skip check when:
23
+ # - there is no column in the database with given name
24
+ def check
25
+ return unless column
26
+
18
27
  can_be_null = column.null
19
28
  has_weak_option = validator.options.slice(*WEAK_OPTIONS).any?
20
29
 
21
30
  if can_be_null == has_weak_option
22
- result(:ok, message)
31
+ report_template(:ok)
23
32
  elsif can_be_null
24
- result(:fail, message(CONSTRAINT_MISSING))
33
+ report_template(:fail, CONSTRAINT_MISSING)
25
34
  else
26
- result(:fail, message(POSSIBLE_NULL))
35
+ report_template(:fail, POSSIBLE_NULL)
27
36
  end
28
37
  end
29
38
 
30
- private
39
+ def column
40
+ @column ||= Helper.find_field(table_or_model, column_or_attribute.to_s)
41
+ end
42
+
43
+ def column_or_attribute_name
44
+ column_or_attribute.to_s
45
+ end
46
+
47
+ def table_or_model_name
48
+ table_or_model.name.to_s
49
+ end
31
50
 
32
- def message(template = nil)
33
- Helper.message(model, column, template)
51
+ def validator
52
+ opts[:validator]
34
53
  end
35
54
  end
36
55
  end
@@ -1,19 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module DatabaseConsistency
4
6
  # The class to access configuration options
5
7
  class Configuration
6
- def initialize(filepath = nil)
7
- @configuration = if filepath
8
+ CONFIGURATION_PATH = '.database_consistency.yml'
9
+
10
+ def initialize(filepath = CONFIGURATION_PATH)
11
+ @configuration = if filepath && File.exist?(filepath)
8
12
  YAML.load_file(filepath)
9
13
  else
10
14
  {}
11
15
  end
12
16
  end
13
17
 
14
- def enabled?(processor)
15
- name = processor.to_s.split('::').last
16
- @configuration[name].nil? || @configuration[name] == true
18
+ # @return [Boolean]
19
+ def enabled?(*path)
20
+ current = configuration
21
+
22
+ path.each do |key|
23
+ current = current[key.to_s]
24
+ return true unless current.is_a?(Hash)
25
+
26
+ next if current['enabled'].nil?
27
+
28
+ return false unless current['enabled']
29
+ end
30
+
31
+ true
17
32
  end
33
+
34
+ private
35
+
36
+ attr_reader :configuration
18
37
  end
19
38
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DatabaseConsistency
2
4
  # The module contains helper methods
3
5
  module Helper
@@ -25,13 +27,6 @@ module DatabaseConsistency
25
27
  model.columns.select.find { |field| field.name == attribute }
26
28
  end
27
29
 
28
- # @return [String]
29
- def message(model, column, template = nil)
30
- str = "column #{column.name} of table #{model.table_name} of model #{model.name}"
31
- str += " #{template}" if template
32
- str
33
- end
34
-
35
30
  # @return [Boolean]
36
31
  def check_inclusion?(array, element)
37
32
  array.include?(element.to_s) || array.include?(element.to_sym)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ # The module for processors
5
+ module Processors
6
+ def self.reports(configuration)
7
+ [ModelsProcessor, TablesProcessor].flat_map do |processor|
8
+ processor.new(configuration).reports
9
+ end
10
+ end
11
+
12
+ # The base class for processors
13
+ class BaseProcessor
14
+ attr_reader :configuration
15
+
16
+ # @param [DatabaseConsistency::Configuration] configuration
17
+ def initialize(configuration = nil)
18
+ @configuration = configuration || Configuration.new
19
+ end
20
+
21
+ # @return [Array<Hash>]
22
+ def reports
23
+ @reports ||= check
24
+ end
25
+
26
+ private
27
+
28
+ def check
29
+ raise NotImplementedError
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Processors
5
+ # The class to process all comparators
6
+ class ModelsProcessor < BaseProcessor
7
+ CHECKERS = {
8
+ presence: Checkers::PresenceValidationChecker
9
+ }.freeze
10
+
11
+ private
12
+
13
+ # @return [Array<Hash>]
14
+ def check
15
+ Helper.parent_models.flat_map do |model|
16
+ model.validators.flat_map do |validator|
17
+ next unless (checker_class = CHECKERS[validator.kind])
18
+
19
+ validator.attributes.map do |attribute|
20
+ checker = checker_class.new(model, attribute, validator: validator)
21
+ checker.report if checker.enabled?(configuration)
22
+ end
23
+ end
24
+ end.compact
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Processors
5
+ # The class to process missing validators
6
+ class TablesProcessor < BaseProcessor
7
+ CHECKERS = [
8
+ Checkers::NullConstraintChecker
9
+ ].freeze
10
+
11
+ private
12
+
13
+ def check
14
+ Helper.parent_models.flat_map do |model|
15
+ model.columns.flat_map do |column|
16
+ CHECKERS.map do |checker_class|
17
+ checker = checker_class.new(model, column)
18
+ checker.report if checker.enabled?(configuration)
19
+ end
20
+ end
21
+ end.compact
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DatabaseConsistency
2
- VERSION = '0.2.5'.freeze
4
+ VERSION = '0.3.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DatabaseConsistency
2
4
  module Writers
3
5
  # The base class for writers
@@ -10,7 +12,7 @@ module DatabaseConsistency
10
12
  end
11
13
 
12
14
  def write?(status)
13
- status == :fail
15
+ status == :fail || debug?
14
16
  end
15
17
 
16
18
  def debug?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DatabaseConsistency
2
4
  # The module contains formatters
3
5
  module Writers
@@ -12,16 +14,12 @@ module DatabaseConsistency
12
14
  next unless write?(result[:status])
13
15
 
14
16
  line(result)
15
- end.tap(&:compact!).map(&:lstrip).delete_if(&:empty?).join(delimiter)
16
- end
17
-
18
- def delimiter
19
- debug? ? "\n\n" : "\n"
17
+ end.tap(&:compact!).map(&:lstrip).delete_if(&:empty?).join("\n")
20
18
  end
21
19
 
22
20
  def line(result)
23
- "#{result[:status]} #{result[:message]}".tap do |str|
24
- str.concat " #{result[:opts].inspect}" if debug?
21
+ "#{result.status} #{result.table_or_model_name} #{result.column_or_attribute_name} #{result.message}".tap do |s|
22
+ s.concat " (checker: #{result.checker_name})" if debug?
25
23
  end
26
24
  end
27
25
  end
@@ -1,34 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  require 'database_consistency/version'
4
- require 'database_consistency/report'
5
6
  require 'database_consistency/helper'
6
7
  require 'database_consistency/configuration'
7
8
 
8
9
  require 'database_consistency/writers/base_writer'
9
10
  require 'database_consistency/writers/simple_writer'
10
11
 
11
- require 'database_consistency/comparators/base_comparator'
12
- require 'database_consistency/comparators/presence_comparator'
13
- require 'database_consistency/validators_processor'
12
+ require 'database_consistency/checkers/base_checker'
13
+ require 'database_consistency/checkers/presence_validation_checker'
14
+ require 'database_consistency/checkers/null_constraint_checker'
14
15
 
15
- require 'database_consistency/column_verifiers/base_verifier'
16
- require 'database_consistency/column_verifiers/presence_missing_verifier'
17
- require 'database_consistency/database_processor'
16
+ require 'database_consistency/processors/base_processor'
17
+ require 'database_consistency/processors/models_processor'
18
+ require 'database_consistency/processors/tables_processor'
18
19
 
19
20
  # The root module
20
21
  module DatabaseConsistency
21
- CONFIGURATION_PATH = '.database_consistency.yml'.freeze
22
-
23
- PROCESSORS = [
24
- ValidatorsProcessor,
25
- DatabaseProcessor
26
- ].freeze
27
-
28
22
  class << self
29
23
  def run
30
24
  Helper.load_environment!
31
25
 
26
+ configuration = Configuration.new
27
+ reports = Processors.reports(configuration)
28
+
32
29
  Writers::SimpleWriter.write(
33
30
  reports,
34
31
  ENV['LOG_LEVEL'] || 'INFO'
@@ -36,21 +33,5 @@ module DatabaseConsistency
36
33
 
37
34
  reports.empty? ? 0 : 1
38
35
  end
39
-
40
- def enabled_processors
41
- @enabled_processors ||= PROCESSORS.select { |processor| configuration.enabled?(processor) }
42
- end
43
-
44
- def reports
45
- @reports ||= enabled_processors.map(&:new).flat_map(&:reports)
46
- end
47
-
48
- def configuration
49
- @configuration ||= if File.exist?(CONFIGURATION_PATH)
50
- Configuration.new(CONFIGURATION_PATH)
51
- else
52
- Configuration.new
53
- end
54
- end
55
36
  end
56
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_consistency
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-24 00:00:00.000000000 Z
11
+ date: 2019-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -110,15 +110,14 @@ extra_rdoc_files: []
110
110
  files:
111
111
  - bin/database_consistency
112
112
  - lib/database_consistency.rb
113
- - lib/database_consistency/column_verifiers/base_verifier.rb
114
- - lib/database_consistency/column_verifiers/presence_missing_verifier.rb
115
- - lib/database_consistency/comparators/base_comparator.rb
116
- - lib/database_consistency/comparators/presence_comparator.rb
113
+ - lib/database_consistency/checkers/base_checker.rb
114
+ - lib/database_consistency/checkers/null_constraint_checker.rb
115
+ - lib/database_consistency/checkers/presence_validation_checker.rb
117
116
  - lib/database_consistency/configuration.rb
118
- - lib/database_consistency/database_processor.rb
119
117
  - lib/database_consistency/helper.rb
120
- - lib/database_consistency/report.rb
121
- - lib/database_consistency/validators_processor.rb
118
+ - lib/database_consistency/processors/base_processor.rb
119
+ - lib/database_consistency/processors/models_processor.rb
120
+ - lib/database_consistency/processors/tables_processor.rb
122
121
  - lib/database_consistency/version.rb
123
122
  - lib/database_consistency/writers/base_writer.rb
124
123
  - lib/database_consistency/writers/simple_writer.rb
@@ -1,23 +0,0 @@
1
- module DatabaseConsistency
2
- module ColumnVerifiers
3
- # The base class for column verifiers
4
- class BaseVerifier
5
- attr_reader :model, :column
6
-
7
- delegate :result, to: :report
8
-
9
- def initialize(model, column)
10
- @model = model
11
- @column = column
12
- end
13
-
14
- def report
15
- Report.new(column: column)
16
- end
17
-
18
- def self.verify(model, column)
19
- new(model, column).verify
20
- end
21
- end
22
- end
23
- end
@@ -1,45 +0,0 @@
1
- module DatabaseConsistency
2
- module ColumnVerifiers
3
- # This class verifies that column needs a presence validator
4
- class PresenceMissingVerifier < BaseVerifier
5
- VALIDATOR_MISSING = 'is required but do not have presence validator'.freeze
6
-
7
- def verify
8
- return if skip? || presence_validator? || inclusion_validator? || belongs_to_reflection?
9
-
10
- result(:fail, Helper.message(model, column, VALIDATOR_MISSING))
11
- end
12
-
13
- private
14
-
15
- def skip?
16
- column.null ||
17
- !column.default.nil? ||
18
- column.name == model.primary_key ||
19
- timestamp_field?
20
- end
21
-
22
- def timestamp_field?
23
- model.record_timestamps? && %w[created_at updated_at].include?(column.name)
24
- end
25
-
26
- def presence_validator?
27
- model.validators.grep(ActiveModel::Validations::PresenceValidator).any? do |validator|
28
- Helper.check_inclusion?(validator.attributes, column.name)
29
- end
30
- end
31
-
32
- def inclusion_validator?
33
- model.validators.grep(ActiveModel::Validations::InclusionValidator).any? do |validator|
34
- Helper.check_inclusion?(validator.attributes, column.name)
35
- end
36
- end
37
-
38
- def belongs_to_reflection?
39
- model.reflect_on_all_associations.grep(ActiveRecord::Reflection::BelongsToReflection).any? do |reflection|
40
- Helper.check_inclusion?([reflection.foreign_key, reflection.foreign_type], column.name)
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,24 +0,0 @@
1
- module DatabaseConsistency
2
- module Comparators
3
- # The base class for comparators
4
- class BaseComparator
5
- attr_reader :validator, :model, :column
6
-
7
- delegate :result, to: :report
8
-
9
- def initialize(validator, model, column)
10
- @validator = validator
11
- @model = model
12
- @column = column
13
- end
14
-
15
- def report
16
- Report.new(validator: validator, column: column)
17
- end
18
-
19
- def self.compare(validator, model, column)
20
- new(validator, model, column).compare
21
- end
22
- end
23
- end
24
- end
@@ -1,18 +0,0 @@
1
- module DatabaseConsistency
2
- # The class to process missing validators
3
- class DatabaseProcessor
4
- VERIFIERS = [
5
- ColumnVerifiers::PresenceMissingVerifier
6
- ].freeze
7
-
8
- def reports
9
- Helper.parent_models.flat_map do |model|
10
- model.columns.flat_map do |column|
11
- VERIFIERS.map do |verifier|
12
- verifier.verify(model, column)
13
- end
14
- end
15
- end.compact
16
- end
17
- end
18
- end
@@ -1,17 +0,0 @@
1
- module DatabaseConsistency
2
- # This class outputs the report result
3
- class Report
4
- attr_reader :opts
5
-
6
- def initialize(opts = {})
7
- @opts = opts
8
- end
9
-
10
- def result(status, message)
11
- {
12
- status: status,
13
- message: message
14
- }.tap { |hash| hash[:opts] = opts unless opts.empty? }
15
- end
16
- end
17
- end
@@ -1,22 +0,0 @@
1
- module DatabaseConsistency
2
- # The class to process all comparators
3
- class ValidatorsProcessor
4
- COMPARATORS = {
5
- presence: DatabaseConsistency::Comparators::PresenceComparator
6
- }.freeze
7
-
8
- def reports
9
- Helper.parent_models.flat_map do |model|
10
- model.validators.flat_map do |validator|
11
- next unless (comparator = COMPARATORS[validator.kind])
12
-
13
- validator.attributes.map do |attribute|
14
- next unless (column = Helper.find_field(model, attribute.to_s))
15
-
16
- comparator.compare(validator, model, column)
17
- end
18
- end
19
- end.compact
20
- end
21
- end
22
- end