hawkei 1.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +48 -0
  5. data/.ruby-version +1 -0
  6. data/.tool-versions +1 -0
  7. data/.travis.yml +11 -0
  8. data/Gemfile +12 -0
  9. data/LICENCE +21 -0
  10. data/Makefile +9 -0
  11. data/README.md +17 -0
  12. data/Rakefile +4 -0
  13. data/hawkei.gemspec +28 -0
  14. data/lib/hawkei/api_operation/delete.rb +38 -0
  15. data/lib/hawkei/api_operation/save.rb +57 -0
  16. data/lib/hawkei/api_resource.rb +130 -0
  17. data/lib/hawkei/batch.rb +18 -0
  18. data/lib/hawkei/config.rb +123 -0
  19. data/lib/hawkei/errors.rb +41 -0
  20. data/lib/hawkei/formated_logger.rb +45 -0
  21. data/lib/hawkei/hawkei_object.rb +179 -0
  22. data/lib/hawkei/library_name.rb +3 -0
  23. data/lib/hawkei/message.rb +79 -0
  24. data/lib/hawkei/plugins/rack/middleware.rb +139 -0
  25. data/lib/hawkei/plugins/rails/data.rb +19 -0
  26. data/lib/hawkei/plugins/rails/middleware_data.rb +28 -0
  27. data/lib/hawkei/plugins/rails/railtie.rb +23 -0
  28. data/lib/hawkei/plugins/sidekiq/client_middleware.rb +19 -0
  29. data/lib/hawkei/plugins/sidekiq/load.rb +14 -0
  30. data/lib/hawkei/plugins/sidekiq/server_middleware.rb +48 -0
  31. data/lib/hawkei/plugins.rb +17 -0
  32. data/lib/hawkei/processor/async.rb +50 -0
  33. data/lib/hawkei/processor/batch.rb +84 -0
  34. data/lib/hawkei/processor/worker.rb +113 -0
  35. data/lib/hawkei/request.rb +134 -0
  36. data/lib/hawkei/store.rb +49 -0
  37. data/lib/hawkei/util.rb +180 -0
  38. data/lib/hawkei/version.rb +3 -0
  39. data/lib/hawkei/watcher.rb +15 -0
  40. data/lib/hawkei.rb +170 -0
  41. data/spec/lib/hawkei/api_resource_spec.rb +109 -0
  42. data/spec/lib/hawkei/batch_spec.rb +14 -0
  43. data/spec/lib/hawkei/config_spec.rb +36 -0
  44. data/spec/lib/hawkei/formated_logger_spec.rb +99 -0
  45. data/spec/lib/hawkei/hawkei_object_spec.rb +123 -0
  46. data/spec/lib/hawkei/message_spec.rb +178 -0
  47. data/spec/lib/hawkei/plugins/rack/middleware_spec.rb +88 -0
  48. data/spec/lib/hawkei/plugins/rails/data_spec.rb +22 -0
  49. data/spec/lib/hawkei/plugins/rails/middleware_data_spec.rb +46 -0
  50. data/spec/lib/hawkei/plugins/sidekiq/client_middleware_spec.rb +15 -0
  51. data/spec/lib/hawkei/plugins/sidekiq/server_middleware_spec.rb +58 -0
  52. data/spec/lib/hawkei/processor/async_spec.rb +36 -0
  53. data/spec/lib/hawkei/processor/batch_spec.rb +51 -0
  54. data/spec/lib/hawkei/processor/worker_spec.rb +100 -0
  55. data/spec/lib/hawkei/store_spec.rb +82 -0
  56. data/spec/lib/hawkei/util_spec.rb +132 -0
  57. data/spec/lib/hawkei/watcher_spec.rb +25 -0
  58. data/spec/lib/hawkei_spec.rb +175 -0
  59. data/spec/spec_helper.rb +33 -0
  60. data/spec/support/rack_app.rb +12 -0
  61. metadata +206 -0
