hyperdrive 0.0.15 → 0.0.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b7cbd700b539127c3ecf2e78907a8c3eae46d0a
4
- data.tar.gz: b8f3dda138e2148b538a2a49e6e91297c83e5504
3
+ metadata.gz: b111cd2840e4210416ca5edbe340199dd206c1bb
4
+ data.tar.gz: 3b3ecb7863fbf7006e6584cbb7250ef261e49d67
5
5
  SHA512:
6
- metadata.gz: 6eeb1f397b297a8a8a2c0c2418b74a354973ab700ccf853e46d2dde4d51bcd5aa788856615d684a21247a9e37a503eeef34920f501419ef42f4a3078f99c08a6
7
- data.tar.gz: f6b738d8f4f0e9dc588449d891cfd1a9794e2c83b0e994b88a48fee4196d23a91c8d998ab02dd1eac218161d2dfe3096fb5dd214be8bc04f5a4630e10f438407
6
+ metadata.gz: 9e461cd608d77b6cbbfb09c9851971c5bd4acdcb1ee4d25b1550db03a0b70c0ed996e104d2179331d91c9012b4eda73a9fda122ead6e5ca53bfaa42fcc38831c
7
+ data.tar.gz: f8d2e7ea4e3307596a1641d0e80d9275bb356e105fb15eb282cc7819568225fe0c1734eea337ae486391ca603c2916d12ea29da67db1150b6929c41c750fa052
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hyperdrive
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/hyperdrive.png)](http://badge.fury.io/rb/hyperdrive) [![Build Status](https://secure.travis-ci.org/styleseek/hyperdrive.png?branch=master)](https://travis-ci.org/styleseek/hyperdrive) [![Code Climate](https://codeclimate.com/github/styleseek/hyperdrive.png)](https://codeclimate.com/github/styleseek/hyperdrive) [![Test Coverage](https://codeclimate.com/github/styleseek/hyperdrive/coverage.png)](https://codeclimate.com/github/styleseek/hyperdrive) [![Dependency Status](https://gemnasium.com/styleseek/hyperdrive.png)](https://gemnasium.com/styleseek/hyperdrive)
3
+ [![Gem Version](https://badge.fury.io/rb/hyperdrive.png)](http://badge.fury.io/rb/hyperdrive) [![Build Status](https://travis-ci.org/styleseek/hyperdrive.svg?branch=master)](https://travis-ci.org/styleseek/hyperdrive) [![Code Climate](https://codeclimate.com/github/styleseek/hyperdrive.png)](https://codeclimate.com/github/styleseek/hyperdrive) [![Test Coverage](https://codeclimate.com/github/styleseek/hyperdrive/coverage.png)](https://codeclimate.com/github/styleseek/hyperdrive) [![Dependency Status](https://gemnasium.com/styleseek/hyperdrive.svg)](https://gemnasium.com/styleseek/hyperdrive)
4
4
 
5
5
 
6
6
  Ruby DSL for defining self-documenting, HATEOAS™ complaint, Hypermedia endpoints.
@@ -26,11 +26,17 @@ module Hyperdrive
26
26
  resource.register_filter(*args)
27
27
  end
28
28
 
29
- def request(request_method)
29
+ def request(request_method, version = 'v1')
30
30
  unless definable_request_methods.include? request_method
31
31
  raise Errors::DSL::UnknownArgument.new(request_method, 'request')
32
32
  end
33
- resource.register_request_handler(request_method, Proc.new)
33
+ resource.register_request_handler(request_method, Proc.new, version)
34
+ end
35
+
36
+ def before(request_methods = [:get, :post, :put, :patch, :delete], version = 'v1')
37
+ Array(request_methods).each do |request_method|
38
+ resource.register_callback(:before, request_method, Proc.new, version)
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -14,8 +14,13 @@ module Hyperdrive
14
14
  self
15
15
  end
16
16
 
17
+ def instrument(*args)
18
+ @config[:instrumenter] = @config[:instrumenter] || Hyperdrive::Instrumenters::Noop
19
+ @config[:instrumenter].instrument(*args)
20
+ end
21
+
17
22
  private
18
-
23
+
19
24
  def name(name)
20
25
  @config[:name] = name
21
26
  end
@@ -39,7 +44,17 @@ module Hyperdrive
39
44
  end
40
45
 
41
46
  def per_page(per_page)
42
- @config[:per_page] = per_page.to_i
47
+ per_page = per_page.to_i
48
+ per_page = default_config[:per_page] if per_page == 0
49
+ @config[:per_page] = per_page
50
+ end
51
+
52
+ def ssl(force_ssl)
53
+ @config[:ssl] = force_ssl
54
+ end
55
+
56
+ def instrumenter(instrumenter)
57
+ @config[:instrumenter] = instrumenter || Hyperdrive::Instrumenters::Noop
43
58
  end
44
59
 
45
60
  def resource(name)
@@ -11,7 +11,7 @@ module Hyperdrive
11
11
  @resource = env['hyperdrive.resource']
12
12
  @headers = Hyperdrive::Values.default_headers.dup
13
13
  @headers.merge!('Allow' => resource.allowed_methods.join(', '), 'Content-Type' => @media_type)
14
-
14
+ before_response
15
15
  response.finish
16
16
  end
17
17
 
@@ -23,11 +23,16 @@ module Hyperdrive
23
23
  end
24
24
 
25
25
  def self.json?
26
- media_type =~ /json$/
26
+ media_type =~ /json$/ ? true : false
27
27
  end
28
28
 
29
29
  def self.xml?
30
- media_type =~ /xml$/
30
+ media_type =~ /xml$/ ? true : false
31
+ end
32
+
33
+ def self.requested_version
34
+ regex = /.*\/vnd.#{hyperdrive.config[:vendor]}\..*\.(.*)\+.*$/
35
+ regex.match(media_type) { |version| version.captures.first } or resource.latest_version(env['REQUEST_METHOD'])
31
36
  end
32
37
 
33
38
  def self.page
@@ -44,13 +49,13 @@ module Hyperdrive
44
49
  if json?
45
50
  MultiJson.dump(body)
46
51
  else
47
- env.logger.error "ENDPOINT: Can't serialize response automatically"
52
+ puts "Hyperdrive::Endpoint: Can't serialize response automatically"
48
53
  raise Errors::NoResponse.new
49
54
  end
50
55
  when String
51
56
  body
52
57
  else
53
- env.logger.debug "ENDPOINT: Coerceing response to string. Probably not what you want"
58
+ puts "Hyperdrive::Endpoint: Coerceing response to string. Probably not what you want"
54
59
  body.to_s
55
60
  end
56
61
  end
@@ -59,6 +64,16 @@ module Hyperdrive
59
64
  raise Errors::HTTPError.new(message, status)
60
65
  end
61
66
 
67
+ def self.instrument(*args)
68
+ hyperdrive.instrument(*args)
69
+ end
70
+
71
+ def self.before_response
72
+ if @resource.has_callback?(:before, env['REQUEST_METHOD'], requested_version)
73
+ instance_eval(&@resource.callback(:before, env['REQUEST_METHOD'], requested_version))
74
+ end
75
+ end
76
+
62
77
  def self.status
63
78
  case env['REQUEST_METHOD']
64
79
  when 'POST'
@@ -71,7 +86,7 @@ module Hyperdrive
71
86
  end
72
87
 
73
88
  def self.body
74
- body = instance_eval(&resource.request_handler(env['REQUEST_METHOD']))
89
+ body = instance_eval(&resource.request_handler(env['REQUEST_METHOD'], requested_version))
75
90
  body = '' if env['REQUEST_METHOD'] == 'DELETE'
76
91
  body
77
92
  end
@@ -5,41 +5,57 @@ module Hyperdrive
5
5
  extend Hyperdrive::Values
6
6
 
7
7
  def self.call(env)
8
+ @env = env
8
9
  if hyperdrive.resources.empty? || env['PATH_INFO'] != '/'
9
10
  raise Hyperdrive::Errors::NotFound
10
11
  end
11
12
 
12
- endpoints = hyperdrive.resources.map do |_,resource|
13
- resource.to_hash
14
- end
13
+ [200, headers, [body]]
14
+ end
15
15
 
16
- api = {
17
- _links: { self: { href: '/' } },
18
- name: hyperdrive.config[:name],
19
- description: hyperdrive.config[:description],
20
- vendor: hyperdrive.config[:vendor],
21
- resources: endpoints
22
- }
16
+ private
23
17
 
24
- media_types = %w(hal+json json).map do |media_type|
18
+ def self.media_types
19
+ %w(hal+json json).map do |media_type|
25
20
  "application/vnd.#{hyperdrive.config[:vendor]}+#{media_type}"
21
+ end + %w(application/hal+json application/json)
22
+ end
23
+
24
+ def self.content_type
25
+ @env['hyperdrive.accept'].best_of(media_types)
26
+ end
27
+
28
+ def self.headers
29
+ {
30
+ 'Access-Control-Allow-Origin' => '*',
31
+ 'Access-Control-Allow-Methods' => 'GET, HEAD, OPTIONS',
32
+ 'Allow' => 'GET, HEAD, OPTIONS',
33
+ 'Content-Type' => content_type
34
+ }
35
+ end
36
+
37
+ def self.endpoints
38
+ hyperdrive.resources.map do |_,resource|
39
+ resource.to_hash
26
40
  end
41
+ end
27
42
 
28
- media_types += %w(application/hal+json application/json)
29
- content_type = env['hyperdrive.accept'].best_of(media_types)
30
- body = if content_type =~ /json$/
31
- MultiJson.dump(api)
32
- else
33
- raise Errors::NotAcceptable.new(env['HTTP_ACCEPT'])
34
- end
35
-
36
- status = 200
37
- headers = {}
38
- headers['Access-Control-Allow-Origin'] = '*'
39
- headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS'
40
- headers['Allow'] = 'GET, HEAD, OPTIONS'
41
- headers['Content-Type'] = content_type
42
- [status, headers, [body]]
43
+ def self.response
44
+ {
45
+ _links: { self: { href: '/' } },
46
+ name: hyperdrive.config[:name],
47
+ description: hyperdrive.config[:description],
48
+ vendor: hyperdrive.config[:vendor],
49
+ resources: endpoints
50
+ }
51
+ end
52
+
53
+ def self.body
54
+ if content_type =~ /json$/
55
+ MultiJson.dump(response)
56
+ else
57
+ raise Errors::NotAcceptable.new(@env['HTTP_ACCEPT'])
58
+ end
43
59
  end
44
60
  end
45
61
  end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ module Hyperdrive
4
+ module Instrumenters
5
+ class Memory
6
+ Event = Struct.new(:name, :payload, :result)
7
+
8
+ attr_reader :events
9
+
10
+ def initialize
11
+ @events = []
12
+ end
13
+
14
+ def instrument(name, payload = {})
15
+ result = if block_given?
16
+ yield payload
17
+ else
18
+ payload
19
+ end
20
+
21
+ @events << Event.new(name, payload, result)
22
+
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Hyperdrive
4
+ module Instrumenters
5
+ class Noop
6
+ def self.instrument(name, payload = {})
7
+ if block_given?
8
+ yield payload
9
+ else
10
+ payload
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,7 +3,7 @@
3
3
  module Hyperdrive
4
4
  class Resource
5
5
  include Hyperdrive::Values
6
- attr_reader :id, :namespace, :endpoint, :params, :filters, :request_handlers, :version
6
+ attr_reader :id, :namespace, :endpoint, :params, :filters, :request_handlers, :callbacks, :version
7
7
  attr_accessor :name, :description
8
8
 
9
9
  def initialize(resource, options = {})
@@ -14,6 +14,7 @@ module Hyperdrive
14
14
  @params = default_params
15
15
  @filters = default_filters
16
16
  @request_handlers = default_request_handlers
17
+ @callbacks = { before: {} }
17
18
  @config = hyperdrive.config
18
19
  @id = [@config[:vendor], @namespace].join(':')
19
20
  end
@@ -26,6 +27,29 @@ module Hyperdrive
26
27
  @filters[filter] = Filter.new(filter, description, options)
27
28
  end
28
29
 
30
+ def register_callback(callback_type, request_method, callback, version = 'v1')
31
+ @callbacks[callback_type] ||={}
32
+ @callbacks[callback_type][request_method] ||= {}
33
+ @callbacks[callback_type][request_method].merge!({ version => callback })
34
+ if request_method == :get
35
+ @callbacks[callback_type][:head] ||= {}
36
+ @callbacks[callback_type][:head].merge!({ version => @callbacks[callback_type][:get][version] })
37
+ end
38
+ end
39
+
40
+ def callback(callback_type, http_request_method, version = nil)
41
+ version ||= latest_version(http_request_method)
42
+ request_method = http_request_methods[http_request_method]
43
+ callbacks[callback_type][request_method][version]
44
+ end
45
+
46
+ def has_callback?(callback_type, http_request_method, version = nil)
47
+ version ||= latest_version(http_request_method)
48
+ request_method = http_request_methods[http_request_method]
49
+ callbacks.has_key?(callback_type) and callbacks[callback_type].has_key?(request_method) and callbacks[callback_type][request_method].has_key?(version)
50
+ end
51
+
52
+
29
53
  def register_request_handler(request_method, request_handler, version = 'v1')
30
54
  @request_handlers[request_method] ||= {}
31
55
  @request_handlers[request_method].merge!({ version => request_handler })
@@ -43,12 +67,12 @@ module Hyperdrive
43
67
 
44
68
  def acceptable_content_types(http_request_method)
45
69
  content_types = []
46
- media_type_namepace = @resource.join('.')
70
+ media_type_namespace = @resource.join('-')
47
71
  @config[:media_types].each do |media_type|
48
72
  available_versions(http_request_method).each do |version|
49
- content_types << "application/vnd.#{@config[:vendor]}.#{media_type_namepace}.#{version}+#{media_type}"
73
+ content_types << "application/vnd.#{@config[:vendor]}.#{media_type_namespace}.#{version}+#{media_type}"
50
74
  end
51
- content_types << "application/vnd.#{@config[:vendor]}.#{media_type_namepace}+#{media_type}"
75
+ content_types << "application/vnd.#{@config[:vendor]}.#{media_type_namespace}+#{media_type}"
52
76
  content_types << "application/vnd.#{@config[:vendor]}+#{media_type}"
53
77
  end
54
78
  content_types
@@ -53,7 +53,9 @@ module Hyperdrive
53
53
  description: "v#{Hyperdrive::VERSION}",
54
54
  vendor: 'hyperdrive',
55
55
  media_types: %w(hal+json json),
56
- per_page: 20
56
+ per_page: 20,
57
+ ssl: false,
58
+ instrumenter: Hyperdrive::Instrumenters::Noop
57
59
  }.freeze
58
60
  end
59
61
 
@@ -1,3 +1,3 @@
1
1
  module Hyperdrive
2
- VERSION = "0.0.15"
2
+ VERSION = "0.0.16"
3
3
  end
data/lib/hyperdrive.rb CHANGED
@@ -16,6 +16,10 @@ require 'hyperdrive/version'
16
16
  require 'hyperdrive/docs'
17
17
  require 'hyperdrive/utils'
18
18
 
19
+ ## instrumenters
20
+ require 'hyperdrive/instrumenters/memory'
21
+ require 'hyperdrive/instrumenters/noop'
22
+
19
23
  ## sugary syntax and state mangagement
20
24
  require 'hyperdrive/dsl'
21
25
  require 'hyperdrive/errors'
@@ -10,6 +10,9 @@ describe Hyperdrive::DSL::Resource do
10
10
  description 'Thing Description'
11
11
  param :name, 'Thing Name'
12
12
  filter :parent_id, "Parent ID"
13
+ before do
14
+ 'before'
15
+ end
13
16
  request(:get) do
14
17
  'ok'
15
18
  end
@@ -37,12 +40,16 @@ describe Hyperdrive::DSL::Resource do
37
40
  hyperdrive.resources[:thing].filters[:parent_id].description.must_equal "Parent ID"
38
41
  end
39
42
 
40
- it "defines how requests are handled" do
43
+ it "registers requests handlers" do
41
44
  hyperdrive.resources[:thing].request_handlers[:get]['v1'].must_be :===, Proc
42
45
  end
43
46
 
44
- it "throws an error if request method is unknown" do
47
+ it "throws an error if request method is unknown" do
45
48
  bad_resource = -> { hyperdrive { resource(:thing) { request(:verb) } } }
46
- bad_resource.must_raise Hyperdrive::Errors::DSL::UnknownArgument
49
+ bad_resource.must_raise Hyperdrive::Errors::DSL::UnknownArgument
50
+ end
51
+
52
+ it "registers a before request callback" do
53
+ hyperdrive.resources[:thing].callbacks[:before][:get]['v1'].must_be :===, Proc
47
54
  end
48
55
  end
@@ -11,6 +11,9 @@ describe Hyperdrive::DSL do
11
11
  media_types %w(json)
12
12
  cors({ origins: '*', allow_headers: %w(Accept), test: 'test'})
13
13
  resource(:thing) {}
14
+ per_page '0'
15
+ ssl true
16
+ instrumenter Hyperdrive::Instrumenters::Memory.new
14
17
  end
15
18
  end
16
19
 
@@ -38,7 +41,7 @@ describe Hyperdrive::DSL do
38
41
  hyperdrive.resources[:thing].must_be_instance_of ::Hyperdrive::Resource
39
42
  end
40
43
 
41
- it "can configure cors options" do
44
+ it "configures cors options" do
42
45
  hyperdrive.config[:cors][:allow_headers].must_equal ['Accept']
43
46
  end
44
47
 
@@ -49,4 +52,21 @@ describe Hyperdrive::DSL do
49
52
  it "removes unsupported cors options" do
50
53
  hyperdrive.config[:cors].key?(:test).must_equal false
51
54
  end
55
+
56
+ it "configures the default per_page option" do
57
+ hyperdrive.config[:per_page].must_equal 20
58
+ end
59
+
60
+ it "configures ssl option" do
61
+ hyperdrive.config[:ssl].must_equal true
62
+ end
63
+
64
+ it "configures instrumenter option" do
65
+ hyperdrive.config[:instrumenter].must_be_instance_of Hyperdrive::Instrumenters::Memory
66
+ end
67
+
68
+ it "can call the instrumenter" do
69
+ hyperdrive.instrument('instrumentation', 'measurement') { |payload| payload + '1' }
70
+ hyperdrive.config[:instrumenter].events.size.must_be :>, 0
71
+ end
52
72
  end
@@ -20,6 +20,11 @@ describe Hyperdrive::Endpoint do
20
20
  last_response.successful?.must_equal true
21
21
  end
22
22
 
23
+ it "runs before callbacks" do
24
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing])
25
+ last_response.headers['X-Resource'].must_equal 'Thing Resource'
26
+ end
27
+
23
28
  it "can raise an HTTPError" do
