fleck 1.0.1 → 2.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.
- checksums.yaml +4 -4
- data/.gitignore +11 -10
- data/CHANGELOG.md +89 -74
- data/Gemfile +6 -4
- data/examples/actions.rb +59 -53
- data/examples/blocking_consumer.rb +42 -42
- data/examples/consumer_initialization.rb +44 -42
- data/examples/deprecation.rb +50 -57
- data/examples/example.rb +76 -74
- data/examples/expired.rb +72 -76
- data/examples/fanout.rb +62 -64
- data/fleck.gemspec +37 -36
- data/lib/fleck/client.rb +124 -124
- data/lib/fleck/configuration.rb +149 -144
- data/lib/fleck/consumer.rb +7 -287
- data/lib/fleck/core/consumer/action_param.rb +106 -0
- data/lib/fleck/core/consumer/actions.rb +76 -0
- data/lib/fleck/core/consumer/base.rb +111 -0
- data/lib/fleck/core/consumer/configuration.rb +69 -0
- data/lib/fleck/core/consumer/decorators.rb +77 -0
- data/lib/fleck/core/consumer/helpers_definers.rb +55 -0
- data/lib/fleck/core/consumer/logger.rb +88 -0
- data/lib/fleck/core/consumer/request.rb +89 -0
- data/lib/fleck/core/consumer/response.rb +77 -0
- data/lib/fleck/core/consumer/response_helpers.rb +81 -0
- data/lib/fleck/core/consumer/validation.rb +163 -0
- data/lib/fleck/core/consumer.rb +166 -0
- data/lib/fleck/core.rb +9 -0
- data/lib/fleck/loggable.rb +15 -10
- data/lib/fleck/{hash_with_indifferent_access.rb → utilities/hash_with_indifferent_access.rb} +80 -85
- data/lib/fleck/utilities/host_rating.rb +104 -0
- data/lib/fleck/version.rb +6 -3
- data/lib/fleck.rb +81 -72
- metadata +35 -24
- data/lib/fleck/consumer/request.rb +0 -52
- data/lib/fleck/consumer/response.rb +0 -80
- data/lib/fleck/host_rating.rb +0 -74
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fleck
|
4
|
+
module Core
|
5
|
+
class Consumer
|
6
|
+
# `Decorators` module implements the feature which allows to use decorators for action methods.
|
7
|
+
# This will provide a easier and cleaner way to define consumer actions
|
8
|
+
module Decorators
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
base.send :include, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
# Defines class methods to import when `Decorators` module is imported.
|
15
|
+
module ClassMethods
|
16
|
+
def method_added(name)
|
17
|
+
super(name)
|
18
|
+
|
19
|
+
# Register method as action `action` or `action_name` decorator has been used
|
20
|
+
method_options[:action_name] && register_action(method_options[:action_name], name, method_options)
|
21
|
+
|
22
|
+
# Reset method options after method has been added
|
23
|
+
reset_method_options!
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_options
|
27
|
+
@method_options ||= default_method_options
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_method_options
|
31
|
+
{
|
32
|
+
action_name: nil,
|
33
|
+
description: nil,
|
34
|
+
params: {}.to_hash_with_indifferent_access,
|
35
|
+
headers: {}.to_hash_with_indifferent_access
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset_method_options!
|
40
|
+
@method_options = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def action(name, description = nil)
|
44
|
+
action_name(name)
|
45
|
+
desc(description)
|
46
|
+
end
|
47
|
+
|
48
|
+
def desc(description)
|
49
|
+
method_options[:description] = description if description
|
50
|
+
end
|
51
|
+
|
52
|
+
def param(name, options = {})
|
53
|
+
method_options[:params][name] = ActionParam.new(name, options[:type], options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def header(name, options = {})
|
57
|
+
raise 'Not Implemented'
|
58
|
+
# method_options[:headers][name] = ActionParam.new(name, options[:type], options)
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def action_name(name)
|
64
|
+
valid = name.is_a?(String) || name.is_a?(Symbol)
|
65
|
+
valid or raise(ArgumentError, "Invalid action name type: #{name.class}, String or Symbol expected!")
|
66
|
+
|
67
|
+
method_options[:action_name] = name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Defines instance methods to import when `Decorators` module is imported.
|
72
|
+
module InstanceMethods
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Fleck
|
2
|
+
module Core
|
3
|
+
class Consumer
|
4
|
+
module HelpersDefiners
|
5
|
+
INTERRUPT_NAME = :terminate_execution
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
# Defines class methods to import when `HelpersDefilers` module is imported.
|
13
|
+
module ClassMethods
|
14
|
+
def error_method(name, code, message)
|
15
|
+
define_method(name) do |error: nil, body: nil, interrupt: true|
|
16
|
+
response.render_error(code, [message] + [error].flatten)
|
17
|
+
response.body = body
|
18
|
+
throw INTERRUPT_NAME if interrupt
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def redirect_method(name, code)
|
23
|
+
success_method(name, code)
|
24
|
+
end
|
25
|
+
|
26
|
+
def success_method(name, code)
|
27
|
+
define_method(name) do |body = nil, interrupt: true|
|
28
|
+
response.status = code
|
29
|
+
response.body = body
|
30
|
+
throw INTERRUPT_NAME if interrupt
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def information_method(name, code)
|
35
|
+
success_method(name, code)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Defines instance methods to import when `HelpersDefilers` module is imported.
|
40
|
+
module InstanceMethods
|
41
|
+
def halt(code, body = nil, errors = nil)
|
42
|
+
response.body = body
|
43
|
+
if code >= 400
|
44
|
+
response.render_error(code, [errors].flatten)
|
45
|
+
else
|
46
|
+
response.status = code
|
47
|
+
end
|
48
|
+
|
49
|
+
throw INTERRUPT_NAME
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fleck
|
4
|
+
module Core
|
5
|
+
class Consumer
|
6
|
+
module Logger
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.send :include, InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
# Defines class methods to import when `Logger` module is imported.
|
13
|
+
module ClassMethods
|
14
|
+
attr_accessor :logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# Defines instance methods to import when `Logger` module is imported.
|
18
|
+
module InstanceMethods
|
19
|
+
def logger
|
20
|
+
return @logger if @logger
|
21
|
+
|
22
|
+
@logger = self.class.logger.clone
|
23
|
+
@logger.progname = self.class.name.to_s + (configs[:concurrency].to_i <= 1 ? '' : "[#{consumer_id}]")
|
24
|
+
|
25
|
+
@logger
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def log_request
|
31
|
+
status = final_response_status
|
32
|
+
message = log_formatted_message
|
33
|
+
|
34
|
+
if status >= 500
|
35
|
+
logger.error message
|
36
|
+
elsif status >= 400 || response.deprecated?
|
37
|
+
logger.warn message
|
38
|
+
else
|
39
|
+
logger.info message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def exchange_type_code
|
44
|
+
rmq_exchange_type.to_s[0].upcase
|
45
|
+
end
|
46
|
+
|
47
|
+
def final_response_status
|
48
|
+
return 406 if request.rejected?
|
49
|
+
return 503 if channel.closed?
|
50
|
+
|
51
|
+
response.status
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_formatted_message
|
55
|
+
[
|
56
|
+
request_origin,
|
57
|
+
exchange_and_queue_name,
|
58
|
+
request_metadata,
|
59
|
+
request_execution_time,
|
60
|
+
deprecation_message
|
61
|
+
].join
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_origin
|
65
|
+
"#{request.ip} #{request.app_id} => "
|
66
|
+
end
|
67
|
+
|
68
|
+
def exchange_and_queue_name
|
69
|
+
ex_name = rmq_exchange_name.to_s == '' ? ''.inspect : rmq_exchange_name
|
70
|
+
"(#{ex_name.to_s.inspect}|#{exchange_type_code}|#{queue_name}) "
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_metadata
|
74
|
+
"##{request.id} \"#{request.action} /#{request.version || 'v1'}\" #{final_response_status} "
|
75
|
+
end
|
76
|
+
|
77
|
+
def request_execution_time
|
78
|
+
"(#{request.execution_time}ms)"
|
79
|
+
end
|
80
|
+
|
81
|
+
def deprecation_message
|
82
|
+
response.deprecated? ? ' DEPRECATED' : ''
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fleck
|
4
|
+
module Core
|
5
|
+
class Consumer
|
6
|
+
class Request
|
7
|
+
include Fleck::Loggable
|
8
|
+
|
9
|
+
attr_reader :id, :response, :metadata, :payload, :action, :data, :headers, :version, :ip, :params, :status, :errors,
|
10
|
+
:delivery_tag, :app_id, :reply_to, :created_at, :processed_at
|
11
|
+
|
12
|
+
def initialize(metadata, payload, delivery_info)
|
13
|
+
@created_at = Time.now
|
14
|
+
@id = metadata.correlation_id
|
15
|
+
logger.progname += " #{@id}"
|
16
|
+
|
17
|
+
@response = Fleck::Core::Consumer::Response.new(metadata.correlation_id)
|
18
|
+
@metadata = metadata
|
19
|
+
@app_id = metadata[:app_id]
|
20
|
+
@reply_to = @metadata.reply_to
|
21
|
+
@payload = payload
|
22
|
+
@exchange = delivery_info.exchange.inspect
|
23
|
+
@queue = delivery_info.routing_key.inspect
|
24
|
+
@delivery_tag = delivery_info.delivery_tag
|
25
|
+
@data = {}
|
26
|
+
@headers = (@metadata.headers || {}).to_hash_with_indifferent_access
|
27
|
+
@action = @metadata.type
|
28
|
+
@version = nil
|
29
|
+
@ip = nil
|
30
|
+
@params = {}
|
31
|
+
@failed = false
|
32
|
+
@rejected = false
|
33
|
+
@requeue = false
|
34
|
+
|
35
|
+
parse_request!
|
36
|
+
end
|
37
|
+
|
38
|
+
def processed!
|
39
|
+
@processed_at = Time.now
|
40
|
+
end
|
41
|
+
|
42
|
+
def execution_time
|
43
|
+
((@processed_at.to_f - @created_at.to_f) * 1000).round(2)
|
44
|
+
end
|
45
|
+
|
46
|
+
def failed?
|
47
|
+
@failed
|
48
|
+
end
|
49
|
+
|
50
|
+
def reject!(requeue: false)
|
51
|
+
@rejected = true
|
52
|
+
@requeue = requeue
|
53
|
+
processed!
|
54
|
+
end
|
55
|
+
|
56
|
+
def rejected?
|
57
|
+
@rejected
|
58
|
+
end
|
59
|
+
|
60
|
+
def requeue?
|
61
|
+
@requeue
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def parse_request!
|
67
|
+
@data = Oj.load(@payload, mode: :compat).to_hash_with_indifferent_access.filtered!
|
68
|
+
@headers.merge!(@data['headers'] || {}).filtered!
|
69
|
+
|
70
|
+
logger.debug "Processing request (exchange: #{@exchange}, queue: #{@queue}, options: #{@headers}, message: #{@data})"
|
71
|
+
|
72
|
+
@action ||= @headers['action']
|
73
|
+
@headers['action'] ||= @action
|
74
|
+
@version = @headers['version']
|
75
|
+
@ip = @headers['ip']
|
76
|
+
@params = @data['params'] || {}
|
77
|
+
rescue Oj::ParseError => e
|
78
|
+
log_error(e)
|
79
|
+
response.render_error(400, 'Bad request', e.inspect)
|
80
|
+
@failed = true
|
81
|
+
rescue StandardError => e
|
82
|
+
log_error(e)
|
83
|
+
response.render_error(500, 'Internal Server Error', e.inspect)
|
84
|
+
@failed = true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
|
2
|
+
module Fleck
|
3
|
+
module Core
|
4
|
+
class Consumer
|
5
|
+
class Response
|
6
|
+
include Fleck::Loggable
|
7
|
+
|
8
|
+
attr_accessor :id, :status, :errors, :headers, :body
|
9
|
+
|
10
|
+
def initialize(request_id)
|
11
|
+
@id = request_id
|
12
|
+
logger.progname += " #{@id}"
|
13
|
+
|
14
|
+
@status = 200
|
15
|
+
@errors = []
|
16
|
+
@headers = {}
|
17
|
+
@body = nil
|
18
|
+
@deprecated = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def errors?
|
22
|
+
!@errors.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def deprecated!
|
26
|
+
@deprecated = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def deprecated?
|
30
|
+
@deprecated
|
31
|
+
end
|
32
|
+
|
33
|
+
def not_found(msg = nil)
|
34
|
+
@status = 404
|
35
|
+
@errors << 'Resource Not Found'
|
36
|
+
@errors << msg if msg
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_error(status, msg = [])
|
40
|
+
raise ArgumentError, "Invalid status code: #{status.inspect}" unless (400..599).cover?(status.to_i)
|
41
|
+
|
42
|
+
@status = status.to_i
|
43
|
+
if msg.is_a?(Array)
|
44
|
+
@errors += msg
|
45
|
+
else
|
46
|
+
@errors << msg
|
47
|
+
end
|
48
|
+
|
49
|
+
@errors.compact!
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_json(filter: false)
|
53
|
+
data = {
|
54
|
+
"status" => @status,
|
55
|
+
"errors" => @errors,
|
56
|
+
"headers" => @headers,
|
57
|
+
"body" => @body,
|
58
|
+
"deprecated" => @deprecated
|
59
|
+
}
|
60
|
+
data.filter! if filter
|
61
|
+
|
62
|
+
return Oj.dump(data, mode: :compat)
|
63
|
+
rescue => e
|
64
|
+
logger.error e.inspect + "\n" + e.backtrace.join("\n")
|
65
|
+
return Oj.dump({
|
66
|
+
"status" => 500,
|
67
|
+
"errors" => ['Internal Server Error', 'Failed to dump the response to JSON']
|
68
|
+
}, mode: :compat)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
return "#<#{self.class} #{self.to_json(filter: true)}>"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fleck
|
4
|
+
module Core
|
5
|
+
# Open `Consumer` class in order to define consumer helpers
|
6
|
+
class Consumer
|
7
|
+
# Define methods for 1xx codes
|
8
|
+
information_method :continue!, 100
|
9
|
+
information_method :switching_protocols!, 101
|
10
|
+
information_method :processing!, 102
|
11
|
+
information_method :early_hints!, 103
|
12
|
+
|
13
|
+
# Define methods for 2xx codes
|
14
|
+
success_method :ok!, 200
|
15
|
+
success_method :created!, 201
|
16
|
+
success_method :accepted!, 202
|
17
|
+
success_method :non_authoritative_information!, 203
|
18
|
+
success_method :no_content!, 204
|
19
|
+
success_method :reset_content!, 205
|
20
|
+
success_method :partial_content!, 206
|
21
|
+
success_method :multi_status!, 207
|
22
|
+
success_method :already_reported!, 208
|
23
|
+
success_method :im_used!, 226
|
24
|
+
|
25
|
+
# Define methods for 3xx codes
|
26
|
+
redirect_method :multiple_choice!, 300
|
27
|
+
redirect_method :moved_permanently!, 301
|
28
|
+
redirect_method :found!, 302
|
29
|
+
redirect_method :see_other!, 303
|
30
|
+
redirect_method :not_modified!, 304
|
31
|
+
redirect_method :use_proxy!, 305
|
32
|
+
redirect_method :unused!, 306
|
33
|
+
redirect_method :temporary_redirect!, 307
|
34
|
+
redirect_method :permanent_redirect!, 308
|
35
|
+
|
36
|
+
# Define methods for 4xx errors
|
37
|
+
error_method :bad_request!, 400, 'Bad Request'
|
38
|
+
error_method :unauthorized!, 401, 'Unauthorized'
|
39
|
+
error_method :payment_required!, 402, 'Payment Required'
|
40
|
+
error_method :forbidden!, 403, 'Forbidden'
|
41
|
+
error_method :not_found!, 404, 'Not Found'
|
42
|
+
error_method :method_not_allowed!, 405, 'Method Not Allowed'
|
43
|
+
error_method :not_acceptable!, 406, 'Not Acceptable'
|
44
|
+
error_method :proxy_authentication_required!, 407, 'Proxy Authentication Required'
|
45
|
+
error_method :request_timeout!, 408, 'Request Timeout'
|
46
|
+
error_method :conflict!, 409, 'Conflict'
|
47
|
+
error_method :gone!, 410, 'Gone'
|
48
|
+
error_method :length_required!, 411, 'Length Required'
|
49
|
+
error_method :precondition_failed!, 412, 'Precondition Failed'
|
50
|
+
error_method :payload_too_large!, 413, 'Payload Too Large'
|
51
|
+
error_method :uri_too_long!, 414, 'URI Too Long'
|
52
|
+
error_method :unsupported_media_type!, 415, 'Unsupported Media Type'
|
53
|
+
error_method :range_not_satisfiable!, 416, 'Range Not Satisfiable'
|
54
|
+
error_method :expectation_failed!, 417, 'Expectation Failed'
|
55
|
+
error_method :im_a_teapot!, 418, "I'm a teapot"
|
56
|
+
error_method :misdirected_request!, 421, 'Misdirected Request'
|
57
|
+
error_method :unprocessable_entity!, 422, 'Unprocessable Entity'
|
58
|
+
error_method :locked!, 423, 'Locked'
|
59
|
+
error_method :failed_dependency!, 424, 'Failed Dependency'
|
60
|
+
error_method :too_early!, 425, 'Too Early'
|
61
|
+
error_method :upgrade_required!, 426, 'Upgrade Required'
|
62
|
+
error_method :precondition_required!, 428, 'Precondition Required'
|
63
|
+
error_method :too_many_requests!, 429, 'Too Many Requests'
|
64
|
+
error_method :request_header_fields_too_large!, 431, 'Request Header Fields Too Large'
|
65
|
+
error_method :unavailable_for_legal_reasons!, 451, 'Unavailable For Legal Reasons'
|
66
|
+
|
67
|
+
# Define methods for 5xx errors
|
68
|
+
error_method :internal_server_error!, 500, 'Internal Server Error'
|
69
|
+
error_method :not_implemented!, 501, 'Not Implemented'
|
70
|
+
error_method :bad_gateway!, 502, 'Bad Gateway'
|
71
|
+
error_method :service_unavailable!, 503, 'Service Unavailable'
|
72
|
+
error_method :gateway_timeout!, 504, 'Gateway Timeout'
|
73
|
+
error_method :http_version_not_supported!, 505, 'HTTP Version Not Supported'
|
74
|
+
error_method :variant_also_negotiates!, 506, 'Variant Also Negotiates'
|
75
|
+
error_method :insufficient_storage!, 507, 'Insufficient Storage'
|
76
|
+
error_method :loop_detected!, 508, 'Loop Detected'
|
77
|
+
error_method :not_extended!, 510, 'Not Extended'
|
78
|
+
error_method :network_authentication_required!, 511, 'Network Authentication Required'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fleck
|
4
|
+
module Core
|
5
|
+
class Consumer
|
6
|
+
class Validation
|
7
|
+
attr_reader :value, :errors
|
8
|
+
|
9
|
+
def initialize(name, type, value, options = {})
|
10
|
+
@name = name
|
11
|
+
@type = type
|
12
|
+
@value = value || options[:default]
|
13
|
+
@required = (options[:required] == true) # default: trues
|
14
|
+
@allow_blank = (options[:allow_blank] != false) # default: false
|
15
|
+
@min = options[:min]
|
16
|
+
@max = options[:max]
|
17
|
+
@clamp = options[:clamp] || [-Float::INFINITY, Float::INFINITY]
|
18
|
+
|
19
|
+
@errors = []
|
20
|
+
|
21
|
+
validate!
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
@errors.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def required?
|
29
|
+
@required
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_error(error_type, message)
|
33
|
+
@errors << { type: 'param', name: @name, value: @value, error: error_type, message: message }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate!
|
39
|
+
case @type
|
40
|
+
when 'string' then validate_string!
|
41
|
+
when 'number' then validate_number!
|
42
|
+
when 'boolean' then validate_boolean!
|
43
|
+
when 'hash' then validate_hash!
|
44
|
+
when 'array' then validate_array!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_string!
|
49
|
+
# if value is required, check for value presence
|
50
|
+
required? && check_if_present!
|
51
|
+
|
52
|
+
# don't go further if value is nil
|
53
|
+
return if value.nil?
|
54
|
+
|
55
|
+
# check if value is a string
|
56
|
+
check_if_string!
|
57
|
+
|
58
|
+
# check string format
|
59
|
+
check_format!
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_number!
|
63
|
+
# if value is required, check for value presence
|
64
|
+
required? && check_if_present!
|
65
|
+
|
66
|
+
# don't go further if value is nil
|
67
|
+
return if value.nil?
|
68
|
+
|
69
|
+
# check if value is a number
|
70
|
+
check_if_number!
|
71
|
+
|
72
|
+
# check if number is between min and max
|
73
|
+
check_min_max!
|
74
|
+
|
75
|
+
# Check if value is within specified clamping range, and correct if necessary
|
76
|
+
check_clamp!
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_boolean!
|
80
|
+
# if value is required, check for value presence
|
81
|
+
required? && check_if_present!
|
82
|
+
|
83
|
+
return if value.nil?
|
84
|
+
|
85
|
+
# check if value is a boolean
|
86
|
+
check_if_boolean!
|
87
|
+
end
|
88
|
+
|
89
|
+
def check_if_present!
|
90
|
+
if value.nil? || (!@allow_blank && value.to_s.strip == '')
|
91
|
+
add_error(:blank, "#{@name} cannot be blank")
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
def check_if_string!
|
99
|
+
return true if value.is_a?(String)
|
100
|
+
|
101
|
+
@value = @value.to_s
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_if_number!
|
107
|
+
return true if value.is_a?(Integer) || value.is_a?(Float)
|
108
|
+
|
109
|
+
if value.is_a?(String) && numeric?
|
110
|
+
@value = value.to_f
|
111
|
+
@value = value.to_i if value.modulo(1).zero?
|
112
|
+
|
113
|
+
return true
|
114
|
+
end
|
115
|
+
|
116
|
+
add_error(:type, "#{@name} has invalid type: #{@type} expected")
|
117
|
+
|
118
|
+
false
|
119
|
+
end
|
120
|
+
|
121
|
+
def check_if_boolean!
|
122
|
+
return true if [true, false].any?(value)
|
123
|
+
|
124
|
+
if value.is_a?(String) && boolean?
|
125
|
+
@value = %w[t true y yes].any?(value.strip.downcase)
|
126
|
+
return true
|
127
|
+
end
|
128
|
+
|
129
|
+
add_error(:type, "#{@name} has invalid type: #{@type} expected")
|
130
|
+
|
131
|
+
false
|
132
|
+
end
|
133
|
+
|
134
|
+
def check_format!
|
135
|
+
return true if @format.nil?
|
136
|
+
|
137
|
+
add_error(:format, "#{@name} has invalid format") if value.match(@format).nil?
|
138
|
+
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
def check_min_max!
|
143
|
+
add_error(:min, "#{@name} should be greater than #{@min}") if @min && value < @min
|
144
|
+
add_error(:max, "#{@name} should be smaller than #{@max}") if @max && value < @max
|
145
|
+
end
|
146
|
+
|
147
|
+
def check_clamp!
|
148
|
+
@value = @value.clamp(@clamp.first || -Float::INFINITY, @clamp.last || Float::INFINITY)
|
149
|
+
end
|
150
|
+
|
151
|
+
def numeric?
|
152
|
+
!Float(value).nil?
|
153
|
+
rescue ArgumentError
|
154
|
+
false
|
155
|
+
end
|
156
|
+
|
157
|
+
def boolean?
|
158
|
+
%w[t true y yes f false n no].any?(value.strip.downcase)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|