better_validations 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 451f27d06fabf944c809fe98c5942634ebb0578c054a70b82dcbcd831cafc25f
4
+ data.tar.gz: 661475b942ccf70af3ae9660fd812c335b4d1b5ee37bf43b05b761505cd5be1c
5
+ SHA512:
6
+ metadata.gz: 270e518392f9702d5640b608b1138d4a8899f4f0fa3cf17a1f1dd22c593a869888dbcc1c405b12113e803d509a81a64b61f6749ee62430c55c49c694e1b9ce0c
7
+ data.tar.gz: 8e198d598f009d01e7ed7b1d33f5e243753b626d94822a4d5d889162d23046a7ad934a381b5347fdee0cdaea0a7d4f865429818e2c08dfef422c31ff9ed38a8b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Petr Bazov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # README
2
+
3
+ ## Development
4
+
5
+ 1. Clone project.
6
+
7
+ 2. Copy `.env.example` file to `.env` file.
8
+
9
+ 3. Feel `.env` file by actual values of DATABASE_USER and DATABASE_PASSWORD.
10
+
11
+ 4. Run `rails db:create`.
12
+
13
+ 5. Run `rails s`.
14
+
15
+ 6. Open `http://localhost:3000/` in browser.
@@ -0,0 +1,85 @@
1
+ require_relative 'better_validations/railtie'
2
+
3
+ require_relative 'better_validations/errors'
4
+ require_relative 'better_validations/object'
5
+ require_relative 'better_validations/list'
6
+ require_relative 'better_validations/nested_validator'
7
+ require_relative 'better_validations/validator'
8
+ require_relative 'better_validations/validators_list'
9
+
10
+ # 1. A module provides a way to get error messages from an active record
11
+ # keeping structure of nested objects represented by relations
12
+ # such as belongs_to, has_one, has_many instead of a flat structure. Just call:
13
+ #
14
+ # active_record.errors.detailed_messages
15
+ #
16
+ # This calling returns a hash such as:
17
+ #
18
+ # { field_one: ['Error 1', 'Error 2'],
19
+ # field_two: ['Error'],
20
+ # nested_object: { field: ['Error'] },
21
+ # nested_list: [{ field: ['Error'] }] }
22
+ #
23
+ # A "detailed_messages" method has a "wrap_attributes_to" parameter.
24
+ # You can use it to wrap all attributes except relations by passed key:
25
+ #
26
+ # active_record.errors.detailed_messages(wrap_attributes_to: :fields)
27
+ # { fields: { field_one: ['Error'], field_two: ['Error'] },
28
+ # nested_object: { fields: { field: ['Error'] } } }
29
+ #
30
+ # This module also provides other features.
31
+ #
32
+ # 2. Ability to validate nested objects in separated validators with
33
+ # included ActiveModel::Validations.
34
+ #
35
+ # Just include BetterValidations::Validator instead of ActiveModel::Validations
36
+ # and you will can to validate nested objects with the other validator by
37
+ # calling a "validate_nested" method:
38
+ #
39
+ # class UserValidator
40
+ # include BetterValidations::Validator
41
+ #
42
+ # attr_accessor :email, :password, :personal_info
43
+ #
44
+ # validates_presence_of :email, :password
45
+ # validate_nested :personal_info, PersonalInfoValidator
46
+ # end
47
+ #
48
+ # class PersonalInfoValidator
49
+ # include BetterValidations::Validator
50
+ #
51
+ # attr_accessor :first_name, :last_name
52
+ # validates_presence_of :first_name, :last_name
53
+ # end
54
+ #
55
+ # After that you can validate active record object, hash or
56
+ # ActionController::Parameters by passing them to the validator.
57
+ # Keys "#{field}" and "#{field}_attributes" are synonyms and fills
58
+ # a "#{field}=" attr_accessor:
59
+ #
60
+ # user = User.new(email: '', personal_info_attributes: { first_name: '' })
61
+ # user = User.new(personal_info: PersonalInfo.new(last_name: ''))
62
+ # user = { email: '', personal_info_attributes: { last_name: '' } }
63
+ # user = { email: '', personal_info: { last_name: '' } }
64
+ # user = ActionController::Parameters.new(email: '')
65
+ #
66
+ # validator = UserValidator.new(user)
67
+ # validator.valid?
68
+ # validator.errors.detailed_messages
69
+ #
70
+ # 3. Ability to merge validators in order to get merged detailed errors.
71
+ # Useful when you needs to get errors from multiple validators
72
+ # or if a part of validations implemented inside a model. Usage:
73
+ #
74
+ # validator = validator_one.merge(validator_two, validator_three, object)
75
+ # validator.valid?
76
+ # validator.errors.detailed_messages
77
+ #
78
+ module BetterValidations
79
+ # Your code goes here...
80
+ end
81
+
82
+ ActiveModel::Errors.include BetterValidations::Errors
83
+ ActiveRecord::Base.include BetterValidations::Object
84
+ ActiveRecord::Associations::CollectionProxy.include BetterValidations::List
85
+ Array.include BetterValidations::List
@@ -0,0 +1,59 @@
1
+ module BetterValidations::Errors
2
+ extend ActiveSupport::Concern
3
+ included do
4
+ # Returns the same hash as a 'messages' method
5
+ # but if an error is happened with a nested object
6
+ # it will be stored in a nested hash under the key without a dot symbol
7
+ # instead of an array under the key with dot symbol.
8
+ #
9
+ # Example:
10
+ # object.errors.messages == {
11
+ # field_one: ["can't be blank"],
12
+ # "nested_object.field_one": ["can't be blank"],
13
+ # "nested_object.field_two": ["can't be blank"]
14
+ # }
15
+ # object.errors.detailed_messages == {
16
+ # field_one: ["can't be blank"],
17
+ # nested_object: {
18
+ # field_one: ["can't be blank"],
19
+ # field_two: ["can't be blank"]
20
+ # }
21
+ # }
22
+ def detailed_messages(wrap_attributes_to: nil)
23
+ return @messages if @messages.blank?
24
+
25
+ # Split errors to field errors and nested objects errors.
26
+ # A dot symbol means that the error is happened with a nested object.
27
+ nested_messages, field_messages = @messages.partition do |field, _|
28
+ base_field_name = field.to_s.split('.').first.to_sym
29
+ @base.class.reflect_on_association(base_field_name)
30
+ end.map(&:to_h)
31
+
32
+ if wrap_attributes_to.present? && field_messages.present?
33
+ field_messages = { wrap_attributes_to => field_messages }
34
+ end
35
+
36
+ detailed_messages = field_messages
37
+ return detailed_messages if nested_messages.blank?
38
+
39
+ # Parse nested messages to structure with nested objects and merge.
40
+ detailed_messages.merge(
41
+ parse_nested_messages(nested_messages, wrap_attributes_to)
42
+ )
43
+ end
44
+
45
+ # Converts nested messages to detailed structure with nested objects
46
+ def parse_nested_messages(nested_messages, wrap_attributes_to)
47
+ # Get names of all relations with errors
48
+ relations = nested_messages.keys
49
+ .map { |field| field.to_s.split('.').first }
50
+ .uniq
51
+
52
+ # Collect messages from nested objects
53
+ relations.map do |relation|
54
+ object = @base.relation_for_nested_messages(relation)
55
+ [relation.to_sym, object.detailed_errors_messages(wrap_attributes_to)]
56
+ end.to_h
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ module BetterValidations::List
2
+ extend ActiveSupport::Concern
3
+ included do
4
+ # A helper method to get messages from the active record collection
5
+ def detailed_errors_messages(wrap_attributes_to)
6
+ select { |object| object.errors.messages.present? }.map do |object|
7
+ messages = object.detailed_errors_messages(wrap_attributes_to)
8
+
9
+ # Add service information about the object in order
10
+ # to distinguish objects in collection from each other
11
+
12
+ unless object.id.nil?
13
+ messages = messages.merge(id: object.id)
14
+ end
15
+
16
+ unless object.client_id.nil?
17
+ messages = messages.merge(client_id: object.client_id)
18
+ end
19
+
20
+ messages
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ class BetterValidations::NestedValidator < ActiveModel::EachValidator
2
+ attr_reader :validator_class
3
+
4
+ def initialize(options)
5
+ @validator_class = options.delete(:validator_class)
6
+ super
7
+ end
8
+
9
+ def validate_each(record, attr_name, value)
10
+ return if value.nil?
11
+
12
+ validator = init_validator(value)
13
+ cache_validator(record, attr_name, validator)
14
+ return if validator_valid?(validator)
15
+
16
+ record.errors.add(error_key(attr_name), error_text)
17
+ end
18
+
19
+ protected
20
+
21
+ def init_validator(value)
22
+ # A value can be a single object or a list of objects
23
+ if value.is_a?(Enumerable)
24
+ value.map { |object| validator_class.new(object) }
25
+ else
26
+ validator_class.new(value)
27
+ end
28
+ end
29
+
30
+ def cache_validator(record, attr_name, validator)
31
+ record.nested_object_validators[attr_name.to_sym] = validator
32
+ end
33
+
34
+ def validator_valid?(validator)
35
+ validators = validator.is_a?(Enumerable) ? validator : [validator]
36
+ validators.map(&:valid?).all?
37
+ end
38
+
39
+ def error_key(attr_name)
40
+ # Create a key with a dot to tell a framework that an error happened in
41
+ # a nested object
42
+ "#{attr_name}.attributes".to_sym
43
+ end
44
+
45
+ def error_text
46
+ I18n.t('errors.messages.invalid')
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ module BetterValidations::Object
2
+ extend ActiveSupport::Concern
3
+ included do
4
+
5
+ # A helper method to get messages without a reference to 'errors'
6
+ def detailed_errors_messages(wrap_attributes_to)
7
+ errors.detailed_messages(wrap_attributes_to: wrap_attributes_to)
8
+ end
9
+
10
+ # Define a 'client_id' attribute for ActiveRecord::Base that can
11
+ # be used to identify objects in the error response.
12
+ def client_id
13
+ @client_id
14
+ end
15
+
16
+ # A setter for 'client_id' attribute. Client can pass this attribute
17
+ # to API and fill it to identify objects in the error response.
18
+ def client_id=(value)
19
+ @client_id = value
20
+ end
21
+
22
+ # Returns relation object for providing nested messages.
23
+ # By default it is the object itself.
24
+ def relation_for_nested_messages(relation)
25
+ public_send(relation)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ module BetterValidations
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,114 @@
1
+ module BetterValidations::Validator
2
+ extend ActiveSupport::Concern
3
+ included do
4
+ include ActiveModel::Validations
5
+ include BetterValidations::Object
6
+
7
+ # A system accessors required by BetterValidations::Object
8
+ attr_accessor :id, :client_id
9
+
10
+ # A hash with cached instances of nested validators by field.
11
+ # Filled by instances of a BetterValidations::NestedValidator class
12
+ # in the process of validation.
13
+ attr_accessor :nested_object_validators
14
+
15
+ # A validation object as a possible data source
16
+ attr_reader :validation_object
17
+
18
+ # A helper method to validate nested object/list
19
+ # represented by other validator.
20
+ #
21
+ # Example of usage in a User model for validating nested PersonalInfo:
22
+ # validate_nested :personal_info, PersonalInfoValidator
23
+ def self.validate_nested(nested_name, validator_class)
24
+ bind_validator(nested_name, validator_class)
25
+ end
26
+
27
+ # Calls a validates_with method to save information about
28
+ # the validating object to the validator and run validations.
29
+ def self.bind_validator(nested_name, validator_class)
30
+ validates_with BetterValidations::NestedValidator,
31
+ attributes: [nested_name],
32
+ validator_class: validator_class
33
+ end
34
+
35
+ # Returns a structure of validators such as:
36
+ # { field: { kind: true }, nested: { field: { kind: true } } }
37
+ # where "kind" is a validator kind such as "presence".
38
+ def self.structure
39
+ validators_by_field_name.reduce({}) do |structure, (field, validator)|
40
+ validations = (structure[field] || {}).merge(validator_hash(validator))
41
+ structure.merge(field => validations)
42
+ end
43
+ end
44
+
45
+ # Returns an array:
46
+ # [[:field_one, validator], [:field_two, validator]]
47
+ # fields can duplicate if have multiple validators.
48
+ def self.validators_by_field_name
49
+ validators.map do |validator|
50
+ validator.attributes.map { |attribute| [attribute, validator] }
51
+ end.flatten(1)
52
+ end
53
+
54
+ # Returns a hash such as:
55
+ # { kind: true }
56
+ # where "kind" is a validator kind such as "presence".
57
+ def self.validator_hash(validator)
58
+ kind = validator.kind
59
+ kind == :nested ? validator.validator_class.structure : { kind => true }
60
+ end
61
+
62
+ # Attributes - Hash, ActionController::Parameters or ActiveRecord::Base
63
+ def initialize(attributes = {})
64
+ assign_attributes(attributes)
65
+ end
66
+
67
+ def assign_attributes(attributes)
68
+ prepare_attributes(attributes).each { |key, value| set_value(key, value) }
69
+ end
70
+
71
+ def merge(*validators)
72
+ BetterValidations::ValidatorsList.new(*([self] + validators))
73
+ end
74
+
75
+ # Override to define a start value and getter by name
76
+ def nested_object_validators(name = nil)
77
+ return nested_object_validators[name.to_sym] unless name.nil?
78
+
79
+ @nested_object_validators ||= {}
80
+ end
81
+
82
+ # The method is overriden for providing a validator object instead of
83
+ # an active record to collecting error messages.
84
+ def relation_for_nested_messages(relation)
85
+ nested_object_validators(relation.to_sym)
86
+ end
87
+
88
+ protected
89
+
90
+ def prepare_attributes(attributes)
91
+ if attributes.is_a? ActiveRecord::Base
92
+ @validation_object = attributes
93
+ attributes = convert_active_record_to_attributes(attributes)
94
+ end
95
+
96
+ attributes
97
+ end
98
+
99
+ def convert_active_record_to_attributes(object)
100
+ attribute_names.reduce({}) do |hash, name|
101
+ hash.merge(name => object.public_send(name))
102
+ end
103
+ end
104
+
105
+ def attribute_names
106
+ (self.class.validators.map(&:attributes).flatten + [:client_id, :id]).uniq
107
+ end
108
+
109
+ def set_value(key, value)
110
+ setter = "#{key}=".to_sym
111
+ public_send(setter, value) if respond_to?(setter)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,84 @@
1
+ # Provides a way to merge multiple validators in order to get
2
+ # merged errors from all of them. Usage:
3
+ #
4
+ # validator = validator_one.merge(validator_two, validator_three)
5
+ # validator.valid?
6
+ # validator.errors.detailed_messages
7
+ #
8
+ class BetterValidations::ValidatorsList
9
+ def initialize(*validators)
10
+ @validators = validators
11
+ end
12
+
13
+ def errors
14
+ self
15
+ end
16
+
17
+ def valid?
18
+ if @invalid_validators.nil?
19
+ @invalid_validators = @validators.select(&:invalid?)
20
+ end
21
+
22
+ @invalid_validators.blank?
23
+ end
24
+
25
+ def invalid?
26
+ !valid?
27
+ end
28
+
29
+ def detailed_messages(wrap_attributes_to: nil)
30
+ (@invalid_validators || []).reduce({}) do |all_messages, validator|
31
+ messages = validator.errors.detailed_messages(
32
+ wrap_attributes_to: wrap_attributes_to
33
+ )
34
+
35
+ deep_merge(all_messages, messages)
36
+ end
37
+ end
38
+
39
+ def deep_merge(hash_one, hash_two)
40
+ # Use a standard method to do a deep merge by specify
41
+ # a values concatination logic
42
+ hash_one.deep_merge(hash_two) do |_key, this_value, other_value|
43
+ # Arrays has a complex logic - they must be merged
44
+ if this_value.is_a?(Array) || other_value.is_a?(Array)
45
+ merge_arrays(this_value, other_value)
46
+ else
47
+ this_value.nil? ? other_value : this_value
48
+ end
49
+ end
50
+ end
51
+
52
+ def merge_arrays(one, two)
53
+ # If we have arrays of strings it means that we have arrays of errors:
54
+ # ['Error 1', 'Error 2'] and ['Error 3', 'Error 4']. In this case we
55
+ # just concat arrays and get unique values.
56
+ if one.first.is_a?(String) || two.first.is_a?(String)
57
+ return (one + two).uniq
58
+ end
59
+
60
+ # Other type of arrays is an array of hashes. Each hash is
61
+ # an object with attributes:
62
+ # [{ id: 1, client_id: 2, key: "String", another_key: {} }]
63
+ merge_arrays_with_errors(one, two)
64
+ end
65
+
66
+ # Merges arrays of hashes to a single array by deep merging items
67
+ # with the same ":id" and ":client_id".
68
+ def merge_arrays_with_errors(one, two)
69
+ all = one + two
70
+ all = merge_hashes_by_key(all, :id)
71
+ merge_hashes_by_key(all, :client_id)
72
+ end
73
+
74
+ def merge_hashes_by_key(hashes, key)
75
+ hashes_by_id = hashes.group_by { |hash| hash[key] }
76
+ hashes_by_id.map do |id, hashes_with_id|
77
+ next hashes_with_id if id.nil?
78
+
79
+ # Merge all hashes with the same id to the single hash.
80
+ # Wrap to array in order to save a format (hashes_with_id is a array)
81
+ [hashes_with_id.reduce({}) { |result, hash| deep_merge(result, hash) }]
82
+ end.flatten(1)
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ module BetterValidations
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: better_validations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Petr Bazov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
33
+ description: Extension for default Rails validations. Adds some useful methods.
34
+ email:
35
+ - petr@sequenia.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - MIT-LICENSE
41
+ - README.md
42
+ - lib/better_validations.rb
43
+ - lib/better_validations/errors.rb
44
+ - lib/better_validations/list.rb
45
+ - lib/better_validations/nested_validator.rb
46
+ - lib/better_validations/object.rb
47
+ - lib/better_validations/railtie.rb
48
+ - lib/better_validations/validator.rb
49
+ - lib/better_validations/validators_list.rb
50
+ - lib/better_validations/version.rb
51
+ homepage: http://sequenia.com/
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.0.3
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Extension for default Rails validations.
74
+ test_files: []