24
29
  hyperdrive do
25
30
  resource(:thing) do
@@ -30,4 +35,206 @@ describe Hyperdrive::Endpoint do
30
35
  end
31
36
  ->{ get '/', {}, default_rack_env(hyperdrive.resources[:thing]) }.must_raise Hyperdrive::Errors::HTTPError
32
37
  end
38
+
39
+ it "can call the instrumenter" do
40
+ hyperdrive do
41
+ instrumenter Hyperdrive::Instrumenters::Memory.new
42
+ resource(:thing) do
43
+ request(:get) do
44
+ instrument('requests.GET', '1')
45
+ end
46
+ end
47
+ end
48
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing])
49
+ hyperdrive.config[:instrumenter].events.size.must_be :>, 0
50
+ end
51
+
52
+ it "returns true if media type ends in json" do
53
+ hyperdrive do
54
+ resource(:thing) do
55
+ request(:get) do
56
+ json?.to_s
57
+ end
58
+ end
59
+ end
60
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/json')
61
+ last_response.body.must_equal 'true'
62
+ end
63
+
64
+ it "returns false if media type does not ends in json" do
65
+ hyperdrive do
66
+ resource(:thing) do
67
+ request(:get) do
68
+ json?.to_s
69
+ end
70
+ end
71
+ end
72
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/xml')
73
+ last_response.body.must_equal 'false'
74
+ end
75
+
76
+ it "returns true if media type ends in xml" do
77
+ hyperdrive do
78
+ resource(:thing) do
79
+ request(:get) do
80
+ xml?.to_s
81
+ end
82
+ end
83
+ end
84
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/xml')
85
+ last_response.body.must_equal 'true'
86
+ end
87
+
88
+ it "returns false if media type does not ends in xml" do
89
+ hyperdrive do
90
+ resource(:thing) do
91
+ request(:get) do
92
+ xml?.to_s
93
+ end
94
+ end
95
+ end
96
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/json')
97
+ last_response.body.must_equal 'false'
98
+ end
99
+
100
+ it "returns version if media type contains a version" do
101
+ hyperdrive do
102
+ resource(:thing) do
103
+ request(:get, 'v2') do
104
+ requested_version.to_s
105
+ end
106
+ end
107
+ end
108
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/vnd.hyperdrive.things.v2+json')
109
+ last_response.body.must_equal 'v2'
110
+ end
111
+
112
+ it "returns latest version if media type does not contain a version" do
113
+ hyperdrive do
114
+ resource(:thing) do
115
+ request(:get) do
116
+ requested_version.to_s
117
+ end
118
+ end
119
+ end
120
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/vnd.hyperdrive.things+json')
121
+ last_response.body.must_equal 'v1'
122
+ end
123
+
124
+ it "renders Arrays as json if media type is json" do
125
+ hyperdrive do
126
+ resource(:thing) do
127
+ request(:get) do
128
+ []
129
+ end
130
+ end
131
+ end
132
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/vnd.hyperdrive.things+json')
133
+ last_response.body.must_equal '[]'
134
+ end
135
+
136
+ it "throws an error if media type is not json and response is an Array" do
137
+ hyperdrive do
138
+ resource(:thing) do
139
+ request(:get) do
140
+ []
141
+ end
142
+ end
143
+ end
144
+ ->{ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge({'hyperdrive.media_type' => 'application/xml' }) }.must_raise Hyperdrive::Errors::NoResponse
145
+ end
146
+
147
+ it "renders Hashes as json if media type is json" do
148
+ hyperdrive do
149
+ resource(:thing) do
150
+ request(:get) do
151
+ {}
152
+ end
153
+ end
154
+ end
155
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/vnd.hyperdrive.things+json')
156
+ last_response.body.must_equal '{}'
157
+ end
158
+
159
+ it "throws an error if media type is not json and response is a Hash" do
160
+ hyperdrive do
161
+ resource(:thing) do
162
+ request(:get) do
163
+ {}
164
+ end
165
+ end
166
+ end
167
+ ->{ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge({'hyperdrive.media_type' => 'application/xml' }) }.must_raise Hyperdrive::Errors::NoResponse
168
+ end
169
+
170
+ it "renders other responses as a string" do
171
+ hyperdrive do
172
+ resource(:thing) do
173
+ request(:get) do
174
+ 0
175
+ end
176
+ end
177
+ end
178
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.media_type' => 'application/vnd.hyperdrive.things+json')
179
+ last_response.body.must_equal '0'
180
+ end
181
+
182
+ it "returns the current page" do
183
+ hyperdrive do
184
+ resource(:thing) do
185
+ request(:get) do
186
+ page
187
+ end
188
+ end
189
+ end
190
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.page' => '2')
191
+ last_response.body.must_equal '2'
192
+ end
193
+
194
+ it "returns the number of results in each page" do
195
+ hyperdrive do
196
+ resource(:thing) do
197
+ request(:get) do
198
+ per_page
199
+ end
200
+ end
201
+ end
202
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('hyperdrive.per_page' => '20')
203
+ last_response.body.must_equal '20'
204
+ end
205
+
206
+ it "returns 200 when request is a GET" do
207
+ get '/', {}, default_rack_env(hyperdrive.resources[:thing])
208
+ last_response.status.must_equal 200
209
+ end
210
+
211
+ it "returns 201 when request is a POST" do
212
+ post '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'POST')
213
+ last_response.status.must_equal 201
214
+ end
215
+
216
+ it "returns 200 when request is a PUT" do
217
+ put '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'PUT')
218
+ last_response.status.must_equal 200
219
+ end
220
+
221
+ it "returns 200 when request is a PATCH" do
222
+ patch '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'PATCH')
223
+ last_response.status.must_equal 200
224
+ end
225
+
226
+ it "returns 200 when request is a OPTIONS" do
227
+ options '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'OPTIONS')
228
+ last_response.status.must_equal 200
229
+ end
230
+
231
+ it "returns 204 when request is a DELETE" do
232
+ delete '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'DELETE')
233
+ last_response.status.must_equal 204
234
+ end
235
+
236
+ it "returns an empty body when request is a DELETE" do
237
+ delete '/', {}, default_rack_env(hyperdrive.resources[:thing]).merge('REQUEST_METHOD' => 'DELETE')
238
+ last_response.body.must_equal ''
239
+ end
33
240
  end
