hyperion_http 0.1.2
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/.gitignore +18 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/CHANGES.md +145 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +421 -0
- data/Rakefile +11 -0
- data/hyperion_http.gemspec +34 -0
- data/lib/hyperion/aux/bug_error.rb +2 -0
- data/lib/hyperion/aux/hash_ext.rb +5 -0
- data/lib/hyperion/aux/logger.rb +54 -0
- data/lib/hyperion/aux/typho.rb +9 -0
- data/lib/hyperion/aux/util.rb +18 -0
- data/lib/hyperion/aux/version.rb +3 -0
- data/lib/hyperion/formats.rb +69 -0
- data/lib/hyperion/headers.rb +43 -0
- data/lib/hyperion/hyperion.rb +79 -0
- data/lib/hyperion/requestor.rb +88 -0
- data/lib/hyperion/result_handling/dispatch_dsl.rb +67 -0
- data/lib/hyperion/result_handling/dispatching_hyperion_result.rb +10 -0
- data/lib/hyperion/result_handling/result_maker.rb +64 -0
- data/lib/hyperion/types/client_error_code.rb +9 -0
- data/lib/hyperion/types/client_error_detail.rb +46 -0
- data/lib/hyperion/types/client_error_response.rb +50 -0
- data/lib/hyperion/types/hyperion_error.rb +6 -0
- data/lib/hyperion/types/hyperion_result.rb +24 -0
- data/lib/hyperion/types/hyperion_status.rb +10 -0
- data/lib/hyperion/types/hyperion_uri.rb +97 -0
- data/lib/hyperion/types/payload_descriptor.rb +9 -0
- data/lib/hyperion/types/response_descriptor.rb +21 -0
- data/lib/hyperion/types/rest_route.rb +20 -0
- data/lib/hyperion.rb +15 -0
- data/lib/hyperion_test/fake.rb +64 -0
- data/lib/hyperion_test/fake_server/config.rb +36 -0
- data/lib/hyperion_test/fake_server/dispatcher.rb +74 -0
- data/lib/hyperion_test/fake_server/types.rb +7 -0
- data/lib/hyperion_test/fake_server.rb +54 -0
- data/lib/hyperion_test/spec_helper.rb +19 -0
- data/lib/hyperion_test/test_framework_hooks.rb +34 -0
- data/lib/hyperion_test.rb +2 -0
- data/spec/lib/hyperion/aux/util_spec.rb +29 -0
- data/spec/lib/hyperion/formats_spec.rb +84 -0
- data/spec/lib/hyperion/headers_spec.rb +61 -0
- data/spec/lib/hyperion/logger_spec.rb +60 -0
- data/spec/lib/hyperion/test_spec.rb +222 -0
- data/spec/lib/hyperion/types/client_error_response_spec.rb +52 -0
- data/spec/lib/hyperion/types/hyperion_result_spec.rb +17 -0
- data/spec/lib/hyperion/types/hyperion_uri_spec.rb +113 -0
- data/spec/lib/hyperion_spec.rb +187 -0
- data/spec/lib/superion_spec.rb +151 -0
- data/spec/lib/types_spec.rb +46 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/core_helpers.rb +5 -0
- metadata +280 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
class Hyperion
|
2
|
+
module Logger
|
3
|
+
class << self
|
4
|
+
attr_accessor :level
|
5
|
+
end
|
6
|
+
|
7
|
+
def logger
|
8
|
+
rails_logger_available? ? Rails.logger : default_logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_request_logging(route, uri, headers)
|
12
|
+
log_request_start(route, uri, headers)
|
13
|
+
start = Time.now
|
14
|
+
begin
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
stop = Time.now
|
18
|
+
log_request_end(((stop - start) * 1000).round)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_stub(rule)
|
23
|
+
mr = rule.mimic_route
|
24
|
+
logger.debug "Stubbed #{mr.method.to_s.upcase} #{mr.path}"
|
25
|
+
log_headers(rule.headers)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def log_request_start(route, uri, headers)
|
31
|
+
logger.debug "Requesting #{route.method.to_s.upcase} #{uri}"
|
32
|
+
log_headers(headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_request_end(ms)
|
36
|
+
logger.debug "Completed in #{ms}ms"
|
37
|
+
logger.debug ''
|
38
|
+
end
|
39
|
+
|
40
|
+
def rails_logger_available?
|
41
|
+
Kernel.const_defined?(:Rails) && !Rails.logger.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_logger
|
45
|
+
logger = ::Logger.new($stdout)
|
46
|
+
logger.level = Hyperion::Logger.level || ::Logger::DEBUG
|
47
|
+
logger
|
48
|
+
end
|
49
|
+
|
50
|
+
def log_headers(headers)
|
51
|
+
headers.each_pair { |k, v| logger.debug " #{k}: #{v}" unless k == 'Expect' }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'hyperion/aux/bug_error'
|
2
|
+
|
3
|
+
class Hyperion
|
4
|
+
class Util
|
5
|
+
def self.nil_if_error
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue StandardError
|
9
|
+
return nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.guard_param(value, what, expected_type=nil, &pred)
|
14
|
+
pred ||= proc { |x| x.is_a?(expected_type) }
|
15
|
+
pred.call(value) or fail BugError, "You passed me #{value.inspect}, which is not #{what}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'oj'
|
2
|
+
require 'stringio'
|
3
|
+
require 'hyperion/aux/logger'
|
4
|
+
require 'abstractivator/enum'
|
5
|
+
|
6
|
+
class Hyperion
|
7
|
+
module Formats
|
8
|
+
# Serializes and deserializes the supported formats.
|
9
|
+
# This is done as gracefully as possible.
|
10
|
+
|
11
|
+
include Hyperion::Logger
|
12
|
+
|
13
|
+
define_enum :Known, :json, :protobuf
|
14
|
+
|
15
|
+
def write(obj, format)
|
16
|
+
return obj if obj.is_a?(String) || obj.nil? || format.nil?
|
17
|
+
|
18
|
+
case Formats.get_from(format)
|
19
|
+
when :json; write_json(obj)
|
20
|
+
when :protobuf; obj
|
21
|
+
else; fail "Unsupported format: #{format}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(bytes, format)
|
26
|
+
return nil if bytes.nil?
|
27
|
+
return bytes if format.nil?
|
28
|
+
|
29
|
+
case Formats.get_from(format)
|
30
|
+
when :json; read_json(bytes)
|
31
|
+
when :protobuf; bytes
|
32
|
+
else; fail "Unsupported format: #{format}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.get_from(x)
|
37
|
+
x.respond_to?(:format) ? x.format : x
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def write_json(obj)
|
43
|
+
Oj.dump(obj, oj_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_json(bytes)
|
47
|
+
begin
|
48
|
+
Oj.compat_load(bytes, oj_options)
|
49
|
+
rescue Oj::ParseError => e
|
50
|
+
logger.error e.message
|
51
|
+
bytes
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def oj_options
|
56
|
+
{
|
57
|
+
mode: :compat,
|
58
|
+
time_format: :xmlschema, # xmlschema == iso8601
|
59
|
+
use_to_json: false,
|
60
|
+
second_precision: 3
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_oj_line_and_col(e)
|
65
|
+
m = e.message.match(/at line (?<line>\d+), column (?<col>\d+)/)
|
66
|
+
m ? [m[:line].to_i, m[:col].to_i] : nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'hyperion/formats'
|
2
|
+
|
3
|
+
class Hyperion
|
4
|
+
module Headers
|
5
|
+
# constructs and destructures HTTP headers
|
6
|
+
|
7
|
+
def route_headers(route)
|
8
|
+
headers = {}
|
9
|
+
rd = route.response_descriptor
|
10
|
+
pd = route.payload_descriptor
|
11
|
+
headers['Expect'] = 'x' # this overrides default libcurl behavior.
|
12
|
+
# see http://devblog.songkick.com/2012/11/27/a-second-here-a-second-there/
|
13
|
+
# the value has to be non-empty or else it is ignored
|
14
|
+
if rd
|
15
|
+
headers['Accept'] = "application/vnd.#{Hyperion.config.vendor_string}.#{short_mimetype(rd)}"
|
16
|
+
end
|
17
|
+
if pd
|
18
|
+
headers['Content-Type'] = content_type_for(pd.format)
|
19
|
+
end
|
20
|
+
headers
|
21
|
+
end
|
22
|
+
|
23
|
+
def short_mimetype(response_descriptor)
|
24
|
+
x = response_descriptor
|
25
|
+
"#{x.type}-v#{x.version}+#{x.format}"
|
26
|
+
end
|
27
|
+
|
28
|
+
ContentTypes = [[:json, 'application/json'],
|
29
|
+
[:protobuf, 'application/x-protobuf']]
|
30
|
+
|
31
|
+
def content_type_for(format)
|
32
|
+
format = Hyperion::Formats.get_from(format)
|
33
|
+
ct = ContentTypes.detect{|x| x.first == format}
|
34
|
+
ct ? ct.last : 'application/octet-stream'
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_for(content_type)
|
38
|
+
ct = ContentTypes.detect{|x| x.last == content_type}
|
39
|
+
fail "Unsupported content type: #{content_type}" unless ct
|
40
|
+
ct.first
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'hyperion/headers'
|
2
|
+
require 'hyperion/formats'
|
3
|
+
require 'hyperion/aux/logger'
|
4
|
+
require 'hyperion/aux/typho'
|
5
|
+
require 'hyperion/result_handling/result_maker'
|
6
|
+
|
7
|
+
class Hyperion
|
8
|
+
include Headers
|
9
|
+
include Formats
|
10
|
+
include Logger
|
11
|
+
|
12
|
+
Config = Struct.new(:vendor_string)
|
13
|
+
|
14
|
+
# @param route [RestRoute]
|
15
|
+
# @param body [String] the body to send with POST or PUT
|
16
|
+
# @param additional_headers [Hash] headers to send in addition to the ones
|
17
|
+
# already determined by the route. Example: +{'User-Agent' => 'Mozilla/5.0'}+
|
18
|
+
# @yield [result] yields the result if a block is provided
|
19
|
+
# @yieldparam [HyperionResult]
|
20
|
+
# @return [HyperionResult, Object] If a block is provided, returns the block's
|
21
|
+
# return value; otherwise, returns the result.
|
22
|
+
def self.request(route, body=nil, additional_headers={}, &block)
|
23
|
+
self.new(route).request(body, additional_headers, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @private
|
27
|
+
def initialize(route)
|
28
|
+
@route = route
|
29
|
+
end
|
30
|
+
|
31
|
+
# @private
|
32
|
+
def request(body=nil, additional_headers={}, &dispatch)
|
33
|
+
uri = transform_uri(route.uri).to_s
|
34
|
+
with_request_logging(route, uri, route_headers(route)) do
|
35
|
+
typho_result = Typho.request(uri,
|
36
|
+
method: route.method,
|
37
|
+
headers: build_headers(additional_headers),
|
38
|
+
body: write(body, route.payload_descriptor))
|
39
|
+
hyperion_result_for(typho_result, dispatch)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.configure
|
44
|
+
yield(config)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :route
|
50
|
+
|
51
|
+
def self.config
|
52
|
+
@config ||= Config.new('indigobio-ascent')
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_headers(additional_headers)
|
56
|
+
route_headers(route).merge(additional_headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
def hyperion_result_for(typho_result, dispatch)
|
60
|
+
result_maker = ResultMaker.new(route)
|
61
|
+
if dispatch
|
62
|
+
# callcc allows control to "jump" back here when the first predicate matches
|
63
|
+
callcc do |cont|
|
64
|
+
dispatch.call(result_maker.make(typho_result, cont))
|
65
|
+
end
|
66
|
+
else
|
67
|
+
result_maker.make(typho_result)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def transform_uri(uri)
|
72
|
+
Hyperion.send(:transform_uri, uri)
|
73
|
+
end
|
74
|
+
|
75
|
+
# give Hyperion::Test a shot at changing the uri for stubbing purposes
|
76
|
+
def self.transform_uri(uri)
|
77
|
+
uri
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'hyperion'
|
2
|
+
|
3
|
+
class Hyperion
|
4
|
+
module Requestor
|
5
|
+
|
6
|
+
# @param [RestRoute] route The route to request
|
7
|
+
# @option opts [Object] :body The payload to POST/PUT. Usually a Hash or Array.
|
8
|
+
# @option opts [Hash<predicate, transformer>] :also_handle Additional handlers to
|
9
|
+
# use besides the default handlers. A predicate is an integer HTTP code, an
|
10
|
+
# integer Range of HTTP codes, a HyperionStatus enumeration value,
|
11
|
+
# or a predicate proc. A transformer is a procedure which accepts a
|
12
|
+
# HyperionResult and returns the final value to return from `request`
|
13
|
+
# @option opts [Proc] :render A transformer, usually a proc returned by
|
14
|
+
# `as` or `as_many`. Only called on HTTP 200.
|
15
|
+
# @yield [rendered] Yields to allow an additional transformation.
|
16
|
+
# Only called on HTTP 200.
|
17
|
+
def request(route, opts={}, &project)
|
18
|
+
Hyperion::Util.guard_param(route, 'a RestRoute', RestRoute)
|
19
|
+
Hyperion::Util.guard_param(opts, 'an options hash', Hash)
|
20
|
+
|
21
|
+
body = opts[:body]
|
22
|
+
additional_handler_hash = opts[:also_handle] || {}
|
23
|
+
render = opts[:render] || Proc.identity
|
24
|
+
project = project || Proc.identity
|
25
|
+
|
26
|
+
Hyperion.request(route, body) do |result|
|
27
|
+
all_handlers = [hash_handler(additional_handler_hash),
|
28
|
+
handler_from_including_class,
|
29
|
+
built_in_handler(project, render)]
|
30
|
+
|
31
|
+
all_handlers.each { |handlers| handlers.call(result) }
|
32
|
+
fallthrough(result)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def hash_handler(hash)
|
39
|
+
proc do |result|
|
40
|
+
hash.each_pair do |condition, consequent|
|
41
|
+
result.when(condition) { Proc.loose_call(consequent, [result]) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def handler_from_including_class
|
47
|
+
respond_to?(:hyperion_handler, true) ? method(:hyperion_handler).loosen_args : proc{}
|
48
|
+
end
|
49
|
+
|
50
|
+
def built_in_handler(project, render)
|
51
|
+
proc do |result|
|
52
|
+
result.when(HyperionStatus::SUCCESS, &Proc.pipe(:body, render, project))
|
53
|
+
result.when(HyperionStatus::BAD_ROUTE, &method(:on_bad_route))
|
54
|
+
result.when(HyperionStatus::CLIENT_ERROR, &method(:on_client_error))
|
55
|
+
result.when(HyperionStatus::SERVER_ERROR, &method(:on_server_error))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_bad_route(response)
|
60
|
+
body = ClientErrorResponse.new("Got HTTP 404 for #{response.route}. Is the route implemented?", [], ClientErrorCode::UNKNOWN)
|
61
|
+
report_client_error(response.route, body)
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_client_error(response)
|
65
|
+
report_client_error(response.route, response.body)
|
66
|
+
end
|
67
|
+
|
68
|
+
def report_client_error(route, body)
|
69
|
+
generic_msg = "The request failed: #{route}"
|
70
|
+
|
71
|
+
if body.is_a?(ClientErrorResponse)
|
72
|
+
hyperion_raise body.message
|
73
|
+
elsif body.nil?
|
74
|
+
hyperion_raise generic_msg
|
75
|
+
else
|
76
|
+
hyperion_raise "#{generic_msg}: #{body}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_server_error(response)
|
81
|
+
hyperion_raise "#{response.route}\n#{response.body}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def fallthrough(result)
|
85
|
+
hyperion_raise "Hyperion error: the response did not match any conditions: #{result.to_s}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'hyperion/aux/util'
|
2
|
+
require 'hyperion/types/hyperion_result'
|
3
|
+
require 'hyperion/types/client_error_detail'
|
4
|
+
|
5
|
+
class Hyperion
|
6
|
+
# This is a DSL of sorts that gives the `request` block a nice way
|
7
|
+
# to dispatch the result based on status, HTTP code, etc.
|
8
|
+
module DispatchDsl
|
9
|
+
|
10
|
+
def __set_escape_continuation__(k)
|
11
|
+
@escape = k
|
12
|
+
end
|
13
|
+
|
14
|
+
def when(condition, &action)
|
15
|
+
pred = as_predicate(condition)
|
16
|
+
is_match = Util.nil_if_error { Proc.loose_call(pred, [self]) }
|
17
|
+
if is_match
|
18
|
+
return_value = action.call(self)
|
19
|
+
@escape.call(return_value) # non-local exit
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def as_predicate(condition)
|
28
|
+
if condition.is_a?(HyperionStatus)
|
29
|
+
status_checker(condition)
|
30
|
+
|
31
|
+
elsif condition.is_a?(ClientErrorCode)
|
32
|
+
client_error_code_checker(condition)
|
33
|
+
|
34
|
+
elsif condition.is_a?(Integer)
|
35
|
+
http_code_checker(condition)
|
36
|
+
|
37
|
+
elsif condition.is_a?(Range)
|
38
|
+
range_checker(condition)
|
39
|
+
|
40
|
+
elsif condition.callable?
|
41
|
+
condition
|
42
|
+
|
43
|
+
else
|
44
|
+
fail "Not a valid condition: #{condition.inspect}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def status_checker(status)
|
49
|
+
proc { |r| r.status == status }
|
50
|
+
end
|
51
|
+
|
52
|
+
def client_error_code_checker(code)
|
53
|
+
proc do |r|
|
54
|
+
r.status == HyperionStatus::CLIENT_ERROR &&
|
55
|
+
r.body.errors.detect(:code, code)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def http_code_checker(code)
|
60
|
+
proc { |r| r.code == code }
|
61
|
+
end
|
62
|
+
|
63
|
+
def range_checker(range)
|
64
|
+
proc { |r| range.include?(r.code) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'hyperion/types/hyperion_result'
|
2
|
+
require 'hyperion/result_handling/dispatch_dsl'
|
3
|
+
|
4
|
+
class Hyperion
|
5
|
+
# PW: distinguishing between this and HyperionResult is of
|
6
|
+
# dubious value. Consider merging the two.
|
7
|
+
class DispatchingHyperionResult < HyperionResult
|
8
|
+
include DispatchDsl
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'hyperion/formats'
|
2
|
+
require 'hyperion/result_handling/dispatching_hyperion_result'
|
3
|
+
require 'hyperion/types/hyperion_result'
|
4
|
+
require 'hyperion/types/client_error_response'
|
5
|
+
|
6
|
+
class Hyperion
|
7
|
+
# Produces a hyperion result object from a typhoeus result object
|
8
|
+
class ResultMaker
|
9
|
+
include Hyperion::Formats
|
10
|
+
|
11
|
+
def self.make(route, typho_result, continuation=nil)
|
12
|
+
self.new(route).make(typho_result, continuation)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(route)
|
16
|
+
@route = route
|
17
|
+
end
|
18
|
+
|
19
|
+
def make(typho_result, continuation=nil)
|
20
|
+
if continuation
|
21
|
+
result = make_from_typho(typho_result, DispatchingHyperionResult)
|
22
|
+
result.__set_escape_continuation__(continuation)
|
23
|
+
result
|
24
|
+
else
|
25
|
+
make_from_typho(typho_result, HyperionResult)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :route
|
32
|
+
|
33
|
+
def make_from_typho(typho_result, result_class)
|
34
|
+
# TODO: should this use the response's 'Content-Type' instead of response_descriptor.format?
|
35
|
+
read_body = proc { read(typho_result.body, route.response_descriptor) }
|
36
|
+
code = typho_result.code
|
37
|
+
|
38
|
+
if typho_result.success?
|
39
|
+
result_class.new(route, HyperionStatus::SUCCESS, code, read_body.call)
|
40
|
+
|
41
|
+
elsif typho_result.timed_out?
|
42
|
+
result_class.new(route, HyperionStatus::TIMED_OUT)
|
43
|
+
|
44
|
+
elsif code == 0
|
45
|
+
result_class.new(route, HyperionStatus::NO_RESPONSE)
|
46
|
+
|
47
|
+
elsif code == 404
|
48
|
+
result_class.new(route, HyperionStatus::BAD_ROUTE, code)
|
49
|
+
|
50
|
+
elsif (400..499).include?(code)
|
51
|
+
hash_body = read(typho_result.body, :json)
|
52
|
+
err = ClientErrorResponse.from_attrs(hash_body) || hash_body
|
53
|
+
result_class.new(route, HyperionStatus::CLIENT_ERROR, code, err)
|
54
|
+
|
55
|
+
elsif (500..599).include?(code)
|
56
|
+
result_class.new(route, HyperionStatus::SERVER_ERROR, code, read_body.call)
|
57
|
+
|
58
|
+
else
|
59
|
+
result_class.new(route, HyperionStatus::CHECK_CODE, code, read_body.call)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'abstractivator/enum'
|
2
|
+
|
3
|
+
define_enum :ClientErrorCode,
|
4
|
+
:missing, # the resource does not exist
|
5
|
+
:missing_field, # a required field on a resource has not been set ()
|
6
|
+
:invalid, # a parameter or the content of a POSTed document/field is invalid
|
7
|
+
:unsupported, # an unsupported message type, message version, or format was requested
|
8
|
+
:already_exists, # another resource has the same unique key
|
9
|
+
:unknown # something else
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'hyperion/types/client_error_code'
|
2
|
+
|
3
|
+
class ClientErrorDetail
|
4
|
+
attr_reader :code # [ClientErrorCode] type of error
|
5
|
+
attr_reader :resource # [String] the thing with the error
|
6
|
+
attr_reader :field # [String, Nil] the location of the error within the resource
|
7
|
+
attr_reader :value # [Object, Nil] the problematic data
|
8
|
+
attr_reader :reason # [Object, Nil] an explanation of the error. usually a String.
|
9
|
+
|
10
|
+
def initialize(code, resource, opts={})
|
11
|
+
@code = canonical_code(code)
|
12
|
+
@resource = resource
|
13
|
+
@field = opts[:field] || ''
|
14
|
+
@value = opts[:value] || ''
|
15
|
+
@reason = opts[:reason] || ''
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_json
|
19
|
+
{
|
20
|
+
'code' => code.value,
|
21
|
+
'resource' => resource,
|
22
|
+
'field' => field,
|
23
|
+
'value' => value,
|
24
|
+
'reason' => reason
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_attrs(attrs)
|
29
|
+
code = ClientErrorCode.from(attrs['code'])
|
30
|
+
resource = attrs['resource']
|
31
|
+
field = attrs['field']
|
32
|
+
value = attrs['value']
|
33
|
+
reason = attrs['reason']
|
34
|
+
self.new(code, resource, field: field, value: value, reason: reason)
|
35
|
+
end
|
36
|
+
|
37
|
+
# make mongoid validations happy
|
38
|
+
def to_s; reason; end
|
39
|
+
def empty?; false; end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def canonical_code(x)
|
44
|
+
x.is_a?(Symbol) ? ClientErrorCode.from_symbol(x) : ClientErrorCode.from(x)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'hyperion/aux/util'
|
2
|
+
require 'hyperion/types/client_error_code'
|
3
|
+
require 'hyperion/types/client_error_detail'
|
4
|
+
|
5
|
+
class ClientErrorResponse
|
6
|
+
# The structure expected in a 400 response.
|
7
|
+
|
8
|
+
attr_reader :code # [ClientErrorCode] The type of error. At least one of the ErrorInfos should have the same code.
|
9
|
+
attr_reader :message # [String] An error message that can be presented to the user
|
10
|
+
attr_reader :errors # [Array<ErrorInfo>] Structured information with error specifics
|
11
|
+
attr_reader :content # [String, nil] Optional content to return; may be an application-specific description of the error.
|
12
|
+
|
13
|
+
def initialize(message, errors, code=nil, content=nil)
|
14
|
+
Hyperion::Util.guard_param(message, 'a message string', String)
|
15
|
+
Hyperion::Util.guard_param(errors, 'an array of errors', &method(:error_array?))
|
16
|
+
code = ClientErrorCode.from(code || errors.first.try(:code) || ClientErrorCode::UNKNOWN)
|
17
|
+
Hyperion::Util.guard_param(code, 'a code') { ClientErrorCode.values.include?(code) }
|
18
|
+
|
19
|
+
@message = message
|
20
|
+
@code = code
|
21
|
+
@errors = errors
|
22
|
+
@content = content
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_json(*_args)
|
26
|
+
{
|
27
|
+
'message' => message,
|
28
|
+
'code' => code.value,
|
29
|
+
'errors' => errors.map(&:as_json),
|
30
|
+
'content' => content
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_attrs(attrs)
|
35
|
+
Hyperion::Util.nil_if_error do
|
36
|
+
message = attrs['message']
|
37
|
+
return nil if message.blank?
|
38
|
+
content = attrs['content']
|
39
|
+
code = code || ClientErrorCode.from(attrs['code'])
|
40
|
+
errors = (attrs['errors'] || []).map(&ClientErrorDetail.method(:from_attrs))
|
41
|
+
self.new(message, errors, code, content)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def error_array?(xs)
|
48
|
+
xs.is_a?(Array) && xs.all?{|x| x.is_a?(ClientErrorDetail)}
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
class HyperionResult
|
4
|
+
attr_reader :route, :status, :code, :body
|
5
|
+
|
6
|
+
# @param status [HyperionStatus]
|
7
|
+
# @param code [Integer] the HTTP response code
|
8
|
+
# @param body [Object, Hash<String,Object>] the deserialized response body.
|
9
|
+
# The type is determined by the content-type.
|
10
|
+
# JSON is deserialized to a Hash<String, Object>
|
11
|
+
def initialize(route, status, code=nil, body=nil)
|
12
|
+
@route, @status, @code, @body = route, status, code, body
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
if status == HyperionStatus::CHECK_CODE
|
17
|
+
"HTTP #{code}: #{route.to_s}"
|
18
|
+
elsif status == HyperionStatus::BAD_ROUTE
|
19
|
+
"#{status.value.to_s.humanize} (#{code}): #{route.to_s}"
|
20
|
+
else
|
21
|
+
"#{status.value.to_s.humanize}: #{route.to_s}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|