fend 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.
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fend
4
+ module Plugins
5
+ # `full_messages` plugin adds `#full_messages` method to `Result` which
6
+ # returns error messages with prependend param name
7
+ #
8
+ # class UserValidation < Fend
9
+ # plugin :full_messages
10
+ #
11
+ # # ...
12
+ # end
13
+ # result = UserValidation.call(email: "invalid", profile: "invalid", address: { })
14
+ #
15
+ # result.full_messages
16
+ # #=> { email: ["email is in invalid format"], profile: ["profile must be hash"], address: { city: ["city must be string"] } }
17
+ #
18
+ # ## Array members
19
+ #
20
+ # When validating array elements, messages are returned with prependend
21
+ # index, since array members don't have a name.
22
+ #
23
+ # { tags: { 0 => ["0 must be string"] } }
24
+ #
25
+ # In order to make full messages nicer for array elements,
26
+ # pass `:array_memeber_names` option when loading the plugin:
27
+ #
28
+ # plugin :full_messages, array_member_names: { tags: :tag }
29
+ #
30
+ # # which will produce
31
+ # { tags: { 0 => ["tag must be string"] } }
32
+ #
33
+ # `:array_member_names` options is inheritable, so it's possible to define
34
+ # it globaly by loading the plugin directly through `Fend` class.
35
+ #
36
+ # Fend.plugin :full_messages, array_member_names: { octopi: :octopus }
37
+ #
38
+ module FullMessages
39
+ def self.configure(validation, opts = {})
40
+ validation.opts[:full_messages_array_member_names] = (validation.opts[:full_messages_array_member_names] || {}).merge(opts[:array_member_names] || {})
41
+ end
42
+
43
+ module ResultMethods
44
+ def full_messages
45
+ @_full_messages ||= generate_full_messages(@errors)
46
+ end
47
+
48
+ private
49
+
50
+ def generate_full_messages(errors, array_param_name = nil)
51
+ errors.each_with_object({}) do |(param, messages), result|
52
+ result[param] = if messages.is_a?(Hash)
53
+ param_is_array = messages.first[0].is_a?(Integer)
54
+
55
+ generate_full_messages(messages, param_is_array ? param : nil)
56
+ else
57
+ param_name = fend_class.opts[:full_messages_array_member_names].fetch(array_param_name, param)
58
+ messages.map { |message| "#{param_name} #{message}"}
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ register_plugin(:full_messages, FullMessages)
66
+ end
67
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fend
4
+ module Plugins
5
+ # `validation_helpers` plugin provides additional `Param` methods for common
6
+ # validation cases.
7
+ #
8
+ # plugin :validation_helpers
9
+ #
10
+ # validate do |i|
11
+ # i.param(:username) do |username|
12
+ # username.validate_presence
13
+ # username.validate_max_length(20)
14
+ # username.validate_type(String)
15
+ # end
16
+ # end
17
+ #
18
+ # You can find list of all available helpers in ParamMethods.
19
+ #
20
+ # ## Overriding default messages
21
+ #
22
+ # You can override default messages by specifying `:default_messages`
23
+ # options when loading the plugin
24
+ #
25
+ # plugin :validation_helpers, default_messages: {
26
+ # exact_length: ->(length) { I18n.t("errors.exact_length", length: length) },
27
+ # presence: "cannot be blank",
28
+ # type: ->(type) { "is not of valid type. Must be #{type.to_s.downcase}" }
29
+ # }
30
+ #
31
+ # Custom messages can be defined by passing `:message` option to validation
32
+ # helper method:
33
+ #
34
+ # username.validate_max_length(20, message: "must be shorter than 20 chars")
35
+
36
+ module ValidationHelpers
37
+ # depends on ValueHelpers plugin, which provides methods that are used in
38
+ # certain validation helpers
39
+ def self.load_dependencies(validation, *args, &block)
40
+ validation.plugin(:value_helpers)
41
+ end
42
+
43
+ def self.configure(validation, opts = {})
44
+ validation.opts[:validation_default_messages] = (validation.opts[:validation_default_messages] || {}).merge(opts[:default_messages] || {})
45
+ end
46
+
47
+ DEFAULT_MESSAGES = {
48
+ absence: -> { "must be absent" },
49
+ acceptance: -> { "must be accepted" },
50
+ equality: ->(value) { "must be equal to '#{value}'" },
51
+ exact_length: ->(length) { "length must be equal to #{length}" },
52
+ exclusion: ->(list) { "cannot be one of: #{list.join(', ')}" },
53
+ format: -> { "is in invalid format" },
54
+ greater_than: ->(value) { "must be greater than #{value}" },
55
+ greater_than_or_equal_to: ->(value) { "must be greater than or equal to #{value}" },
56
+ inclusion: ->(list) { "must be one of: #{list.join(', ')}" },
57
+ length_range: ->(range) { "length must be between #{range.min} and #{range.max}" },
58
+ less_than: ->(value) { "must be less than #{value}" },
59
+ less_than_or_equal_to: ->(value) { "must be less than or equal to #{value}" },
60
+ max_length: ->(value) { "length cannot be greater than #{value}" },
61
+ min_length: ->(value) { "length cannot be less than #{value}" },
62
+ presence: -> { "must be present" },
63
+ type: ->(type) { "must be #{type.to_s.downcase}" }
64
+ }.freeze
65
+
66
+ ACCEPTABLE = [1, "1", true, "true", "TRUE", :yes, "YES", "yes"].freeze
67
+ UNSUPPORTED_TYPE = "__unsupported_type__".freeze
68
+
69
+ module ParamClassMethods
70
+ def default_messages
71
+ @default_messages ||= DEFAULT_MESSAGES.merge(fend_class.opts[:validation_default_messages])
72
+ end
73
+ end
74
+
75
+ module ParamMethods
76
+ # Validates that param value is blank. To see what values are considered
77
+ # as blank, check ValueHelpers::ParamMethods#blank?.
78
+ #
79
+ # id.validate_absence
80
+ def validate_absence(opts = {})
81
+ add_error(:absence, opts[:message]) if present?
82
+ end
83
+
84
+ # Validates acceptance. Potential use case would be checking if Terms of
85
+ # Service has been accepted.
86
+ #
87
+ # By default, validation will pass if value is one of:
88
+ # `[1, "1", :true, true, "true", "TRUE", :yes, "YES", "yes"]`
89
+ #
90
+ # You can pass the `:as` option with custom list of acceptable values:
91
+ #
92
+ # tos.validate_acceptance(as: ["Agreed", "OK"])
93
+ def validate_acceptance(opts = {})
94
+ as = Array(opts.fetch(:as, ACCEPTABLE))
95
+
96
+ add_error(:acceptance, opts[:message]) unless as.include?(value)
97
+ end
98
+
99
+ # Validates that param value is equal to the specified value.
100
+ #
101
+ # color.validate_equality("black")
102
+ def validate_equality(rhs, opts = {})
103
+ add_error(:equality, opts[:message], rhs) unless value.eql?(rhs)
104
+ end
105
+
106
+ # Validates that param value length is equal to the specified value.
107
+ # Works with any object that responds to `#length` method.
108
+ #
109
+ # code.validate_exact_length(10)
110
+ def validate_exact_length(exact_length, opts = {})
111
+ value_length = value.respond_to?(:length) ? value.length : UNSUPPORTED_TYPE
112
+
113
+ return if !value_length.eql?(UNSUPPORTED_TYPE) && value_length.eql?(exact_length)
114
+
115
+ add_error(:exact_length, opts[:message], exact_length)
116
+ end
117
+
118
+ # Validates that param value is not one of the specified values.
119
+ #
120
+ # account_type.validate_exclusion(["admin", "editor"])
121
+ def validate_exclusion(exclude_from, opts = {})
122
+ add_error(:exclusion, opts[:message], exclude_from) if exclude_from.include?(value)
123
+ end
124
+
125
+ # Validates that param value is a match for specified regex.
126
+ #
127
+ # name.validate_format(/\A[a-z]\z/i)
128
+ def validate_format(format, opts = {})
129
+ add_error(:format, opts[:message]) if format.match(value.to_s).nil?
130
+ end
131
+
132
+ # Validates that param value is greater than specified value
133
+ #
134
+ # age.validate_greater_than(18)
135
+ def validate_greater_than(rhs, opts = {})
136
+ add_error(:greater_than, opts[:message], rhs) unless value.is_a?(Numeric) && value > rhs
137
+ end
138
+
139
+ # Validates that param value is greater than or equal to specified value
140
+ #
141
+ # age.validate_greater_than_or_equal_to(18)
142
+ #
143
+ # Aliased as `validate_gteq`
144
+ #
145
+ # age.validate_gteq(10)
146
+ def validate_greater_than_or_equal_to(rhs, opts = {})
147
+ add_error(:greater_than_or_equal_to, opts[:message], rhs) unless value.is_a?(Numeric) && value >= rhs
148
+ end
149
+ alias_method :validate_gteq, :validate_greater_than_or_equal_to
150
+
151
+ # Validates that param value is one of the specified values.
152
+ #
153
+ # account_type.validate_inclusion(["admin", "editor"])
154
+ def validate_inclusion(include_in, opts = {})
155
+ add_error(:inclusion, opts[:message], include_in) unless include_in.include?(value)
156
+ end
157
+
158
+ # Validates that param value length is within specified range
159
+ #
160
+ # code.validate_length_range(10..15)
161
+ def validate_length_range(range, opts = {})
162
+ value_length = value.respond_to?(:length) ? value.length : UNSUPPORTED_TYPE
163
+
164
+ return if !value_length.eql?(UNSUPPORTED_TYPE) && range.include?(value_length)
165
+
166
+ add_error(:length_range, opts[:message], range)
167
+ end
168
+
169
+ # Validates that param value is less than specified value
170
+ #
171
+ # funds.validate_less_than(100)
172
+ def validate_less_than(rhs, opts = {})
173
+ add_error(:less_than, opts[:message], rhs) unless value.is_a?(Numeric) && value < rhs
174
+ end
175
+
176
+ # Validates that param value is less than or equal to specified value
177
+ #
178
+ # funds.validate_less_than_or_equal_to(100)
179
+ #
180
+ # Aliased as `validate_lteq`
181
+ #
182
+ # funds.validate_lteq(100)
183
+ def validate_less_than_or_equal_to(rhs, opts = {})
184
+ add_error(:less_than_or_equal_to, opts[:message], rhs) unless value.is_a?(Numeric) && value <= rhs
185
+ end
186
+ alias_method :validate_lteq, :validate_less_than_or_equal_to
187
+
188
+ # Validates that param value length is not greater than specified value
189
+ #
190
+ # password.validate_max_length(15)
191
+ def validate_max_length(length, opts = {})
192
+ value_length = value.respond_to?(:length) ? value.length : UNSUPPORTED_TYPE
193
+
194
+ return if !value_length.eql?(UNSUPPORTED_TYPE) && value_length <= length
195
+
196
+ add_error(:max_length, opts[:message], length)
197
+ end
198
+
199
+ # Validates that param value length is not less than specified value
200
+ #
201
+ # password.validate_min_length(5)
202
+ def validate_min_length(length, opts = {})
203
+ value_length = value.respond_to?(:length) ? value.length : UNSUPPORTED_TYPE
204
+
205
+ return if !value_length.eql?(UNSUPPORTED_TYPE) && value_length >= length
206
+
207
+ add_error(:min_length, opts[:message], length)
208
+ end
209
+
210
+ # Validates that param value is present. To see what values are
211
+ # considered as present, check ValueHelpers::ParamMethods#present?
212
+ #
213
+ # name.validate_presence
214
+ def validate_presence(opts = {})
215
+ add_error(:presence, opts[:message]) if blank?
216
+ end
217
+
218
+ # Uses ValueHelpers::ParamMethods#type_of? method to validate that
219
+ # param value is of specified type.
220
+ #
221
+ # tags.validate_type(Array)
222
+ def validate_type(type, opts = {})
223
+ add_error(:type, opts[:message], type) unless type_of?(type)
224
+ end
225
+
226
+ # :nodoc:
227
+ def add_error(*args)
228
+ if args.size == 1 && args.first.is_a?(String)
229
+ super(*args)
230
+ else
231
+ @errors << error_message(*args)
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ def error_message(type, message, *args)
238
+ message ||= self.class.default_messages.fetch(type)
239
+ message.is_a?(String) ? message : message.call(*args)
240
+ end
241
+ end
242
+ end
243
+
244
+ register_plugin(:validation_helpers, ValidationHelpers)
245
+ end
246
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fend
4
+ module Plugins
5
+ # Instead of calling ValidationHelpers::ParamMethods separately,
6
+ # you can use `validation_options` plugin in order to specify all
7
+ # validations as options and pass them to `Param#validate` method.
8
+ #
9
+ # plugin :validation_options
10
+ #
11
+ # validate do |i|
12
+ # i.param(:email) do |email|
13
+ # email.validate(presence: true, type: String, format: EMAIL_REGEX)
14
+ # end
15
+ # end
16
+ #
17
+ # ## Custom error messages
18
+ #
19
+ # Custom error messages can be defined with `:message` option:
20
+ #
21
+ # email.validate(presence: { message: "cannot be blank"})
22
+ #
23
+ # ## Mandatory arguments
24
+ #
25
+ # For ValidationHelpers::ParamMethods that expect mandatory arguments, there
26
+ # are predefined option keys that you can use. To see them all check
27
+ # MANDATORY_ARG_KEYS constant.
28
+ #
29
+ # email.validate type: { of: String, message: "is not a string" }, format: { with: EMAIL_REGEX }
30
+ # account_type.validate inclusion: { in: %w(admin, moderator) }
31
+ #
32
+ # You can also use the DEFAULT_ARG_KEY (`:value`) if you find it hard to
33
+ # remember the specific ones.
34
+ #
35
+ # email.validate type: { value: String }, format: { value: EMAIL_REGEX }
36
+ #
37
+ # `validation_options` supports ExternalValidation plugin:
38
+ #
39
+ # plugin :external_validation
40
+ #
41
+ # # ...
42
+ #
43
+ # email.validate(with: CustomEmailValidator)
44
+ module ValidationOptions
45
+ NO_ARG_METHODS = [:absence, :presence, :acceptance].freeze
46
+ ARRAY_ARG_METHODS = [:exclusion, :inclusion, :length_range].freeze
47
+
48
+ DEFAULT_ARG_KEY = :value
49
+
50
+ # List of keys to use when specifying mandatory validation arguments
51
+ MANDATORY_ARG_KEYS = {
52
+ equality: :value,
53
+ exact_length: :of,
54
+ exclusion: :from,
55
+ format: :with,
56
+ greater_than: :value,
57
+ greater_than_or_equal_to: :value,
58
+ gteq: :value,
59
+ inclusion: :in,
60
+ length_range: :within,
61
+ less_than: :value,
62
+ less_than_or_equal_to: :value,
63
+ lteq: :value,
64
+ max_length: :of,
65
+ min_length: :of,
66
+ type: :of
67
+ }.freeze
68
+
69
+ # Depends on ValidationHelpers plugin
70
+ def self.load_dependencies(validation, *args, &block)
71
+ validation.plugin(:validation_helpers)
72
+ end
73
+
74
+ module ParamMethods
75
+ def validate(opts = {})
76
+ return if opts.empty?
77
+
78
+ opts.each do |validator_name, args|
79
+ method_name = "validate_#{validator_name}"
80
+
81
+ raise Error, "undefined validation method '#{validator_name}'" unless respond_to?(method_name)
82
+
83
+ if NO_ARG_METHODS.include?(validator_name)
84
+ if !!args == args
85
+ next unless args
86
+
87
+ validation_method_args = []
88
+ else
89
+ validation_method_args = [args]
90
+ end
91
+ elsif args.is_a?(Hash)
92
+ next if args[:allow_nil] == true && value.nil?
93
+ next if args[:allow_blank] == true && blank?
94
+
95
+ mandatory_arg_key = MANDATORY_ARG_KEYS[validator_name]
96
+
97
+ unless args.key?(mandatory_arg_key) || args.key?(DEFAULT_ARG_KEY)
98
+ raise Error, "missing mandatory argument for '#{validator_name}' validator"
99
+ end
100
+
101
+ mandatory_arg = args.delete(mandatory_arg_key) || args.delete(DEFAULT_ARG_KEY)
102
+
103
+ validation_method_args = [mandatory_arg, args]
104
+ else
105
+ validation_method_args = ARRAY_ARG_METHODS.include?(validator_name) ? [args] : args
106
+ end
107
+
108
+ public_send(method_name, *validation_method_args)
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ register_plugin(:validation_options, ValidationOptions)
115
+ end
116
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ class Fend
6
+ module Plugins
7
+ # `value_helpers` plugin provides helper methods you can use to
8
+ # check/fetch param values.
9
+ #
10
+ # plugin :value_helpers
11
+ #
12
+ # validate do |i|
13
+ # i.param(:username) do |username|
14
+ # username.present? #=> true
15
+ # username.blank? #=> false
16
+ # username.empty_string? #=> false
17
+ # end
18
+ # end
19
+ #
20
+ # For a complete list of available methods, see ParamMethods.
21
+ module ValueHelpers
22
+ module ParamMethods
23
+ # Returns `true` when:
24
+ #
25
+ # * `value.empty?`
26
+ # * `value.nil?`
27
+ # * `value == false`
28
+ # * `value.empty_string?`
29
+ def blank?
30
+ case value
31
+ when Array, Hash
32
+ value.empty?
33
+ when NilClass, FalseClass
34
+ true
35
+ when Integer, Float, Numeric, Time, TrueClass, Symbol
36
+ false
37
+ when String
38
+ empty_string?
39
+ else
40
+ value.respond_to?(:empty?) ? !!value.empty? : !value
41
+ end
42
+ end
43
+
44
+ # Enables easier fetching of nested data values.
45
+ # Works with hashes and arrays.
46
+ #
47
+ # validate do |i|
48
+ # # { user: { address: { city: "Amsterdam" } } }
49
+ # i.dig(:user, :address, :city) #=> "Amsterdam"
50
+ # i.dig(:user, :profile, :username) #=> nil
51
+ #
52
+ # # { tags: [ { id: 2, name: "JS" }, { id: 3, name: "Ruby" }] }
53
+ # i.dig(:tags, 1, :name) #=> "Ruby"
54
+ # i.dig(:tags, 5, :id) #=> nil
55
+ #
56
+ # i.param(:accounts) do |accounts|
57
+ # accounts.dig(0, :transactions, 3) #=> "$100.00"
58
+ # end
59
+ # end
60
+ def dig(*path)
61
+ result = value
62
+
63
+ path.each do |point|
64
+ break if result.is_a?(Array) && !point.is_a?(Integer)
65
+
66
+ result = result.is_a?(Enumerable) ? result[point] : nil
67
+
68
+ break if result.nil?
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ # Returns `true` when value is an empty string (_space_, _tab_, _newline_,
75
+ # _carriage_return_, etc...)
76
+ #
77
+ # # email.value #=> ""
78
+ # # email.value #=> " "
79
+ # # email.value #=> "\n"
80
+ # # email.value #=> "\r"
81
+ # # email.value #=> "\t"
82
+ # # email.value #=> "\n\r\t"
83
+ #
84
+ # email.empty_string? #=> true
85
+ def empty_string?
86
+ return false unless value.is_a?(String) || value.is_a?(Symbol)
87
+
88
+ regex = /\A[[:space:]]*\z/
89
+
90
+ !regex.match(value).nil?
91
+ end
92
+
93
+ # Returns `true` if value is present/not blank
94
+ def present?
95
+ !blank?
96
+ end
97
+
98
+ # Returns `true` if value is of specified type. Accepts constants, their
99
+ # string representations and symbols:
100
+ #
101
+ # email.type_of?(String)
102
+ #
103
+ # # or
104
+ #
105
+ # email.type_of?("string")
106
+ #
107
+ # # or
108
+ #
109
+ # email.type_of?(:string)
110
+ #
111
+ # Additional examples:
112
+ #
113
+ # # these are all checking the same thing
114
+ # user.type_of?(AdminUser)
115
+ # user.type_of?(:admin_user)
116
+ # user.type_of?("admin_user")
117
+ #
118
+ # Provides a convenient way for checking if value is boolean, decimal or
119
+ # nil:
120
+ #
121
+ # # true if value is TrueClass or FalseClass
122
+ # confirmed.type_of?(:boolean)
123
+ #
124
+ # # true if value is Float or BigDecimal
125
+ # amount.type_of?(:decimal)
126
+ #
127
+ # # true if value is nil/NilClass
128
+ # email.type_of?(:nil)
129
+ def type_of?(type_ref)
130
+ return value.is_a?(type_ref) unless type_ref.is_a?(String) || type_ref.is_a?(Symbol)
131
+
132
+ case type_ref.to_s
133
+ when "boolean" then !!value == value
134
+ when "decimal" then value.is_a?(Float) || value.is_a?(BigDecimal)
135
+ when "nil" then value.is_a?(NilClass)
136
+ else
137
+ camelized_type_ref = type_ref.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:\A|_)(.)/) { $1.upcase }
138
+ type_class = Object.const_get(camelized_type_ref)
139
+
140
+ value.is_a?(type_class)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ register_plugin(:value_helpers, ValueHelpers)
147
+ end
148
+ end