@@ -19,6 +19,10 @@ describe Hyperdrive::Filter do
19
19
  @filter.required.must_equal %w(GET HEAD)
20
20
  end
21
21
 
22
+ it "returns an array if only a single HTTP method is required" do
23
+ Hyperdrive::Filter.new(:filter, '', required: 'GET').required.must_equal ['GET']
24
+ end
25
+
22
26
  it "returns true if the param is required for the given HTTP method" do
23
27
  @filter.required?('GET').must_equal true
24
28
  end
@@ -27,7 +31,7 @@ describe Hyperdrive::Filter do
27
31
  @filter.required?('OPTIONS').must_equal false
28
32
  end
29
33
 
30
- it "converts itself as a hash" do
34
+ it "converts itself to a hash" do
31
35
  constraints = { name: 'parent_id', description: 'Parent Identifier', type: 'String', constraints: 'Required for: GET, HEAD. Must be a valid BSON Object ID.' }
32
36
  @filter.to_hash.must_equal constraints
33
37
  end
@@ -38,5 +38,9 @@ describe Hyperdrive::HATEOAS do
38
38
  it "responds with a description of all resources" do
39
39
  last_response.body.must_equal %Q({"_links":{"self":{"href":"/"}},"name":"Hyperdrive API","description":"v#{Hyperdrive::VERSION}","vendor":"hyperdrive","resources":[{"_links":{"self":{"href":"/things"}},"id":"hyperdrive:things","name":"Thing Resource","description":"Description of Thing Resource","methods":["OPTIONS","GET","HEAD","POST","PUT","PATCH","DELETE"],"params":[{"name":"id","description":"Identifier","type":"String","constraints":"Required for: PUT, PATCH, DELETE. "},{"name":"name","description":"50 Chars or less","type":"String","constraints":"Required for: POST, PUT, PATCH. "}],"filters":[{"name":"id","description":"Resource Identifier","type":"String","constraints":" "},{"name":"parent_id","description":"Parent ID of Thing","type":"String","constraints":"Required for: GET, HEAD. "}],"media_types":[["application/vnd.hyperdrive.things.v1+hal+json","application/vnd.hyperdrive.things+hal+json","application/vnd.hyperdrive+hal+json","application/vnd.hyperdrive.things.v1+json","application/vnd.hyperdrive.things+json","application/vnd.hyperdrive+json"]]}]})