@@ -0,0 +1,113 @@
1
+ module Hawkei
2
+ module Processor
3
+ class Worker
4
+
5
+ FLUSH_INTERVAL_SECONDS = 3
6
+
7
+ FLUSH_MESSAGE = Object.new
8
+ SHUTDOWN_MESSAGE = Object.new
9
+
10
+ def initialize(queue, state)
11
+ @queue = queue
12
+ @state = state
13
+
14
+ @batch = Batch.new
15
+ @promises = Concurrent::Array.new
16
+
17
+ @timer = Concurrent::TimerTask.new(execution_interval: FLUSH_INTERVAL_SECONDS) { @queue << FLUSH_MESSAGE }
18
+ @timer.execute
19
+ end
20
+
21
+ def run
22
+ while thread_active?
23
+ message = @queue.pop
24
+
25
+ add_message_to_batch(message)
26
+ end
27
+
28
+ shutdown_worker
29
+ end
30
+
31
+ private
32
+
33
+ ##
34
+ # Add the message to a batch
35
+ #
36
+ # - Add message to a batch if it's not a flush message
37
+ # - flush batch if flush message is receive or batch is full
38
+ def add_message_to_batch(message)
39
+ @batch << message if message != FLUSH_MESSAGE
40
+ flush if message == FLUSH_MESSAGE || @batch.full?
41
+ end
42
+
43
+ ##
44
+ # Shutdown the worker
45
+ #
46
+ # - Close the timer
47
+ # - Retreive the last messages
48
+ # - Wait for all the request to be completed
49
+ #
50
+ # rubocop:disable Lint/HandleExceptions
51
+ def shutdown_worker
52
+ @timer.shutdown
53
+
54
+ begin
55
+ until (message = @queue.pop(true)).nil?
56
+ add_message_to_batch(message)
57
+ end
58
+ rescue ThreadError => _
59
+ end
60
+
61
+ flush
62
+
63
+ @promises.each do |promise|
64
+ promise.wait if promise && !promise.fulfilled?
65
+ end
66
+ end
67
+ # rubocop:enable Lint/HandleExceptions
68
+
69
+ def flush
70
+ return if @batch.empty?
71
+
72
+ send_batch(@batch)
73
+ @batch = Batch.new
74
+ end
75
+
76
+ # rubocop:disable Metrics/AbcSize
77
+ def send_batch(batch)
78
+ promise =
79
+ Concurrent::Promise
80
+ .new { Hawkei::Batch.create(Message.base.merge(data: batch.messages)) }
81
+ .on_success { @promises.delete(promise) }
82
+ .rescue do |error|
83
+ batch.update_retry
84
+
85
+ if error.is_a?(Hawkei::RequestError) && [401, 404].include?(error.http_status)
86
+ return logger.error(error.message)
87
+ end
88
+
89
+ if thread_active? && batch.can_retry?
90
+ Concurrent::ScheduledTask.new(batch.next_retry) { send_batch(batch) }.execute
91
+ else
92
+ @promises.delete(promise)
93
+ end
94
+ end
95
+
96
+ promise.execute
97
+ @promises << promise
98
+ rescue Concurrent::RejectedExecutionError => _
99
+ logger.error('Impossible to start a thread in closing application')
100
+ end
101
+ # rubocop:enable Metrics/AbcSize
102
+
103
+ def thread_active?
104
+ @state.true?
105
+ end
106
+
107
+ def logger
108
+ Hawkei.configurations.logger
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,134 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Request
4
+ #
5
+ # Build request
6
+ class Request
7
+ class << self
8
+
9
+ def execute(params = {})
10
+ klass = new(params)
11
+
12
+ if params[:method] == :post
13
+ klass.post
14
+ elsif params[:method] == :put
15
+ klass.put
16
+ elsif params[:method] == :delete
17
+ klass.delete
18
+ elsif params[:method] == :get
19
+ klass.get
20
+ else
21
+ raise Hawkei::UnknownRequestMethod, "#{params[:method]} haven't been implemented"
22
+ end
23
+ end
24
+
25
+ def post(params = {})
26
+ new(params).post
27
+ end
28
+
29
+ def put(params = {})
30
+ new(params).put
31
+ end
32
+
33
+ def get(params = {})
34
+ new(params).get
35
+ end
36
+
37
+ def delete(params = {})
38
+ new(params).delete
39
+ end
40
+ end
41
+
42
+ attr_reader :params, :http, :request
43
+
44
+ def initialize(params = {})
45
+ @params = params
46
+ @http = Net::HTTP.new(uri.host, uri.port, uri_proxy.host, uri_proxy.port)
47
+
48
+ setup_ssl if params[:use_ssl]
49
+ end
50
+
51
+ def post
52
+ @request = Net::HTTP::Post.new(uri.path)
53
+
54
+ set_header
55
+ request.body = params[:payload]
56
+
57
+ handle_request
58
+ end
59
+
60
+ def put
61
+ @request = Net::HTTP::Put.new(uri.path)
62
+
63
+ set_header
64
+ request.body = params[:payload]
65
+
66
+ handle_request
67
+ end
68
+
69
+ def get
70
+ @request = Net::HTTP::Get.new(uri)
71
+
72
+ set_header
73
+
74
+ handle_request
75
+ end
76
+
77
+ def delete
78
+ @request = Net::HTTP::Delete.new(uri.path)
79
+
80
+ set_header
81
+ request.body = params[:payload]
82
+
83
+ handle_request
84
+ end
85
+
86
+ private
87
+
88
+ def handle_request
89
+ response = http.request(request)
90
+
91
+ case [response.code_type]
92
+ when [Net::HTTPOK], [Net::HTTPCreated], [Net::HTTPAccepted]
93
+ response
94
+ when [Net::HTTPUnauthorized]
95
+ raise RequestError.new(response, message: 'Unauthorized, please check your api key')
96
+ when [Net::HTTPNotFound]
97
+ raise RequestError.new(response, message: 'Requested Resource not found')
98
+ when [Net::HTTPRequestTimeOut]
99
+ raise RequestError.new(response, message: 'Server timeout, verify status of the server')
100
+ when [Net::HTTPInternalServerError]
101
+ raise RequestError.new(response, message: 'Server Error, please check with Hawkei')
102
+ else
103
+ raise RequestError, response
104
+ end
105
+ end
106
+
107
+ def set_header
108
+ params[:headers].each do |(type, value)|
109
+ formated_type = type.to_s.split(/_/).map(&:capitalize).join('-')
110
+ request[formated_type] = value
111
+ end
112
+ end
113
+
114
+ def setup_ssl
115
+ http.use_ssl = true
116
+ http.ca_file = params[:ca_file]
117
+ http.verify_mode = params[:verify_mode]
118
+ end
119
+
120
+ def uri
121
+ @uri ||= URI(params[:url])
122
+ end
123
+
124
+ def uri_proxy
125
+ @uri_proxy ||=
126
+ if params[:proxy]
127
+ URI(params[:proxy])
128
+ else
129
+ OpenStruct.new
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,49 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Store
4
+ #
5
+ # Store environment data
6
+ #
7
+ class Store
8
+ class << self
9
+
10
+ def store
11
+ Thread.current[:request_store] ||= {}
12
+ end
13
+
14
+ def clear!
15
+ Thread.current[:request_store] = {}
16
+ end
17
+
18
+ def load_from_hash(object = {})
19
+ clear!
20
+ bulk_set(Util.deep_symbolize_key(object)) if object.is_a?(Hash)
21
+ end
22
+
23
+ def get(key)
24
+ store[key]
25
+ end
26
+ alias [] get
27
+
28
+ def set(key, value)
29
+ store[key] = value
30
+ end
31
+ alias []= set
32
+
33
+ def bulk_set(attributes = {})
34
+ attributes.each do |(key, value)|
35
+ set(key, value)
36
+ end
37
+ end
38
+
39
+ def exist?(key)
40
+ store.key?(key)
41
+ end
42
+
43
+ def delete(key, &block)
44
+ store.delete(key, &block)
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,180 @@
1
+ module Hawkei
2
+ module Util
3
+ class << self
4
+
5
+ DATE_FORMAT = '%Y/%m/%d'.freeze
6
+ OBFUSCATED = '[HIDDEN]'.freeze
7
+ DEFAULT_OBJECT_DEF = %w[id name].freeze
8
+
9
+ ##
10
+ # Remove keys from a Hash
11
+ #
12
+ # @param [Hash] to be excepted
13
+ # @param [List[String]] to be excepted
14
+ #
15
+ # @return [Hash]
16
+ def except_keys(hash, *keys)
17
+ hash.dup.delete_if { |(key, _value)| keys.include?(key) }
18
+ end
19
+
20
+ ##
21
+ # Convert string to camelize
22
+ #
23
+ # @param [String]
24
+ #
25
+ # @example
26
+ # camelize('my_model') => 'MyModel'
27
+ def camelize(string)
28
+ string.split('_').map(&:capitalize).join
29
+ end
30
+
31
+ ##
32
+ # Remove nil value from hash
33
+ #
34
+ # @return [Hash]
35
+ def compact(hash)
36
+ hash.reject { |_, value| value.nil? }
37
+ end
38
+
39
+ ##
40
+ # Remove nil value from hash recursively
41
+ #
42
+ # @return [Hash]
43
+ def deep_compact(object)
44
+ object.each_with_object({}) do |(key, value), result|
45
+ if value.is_a?(Hash)
46
+ value = deep_compact(value)
47
+ result[key] = value if !value.nil? && !value.empty?
48
+ else
49
+ result[key] = value unless value.nil?
50
+ end
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Encodes a hash of parameters in a way that's suitable for use as query
56
+ # parameters in a URI or as form parameters in a request body. This mainly
57
+ # involves escaping special characters from parameter keys and values (e.g.
58
+ # `&`).
59
+ def encode_parameters(params = {})
60
+ params.map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join('&')
61
+ end
62
+
63
+ ##
64
+ # Encodes a string in a way that makes it suitable for use in a set of
65
+ # query parameters in a URI or in a set of form parameters in a request
66
+ # body.
67
+ def url_encode(key)
68
+ CGI.escape(key.to_s).gsub('%5B', '[').gsub('%5D', ']')
69
+ end
70
+
71
+ ##
72
+ # Convert string to underscore
73
+ #
74
+ # @param [String]
75
+ #
76
+ # @example
77
+ # underscore('MyModel') => 'my_model'
78
+ def underscore(string)
79
+ string
80
+ .gsub(/::/, '/')
81
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
82
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
83
+ .tr('-', '_')
84
+ .downcase
85
+ end
86
+
87
+ ##
88
+ # Deep convert hash to underscore case keys
89
+ #
90
+ # @param [Hash] hash to transform
91
+ #
92
+ # @return [Hash] transformed
93
+ def deep_underscore_key(hash_object)
94
+ deep_transform_keys_in_object(hash_object) do |key|
95
+ begin
96
+ underscore(key).to_sym
97
+ rescue StandardError => _e
98
+ key
99
+ end
100
+ end
101
+ end
102
+
103
+ ##
104
+ # Deep convert hash to string keys
105
+ #
106
+ # @param [Hash] hash to transform
107
+ #
108
+ # @return [Hash] transformed
109
+ def deep_stringify_key(hash_object)
110
+ deep_transform_keys_in_object(hash_object) do |key|
111
+ begin
112
+ key.to_s
113
+ rescue StandardError => _e
114
+ key
115
+ end
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Deep convert hash to symbol keys
121
+ #
122
+ # @param [Hash] hash to transform
123
+ #
124
+ # @return [Hash] transformed
125
+ def deep_symbolize_key(hash_object)
126
+ deep_transform_keys_in_object(hash_object) do |key|
127
+ begin
128
+ key.to_sym
129
+ rescue StandardError => _e
130
+ key
131
+ end
132
+ end
133
+ end
134
+
135
+ ##
136
+ # Deep remove key from hash
137
+ #
138
+ # @param [Hash] hash to obfuscate
139
+ #
140
+ # @return [Hash] hash obfuscated
141
+ def deep_obfuscate_value(object, fields, obfuscate_name = OBFUSCATED)
142
+ case object
143
+ when Hash
144
+ object.each_with_object({}) do |(key, value), result|
145
+ result[key] = fields.include?(key.to_s) ? obfuscate_name : deep_obfuscate_value(value, fields)
146
+ end
147
+ when Array
148
+ object.map { |e| deep_obfuscate_value(e, fields) }
149
+ else
150
+ object
151
+ end
152
+ end
153
+
154
+ ##
155
+ # Parse JSON without raise
156
+ #
157
+ def safe_json_parse(data)
158
+ JSON.parse(data.to_s)
159
+ rescue JSON::ParserError
160
+ {}
161
+ end
162
+
163
+ private
164
+
165
+ def deep_transform_keys_in_object(object, &block)
166
+ case object
167
+ when Hash
168
+ object.each_with_object({}) do |(key, value), result|
169
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
170
+ end
171
+ when Array
172
+ object.map { |e| deep_transform_keys_in_object(e, &block) }
173
+ else
174
+ object
175
+ end
176
+ end
177
+
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,3 @@
1
+ module Hawkei # :nodoc:
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,15 @@
1
+ module Hawkei
2
+ ##
3
+ # == Hawkei \Watcher
4
+ #
5
+ # Create and update groups
6
+ #
7
+ # @example: Usage
8
+ #
9
+ # request = Hawkei::Watcher.create(template_flow: 'My Hawkei', expected_times: 42) #=> #<Hawkei::Response...>
10
+ #
11
+ class Watcher < APIResource
12
+ include Hawkei::APIOperation::Save
13
+ include Hawkei::APIOperation::Delete
14
+ end
15
+ end
data/lib/hawkei.rb ADDED
@@ -0,0 +1,170 @@
1
+ # Lib
2
+ require 'securerandom'
3
+ require 'net/http'
4
+ require 'ostruct'
5
+ require 'json'
6
+ require 'time'
7
+ require 'cgi'
8
+ require 'logger'
9
+ require 'concurrent'
10
+
11
+ # Base
12
+ require 'hawkei/version'
13
+ require 'hawkei/formated_logger'
14
+ require 'hawkei/library_name'
15
+ require 'hawkei/util'
16
+ require 'hawkei/config'
17
+ require 'hawkei/errors'
18
+ require 'hawkei/hawkei_object'
19
+ require 'hawkei/request'
20
+ require 'hawkei/api_resource'
21
+ require 'hawkei/store'
22
+ require 'hawkei/message'
23
+
24
+ # Processor
25
+ require 'hawkei/processor/async'
26
+ require 'hawkei/processor/worker'
27
+ require 'hawkei/processor/batch'
28
+
29
+ # Operations
30
+ require 'hawkei/api_operation/save'
31
+ require 'hawkei/api_operation/delete'
32
+
33
+ # Resources
34
+ require 'hawkei/watcher'
35
+ require 'hawkei/batch'
36
+
37
+ # Plugins
38
+ require 'hawkei/plugins'
39
+
40
+ ##
41
+ # Implementation of the Hawkei
42
+ module Hawkei
43
+ class << self
44
+
45
+ TRACK_EVENTS_CREATE = 'track_events:create'.freeze
46
+ IDENTIFY_EVENTS_CREATE = 'identify_events:create'.freeze
47
+ GROUP_EVENTS_CREATE = 'group_events:create'.freeze
48
+ WATCHERS_CREATE_ACTION = 'watchers:create'.freeze
49
+
50
+ ##
51
+ # @return [Hawkei::Config] configurations
52
+ attr_reader :configurations
53
+
54
+ ##
55
+ # Configures the Hawkei API
56
+ #
57
+ # @example Default configuration
58
+ # Hawkei.configure do |config|
59
+ # config.api_key = 'acc_xx'
60
+ # config.space_name = 'Hawkei'
61
+ # config.environment_name = 'production'
62
+ # end
63
+ def configure
64
+ yield @configurations = Hawkei::Config.new
65
+
66
+ Hawkei.configurations.valid!
67
+ end
68
+
69
+ ##
70
+ # Send a track event to the server
71
+ #
72
+ # @param [String] event name
73
+ # @param [Hash] payload to be send
74
+ # @param [Hash] options for the request
75
+ #
76
+ # @return [Boolean] status of the request
77
+ def track(name, payload = {}, options = {})
78
+ return true unless configurations.enabled
79
+
80
+ payload[:name] = name
81
+ Hawkei::Plugins::Rails::Data.store_data if defined?(Hawkei::Plugins::Rails::Data)
82
+
83
+ payload = Message.extended.merge(payload)
84
+
85
+ processor.enqueue(
86
+ action: TRACK_EVENTS_CREATE,
87
+ payload: payload,
88
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
89
+ )
90
+ end
91
+
92
+ ##
93
+ # Send an identify event to the server
94
+ #
95
+ # @param [String] user id from your database
96
+ # @param [Hash] payload to be send
97
+ # @param [Hash] options for the request
98
+ #
99
+ # @return [Boolean] status of the request
100
+ def identify(user_id, payload = {}, options = {})
101
+ return true unless configurations.enabled
102
+
103
+ payload[:user_id] = user_id
104
+ Hawkei::Plugins::Rails::Data.store_data if defined?(Hawkei::Plugins::Rails::Data)
105
+
106
+ payload = Message.extended.merge(payload)
107
+
108
+ processor.enqueue(
109
+ action: IDENTIFY_EVENTS_CREATE,
110
+ payload: payload,
111
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
112
+ )
113
+ end
114
+
115
+ ##
116
+ # Send a create/update for a group
117
+ #
118
+ # @param [String] group id from your database
119
+ # @param [Hash] payload to be send
120
+ # @param [Hash] options for the request
121
+ #
122
+ # @return [Boolean] status of the request
123
+ def group(group_id, payload = {}, options = {})
124
+ return true unless configurations.enabled
125
+
126
+ payload = Message.base.merge(payload)
127
+ payload[:group_id] = group_id
128
+
129
+ processor.enqueue(
130
+ action: GROUP_EVENTS_CREATE,
131
+ payload: payload,
132
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
133
+ )
134
+ end
135
+
136
+ ##
137
+ # Send a create a watcher
138
+ #
139
+ # @param [String|Integer] template hawkei name or id
140
+ # @param [Hash] payload to be send
141
+ # @param [Hash] options for the request
142
+ #
143
+ # @return [Boolean|Watcher] status of the request
144
+ def watch(flow, payload = {}, options = {})
145
+ return true unless configurations.enabled
146
+
147
+ payload[:template_flow] = flow
148
+ payload = Message.base.merge(payload)
149
+
150
+ payload.delete(:session_tracker_id) if payload.delete(:without_session)
151
+
152
+ if options[:sync]
153
+ Watcher.create(payload, options)
154
+ else
155
+ processor.enqueue(
156
+ action: WATCHERS_CREATE_ACTION,
157
+ payload: payload,
158
+ options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
159
+ )
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def processor
166
+ @processor ||= Processor::Async.new
167
+ end
168
+
169
+ end
170
+ end