activevalidation 0.1.0 → 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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +32 -9
  3. data/.overcommit.yml +93 -0
  4. data/.rspec +1 -1
  5. data/.rubocop.yml +80 -0
  6. data/.rubocop_todo.yml +7 -0
  7. data/.travis.yml +42 -2
  8. data/.yardops +9 -0
  9. data/Appraisals +27 -0
  10. data/Gemfile +24 -1
  11. data/MIT-LICENSE +20 -0
  12. data/README.md +142 -20
  13. data/Rakefile +21 -3
  14. data/activevalidation.gemspec +35 -25
  15. data/bin/console +4 -7
  16. data/gemfiles/am_5.0.gemfile +17 -0
  17. data/gemfiles/am_5.0.gemfile.lock +159 -0
  18. data/gemfiles/am_5.1.gemfile +17 -0
  19. data/gemfiles/am_5.1.gemfile.lock +159 -0
  20. data/gemfiles/am_5.2.gemfile +17 -0
  21. data/gemfiles/am_5.2.gemfile.lock +159 -0
  22. data/gemfiles/am_6.0.gemfile +18 -0
  23. data/gemfiles/am_6.0.gemfile.lock +158 -0
  24. data/lib/active_validation.rb +27 -0
  25. data/lib/active_validation/base_adapter.rb +103 -0
  26. data/lib/active_validation/configuration.rb +52 -0
  27. data/lib/active_validation/decorator.rb +27 -0
  28. data/lib/active_validation/decorators/consistent_registry.rb +36 -0
  29. data/lib/active_validation/decorators/disallows_duplicates_registry.rb +17 -0
  30. data/lib/active_validation/errors.rb +28 -0
  31. data/lib/active_validation/ext/add_active_validation_context_check.rb +21 -0
  32. data/lib/active_validation/formatters/manifest_name_formatter.rb +13 -0
  33. data/lib/active_validation/formatters/validation_context_formatter.rb +28 -0
  34. data/lib/active_validation/frameworks/rspec.rb +10 -0
  35. data/lib/active_validation/frameworks/rspec/helpers.rb +15 -0
  36. data/lib/active_validation/internal/models/check.rb +51 -0
  37. data/lib/active_validation/internal/models/concerns/to_internal.rb +27 -0
  38. data/lib/active_validation/internal/models/manifest.rb +122 -0
  39. data/lib/active_validation/internal/models/manifest/installer.rb +86 -0
  40. data/lib/active_validation/internal/observers/manifest.rb +114 -0
  41. data/lib/active_validation/model_extension_base.rb +33 -0
  42. data/lib/active_validation/orm_plugins/active_record_plugin/adapter.rb +59 -0
  43. data/lib/active_validation/orm_plugins/active_record_plugin/internals/active_validation/internal_model_extensions/check.rb +11 -0
  44. data/lib/active_validation/orm_plugins/active_record_plugin/model_extension/active_validation/active_record_model_extension.rb +25 -0
  45. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/check.rb +31 -0
  46. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/concerns/method_must_be_allowed.rb +38 -0
  47. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validate_method.rb +9 -0
  48. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validates_method.rb +9 -0
  49. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validates_with_method.rb +19 -0
  50. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/concerns/protect_from_mutable_instance_methods.rb +31 -0
  51. data/lib/active_validation/orm_plugins/active_record_plugin/models/active_validation/manifest.rb +21 -0
  52. data/lib/active_validation/orm_plugins/active_record_plugin/types/active_validation/type/version.rb +17 -0
  53. data/lib/active_validation/registry.rb +55 -0
  54. data/lib/active_validation/values/base.rb +39 -0
  55. data/lib/active_validation/values/method_name.rb +22 -0
  56. data/lib/active_validation/values/version.rb +17 -0
  57. data/lib/active_validation/verifier.rb +150 -0
  58. data/lib/active_validation/version.rb +5 -0
  59. data/spec/active_validation/base_adapter_spec.rb +23 -0
  60. data/spec/active_validation/configuration_spec.rb +52 -0
  61. data/spec/active_validation/decorators/consistent_registry_spec.rb +117 -0
  62. data/spec/active_validation/decorators/disallows_duplicates_registry_spec.rb +21 -0
  63. data/spec/active_validation/formatters/manifest_name_formatter_spec.rb +7 -0
  64. data/spec/active_validation/formatters/validation_context_formatter_spec.rb +39 -0
  65. data/spec/active_validation/internal/models/check_spec.rb +67 -0
  66. data/spec/active_validation/internal/models/manifest/installer_spec.rb +177 -0
  67. data/spec/active_validation/internal/models/manifest_spec.rb +136 -0
  68. data/spec/active_validation/internal/observers/manifest_spec.rb +201 -0
  69. data/spec/active_validation/model_extension_base_spec.rb +71 -0
  70. data/spec/active_validation/orm_plugins/active_record_plugin/adapter_spec.rb +31 -0
  71. data/spec/active_validation/orm_plugins/active_record_plugin/adapter_spec_orm_specific_spec.rb +84 -0
  72. data/spec/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validate_method_spec.rb +26 -0
  73. data/spec/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validates_method_spec.rb +26 -0
  74. data/spec/active_validation/orm_plugins/active_record_plugin/models/active_validation/check/validates_with_method_spec.rb +34 -0
  75. data/spec/active_validation/orm_plugins/active_record_plugin/models/active_validation/check_spec.rb +48 -0
  76. data/spec/active_validation/orm_plugins/active_record_plugin/models/active_validation/manifest_spec.rb +61 -0
  77. data/spec/active_validation/orm_plugins/active_record_plugin/readme_spec.rb +89 -0
  78. data/spec/active_validation/registry_spec.rb +76 -0
  79. data/spec/active_validation/values/base_spec.rb +61 -0
  80. data/spec/active_validation/values/method_name_spec.rb +16 -0
  81. data/spec/active_validation/values/version_spec.rb +36 -0
  82. data/spec/active_validation/verifier_spec.rb +214 -0
  83. data/spec/active_validation_spec.rb +19 -0
  84. data/spec/factories/internal/internal_check.rb +43 -0
  85. data/spec/features/active_record/child_record.feature +32 -0
  86. data/spec/features/active_record/new_record.feature +22 -0
  87. data/spec/features/no_orm/install.feature +19 -0
  88. data/spec/features/no_orm/validate.feature +27 -0
  89. data/spec/features/no_orm/validate_with_multiple_manifests.feature +29 -0
  90. data/spec/features/no_orm/validate_with_multiple_versions.feature +42 -0
  91. data/spec/features/placeholders/be_matcher.rb +7 -0
  92. data/spec/features/placeholders/klass.rb +5 -0
  93. data/spec/features/placeholders/version.rb +11 -0
  94. data/spec/features/placeholders/whether_to.rb +11 -0
  95. data/spec/features/step_definitions/active_record_steps.rb +7 -0
  96. data/spec/features/step_definitions/steps.rb +85 -0
  97. data/spec/orm/active_record/db_adapters/database.mysql.yml +12 -0
  98. data/spec/orm/active_record/db_adapters/database.postgres.yml +11 -0
  99. data/spec/orm/active_record/db_adapters/database.sqlite.yml +8 -0
  100. data/spec/orm/active_record/factories/check/check_validate.rb +8 -0
  101. data/spec/orm/active_record/factories/check/check_validates.rb +8 -0
  102. data/spec/orm/active_record/factories/check/check_validates_with.rb +19 -0
  103. data/spec/orm/active_record/factories/manifest.rb +29 -0
  104. data/spec/orm/active_record/prepare_db.rb +89 -0
  105. data/spec/orm/active_record/setup.rb +11 -0
  106. data/spec/orm/mongoid/setup.rb +15 -0
  107. data/spec/spec_helper.rb +38 -0
  108. data/spec/support/database_cleaner.rb +16 -0
  109. data/spec/support/deferred_garbage_collection.rb +31 -0
  110. data/spec/support/define_constant_macros.rb +17 -0
  111. data/spec/support/factory_bot.rb +12 -0
  112. data/spec/support/helpers.rb +3 -0
  113. data/spec/support/helpers/only_with_active_record.rb +15 -0
  114. data/spec/support/matchers/delegate.rb +50 -0
  115. data/spec/support/matchers/have_attr.rb +58 -0
  116. data/spec/support/mongoid.yml +6 -0
  117. data/spec/support/shared_examples/check_attributes.rb +9 -0
  118. data/spec/support/shared_examples/verifiers_registry.rb +10 -0
  119. data/spec/support/simplecov.rb +11 -0
  120. data/spec/turnip_helper.rb +4 -0
  121. metadata +304 -20
  122. data/lib/activevalidation.rb +0 -6
  123. data/lib/activevalidation/version.rb +0 -3
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ using ActiveValidation::Internal::Models::Concerns::ToInternal::Check
4
+
5
+ module ActiveValidation
6
+ module Internal
7
+ module Models
8
+ class Manifest
9
+ class Installer
10
+ attr_reader :base_class, :installed_callbacks, :checks, :context
11
+
12
+ def initialize(base_class:, checks: [], context:)
13
+ @base_class = base_class
14
+ @checks = checks
15
+ @context = context
16
+ @installed_callbacks = []
17
+ end
18
+
19
+ # Add all checks (validations) to base_class
20
+ #
21
+ # @note we have to use the hack with
22
+ # `callbacks_chain.each.to_a # => chain` since `chain` method is
23
+ # protected.
24
+ #
25
+ # @return [TrueClass]
26
+ def install
27
+ @installed_callbacks = chain_mutex.synchronize do
28
+ before = callbacks_chain_elements
29
+ checks.each { |check| base_class.public_send(*check.to_validation_arguments(context: context)) }
30
+ callbacks_chain_elements - before
31
+ end
32
+ true
33
+ end
34
+
35
+ # Remove all checks (validations) from base_class
36
+ #
37
+ # we can not use ActiveSupport provided methods, since we already have
38
+ # our installed callbacks.
39
+ #
40
+ # @return [FalseClass]
41
+ def uninstall
42
+ installed_validators = installed_callbacks.map(&:filter).select { |f| f.is_a? ActiveModel::Validator }
43
+
44
+ ([base_class] + ActiveSupport::DescendantsTracker.descendants(base_class)).reverse_each do |target|
45
+ uninstall! target: target, installed_validators: installed_validators
46
+ end
47
+ installed_callbacks.clear
48
+ false
49
+ end
50
+
51
+ def callbacks_chain
52
+ base_class._validate_callbacks
53
+ end
54
+
55
+ private
56
+
57
+ # We cannot call `chain` method directly, so we have to use this hack
58
+ #
59
+ # @return [Array<ActiveSupport::Callbacks>]
60
+ def callbacks_chain_elements
61
+ callbacks_chain.each.to_a
62
+ end
63
+
64
+ # Mutex from ActiveSupport::Callbacks::CallbackChain for `base_class`
65
+ #
66
+ # @return [Mutex]
67
+ def chain_mutex
68
+ callbacks_chain.instance_variable_get(:@mutex)
69
+ end
70
+
71
+ def uninstall!(target:, installed_validators:)
72
+ chain = target._validate_callbacks
73
+
74
+ chain.instance_variable_get(:@mutex).synchronize do
75
+ target._validators
76
+ .each_value { |v| v.reject! { |el| installed_validators.include?(el) } }
77
+ .delete_if { |_, v| v.empty? }
78
+
79
+ installed_callbacks.each { |callback| chain.delete(callback) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ module Internal
5
+ module Observers
6
+ class Manifest
7
+ DISABLED_VERIFIER = :verifier_is_not_enabled
8
+ RECENT_FAILURE = :it_was_failed_too_recently
9
+ NOT_FOUND = :not_found
10
+ ALREADY_INSTALLED = :already_installed
11
+ INSTALLED = :installed
12
+ UNINSTALLED = :uninstalled
13
+
14
+ attr_reader :verifier, :installed_manifests, :last_failed_attempt_time
15
+
16
+ delegate :manifest, :failed_attempt_retry_time, :enabled?, to: :verifier
17
+
18
+ def initialize(verifier)
19
+ @verifier = verifier
20
+ @installed_manifests = Concurrent::Set.new
21
+ @lock = Concurrent::ReadWriteLock.new
22
+ @last_failed_attempt_time = nil
23
+ end
24
+
25
+ # Install manifest and store the result. Load priority:
26
+ # * `manifest_id` from the arguments
27
+ # * `verifier.manifest` - defined by user in verifier block
28
+ # * `current_manifest` - the latest manifest with current version
29
+ #
30
+ # The process will be skipped if:
31
+ # - `verifier` not `enabled?`
32
+ # - The last failed attempt was too recent.
33
+ #
34
+ # @return Symbol
35
+ def install(manifest_id: nil)
36
+ return DISABLED_VERIFIER unless enabled?
37
+ return RECENT_FAILURE if attempt_to_was_failed_recently?
38
+
39
+ found = lock.with_read_lock { find(manifest_id: manifest_id) } || current_manifest
40
+ return NOT_FOUND unless found
41
+ return ALREADY_INSTALLED if found.installed?
42
+
43
+ install! internal_manifest: found
44
+ end
45
+
46
+ # Uninstall the manifest.
47
+ #
48
+ # @return Symbol
49
+ def uninstall(manifest_id:)
50
+ lock.with_write_lock do
51
+ internal_manifest = find(manifest_id: manifest_id)
52
+ return NOT_FOUND unless internal_manifest&.installed?
53
+
54
+ internal_manifest.uninstall
55
+ installed_manifests.delete internal_manifest
56
+ end
57
+ UNINSTALLED
58
+ end
59
+
60
+ # We omit the error and only save the error time to prevent
61
+ # DB flooding, when there is no any manifest.
62
+ #
63
+ # We do not know all possible errors so we can not be more
64
+ # specific here.
65
+ #
66
+ # @return [Internal::Manifest]
67
+ def current_manifest
68
+ verifier.current_manifest.to_internal_manifest
69
+ rescue StandardError => _e
70
+ @last_failed_attempt_time = Time.now
71
+ nil
72
+ end
73
+
74
+ # We need this method for been able to restore manifest lookup
75
+ #
76
+ # @return nil
77
+ def reset_last_failed_attempt_time
78
+ @last_failed_attempt_time = nil
79
+ end
80
+
81
+ # The actual install process
82
+ #
83
+ # @return Symbol
84
+ def install!(internal_manifest:)
85
+ lock.with_write_lock do
86
+ return ALREADY_INSTALLED if find(manifest_id: internal_manifest.id)&.installed?
87
+
88
+ internal_manifest.install
89
+ installed_manifests << internal_manifest
90
+ end
91
+ INSTALLED
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :lock
97
+
98
+ def find(manifest_id: nil)
99
+ return unless manifest_id
100
+
101
+ @installed_manifests.detect do |manifest|
102
+ manifest.id == manifest_id
103
+ end
104
+ end
105
+
106
+ def attempt_to_was_failed_recently?
107
+ return unless last_failed_attempt_time
108
+
109
+ Time.now < @last_failed_attempt_time + failed_attempt_retry_time
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ module ModelExtensionBase
5
+ extend ActiveSupport::Concern
6
+ module ClassMethods
7
+ # This is entry point, which setup or return existed Verifier.
8
+ #
9
+ # @return Verifier
10
+ def active_validation(&block)
11
+ register_active_validation_relations unless ActiveValidation::Verifier.registry.registered? self
12
+ ::ActiveValidation::Verifier.find_or_build self, &block
13
+ end
14
+
15
+ private
16
+
17
+ # Invoke the checks that validations are in place. Should be overwritten in ORM
18
+ # adapter
19
+ #
20
+ # @return nil
21
+ def register_active_validation_relations; end
22
+ end
23
+
24
+ protected
25
+
26
+ # We need to be sure, that all validation call backs are in place before validation
27
+ #
28
+ # @return nil
29
+ def process_active_validation
30
+ ::ActiveValidation::Verifier.find_or_build(self.class).install!(manifest_id: manifest_id)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ module OrmPlugins
5
+ module ActiveRecordPlugin
6
+ class Adapter < ActiveValidation::BaseAdapter
7
+ loading_paths << "models"
8
+ loading_paths << "types"
9
+ loading_paths << "internals"
10
+ loading_paths << "model_extension"
11
+
12
+ def initialize
13
+ setup unless self.class.initialised
14
+ self.class.initialised = true
15
+ end
16
+
17
+ # @return [true]
18
+ def setup
19
+ return installer if defined?(::ActiveRecord::Base)
20
+
21
+ ::ActiveSupport.on_load(:active_record_adapter, &method(:installer))
22
+ end
23
+
24
+ # @see BaseAdapter
25
+ def add_manifest(manifest)
26
+ Manifest.create manifest.as_json(checks: { only: %i[type argument options], as: :checks_attributes })
27
+ manifest
28
+ end
29
+
30
+ # @see BaseAdapter
31
+ def find_manifests(wheres)
32
+ search(wheres)
33
+ end
34
+
35
+ # @see BaseAdapter
36
+ def find_manifest(wheres)
37
+ search(wheres, &:first!)
38
+ end
39
+
40
+ private
41
+
42
+ # @api internal
43
+ def search(wheres)
44
+ relation = Manifest.includes(:checks).where(wheres).order(created_at: :desc, id: :desc)
45
+ relation = yield relation if block_given?
46
+ relation.is_a?(ActiveRecord::Base) ? relation.to_internal_manifest : relation.map(&:to_internal_manifest)
47
+ end
48
+
49
+ # @return [true]
50
+ def installer
51
+ ActiveValidation::OrmPlugins::ActiveRecordPlugin::Adapter.loader
52
+ ActiveValidation::Internal::Models::Check.include(ActiveValidation::InternalModelExtensions::Check)
53
+ ::ActiveRecord::Base.include ActiveValidation::ActiveRecordModelExtension
54
+ true
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ module InternalModelExtensions
5
+ module Check
6
+ def type
7
+ method_name.to_s.camelcase.concat "Method"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ module ActiveRecordModelExtension
5
+ extend ActiveSupport::Concern
6
+ include ModelExtensionBase
7
+
8
+ module ClassMethods
9
+ private
10
+
11
+ def register_active_validation_relations
12
+ before_validation :process_active_validation
13
+
14
+ belongs_to :manifest, class_name: "::ActiveValidation::Manifest", dependent: :destroy
15
+
16
+ ActiveValidation::Manifest.has_many realtion_plural_name
17
+ nil
18
+ end
19
+
20
+ def realtion_plural_name
21
+ name.underscore.pluralize.to_sym
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ class Check < ActiveRecord::Base
5
+ prepend ActiveValidation::Concerns::ProtectFromMutableInstanceMethods
6
+
7
+ serialize :options
8
+
9
+ self.table_name = :active_validation_checks
10
+ self.store_full_sti_class = false
11
+
12
+ belongs_to :manifest, inverse_of: :checks
13
+
14
+ validates :argument, presence: true
15
+
16
+ def method_name
17
+ raise NotImplementedError, "abstract" unless self.class < Check
18
+
19
+ self.class.name.demodulize.sub(/Method\z/, "").underscore
20
+ end
21
+
22
+ def method_name=(other)
23
+ self.type = other.camelcase + "Method"
24
+ end
25
+
26
+ def to_internal_check
27
+ json_options = { methods: %i[method_name], root: false }
28
+ ActiveValidation::Internal::Models::Check.new as_json(json_options).to_options!
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ class Check < ActiveRecord::Base
5
+ module Concerns
6
+ module MethodMustBeAllowed
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ validate :method_must_be_allowed
11
+ end
12
+
13
+ private
14
+
15
+ def method_must_be_allowed
16
+ verify_options
17
+ verify_argument
18
+ end
19
+
20
+ def verify_options
21
+ return unless options
22
+
23
+ danger_values = options.slice([:if, :unless, "if", "unless"]).values
24
+ return if (danger_values & restricted_instance_methods).empty?
25
+
26
+ errors.add :options, "Options contain dangerous checks"
27
+ end
28
+
29
+ def verify_argument
30
+ return unless argument && manifest.try(:base_klass)
31
+ return unless restricted_instance_methods.include?(argument.to_sym)
32
+
33
+ errors.add :argument, "method #{argument} is restricted for usage in validation"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ class Check < ActiveRecord::Base
5
+ class ValidateMethod < Check
6
+ include Concerns::MethodMustBeAllowed
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ class Check < ActiveRecord::Base
5
+ class ValidatesMethod < Check
6
+ include Concerns::MethodMustBeAllowed
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveValidation
4
+ class Check < ActiveRecord::Base
5
+ class ValidatesWithMethod < Check
6
+ validate :validator_must_be_defined
7
+
8
+ private
9
+
10
+ def validator_must_be_defined
11
+ return if argument.blank?
12
+
13
+ argument.constantize.is_a? ActiveModel::Validator
14
+ rescue NameError
15
+ errors.add :argument, "Validator not exist"
16
+ end
17
+ end
18
+ end
19
+ end