40
40
  end
41
+
42
+ it "throws an error if request doesn't accept json" do
43
+ ->{ get '/', {}, default_hyperdrive_env.merge({ 'Accept' => 'application/xml' }) }.must_raise Hyperdrive::Errors::NotAcceptable
44
+ end
41
45
  end
42
46
  end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Hyperdrive::Instrumenters::Memory do
6
+ before do
7
+ @instrumenter = Hyperdrive::Instrumenters::Memory.new
8
+ end
9
+
10
+ it "responds to instrument" do
11
+ @instrumenter.respond_to?(:instrument).must_equal true
12
+ end
13
+
14
+ it "returns the result" do
15
+ @instrumenter.instrument('instrumentation', 'measurement').must_equal 'measurement'
16
+ end
17
+
18
+ it "yields to the block and returns the result" do
19
+ @instrumenter.instrument('instrumentation', 'measurement') { |payload| payload + '1' }.must_equal 'measurement1'
20
+ end
21
+
22
+ it "returns events that have been instrumented" do
23
+ @instrumenter.instrument('instrumentation', 'measurement')
24
+ event = @instrumenter.events.first
25
+ event.name.must_equal 'instrumentation'
26
+ event.payload.must_equal 'measurement'
27
+ event.result.must_equal 'measurement'
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Hyperdrive::Instrumenters::Noop do
6
+ before do
7
+ @instrumenter = Hyperdrive::Instrumenters::Noop
8
+ end
9
+
10
+ it "responds to instrument" do
11
+ @instrumenter.respond_to?(:instrument).must_equal true
12
+ end
13
+
14
+ it "returns the result" do
15
+ @instrumenter.instrument('instrumentation', 'measurement').must_equal 'measurement'
16
+ end
17
+
18
+ it "optionally takes a block" do
19
+ @instrumenter.instrument('instrumentation', 'measurement') { |payload| payload + '1' } .must_equal 'measurement1'
20
+ end
21
+ end
@@ -23,6 +23,10 @@ describe Hyperdrive::Middleware::RequiredParams do
23
23
  it "raises an error if required filter is missing" do
