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.
- checksums.yaml +7 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +18 -0
- data/README.rdoc +120 -0
- data/bin/hatt +111 -0
- data/hatt.gemspec +94 -0
- data/lib/hatt/api_clients.rb +40 -0
- data/lib/hatt/base.rb +17 -0
- data/lib/hatt/blankslateproxy.rb +33 -0
- data/lib/hatt/configuration.rb +85 -0
- data/lib/hatt/dsl.rb +62 -0
- data/lib/hatt/hattmixin.rb +21 -0
- data/lib/hatt/http.rb +243 -0
- data/lib/hatt/json_helpers.rb +34 -0
- data/lib/hatt/log.rb +43 -0
- data/lib/hatt/mixin.rb +33 -0
- data/lib/hatt/singleton_mixin.rb +34 -0
- data/lib/hatt.rb +1 -0
- metadata +246 -0
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'
|