activevalidation 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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