24
24
  ->{ get '/', {}, @env }.must_raise Hyperdrive::Errors::MissingRequiredParam
25
25
  end
26
+
27
+ it "raises an error if required filter is missing" do
28
+ ->{ get '/', {}, @env.merge('hyperdrive.params' => { parent_id: '' }) }.must_raise Hyperdrive::Errors::MissingRequiredParam
29
+ end
26
30
  end
27
31
 
28
32
  context "Params" do
@@ -19,6 +19,10 @@ describe Hyperdrive::Param do
19
19
  @param.required.must_equal %w(PUT PATCH DELETE)
20
20
  end
21
21
 
22
+ it "returns an array if only a single HTTP method is required" do
23
+ Hyperdrive::Filter.new(:param, '', required: 'GET').required.must_equal ['GET']
24
+ end
25
+
22
26
  it "returns true if the param is required for the given HTTP method" do
23
27
  @param.required?('PUT').must_equal true
24
28
  end
@@ -27,7 +31,7 @@ describe Hyperdrive::Param do
27
31
  @param.required?('POST').must_equal false
28
32
  end
29
33
 
30
- it "converts itself as a hash" do
34
+ it "converts itself to a hash" do
31
35
  constraints = { name: 'id', description: 'Identifier', type: 'String', constraints: 'Required for: PUT, PATCH, DELETE. Must be a valid BSON Object ID.' }
