database_consistency 0.2.5 → 0.3.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.
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