hatt 0.0.1

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.
data/lib/hatt/dsl.rb ADDED
@@ -0,0 +1,62 @@
1
+ require_relative 'configuration'
2
+ require_relative 'log'
3
+
4
+ module Hatt
5
+ module DSL
6
+ include Hatt::Configuration
7
+ include Hatt::Log
8
+
9
+ def load_hatts_using_configuration
10
+ hatt_globs = hatt_configuration['hatt_globs']
11
+ if hatt_configuration['hatt_config_file']
12
+ debug "Using HATT configuration file: #{hatt_configuration['hatt_config_file']}"
13
+ glob_home_dir = File.dirname hatt_configuration['hatt_config_file']
14
+ hatt_globs.map! { |g| File.join glob_home_dir, g }
15
+ else
16
+ debug 'Not using a hatt configuration file (none defined).'
17
+ end
18
+ if hatt_globs.is_a? Array
19
+ hatt_globs.each do |gg|
20
+ hatt_load_hatt_glob gg
21
+ end
22
+ end
23
+ nil
24
+ end
25
+
26
+ def hatt_load_hatt_glob(glob)
27
+ globbed_files = Dir[glob]
28
+ Log.debug "Found '#{globbed_files.length}' hatt files using hatt glob '#{glob}'"
29
+ Dir[glob].each do |filename|
30
+ hatt_load_hatt_file filename
31
+ end
32
+ end
33
+
34
+ def hatt_load_hatt_file(filename)
35
+ Log.debug "Loading hatt file '#{filename}'"
36
+
37
+ unless File.exist? filename
38
+ raise HattNoSuchHattFile, "No such hatt file '#{filename}'"
39
+ end
40
+
41
+ # by evaling in a anonymous module, we protect this class's namespace
42
+ anon_mod = Module.new
43
+ with_local_load_path File.dirname(filename) do
44
+ anon_mod.class_eval(IO.read(filename), filename, 1)
45
+ end
46
+ extend anon_mod
47
+ end
48
+
49
+ private
50
+
51
+ def with_local_load_path(load_path, &block)
52
+ $LOAD_PATH << load_path
53
+ rtn = yield block
54
+ # delete only the first occurrence, in case something else if changing load path too
55
+ idx = $LOAD_PATH.index(load_path)
56
+ $LOAD_PATH.delete_at(idx) if idx
57
+ rtn
58
+ end
59
+ end
60
+
61
+ class HattNoSuchHattFile < StandardError; end
62
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'configuration'
2
+ require_relative 'api_clients'
3
+ require_relative 'dsl'
4
+
5
+ module Hatt
6
+ module HattMixin
7
+ include Hatt::Configuration
8
+ include Hatt::DSL
9
+ include Hatt::ApiClients
10
+
11
+ def initialize(*opts)
12
+ hatt_initialize
13
+ super(*opts)
14
+ end
15
+
16
+ def hatt_initialize
17
+ hatt_build_client_methods
18
+ load_hatts_using_configuration
19
+ end
20
+ end
21
+ end
data/lib/hatt/http.rb ADDED
@@ -0,0 +1,243 @@
1
+ require 'faraday'
2
+ require 'typhoeus'
3
+ require 'typhoeus/adapters/faraday' # https://github.com/typhoeus/typhoeus/issues/226#issuecomment-9919517
4
+ require 'active_support/notifications'
5
+
6
+ require 'yaml'
7
+
8
+ require_relative 'json_helpers'
9
+ require_relative 'log'
10
+
11
+ module Hatt
12
+ class HTTP
13
+ include Hatt::Log
14
+ include Hatt::JsonHelpers
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ @name = @config[:name]
19
+ @base_uri = @config[:base_uri]
20
+ @log_headers = @config.fetch(:log_headers, true)
21
+ @log_bodies = @config.fetch(:log_bodies, true)
22
+
23
+ logger.debug "Configuring service:\n#{@config.to_hash.to_yaml}\n"
24
+
25
+ @faraday_connection = Faraday.new @config[:faraday_url] do |conn_builder|
26
+ # do our own logging
27
+ # conn_builder.response logger: logger
28
+ # conn_builder.adapter Faraday.default_adapter # make requests with Net::HTTP
29
+ conn_builder.adapter @config.fetch(:adapter, :typhoeus).intern
30
+ conn_builder.ssl[:verify] = false if @config[:ignore_ssl_cert]
31
+
32
+ # defaulting this to flat adapter avoids issues when duplicating parameters
33
+ conn_builder.options[:params_encoder] = Faraday.const_get(@config.fetch(:params_encoder, 'FlatParamsEncoder'))
34
+
35
+ # this nonsense dont work?! https://github.com/lostisland/faraday_middleware/issues/76
36
+ # conn_builder.use :instrumentation
37
+ end
38
+
39
+ @headers = {
40
+ 'accept' => 'application/json',
41
+ 'content-type' => 'application/json'
42
+ }
43
+ if @config[:default_headers]
44
+ logger.debug 'Default headers configured: ' + @config[:default_headers].inspect
45
+ @config[:default_headers].each_pair do |k, v|
46
+ @headers[k.to_s] = v.to_s
47
+ end
48
+ end
49
+ @default_timeout = @config.fetch(:timeout, 10)
50
+ logger.info "Initialized hatt service '#{@name}'"
51
+ end
52
+
53
+ def stubs
54
+ stubs = Faraday::Adapter::Test::Stubs.new
55
+ @faraday_connection = Faraday.new @config[:faraday_url] do |conn_builder|
56
+ conn_builder.adapter :test, stubs
57
+ end
58
+ stubs
59
+ end
60
+
61
+ # allow stubbing http if we are testing
62
+ attr_reader :http if defined?(RSpec)
63
+ attr_reader :name, :config, :faraday_connection
64
+ attr_accessor :headers # allows for doing some fancy stuff in threading
65
+
66
+ # this is useful for testing apis, and other times
67
+ # you want to interrogate the http details of a response
68
+ attr_reader :last_request, :last_response
69
+
70
+ def in_parallel(&blk)
71
+ @faraday_connection.headers = @headers
72
+ @faraday_connection.in_parallel(&blk)
73
+ end
74
+
75
+ # do_request performs the actual request, and does associated logging
76
+ # options can include:
77
+ # - :timeout, which specifies num secs the request should timeout in
78
+ # (this turns out to be kind of annoying to implement)
79
+ def do_request(method, path, obj = nil, options = {})
80
+ # hatt clients pass in path possibly including query params.
81
+ # Faraday needs the query and path seperately.
82
+ parsed_uri = URI.parse make_path(path)
83
+ # faraday needs the request params as a hash.
84
+ # this turns out to be non-trivial
85
+ query_hash = if parsed_uri.query
86
+ cgi_hash = CGI.parse(parsed_uri.query)
87
+ # this next line accounts for one param having multiple values
88
+ cgi_hash.each_with_object({}) { |(k, v), h| h[k] = v[1] ? v : v.first; }
89
+ end
90
+
91
+ req_headers = make_headers(options)
92
+
93
+ body = if options[:form]
94
+ URI.encode_www_form obj
95
+ else
96
+ jsonify(obj)
97
+ end
98
+
99
+
100
+ log_request(method, parsed_uri, query_hash, headers, body)
101
+
102
+ # doing it this way avoids problem with OPTIONS method: https://github.com/lostisland/faraday/issues/305
103
+ response = nil
104
+ metrics_obj = { method: method, service: @name, path: parsed_uri.path }
105
+ ActiveSupport::Notifications.instrument('request.hatt', metrics_obj) do
106
+ response = @faraday_connection.run_request(method, nil, nil, nil) do |req|
107
+ req.path = parsed_uri.path
108
+ req.params = metrics_obj[:params] = query_hash if query_hash
109
+
110
+ req.headers = req_headers
111
+ req.body = body
112
+ req.options[:timeout] = options.fetch(:timeout, @default_timeout)
113
+ end
114
+ metrics_obj[:response] = response
115
+ end
116
+
117
+ logger.info "Request status: (#{response.status}) #{@name}: #{method.to_s.upcase} #{path}"
118
+ @last_request = {
119
+ method: method,
120
+ path: parsed_uri.path,
121
+ query: parsed_uri.query,
122
+ headers: req_headers,
123
+ body: body
124
+ }
125
+ @last_response = response
126
+
127
+ response_obj = objectify response.body
128
+
129
+ log_response(response, response_obj)
130
+
131
+ raise RequestException.new(nil, response) unless response.status >= 200 && response.status < 300
132
+
133
+ response_obj
134
+ end
135
+
136
+ def get(path, options = {})
137
+ do_request :get, path, nil, options
138
+ end
139
+
140
+ def head(path, options = {})
141
+ do_request :head, path, nil, options
142
+ end
143
+
144
+ def options(path, options = {})
145
+ do_request :options, path, nil, options
146
+ end
147
+
148
+ def delete(path, options = {})
149
+ do_request :delete, path, nil, options
150
+ end
151
+
152
+ def post(path, obj, options = {})
153
+ do_request :post, path, obj, options
154
+ end
155
+
156
+ def put(path, obj, options = {})
157
+ do_request :put, path, obj, options
158
+ end
159
+
160
+ def patch(path, obj, options = {})
161
+ do_request :patch, path, obj, options
162
+ end
163
+
164
+ def post_form(path, params, options = {})
165
+ do_request :post, path, params, options.merge(form: true)
166
+ end
167
+
168
+ # convenience method for accessing the last response status code
169
+ def last_response_status
170
+ @last_response.status
171
+ end
172
+
173
+ private
174
+
175
+ # add base uri to request
176
+ def make_path(path_suffix)
177
+ if @base_uri
178
+ @base_uri + path_suffix
179
+ else
180
+ path_suffix
181
+ end
182
+ end
183
+
184
+ def make_headers(options)
185
+ headers = if options[:additional_headers]
186
+ @headers.merge options[:additional_headers]
187
+ elsif options[:headers]
188
+ options[:headers]
189
+ else
190
+ @headers.clone
191
+ end
192
+ headers['content-type'] = 'application/x-www-form-urlencoded' if options[:form]
193
+ headers
194
+ end
195
+
196
+ def log_request(method, parsed_uri, query_hash={}, headers, body)
197
+ # log the request
198
+ logger.debug [
199
+ "Doing request: #{@name}: #{method.to_s.upcase} #{parsed_uri.path}?#{query_hash.is_a?(Hash) ? URI.encode_www_form(query_hash) : ''}",
200
+ @log_headers ? ['Request Headers:',
201
+ headers.map { |k, v| "#{k}: #{v.inspect}" }] : nil,
202
+ @log_bodies ? ['Request Body:', body] : nil
203
+ ].flatten.compact.join("\n")
204
+ end
205
+
206
+ def log_response(response, response_obj)
207
+ if response.headers['content-type'] =~ /json/
208
+ logger.debug [
209
+ 'Response Details:',
210
+ @log_headers ? ['Response Headers:',
211
+ response.headers.map { |k, v| "#{k}: #{v.inspect}" }] : nil,
212
+ @log_bodies ? ['Response Body:', jsonify(response_obj)] : nil,
213
+ ''
214
+ ].flatten.compact.join("\n")
215
+ end
216
+ end
217
+
218
+ end
219
+
220
+ class RequestException < RuntimeError
221
+ include Hatt::JsonHelpers
222
+
223
+ def initialize(request, response)
224
+ @request = request
225
+ @response = response
226
+ end
227
+ attr_reader :request, :response
228
+
229
+ # this makes good info show up in rspec reports
230
+ def to_s
231
+ "#{self.class}\nResponseCode: #{code}\nResponseBody:\n#{body}"
232
+ end
233
+
234
+ # shortcut methods
235
+ def code
236
+ @response.status
237
+ end
238
+
239
+ def body
240
+ objectify(@response.body)
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+
3
+ module Hatt
4
+ module JsonHelpers
5
+ # always returns a string, intended for request bodies
6
+ # every attempt is made to ensure string is valid json
7
+ # but if that is not possible, then its returned as is
8
+ def jsonify(obj)
9
+ case obj
10
+ when String
11
+ JSON.pretty_generate(JSON.parse(obj))
12
+ when Hash, Array
13
+ JSON.pretty_generate(obj)
14
+ else
15
+ obj.to_s
16
+ end
17
+ rescue Exception
18
+ obj.to_s
19
+ end
20
+
21
+ # attempts to parse json strings into native ruby objects
22
+ def objectify(json_string)
23
+ return nil if json_string.nil? || json_string == ''
24
+ case json_string
25
+ when Hash, Array
26
+ return json_string
27
+ else
28
+ JSON.parse(json_string.to_s)
29
+ end
30
+ rescue Exception
31
+ json_string
32
+ end
33
+ end
34
+ end
data/lib/hatt/log.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'logger'
2
+
3
+ module Hatt
4
+ module Log
5
+ extend self
6
+
7
+ HattFormatter = proc do |severity, datetime, progname, msg|
8
+ "#{severity[0]}: [#{datetime.strftime('%m/%d/%y %H:%M:%S')}][#{progname}] - #{msg}\n"
9
+ end
10
+
11
+ @@loggers = []
12
+ def loggers
13
+ @@loggers
14
+ end
15
+
16
+ def add_logger(handle)
17
+ new_logger = Logger.new handle
18
+ new_logger.progname = 'hatt'
19
+ new_logger.formatter = HattFormatter
20
+ @@loggers << new_logger
21
+ end
22
+
23
+ add_logger(STDOUT)
24
+
25
+ def level=(log_level)
26
+ loggers.each { |logger| logger.level = log_level }
27
+ end
28
+
29
+ def log(level, msg)
30
+ loggers.each { |logger| logger.send(level, msg) }
31
+ end
32
+
33
+ %i[fatal error warn info debug].each do |log_method|
34
+ define_method log_method do |msg|
35
+ log(log_method, msg)
36
+ end
37
+ end
38
+
39
+ def logger
40
+ self
41
+ end
42
+ end
43
+ end
data/lib/hatt/mixin.rb ADDED
@@ -0,0 +1,33 @@
1
+ require_relative 'configuration'
2
+ require_relative 'api_clients'
3
+ require_relative 'dsl'
4
+ require_relative 'blankslateproxy'
5
+
6
+ module Hatt
7
+ module Mixin
8
+ include Hatt::Configuration
9
+ include Hatt::Log
10
+ include Hatt::ApiClients
11
+ include Hatt::DSL
12
+
13
+ def hatt_initialize
14
+ hatt_build_client_methods
15
+ load_hatts_using_configuration
16
+ self
17
+ end
18
+
19
+ def run_script_file(filename)
20
+ info "Running data script '#{filename}'"
21
+ raise(ArgumentError, "No such file '#{filename}'") unless File.exist? filename
22
+ # by running in a anonymous class, we protect this class's namespace
23
+ anon_class = BlankSlateProxy.new(self)
24
+ with_local_load_path File.dirname(filename) do
25
+ anon_class.instance_eval(IO.read(filename), filename, 1)
26
+ end
27
+ end
28
+
29
+ def launch_pry_repl
30
+ require 'pry';binding.pry
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'mixin'
2
+
3
+ module Hatt
4
+ module SingletonMixin
5
+ class InitOnceHattClass
6
+ include Hatt::Mixin
7
+ end
8
+
9
+ def hatt_instance
10
+ @@hatt_instance ||= InitOnceHattClass.new
11
+ end
12
+ module_function :hatt_instance
13
+
14
+ def method_missing(method_id, *arguments, &block)
15
+ if hatt_instance_has_method?(method_id)
16
+ hatt_instance.send(method_id, *arguments, &block)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def hatt_instance_has_method?(name, include_private = false)
23
+ if include_private
24
+ hatt_instance.methods.include?(name.to_sym)
25
+ else
26
+ hatt_instance.public_methods.include?(name.to_sym)
27
+ end
28
+ end
29
+
30
+ def respond_to_missing?(name, include_private = false)
31
+ hatt_instance_has_method?(name, include_private) || super
32
+ end
33
+ end
34
+ end
data/lib/hatt.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'hatt/base'