32
36
  @param.to_hash.must_equal constraints
33
37
  end
@@ -5,6 +5,8 @@ describe Hyperdrive::Resource do
5
5
  @resource = Hyperdrive::Resource.new(:thing)
6
6
  @resource.register_param(:name, 'Thing Name')
7
7
  @resource.register_filter(:parent_id, 'Parent ID', required: true)
8
+ @resource.register_callback(:before, :get, Proc.new { |env| 'before v1' })
9
+ @resource.register_callback(:before, :get, Proc.new { |env| 'before v2' }, 'v2')
8
10
  @resource.register_request_handler(:get, Proc.new { |env| 'v1' })
9
11
  @resource.register_request_handler(:get, Proc.new { |env| 'v2' }, 'v2')
10
12
  @media_types = ["application/vnd.hyperdrive.things.v2+hal+json",
@@ -87,6 +89,26 @@ describe Hyperdrive::Resource do
87
89
  @resource.required?(:parent_id, 'DELETE').must_equal false
88
90
  end
89
91
 
92
+ it "registers a before callback" do
93
+ @resource.callbacks[:before][:get]['v1'].must_be :===, Proc
94
+ end
95
+
96
+ it "auto-registers HEAD before callback when GET before callback is registered" do
97
+ @resource.callbacks[:before][:head]['v1'].must_be :===, Proc
98
+ end
99
+
100
+ it "returns the specified before callback" do
101
+ @resource.callback(:before, 'GET').must_be :===, Proc
102
+ end
103
+
104
+ it "returns true if the specified before callback exists" do
105
+ @resource.has_callback?(:before, 'GET').must_equal true
106
+ end
107
+
108
+ it "returns false if the specified before callback does not exist" do
109
+ @resource.has_callback?(:before, 'GET', 'v3').must_equal false
110
+ end
111
+
90
112
  it "registers a request handler" do
91
113
  @resource.request_handlers[:get]['v1'].must_be :===, Proc
92
114
  end
@@ -16,37 +16,37 @@ describe Hyperdrive::Server do
16
16
  end
17
17
 
18
18
  it "responds to GET requests successfully" do
19
- get '/things', { parent_id: 42 }
19
+ get '/things', { parent_id: 42 }, default_hyperdrive_env
20
20
  last_response.successful?.must_equal true
21
21
  end
22
22
 
23
23
  it "responds to HEAD requests successfully" do
24
- head '/things', { parent_id: 42 }
24
+ head '/things', { parent_id: 42 }, default_hyperdrive_env
25
25
  last_response.successful?.must_equal true
26
26
  end
27
27
 
28
28
  it "responds to OPTIONS requests successfully" do
29
- options '/things'
29
+ options '/things', {}, default_hyperdrive_env
30
30
  last_response.successful?.must_equal true
31
31
  end
32
32
 
33
33
  it "responds to POST requests successfully" do
34
- post '/things', { name: 'bender' }
34
+ post '/things', { name: 'bender' }, default_hyperdrive_env
35
35
  last_response.successful?.must_equal true
36
36
  end
37
37
 
38
38
  it "responds to PUT requests successfully" do
39
- put '/things', { id: 1, name: 'bender' }
39
+ put '/things', { id: 1, name: 'bender' }, default_hyperdrive_env
40
40
  last_response.successful?.must_equal true
41
41
  end
42
42
 
43
43
  it "responds to PATCH requests successfully" do
44
- patch '/things', { id: 1, name: 'bender' }
44
+ patch '/things', { id: 1, name: 'bender' }, default_hyperdrive_env
45
45
  last_response.successful?.must_equal true
46
46
  end
47
47
 
48
48
  it "responds to DELETE requests successfully" do
49
- delete '/things', { id: 1 }
49
+ delete '/things', { id: 1 }, default_hyperdrive_env
50
50
  last_response.successful?.must_equal true
51
51
  end
52
52
  end
@@ -19,7 +19,7 @@ describe Hyperdrive::Utils do
19
19
 
20
20
  context '.symbolize_keys' do
21
21
  before do
22
- @hash = { 'string' => 'cheese', 'collection' => [{'skylanders' => 155}], 'map' => { 'oceans' => 'blue' } }
22
+ @hash = { :symbol => 'symbol', 'string' => 'cheese', 'collection' => [{'skylanders' => 155}], 'map' => { 'oceans' => 'blue' } }
23
23
  @subject = Hyperdrive::Utils.symbolize_keys(@hash)
24
24
  end
25
25
 
@@ -27,6 +27,10 @@ describe Hyperdrive::Utils do
27
27
  @subject[:string].must_equal 'cheese'
28
28
  end
29
29
 
30
+ it "doesn't symbolize keys that aren't a string" do
31
+ @subject[:symbol].must_equal 'symbol'
32
+ end
33
+
30
34
  it "can symbolize the keys of nested hashes" do
31
35
  @subject[:map][:oceans].must_equal 'blue'
32
36
  end
data/spec/spec_helper.rb CHANGED
@@ -29,6 +29,15 @@ include Rack::Test::Methods
29
29
 
30
30
  module Hyperdrive
31
31
  module TestData
32
+ def default_hyperdrive_env
33
+ {
34
+ 'HTTP_ACCEPT' => 'application/vnd.hyperdrive.things+hal+json',
35
+ 'hyperdrive.accept' => Rack::Accept::MediaType.new('HTTP_ACCEPT' => 'application/vnd.hyperdrive.things+hal+json;q=0.8'),
36
+ 'hyperdrive.resource' => hyperdrive.resources[:things],
37
+ 'hyperdrive.media_type' => 'application/vnd.hyperdrive.things+hal+json'
38
+ }
39
+ end
40
+
32
41
  def default_rack_env(resource = nil)
33
42
  default_env = {
34
43
  "rack.version" => Rack::VERSION,
@@ -41,7 +50,8 @@ module Hyperdrive
41
50
  'HTTP_ACCEPT' => 'application/vnd.hyperdrive.things+hal+json;q=0.8, application/json;q=1',
42
51
  'HTTP_ACCEPT_LANGUAGE' => 'en',
43
52
  'REQUEST_METHOD' => 'GET',
44
- 'QUERY_STRING' => 'id=1001'
53
+ 'QUERY_STRING' => 'id=1001',
54
+ 'hyperdrive.media_type' => 'application/vnd.hyperdrive.things+hal+json'
45
55
  }
46
56
  default_env.merge!('hyperdrive.accept' => Rack::Accept::MediaType.new(default_env['HTTP_ACCEPT']))
47
57
  default_env.merge!('hyperdrive.resource' => resource) if resource
@@ -72,6 +82,10 @@ module Hyperdrive
72
82
  param :name, '50 Chars or less', required: true
73
83
  filter :parent_id, 'Parent ID of Thing', required: true
74
84
 
85
+ before(:get) do
86
+ @headers.merge!('X-Resource' => resource.name)
87
+ end
88
+
75
89
  request(:get) do
76
90
  'ok'
77
91
  end
@@ -89,7 +103,7 @@ module Hyperdrive
89
103
  end
90
104
 
91
105
  request(:delete) do
92
- ''
106
+ 'ok'
93
107
  end
94
108
  end
95
109
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperdrive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - StyleSeek Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-12 00:00:00.000000000 Z
11
+ date: 2014-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: linguistics
@@ -147,6 +147,8 @@ files:
147
147
  - lib/hyperdrive/errors/unknown_error.rb
148
148
  - lib/hyperdrive/filter.rb
149
149
  - lib/hyperdrive/hateoas.rb
150
+ - lib/hyperdrive/instrumenters/memory.rb
151
+ - lib/hyperdrive/instrumenters/noop.rb
150
152
  - lib/hyperdrive/middleware.rb
151
153
  - lib/hyperdrive/middleware/accept.rb
152
154
  - lib/hyperdrive/middleware/content_negotiation.rb
@@ -183,6 +185,8 @@ files:
183
185
  - spec/hyperdrive/errors/unknown_error_spec.rb
184
186
  - spec/hyperdrive/filter_spec.rb
185
187
  - spec/hyperdrive/hateoas_spec.rb
188
+ - spec/hyperdrive/instrumenters/memory_spec.rb
189
+ - spec/hyperdrive/instrumenters/noop_spec.rb
186
190
  - spec/hyperdrive/middleware/accept_spec.rb
187
191
  - spec/hyperdrive/middleware/content_negotiation_spec.rb
188
192
  - spec/hyperdrive/middleware/cors_spec.rb
@@ -243,6 +247,8 @@ test_files:
243
247
  - spec/hyperdrive/errors/unknown_error_spec.rb
244
248
  - spec/hyperdrive/filter_spec.rb
245
249
  - spec/hyperdrive/hateoas_spec.rb
250
+ - spec/hyperdrive/instrumenters/memory_spec.rb
251
+ - spec/hyperdrive/instrumenters/noop_spec.rb
246
252
  - spec/hyperdrive/middleware/accept_spec.rb
247
253
  - spec/hyperdrive/middleware/content_negotiation_spec.rb
248
254
  - spec/hyperdrive/middleware/cors_spec.rb