hyperion_http 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGES.md +145 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +421 -0
  9. data/Rakefile +11 -0
  10. data/hyperion_http.gemspec +34 -0
  11. data/lib/hyperion/aux/bug_error.rb +2 -0
  12. data/lib/hyperion/aux/hash_ext.rb +5 -0
  13. data/lib/hyperion/aux/logger.rb +54 -0
  14. data/lib/hyperion/aux/typho.rb +9 -0
  15. data/lib/hyperion/aux/util.rb +18 -0
  16. data/lib/hyperion/aux/version.rb +3 -0
  17. data/lib/hyperion/formats.rb +69 -0
  18. data/lib/hyperion/headers.rb +43 -0
  19. data/lib/hyperion/hyperion.rb +79 -0
  20. data/lib/hyperion/requestor.rb +88 -0
  21. data/lib/hyperion/result_handling/dispatch_dsl.rb +67 -0
  22. data/lib/hyperion/result_handling/dispatching_hyperion_result.rb +10 -0
  23. data/lib/hyperion/result_handling/result_maker.rb +64 -0
  24. data/lib/hyperion/types/client_error_code.rb +9 -0
  25. data/lib/hyperion/types/client_error_detail.rb +46 -0
  26. data/lib/hyperion/types/client_error_response.rb +50 -0
  27. data/lib/hyperion/types/hyperion_error.rb +6 -0
  28. data/lib/hyperion/types/hyperion_result.rb +24 -0
  29. data/lib/hyperion/types/hyperion_status.rb +10 -0
  30. data/lib/hyperion/types/hyperion_uri.rb +97 -0
  31. data/lib/hyperion/types/payload_descriptor.rb +9 -0
  32. data/lib/hyperion/types/response_descriptor.rb +21 -0
  33. data/lib/hyperion/types/rest_route.rb +20 -0
  34. data/lib/hyperion.rb +15 -0
  35. data/lib/hyperion_test/fake.rb +64 -0
  36. data/lib/hyperion_test/fake_server/config.rb +36 -0
  37. data/lib/hyperion_test/fake_server/dispatcher.rb +74 -0
  38. data/lib/hyperion_test/fake_server/types.rb +7 -0
  39. data/lib/hyperion_test/fake_server.rb +54 -0
  40. data/lib/hyperion_test/spec_helper.rb +19 -0
  41. data/lib/hyperion_test/test_framework_hooks.rb +34 -0
  42. data/lib/hyperion_test.rb +2 -0
  43. data/spec/lib/hyperion/aux/util_spec.rb +29 -0
  44. data/spec/lib/hyperion/formats_spec.rb +84 -0
  45. data/spec/lib/hyperion/headers_spec.rb +61 -0
  46. data/spec/lib/hyperion/logger_spec.rb +60 -0
  47. data/spec/lib/hyperion/test_spec.rb +222 -0
  48. data/spec/lib/hyperion/types/client_error_response_spec.rb +52 -0
  49. data/spec/lib/hyperion/types/hyperion_result_spec.rb +17 -0
  50. data/spec/lib/hyperion/types/hyperion_uri_spec.rb +113 -0
  51. data/spec/lib/hyperion_spec.rb +187 -0
  52. data/spec/lib/superion_spec.rb +151 -0
  53. data/spec/lib/types_spec.rb +46 -0
  54. data/spec/spec_helper.rb +3 -0
  55. data/spec/support/core_helpers.rb +5 -0
  56. 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,9 @@
1
+ class Hyperion
2
+ # all Typhoeus interation goes through this module
3
+ # for maintenance and mocking purposes
4
+ class Typho
5
+ def self.request(uri, options={})
6
+ Typhoeus::Request.new(uri, options).run
7
+ end
8
+ end
9
+ 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,3 @@
1
+ class Hyperion
2
+ VERSION = '0.1.2'
3
+ 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,6 @@
1
+ class HyperionError < RuntimeError
2
+ end
3
+
4
+ def hyperion_raise(msg)
5
+ raise HyperionError, msg
6
+ 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
@@ -0,0 +1,10 @@
1
+ require 'abstractivator/enum'
2
+
3
+ define_enum :HyperionStatus,
4
+ :success,
5
+ :timed_out,
6
+ :no_response,
7
+ :bad_route, # 404 (route not implemented)
8
+ :client_error, # 400
9
+ :server_error, # 500
10
+ :check_code # everything else