hyperdrive 0.0.15 → 0.0.16

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