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.
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