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,97 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'delegate'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
4
|
+
require 'rack/utils'
|
5
|
+
require 'abstractivator/array_ext'
|
6
|
+
|
7
|
+
class HyperionUri < SimpleDelegator
|
8
|
+
# An enhanced version of URI. Namely,
|
9
|
+
# - the base uri is a first class citizen,
|
10
|
+
# - it accepts a hash containing query key/values, and
|
11
|
+
# - it form-encodes query values that are arrays.
|
12
|
+
|
13
|
+
attr_accessor :query_hash
|
14
|
+
|
15
|
+
def initialize(uri, query_hash={})
|
16
|
+
@uri = make_ruby_uri(uri)
|
17
|
+
query_from_uri = parse_query(@uri.query)
|
18
|
+
additional_query_params = validate_query(query_hash)
|
19
|
+
@query_hash = query_from_uri.merge(additional_query_params)
|
20
|
+
__setobj__(@uri)
|
21
|
+
end
|
22
|
+
|
23
|
+
def query
|
24
|
+
query_string(@query_hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
def query=(query)
|
28
|
+
@query_hash = parse_query(query)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
fixed = @uri.dup
|
33
|
+
fixed.query = query
|
34
|
+
make_ruby_uri(fixed).to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect
|
38
|
+
"#<HyperionUri:0x#{(object_id << 1).to_s(16)} #{to_s}>"
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] the URI base e.g., "h\ttp://somehost:80"
|
42
|
+
def base
|
43
|
+
"#{scheme}://#{host}:#{port}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def base=(uri)
|
47
|
+
uri = uri.is_a?(HyperionUri) ? uri : HyperionUri.new(uri)
|
48
|
+
self.scheme = uri.scheme
|
49
|
+
self.host = uri.host
|
50
|
+
self.port = uri.port
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def validate_query(query)
|
56
|
+
query ||= {}
|
57
|
+
query.is_a?(Hash) or fail 'query must be a hash'
|
58
|
+
query.values.all?(&method(:simple_value?)) or fail 'query values must be simple'
|
59
|
+
query.stringify_keys
|
60
|
+
end
|
61
|
+
|
62
|
+
def simple_value?(x)
|
63
|
+
case x
|
64
|
+
when Array; x.all?(&method(:primitive_value?))
|
65
|
+
else; primitive_value?(x)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def primitive_value?(x)
|
70
|
+
x.is_a?(String) || x.is_a?(Numeric) || x.is_a?(Symbol)
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_query(query)
|
74
|
+
query ||= ''
|
75
|
+
Rack::Utils.parse_nested_query(query)
|
76
|
+
end
|
77
|
+
|
78
|
+
def query_string(query_hash)
|
79
|
+
return nil if query_hash == {}
|
80
|
+
sorted = query_hash.map{|(k, v)| [k.to_s, stringify(v)]}.sort_by(&:key).to_h
|
81
|
+
Rack::Utils.build_nested_query(sorted)
|
82
|
+
end
|
83
|
+
|
84
|
+
def stringify(x)
|
85
|
+
x.is_a?(Array) ? x.map(&:to_s) : x.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
def make_ruby_uri(x)
|
89
|
+
input = x.is_a?(HyperionUri) ? x.to_s : x
|
90
|
+
|
91
|
+
# URI is an oddball. It's a module but also a method on Kernel.
|
92
|
+
# Since this class is a SimpleDelegator and SimpleDelegator is
|
93
|
+
# a BasicObject, we need to pick the method off of Kernel.
|
94
|
+
# We don't want to include Kernel because that would mess up delegation.
|
95
|
+
Kernel.instance_method(:URI).bind(self).call(input)
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'hyperion/headers'
|
2
|
+
|
3
|
+
class ResponseDescriptor
|
4
|
+
# Describes properties of an acceptable response
|
5
|
+
|
6
|
+
include Hyperion::Headers
|
7
|
+
|
8
|
+
attr_reader :type, :version, :format
|
9
|
+
|
10
|
+
# @param type [String]
|
11
|
+
# @param version [Integer]
|
12
|
+
# @param format [Symbol] :json
|
13
|
+
def initialize(type, version, format)
|
14
|
+
@type, @version, @format = type, version, format
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
short_mimetype(self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'hyperion/types/hyperion_uri'
|
2
|
+
|
3
|
+
class RestRoute
|
4
|
+
attr_reader :method, :uri, :response_descriptor, :payload_descriptor
|
5
|
+
|
6
|
+
# @param method [Symbol] the HTTP method
|
7
|
+
# @param uri [String, URI]
|
8
|
+
# @param response_descriptor [ResponseDescriptor]
|
9
|
+
# @param payload_descriptor [PayloadDescriptor]
|
10
|
+
def initialize(method, uri, response_descriptor=nil, payload_descriptor=nil)
|
11
|
+
@method = method
|
12
|
+
@uri = HyperionUri.new(uri)
|
13
|
+
@response_descriptor = response_descriptor
|
14
|
+
@payload_descriptor = payload_descriptor
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"#{method.to_s.upcase} #{uri}"
|
19
|
+
end
|
20
|
+
end
|
data/lib/hyperion.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# hyperion's dependencies
|
2
|
+
require 'immutable_struct'
|
3
|
+
require 'typhoeus'
|
4
|
+
require 'oj'
|
5
|
+
require 'continuation'
|
6
|
+
require 'abstractivator/proc_ext'
|
7
|
+
require 'abstractivator/enumerable_ext'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
9
|
+
require 'active_support/core_ext/object/try'
|
10
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
11
|
+
|
12
|
+
# ensure the requirer gets everything (except hyperion_test)
|
13
|
+
require 'hyperion/hyperion'
|
14
|
+
require 'hyperion/requestor'
|
15
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'hyperion/types/**/*.rb')).each { |path| require_relative(path) }
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'immutable_struct'
|
2
|
+
require 'mimic'
|
3
|
+
require 'hyperion/headers'
|
4
|
+
require 'hyperion/formats'
|
5
|
+
require 'uri'
|
6
|
+
require 'hyperion_test/fake_server'
|
7
|
+
|
8
|
+
class Hyperion
|
9
|
+
class << self
|
10
|
+
# maintains a collection of fake servers, one for each base_uri.
|
11
|
+
# manages rspec integration for automatic teardown after each test.
|
12
|
+
|
13
|
+
include Formats
|
14
|
+
include Headers
|
15
|
+
include TestFrameworkHooks
|
16
|
+
include Logger
|
17
|
+
|
18
|
+
def fake(base_uri, &routes)
|
19
|
+
base_uri = normalized_base(base_uri)
|
20
|
+
if !@running
|
21
|
+
hook_teardown if can_hook_teardown? && !teardown_registered?
|
22
|
+
@running = true
|
23
|
+
end
|
24
|
+
servers[base_uri].configure(&routes)
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
servers.values.each(&:teardown)
|
29
|
+
servers.clear
|
30
|
+
@running = false
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def servers
|
36
|
+
@servers ||= Hash.new{|hash, key| hash[key] = FakeServer.new(next_port)}
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_port
|
40
|
+
@last_port ||= 9000
|
41
|
+
@last_port += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def normalized_base(uri)
|
47
|
+
HyperionUri.new(uri).base
|
48
|
+
end
|
49
|
+
|
50
|
+
# hook into the production code so we can redirect requests to the appropriate fake server
|
51
|
+
def transform_uri(uri)
|
52
|
+
server_uri = servers.keys.detect{|server_uri| normalized_base(server_uri) == uri.base}
|
53
|
+
if server_uri
|
54
|
+
new_uri = HyperionUri.new(uri)
|
55
|
+
new_uri.base = "http://localhost:#{servers[server_uri].port}"
|
56
|
+
logger.debug "Hyperion is redirecting #{uri} ==> #{new_uri}"
|
57
|
+
new_uri
|
58
|
+
else
|
59
|
+
uri
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Hyperion
|
2
|
+
class FakeServer
|
3
|
+
class Config
|
4
|
+
# this is passed to the block to allow the caller to configure the fake server
|
5
|
+
|
6
|
+
include Hyperion::Headers
|
7
|
+
include Hyperion::Logger
|
8
|
+
|
9
|
+
def rules
|
10
|
+
@rules ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
# allow(route)
|
14
|
+
# allow(method, path, headers={})
|
15
|
+
def allow(*args, &handler)
|
16
|
+
rule = allowed_rule(args, handler)
|
17
|
+
rules << rule
|
18
|
+
log_stub(rule)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def allowed_rule(args, handler)
|
24
|
+
if args.size == 1 && args.first.is_a?(RestRoute)
|
25
|
+
route = args.first
|
26
|
+
Rule.new(MimicRoute.new(route.method, route.uri.path), route_headers(route), handler, route)
|
27
|
+
else
|
28
|
+
# TODO: deprecate this
|
29
|
+
method, path, headers = args
|
30
|
+
headers ||= {}
|
31
|
+
Rule.new(MimicRoute.new(method, path), headers, handler, nil)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'hyperion/headers'
|
2
|
+
|
3
|
+
class Hyperion
|
4
|
+
class FakeServer
|
5
|
+
class Dispatcher
|
6
|
+
# Directs an incoming request to the correct handler
|
7
|
+
|
8
|
+
include Hyperion::Formats
|
9
|
+
include Hyperion::Headers
|
10
|
+
|
11
|
+
def initialize(rules)
|
12
|
+
@rules = rules
|
13
|
+
end
|
14
|
+
|
15
|
+
def dispatch(mimic_route, request)
|
16
|
+
rule = find_matching_rule(mimic_route, request)
|
17
|
+
rule or return [404, {}, "Not stubbed: #{mimic_route.inspect} #{request.env}"]
|
18
|
+
request = make_req_obj(request.body.read, request.env['CONTENT_TYPE'])
|
19
|
+
response = rule.handler.call(request)
|
20
|
+
if rack_response?(response)
|
21
|
+
code, headers, body = *response
|
22
|
+
[code, headers, write(body, :json)]
|
23
|
+
else
|
24
|
+
if rule.rest_route
|
25
|
+
rd = rule.rest_route.response_descriptor
|
26
|
+
[200, {'Content-Type' => content_type_for(rd)}, write(response, rd)]
|
27
|
+
else
|
28
|
+
# better to return a 500 than raise an error, since we're executing in the forked server.
|
29
|
+
[500, {}, "An 'allow' block must return a rack-style response if it was not passed a RestRoute"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :rules
|
37
|
+
|
38
|
+
def rack_response?(x)
|
39
|
+
x.is_a?(Array) && x.size == 3 && x.first.is_a?(Integer) && x.drop(1).any?{|y| !y.is_a?(Integer)}
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_matching_rule(mimic_route, request)
|
43
|
+
matching_rules = rules.select{|rule| rule.mimic_route == mimic_route}
|
44
|
+
matching_rules.reverse.detect{|rule| headers_match?(rule.headers, request.env)}
|
45
|
+
# reverse so that if there are duplicates, the last one wins
|
46
|
+
end
|
47
|
+
|
48
|
+
def make_req_obj(raw_body, content_type)
|
49
|
+
body = raw_body.empty? ? '' : read(raw_body, format_for(content_type))
|
50
|
+
Request.new(body)
|
51
|
+
end
|
52
|
+
|
53
|
+
def headers_match?(rule_headers, actual_headers)
|
54
|
+
sinatrize_headers(rule_headers).subhash?(actual_headers)
|
55
|
+
end
|
56
|
+
|
57
|
+
def sinatrize_headers(headers)
|
58
|
+
headers.map{|k, v| [sinatra_header(k), v]}.to_h
|
59
|
+
end
|
60
|
+
|
61
|
+
def sinatra_header(header)
|
62
|
+
# TODO: there should be a function in Sinatra that does this already
|
63
|
+
cased_header = header.upcase.gsub('-', '_')
|
64
|
+
case cased_header
|
65
|
+
when 'ACCEPT' then 'HTTP_ACCEPT'
|
66
|
+
when 'EXPECT' then 'HTTP_EXPECT'
|
67
|
+
when 'HOST' then 'HTTP_HOST'
|
68
|
+
when 'USER_AGENT' then 'HTTP_USER_AGENT'
|
69
|
+
else cased_header
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'hyperion_test/test_framework_hooks'
|
2
|
+
require 'hyperion/aux/hash_ext'
|
3
|
+
require 'hyperion_test/fake_server/dispatcher'
|
4
|
+
require 'hyperion_test/fake_server/types'
|
5
|
+
require 'hyperion_test/fake_server/config'
|
6
|
+
|
7
|
+
class Hyperion
|
8
|
+
class FakeServer
|
9
|
+
# Runs a Mimic server configured per the specified routing rules.
|
10
|
+
# Restarts the server when the rules change.
|
11
|
+
# The server must be restarted because it runs in a forked process
|
12
|
+
# and it is easier to kill it than try to communicate with it.
|
13
|
+
|
14
|
+
attr_accessor :port, :rules
|
15
|
+
|
16
|
+
def initialize(port)
|
17
|
+
@port = port
|
18
|
+
@rules = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure(&configure_routes)
|
22
|
+
config = Config.new
|
23
|
+
configure_routes.call(config)
|
24
|
+
rules.concat(config.rules)
|
25
|
+
restart_server
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown
|
29
|
+
rules.clear
|
30
|
+
@mimic_running = true
|
31
|
+
Mimic.cleanup!
|
32
|
+
end
|
33
|
+
|
34
|
+
def restart_server
|
35
|
+
server = self
|
36
|
+
dispatcher = Dispatcher.new(rules)
|
37
|
+
if @mimic_running
|
38
|
+
Mimic.cleanup!
|
39
|
+
Mimic::Server.instance.instance_variable_get(:@thread).join
|
40
|
+
end
|
41
|
+
Mimic.mimic(port: @port) do
|
42
|
+
# this block executes in a strange context, which is why we
|
43
|
+
# have to close over server and dispatcher
|
44
|
+
server.rules.map(&:mimic_route).uniq.each do |mimic_route|
|
45
|
+
# register the route handlers. this is basically Sinatra.
|
46
|
+
send(mimic_route.method, mimic_route.path) do
|
47
|
+
dispatcher.dispatch(mimic_route, request)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@mimic_running = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'hyperion_test'
|
2
|
+
|
3
|
+
# A simple wrapper around Hyperion::fake for the typical
|
4
|
+
# use case of one faked route. The return value can either be specified
|
5
|
+
# as an argument or as a function of the request (using the block).
|
6
|
+
#
|
7
|
+
# @param [RestRoute] route The route to handle
|
8
|
+
# @param [Hash, String] return_value The data to return in response to a request
|
9
|
+
# @yield [Hyperion::FakeServer::Request] Yields a request object containing the deserialized request body
|
10
|
+
# @yieldreturn [Hash, String, rack_response] The data to return in response to a request
|
11
|
+
def fake_route(route, return_value=nil, &block)
|
12
|
+
if return_value && block
|
13
|
+
fail 'cannot provide both a return_value and block'
|
14
|
+
end
|
15
|
+
block = block || proc{return_value}
|
16
|
+
Hyperion.fake(route.uri.base) do |svr|
|
17
|
+
svr.allow(route, &block)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'hyperion'
|
2
|
+
require 'rspec/core'
|
3
|
+
|
4
|
+
module TestFrameworkHooks
|
5
|
+
def teardown_registered?
|
6
|
+
rspec_after_example_hooks.any? do |hook_proc|
|
7
|
+
hook_proc.source_location == method(:teardown).to_proc.source_location
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def can_hook_teardown?
|
12
|
+
RSpec.current_example
|
13
|
+
end
|
14
|
+
|
15
|
+
def hook_teardown
|
16
|
+
hyperion = self
|
17
|
+
rspec_hooks.register(:prepend, :after, :each) { hyperion.teardown }
|
18
|
+
end
|
19
|
+
|
20
|
+
def rspec_after_example_hooks
|
21
|
+
if rspec_hooks.respond_to?(:[]) # approximately rspec 3.1.0
|
22
|
+
rspec_hooks[:after][:example].to_a.map(&:block)
|
23
|
+
else # approximately rspec 3.3.0
|
24
|
+
default_if_no_hooks = nil
|
25
|
+
hook_collection = rspec_hooks.send(:hooks_for, :after, :example) {default_if_no_hooks}
|
26
|
+
return [] unless hook_collection
|
27
|
+
hook_collection.items_and_filters.map(&:first).map(&:block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def rspec_hooks
|
32
|
+
RSpec.current_example.example_group.hooks
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'hyperion/aux/util'
|
3
|
+
|
4
|
+
class Hyperion
|
5
|
+
describe Util do
|
6
|
+
|
7
|
+
describe '::nil_if_error' do
|
8
|
+
it 'catches StandardErrors in the block' do
|
9
|
+
expect(Util.nil_if_error { raise 'oops' }).to be_nil
|
10
|
+
end
|
11
|
+
it 'does not catch Exceptions in the block' do
|
12
|
+
expect{Util.nil_if_error { raise Exception }}.to raise_error Exception
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '::guard' do
|
17
|
+
it 'raises a BugException if the value is invalid' do
|
18
|
+
expect{Util.guard_param(7, 'a string', String)}.to report 'You passed me 7, which is not a string'
|
19
|
+
expect{Util.guard_param('foo', 'any old number', Numeric)}.to report 'You passed me "foo", which is not any old number'
|
20
|
+
expect{Util.guard_param(:zero, 'callable') { |x| x.respond_to?(:call) }}.to report 'You passed me :zero, which is not callable'
|
21
|
+
end
|
22
|
+
|
23
|
+
def report(msg)
|
24
|
+
raise_error BugError, msg
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'time'
|
3
|
+
require 'hyperion'
|
4
|
+
|
5
|
+
class Hyperion
|
6
|
+
describe Formats do
|
7
|
+
include Formats
|
8
|
+
|
9
|
+
describe '#write' do
|
10
|
+
it 'returns the input if the input is a string or nil' do
|
11
|
+
expect(write('hello', :json)).to eql 'hello'
|
12
|
+
expect(write(nil, :json)).to be_nil
|
13
|
+
end
|
14
|
+
it 'returns the input if format is nil' do
|
15
|
+
expect(write('hello', nil)).to eql 'hello'
|
16
|
+
end
|
17
|
+
it 'accepts format as a symbol' do
|
18
|
+
expect(write({'a' => 1}, :json)).to eql '{"a":1}'
|
19
|
+
end
|
20
|
+
it 'accepts format as a descriptor' do
|
21
|
+
descriptor = double(format: :json)
|
22
|
+
expect(write({'a' => 1}, descriptor)).to eql '{"a":1}'
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'formats' do
|
26
|
+
context 'when writing json' do
|
27
|
+
it 'writes hashes with string keys' do
|
28
|
+
expect(write({'a' => 1}, :json)).to be_json_eql '{"a":1}'
|
29
|
+
end
|
30
|
+
it 'writes hashes with symbol keys' do
|
31
|
+
expect(write({a: 1}, :json)).to be_json_eql '{"a":1}'
|
32
|
+
end
|
33
|
+
context 'when writing times' do
|
34
|
+
let!(:time) { Time.parse('2015-02-13 08:40:20.321 +1200').localtime('+12:00') }
|
35
|
+
it 'writes Time objects in UTC ISO 8601 format using the timezone that was passed in with milliseconds precision' do
|
36
|
+
expect(write({'a' => time}, :json)).to be_json_eql '{"a":"' + time.localtime('+12:00').iso8601(3) + '"}'
|
37
|
+
end
|
38
|
+
it 'preserves default behavior for non-hyperion code' do
|
39
|
+
expect(Oj.dump({'a' => time})).to start_with '{"a":{"^t":' # be non-specific, since this changes based on something outside the control of bundler
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
it 'allows protobuf format but just passes it through' do
|
44
|
+
expect(write('x', :protobuf)).to eql 'x'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#read' do
|
50
|
+
it 'returns nil if input is nil' do
|
51
|
+
expect(read(nil, :json)).to be_nil
|
52
|
+
end
|
53
|
+
it 'returns the input if format is nil' do
|
54
|
+
expect(read('abc', nil)).to eql 'abc'
|
55
|
+
end
|
56
|
+
it 'returns the input if the input is not parseable' do
|
57
|
+
expect(read('abc', :json)).to eql 'abc'
|
58
|
+
end
|
59
|
+
context 'when reading json' do
|
60
|
+
it 'accepts format as a symbol' do
|
61
|
+
expect(read('{"a":1}', :json)).to eql({'a' => 1})
|
62
|
+
end
|
63
|
+
it 'reads times as strings' do
|
64
|
+
# sadly, Oj does not appear to support reading real Time objects, even when using
|
65
|
+
# its own representation ({"^t":1423834820.321})
|
66
|
+
expect(read('{"a":"2015-02-13T13:40:20.321Z"}', :json)['a']).to eql '2015-02-13T13:40:20.321Z'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
it 'accepts format as a descriptor' do
|
70
|
+
descriptor = double(format: :json)
|
71
|
+
expect(read('{"a":1}', descriptor)).to eql({'a' => 1})
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'formats' do
|
75
|
+
it 'read json' do
|
76
|
+
expect(read('{"a":1}', :json)).to eql({'a' => 1})
|
77
|
+
end
|
78
|
+
it 'allows protobuf format but just passes it through' do
|
79
|
+
expect(read('x', :protobuf)).to eql 'x'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'hyperion'
|
2
|
+
require 'hyperion/headers'
|
3
|
+
|
4
|
+
class Hyperion
|
5
|
+
describe Headers do
|
6
|
+
include Headers
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
Hyperion.configure do |config|
|
10
|
+
config.vendor_string = 'schmindigo-descent'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let!(:uri){'http://somesite.org'}
|
15
|
+
|
16
|
+
describe '#route_headers' do
|
17
|
+
it 'creates an accept header for the response descriptor' do
|
18
|
+
headers = route_headers(RestRoute.new(:get, uri, ResponseDescriptor.new('ttt', 999, :json)))
|
19
|
+
expect(headers['Accept']).to eql 'application/vnd.schmindigo-descent.ttt-v999+json'
|
20
|
+
end
|
21
|
+
it 'creates a content-type header for the payload descriptor' do
|
22
|
+
headers = route_headers(RestRoute.new(:get, uri, ResponseDescriptor.new('ttt', 999, :json), PayloadDescriptor.new(:json)))
|
23
|
+
expect(headers['Content-Type']).to eql 'application/json'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#content_type_for' do
|
28
|
+
it 'accepts format as a symbol' do
|
29
|
+
expect(content_type_for(:json)).to eql 'application/json'
|
30
|
+
end
|
31
|
+
it 'accepts format as a descriptor' do
|
32
|
+
descriptor = double(format: :json)
|
33
|
+
expect(content_type_for(descriptor)).to eql 'application/json'
|
34
|
+
end
|
35
|
+
it 'returns the content type for the given format' do
|
36
|
+
expect(content_type_for(:json)).to eql 'application/json'
|
37
|
+
expect(content_type_for(:protobuf)).to eql 'application/x-protobuf'
|
38
|
+
end
|
39
|
+
it 'returns application/octet-stream if the format is unknown' do
|
40
|
+
expect(content_type_for(:aaa)).to eql 'application/octet-stream'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#format_for' do
|
45
|
+
it 'returns the format for the given content type' do
|
46
|
+
expect(format_for('application/json')).to eql :json
|
47
|
+
expect(format_for('application/x-protobuf')).to eql :protobuf
|
48
|
+
end
|
49
|
+
it 'raises an error if the content type is unknown' do
|
50
|
+
expect{format_for('aaa/bbb')}.to raise_error
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#short_mimetype' do
|
55
|
+
it 'returns the short mime type' do
|
56
|
+
expect(short_mimetype(ResponseDescriptor.new('ttt', 999, :json))).to eql 'ttt-v999+json'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|