hyperion_http 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|