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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -10
  3. data/CHANGELOG.md +89 -74
  4. data/Gemfile +6 -4
  5. data/examples/actions.rb +59 -53
  6. data/examples/blocking_consumer.rb +42 -42
  7. data/examples/consumer_initialization.rb +44 -42
  8. data/examples/deprecation.rb +50 -57
  9. data/examples/example.rb +76 -74
  10. data/examples/expired.rb +72 -76
  11. data/examples/fanout.rb +62 -64
  12. data/fleck.gemspec +37 -36
  13. data/lib/fleck/client.rb +124 -124
  14. data/lib/fleck/configuration.rb +149 -144
  15. data/lib/fleck/consumer.rb +7 -287
  16. data/lib/fleck/core/consumer/action_param.rb +106 -0
  17. data/lib/fleck/core/consumer/actions.rb +76 -0
  18. data/lib/fleck/core/consumer/base.rb +111 -0
  19. data/lib/fleck/core/consumer/configuration.rb +69 -0
  20. data/lib/fleck/core/consumer/decorators.rb +77 -0
  21. data/lib/fleck/core/consumer/helpers_definers.rb +55 -0
  22. data/lib/fleck/core/consumer/logger.rb +88 -0
  23. data/lib/fleck/core/consumer/request.rb +89 -0
  24. data/lib/fleck/core/consumer/response.rb +77 -0
  25. data/lib/fleck/core/consumer/response_helpers.rb +81 -0
  26. data/lib/fleck/core/consumer/validation.rb +163 -0
  27. data/lib/fleck/core/consumer.rb +166 -0
  28. data/lib/fleck/core.rb +9 -0
  29. data/lib/fleck/loggable.rb +15 -10
  30. data/lib/fleck/{hash_with_indifferent_access.rb → utilities/hash_with_indifferent_access.rb} +80 -85
  31. data/lib/fleck/utilities/host_rating.rb +104 -0
  32. data/lib/fleck/version.rb +6 -3
  33. data/lib/fleck.rb +81 -72
  34. metadata +35 -24
  35. data/lib/fleck/consumer/request.rb +0 -52
  36. data/lib/fleck/consumer/response.rb +0 -80
  37. 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