hyperdrive 0.0.5 → 0.0.6
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 +4 -4
- data/.travis.yml +12 -4
- data/Gemfile +2 -0
- data/hyperdrive.gemspec +6 -2
- data/lib/hyperdrive/docs.rb +10 -11
- data/lib/hyperdrive/dsl/resource.rb +10 -15
- data/lib/hyperdrive/dsl.rb +49 -3
- data/lib/hyperdrive/endpoint.rb +65 -0
- data/lib/hyperdrive/errors/missing_required_param.rb +19 -0
- data/lib/hyperdrive/errors/not_acceptable.rb +18 -0
- data/lib/hyperdrive/errors.rb +13 -8
- data/lib/hyperdrive/filter.rb +20 -0
- data/lib/hyperdrive/hateoas.rb +45 -0
- data/lib/hyperdrive/middleware/accept.rb +16 -0
- data/lib/hyperdrive/middleware/content_negotiation.rb +19 -0
- data/lib/hyperdrive/middleware/cors.rb +36 -0
- data/lib/hyperdrive/middleware/error.rb +35 -0
- data/lib/hyperdrive/middleware/request_method.rb +31 -0
- data/lib/hyperdrive/middleware/required_params.rb +35 -0
- data/lib/hyperdrive/middleware/resource.rb +18 -0
- data/lib/hyperdrive/middleware/sanitize_params.rb +25 -0
- data/lib/hyperdrive/middleware.rb +10 -0
- data/lib/hyperdrive/param.rb +44 -0
- data/lib/hyperdrive/resource.rb +74 -30
- data/lib/hyperdrive/server.rb +16 -17
- data/lib/hyperdrive/utils.rb +27 -0
- data/lib/hyperdrive/values.rb +37 -3
- data/lib/hyperdrive/version.rb +1 -1
- data/lib/hyperdrive.rb +21 -7
- data/spec/hyperdrive/docs_spec.rb +14 -8
- data/spec/hyperdrive/dsl/resource_spec.rb +27 -37
- data/spec/hyperdrive/dsl_spec.rb +52 -0
- data/spec/hyperdrive/endpoint_spec.rb +22 -0
- data/spec/hyperdrive/filter_spec.rb +34 -0
- data/spec/hyperdrive/hateoas_spec.rb +42 -0
- data/spec/hyperdrive/middleware/accept_spec.rb +15 -0
- data/spec/hyperdrive/middleware/content_negotiation_spec.rb +29 -0
- data/spec/hyperdrive/middleware/cors_spec.rb +47 -0
- data/spec/hyperdrive/middleware/error_spec.rb +25 -0
- data/spec/hyperdrive/middleware/request_method_spec.rb +28 -0
- data/spec/hyperdrive/middleware/required_params_spec.rb +43 -0
- data/spec/hyperdrive/middleware/resource_spec.rb +15 -0
- data/spec/hyperdrive/middleware/sanitize_params_spec.rb +24 -0
- data/spec/hyperdrive/param_spec.rb +34 -0
- data/spec/hyperdrive/resource_spec.rb +87 -25
- data/spec/hyperdrive/server_spec.rb +48 -7
- data/spec/hyperdrive/utils_spec.rb +38 -0
- data/spec/hyperdrive/values_spec.rb +37 -0
- data/spec/spec_helper.rb +71 -12
- metadata +106 -11
- data/.coveralls.yml +0 -1
- data/lib/hyperdrive/dsl/main.rb +0 -19
- data/lib/hyperdrive/response.rb +0 -42
- data/spec/hyperdrive/dsl/main_spec.rb +0 -16
- data/spec/hyperdrive/response_spec.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 898b32e10dc9b2836f112818125fcb5a07f28ce0
|
4
|
+
data.tar.gz: 917e3c1f3f9ba879170bbbaefc216593de6e2288
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 055bf9088099afc3dadb018b7042a44a2589a52d91117e7c7e70eeeaf1e4fc16642980d6a5500a84fd3cffac9511c164f6ed3c51687469cdc24dfbc57b5986ad
|
7
|
+
data.tar.gz: 6dd19b1ddbd02b84a400ddd3aa22a952a7b06172e6164badd1de7a03b8b3037fb2f66ec90c9c206277c7dda561f89108739e3ab273f0f11bc9285f7c77b23a81
|
data/.travis.yml
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
- 2.1.1
|
4
|
+
- 1.9.3
|
5
|
+
- jruby-head
|
6
|
+
- rbx-2.1.1
|
7
|
+
matrix:
|
8
|
+
allow_failures:
|
9
|
+
- rvm: jruby-head
|
10
|
+
- rvm: rbx-2.1.1
|
11
|
+
env:
|
12
|
+
global:
|
13
|
+
secure: Tl3oBsBZRI5bMrtNH5mtTDvSTlTxNbsKaAnQ7KQ2KMyiH/eHolOSIbUYB9NB3GRMC6XSyFShRCaYPA/ET36HI+Jw3PFT/XADdiOdFqlk6VyJEUPznNQ1nlp8AsyNn+IexZXIl8DdqmZxGHlY6YKDbQV/U1v+FeW8X5j3TLN1mcw=
|
14
|
+
before_install: gem install bundler
|
data/Gemfile
CHANGED
data/hyperdrive.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.authors = ['StyleSeek Engineering']
|
10
10
|
gem.email = ['engineering@styleseek.com']
|
11
11
|
gem.summary = %q{Hypermedia State Machine}
|
12
|
-
gem.description = %q{Ruby DSL for defining self-documenting, HATEOAS™
|
12
|
+
gem.description = %q{Ruby DSL for defining self-documenting, HATEOAS™ compliant, Hypermedia API endpoints.}
|
13
13
|
gem.homepage = "https://github.com/styleseek/hyperdrive"
|
14
14
|
gem.license = "MIT"
|
15
15
|
|
@@ -18,7 +18,11 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
19
|
gem.require_paths = ["lib"]
|
20
20
|
|
21
|
-
gem.add_dependency 'rack'
|
22
21
|
gem.add_dependency 'linguistics'
|
22
|
+
gem.add_dependency 'oj'
|
23
|
+
gem.add_dependency 'rack'
|
24
|
+
gem.add_dependency 'rack-cache'
|
25
|
+
gem.add_dependency 'rack-ssl'
|
26
|
+
gem.add_dependency 'rack-accept'
|
23
27
|
gem.add_dependency 'thor'
|
24
28
|
end
|
data/lib/hyperdrive/docs.rb
CHANGED
@@ -12,13 +12,13 @@ module Hyperdrive
|
|
12
12
|
out = ""
|
13
13
|
resources.each_value do |resource|
|
14
14
|
out += header(resource.name)
|
15
|
-
out += paragraph(resource.
|
15
|
+
out += paragraph(resource.description)
|
16
16
|
out += header("Endpoint URL", 2)
|
17
17
|
out += paragraph(bullet(code(resource.endpoint), 1))
|
18
18
|
out += header("Params", 2)
|
19
|
-
out += list(resource.
|
19
|
+
out += list(resource.params.map { |_,param| param.to_hash })
|
20
20
|
out += header("Filters", 2)
|
21
|
-
out += list(resource.filters)
|
21
|
+
out += list(resource.filters.map { |_,filter| filter.to_hash })
|
22
22
|
end
|
23
23
|
out
|
24
24
|
end
|
@@ -53,16 +53,15 @@ module Hyperdrive
|
|
53
53
|
|
54
54
|
def list(items)
|
55
55
|
list = ""
|
56
|
-
items.each do |
|
57
|
-
list += bullet(bold(
|
56
|
+
items.each do |item|
|
57
|
+
list += bullet(bold(item[:name]), 1)
|
58
|
+
item.each do |key, value|
|
59
|
+
list += bullet(italics(key), 2)
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
if subvalue.kind_of? Array
|
63
|
-
list += bullet(code_options(subvalue), 3)
|
61
|
+
if value.kind_of? Array
|
62
|
+
list += bullet(code_options(value), 3)
|
64
63
|
else
|
65
|
-
list += bullet(
|
64
|
+
list += bullet(value, 3)
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
@@ -3,18 +3,19 @@
|
|
3
3
|
module Hyperdrive
|
4
4
|
module DSL
|
5
5
|
class Resource
|
6
|
+
include Values
|
6
7
|
attr_reader :resource
|
7
|
-
def initialize(
|
8
|
-
@resource = ::Hyperdrive::Resource.new(
|
9
|
-
instance_eval(&
|
8
|
+
def initialize(name, hyperdrive_config)
|
9
|
+
@resource = ::Hyperdrive::Resource.new(name, hyperdrive_config)
|
10
|
+
instance_eval(&Proc.new) if block_given?
|
10
11
|
end
|
11
12
|
|
12
13
|
def name(name)
|
13
14
|
resource.name = name
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
-
resource.
|
17
|
+
def description(description)
|
18
|
+
resource.description = description
|
18
19
|
end
|
19
20
|
|
20
21
|
def param(*args)
|
@@ -25,17 +26,11 @@ module Hyperdrive
|
|
25
26
|
resource.register_filter(*args)
|
26
27
|
end
|
27
28
|
|
28
|
-
def request(
|
29
|
-
unless definable_request_methods.include?
|
30
|
-
raise Errors::DSL::UnknownArgument.new(
|
29
|
+
def request(request_method)
|
30
|
+
unless definable_request_methods.include? request_method
|
31
|
+
raise Errors::DSL::UnknownArgument.new(request_method, 'request')
|
31
32
|
end
|
32
|
-
resource.
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def definable_request_methods
|
38
|
-
[:get, :post, :put, :patch, :delete].freeze
|
33
|
+
resource.register_request_handler(request_method, Proc.new)
|
39
34
|
end
|
40
35
|
end
|
41
36
|
end
|
data/lib/hyperdrive/dsl.rb
CHANGED
@@ -1,9 +1,55 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require 'hyperdrive/dsl/main'
|
4
3
|
require 'hyperdrive/dsl/resource'
|
5
4
|
|
5
|
+
module Hyperdrive
|
6
|
+
module DSL
|
7
|
+
include Values
|
8
|
+
extend self
|
9
|
+
attr_reader :config, :resources
|
10
|
+
|
11
|
+
def instance
|
12
|
+
@config ||= default_config.dup
|
13
|
+
@resources ||= {}
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def name(name)
|
20
|
+
@config[:name] = name
|
21
|
+
end
|
22
|
+
|
23
|
+
def description(description)
|
24
|
+
@config[:description] = description
|
25
|
+
end
|
26
|
+
|
27
|
+
def vendor(vendor)
|
28
|
+
@config[:vendor] = vendor
|
29
|
+
end
|
30
|
+
|
31
|
+
def media_types(media_types)
|
32
|
+
@config[:media_types] = media_types
|
33
|
+
end
|
34
|
+
|
35
|
+
def cors(options = {})
|
36
|
+
allowed_options = default_cors_options.keys
|
37
|
+
options = Utils.sanitize_keys(allowed_options, options)
|
38
|
+
@config[:cors] = config[:cors].merge(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def resource(name)
|
42
|
+
@resources[name] = Resource.new(name, @config, &Proc.new).resource
|
43
|
+
end
|
44
|
+
|
45
|
+
def reset! # not terribly useful outside of a test environment :(
|
46
|
+
@config = default_config.dup
|
47
|
+
@resources = {}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
6
52
|
def hyperdrive(&block)
|
7
|
-
Hyperdrive::DSL
|
8
|
-
Hyperdrive::DSL
|
53
|
+
Hyperdrive::DSL.instance.instance_eval(&block) if block_given?
|
54
|
+
Hyperdrive::DSL.instance
|
9
55
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class Endpoint
|
5
|
+
|
6
|
+
def self.call(env)
|
7
|
+
@env = env
|
8
|
+
@request = Rack::Request.new(@env)
|
9
|
+
@media_type = env['hyperdrive.media_type']
|
10
|
+
@params = env['hyperdrive.params']
|
11
|
+
@resource = env['hyperdrive.resource']
|
12
|
+
@headers = Hyperdrive::Values.default_headers.dup
|
13
|
+
@headers.merge!('Allow' => resource.allowed_methods, 'Content-Type' => @media_type)
|
14
|
+
|
15
|
+
response.finish
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_reader :env, :request, :media_type, :params, :resource
|
22
|
+
attr_accessor :headers
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.json?
|
26
|
+
media_type =~ /json$/
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.xml?
|
30
|
+
media_type =~ /xml$/
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.render(body)
|
34
|
+
case body
|
35
|
+
when Array, Hash
|
36
|
+
Oj.dump(body, mode: :compat) if json?
|
37
|
+
when String
|
38
|
+
body
|
39
|
+
else
|
40
|
+
body.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.status
|
45
|
+
case env['REQUEST_METHOD']
|
46
|
+
when 'POST'
|
47
|
+
201
|
48
|
+
when 'DELETE'
|
49
|
+
204
|
50
|
+
else
|
51
|
+
200
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.body
|
56
|
+
body = instance_eval(&resource.request_handler(env['REQUEST_METHOD']))
|
57
|
+
body = '' if env['REQUEST_METHOD'] == 'DELETE'
|
58
|
+
body
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.response
|
62
|
+
::Rack::Response.new(body, status, headers)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Errors
|
3
|
+
class MissingRequiredParam < HTTPError
|
4
|
+
attr_reader :param, :http_request_method
|
5
|
+
def initialize(param, http_request_method)
|
6
|
+
@param = param
|
7
|
+
@http_request_method = http_request_method
|
8
|
+
end
|
9
|
+
|
10
|
+
def message
|
11
|
+
"The #{param} param is required by #{http_request_method} requests."
|
12
|
+
end
|
13
|
+
|
14
|
+
def http_status_code
|
15
|
+
400
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Hyperdrive
|
2
|
+
module Errors
|
3
|
+
class NotAcceptable < HTTPError
|
4
|
+
def initialize(http_accept)
|
5
|
+
@http_accept = http_accept
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"This resource is not capable of generating content in the format requested by the Accept headers (#{@http_accept})."
|
10
|
+
end
|
11
|
+
|
12
|
+
def http_status_code
|
13
|
+
406
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
data/lib/hyperdrive/errors.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
|
4
|
-
require 'hyperdrive/errors/
|
5
|
-
|
6
|
-
|
7
|
-
require 'hyperdrive/errors/
|
8
|
-
require 'hyperdrive/errors/
|
9
|
-
require 'hyperdrive/errors/
|
10
|
-
require 'hyperdrive/errors/
|
3
|
+
# DSL Errors
|
4
|
+
require 'hyperdrive/errors/dsl/unknown_argument'
|
5
|
+
|
6
|
+
# HTTP Errors
|
7
|
+
require 'hyperdrive/errors/http_error' # 500 (Catch All)
|
8
|
+
require 'hyperdrive/errors/bad_request' # 400 (Generic)
|
9
|
+
require 'hyperdrive/errors/internal_server_error' # 500 (Generic)
|
10
|
+
require 'hyperdrive/errors/method_not_allowed' # 405
|
11
|
+
require 'hyperdrive/errors/missing_required_param' # 400 (Specific)
|
12
|
+
require 'hyperdrive/errors/not_acceptable' # 406
|
13
|
+
require 'hyperdrive/errors/not_found' # 404
|
14
|
+
require 'hyperdrive/errors/not_implemented' # 501
|
15
|
+
require 'hyperdrive/errors/unauthorized' # 401
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class Filter < Param
|
5
|
+
def initialize(name, description, options = {})
|
6
|
+
@name = name.to_s
|
7
|
+
@description = description
|
8
|
+
options = default_options.merge(options)
|
9
|
+
@required = if options[:required] == true
|
10
|
+
%w(GET HEAD)
|
11
|
+
elsif options[:required] == false
|
12
|
+
[]
|
13
|
+
else
|
14
|
+
Array(options[:required])
|
15
|
+
end
|
16
|
+
@type = options[:type]
|
17
|
+
@constraints = "#{required_constraint} #{options[:constraints]}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
class HATEOAS
|
5
|
+
extend Hyperdrive::Values
|
6
|
+
|
7
|
+
def self.call(env)
|
8
|
+
if hyperdrive.resources.empty? || env['PATH_INFO'] != '/'
|
9
|
+
raise Hyperdrive::Errors::NotFound
|
10
|
+
end
|
11
|
+
|
12
|
+
endpoints = hyperdrive.resources.map do |_,resource|
|
13
|
+
resource.to_hash
|
14
|
+
end
|
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
|
+
}
|
23
|
+
|
24
|
+
media_types = %w(hal+json json).map do |media_type|
|
25
|
+
"application/vnd.#{hyperdrive.config[:vendor]}+#{media_type}"
|
26
|
+
end
|
27
|
+
|
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
|
+
Oj.dump(api, mode: :compat)
|
32
|
+
else
|
33
|
+
raise Hyperdrive::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
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class Accept
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env['hyperdrive.accept'] = Rack::Accept::MediaType.new(env['HTTP_ACCEPT'])
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class ContentNegotiation
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
accept = env['hyperdrive.accept']
|
12
|
+
acceptable_content_types = env['hyperdrive.resource'].acceptable_content_types(env['REQUEST_METHOD'])
|
13
|
+
env['hyperdrive.media_type'] = accept.best_of(acceptable_content_types)
|
14
|
+
raise Hyperdrive::Errors::NotAcceptable.new(env['HTTP_ACCEPT']) unless env['hyperdrive.media_type']
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class CORS
|
6
|
+
def initialize(app, options = {})
|
7
|
+
@app = app
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
status, headers, body = @app.call(env)
|
13
|
+
allowed_methods = '*'
|
14
|
+
unless env['hyperdrive.resource'].nil?
|
15
|
+
allowed_methods = env['hyperdrive.resource'].allowed_methods
|
16
|
+
end
|
17
|
+
cors = cors_headers(allowed_methods)
|
18
|
+
headers.merge!(cors)
|
19
|
+
[status, headers, body]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def cors_headers(allowed_methods = '*')
|
25
|
+
{
|
26
|
+
'Access-Control-Allow-Origin' => Array(@options[:origins]).join(", "),
|
27
|
+
'Access-Control-Allow-Methods' => Array(allowed_methods).join(", "),
|
28
|
+
'Access-Control-Allow-Headers' => Array(@options[:allow_headers]).join(", "),
|
29
|
+
'Access-Control-Allow-Credentials' => (@options[:credentials] || false).to_s,
|
30
|
+
'Access-Control-Max-Age' => @options[:max_age].to_i.to_s,
|
31
|
+
'Access-Control-Expose-Headers' => Array(@options[:expose_headers]).join(", ")
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class Error
|
6
|
+
include Hyperdrive::Values
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
@app.call(env)
|
14
|
+
rescue Hyperdrive::Errors::HTTPError => e
|
15
|
+
status = e.http_status_code
|
16
|
+
headers = { 'Content-Type' => 'application/json',
|
17
|
+
'Allow' => supported_request_methods.join(', ') }
|
18
|
+
body = Oj.dump(error_message(e), mode: :compat)
|
19
|
+
[status, headers, [body]]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def error_message(e)
|
25
|
+
{
|
26
|
+
_links: { root: { href: '/', title: 'API Root' } },
|
27
|
+
error: {
|
28
|
+
type: "#{e.class.to_s.split('::').last}",
|
29
|
+
message: e.message
|
30
|
+
}
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class RequestMethod
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
http_request_method = env['REQUEST_METHOD']
|
12
|
+
|
13
|
+
unless request_method_supported?(http_request_method)
|
14
|
+
raise Hyperdrive::Errors::NotImplemented.new(http_request_method)
|
15
|
+
end
|
16
|
+
|
17
|
+
unless env['hyperdrive.resource'].request_method_allowed?(http_request_method)
|
18
|
+
raise Hyperdrive::Errors::MethodNotAllowed.new(http_request_method)
|
19
|
+
end
|
20
|
+
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def request_method_supported?(http_request_method)
|
27
|
+
Hyperdrive::Values.http_request_methods.key?(http_request_method)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class RequiredParams
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
@env = env
|
12
|
+
if %w(GET HEAD).include? env['REQUEST_METHOD']
|
13
|
+
check_required_params(env['hyperdrive.resource'].filters)
|
14
|
+
elsif %W(POST PUT PATCH DELETE).include? env['REQUEST_METHOD']
|
15
|
+
check_required_params(env['hyperdrive.resource'].params)
|
16
|
+
end
|
17
|
+
@app.call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_required_params(params)
|
21
|
+
params.each do |param, options|
|
22
|
+
if @env['hyperdrive.resource'].required?(param, @env['REQUEST_METHOD'])
|
23
|
+
if @env['hyperdrive.params'].key?(param)
|
24
|
+
if @env['hyperdrive.params'][param] == ''
|
25
|
+
raise Errors::MissingRequiredParam.new(param, @env['REQUEST_METHOD'])
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise Errors::MissingRequiredParam.new(param, @env['REQUEST_METHOD'])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Internal: Adds the requested resource to rack's env
|
4
|
+
module Hyperdrive
|
5
|
+
module Middleware
|
6
|
+
class Resource
|
7
|
+
def initialize(app, resource)
|
8
|
+
@app = app
|
9
|
+
@resource = resource
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
env['hyperdrive.resource'] = @resource
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Hyperdrive
|
4
|
+
module Middleware
|
5
|
+
class SanitizeParams
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
unless %w(OPTIONS TRACE).include? env['REQUEST_METHOD']
|
12
|
+
request = Rack::Request.new(env)
|
13
|
+
params = Hyperdrive::Utils.symbolize_keys(request.params)
|
14
|
+
if %w(GET HEAD).include? env['REQUEST_METHOD']
|
15
|
+
params_to_keep = env['hyperdrive.resource'].filters.keys
|
16
|
+
else
|
17
|
+
params_to_keep = env['hyperdrive.resource'].params.keys
|
18
|
+
end
|
19
|
+
env['hyperdrive.params'] = Hyperdrive::Utils.sanitize_keys(params_to_keep, params)
|
20
|
+
end
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'hyperdrive/middleware/accept'
|
4
|
+
require 'hyperdrive/middleware/cors'
|
5
|
+
require 'hyperdrive/middleware/content_negotiation'
|
6
|
+
require 'hyperdrive/middleware/error'
|
7
|
+
require 'hyperdrive/middleware/resource'
|
8
|
+
require 'hyperdrive/middleware/request_method'
|
9
|
+
require 'hyperdrive/middleware/required_params'
|
10
|
+
require 'hyperdrive/middleware/sanitize_params'
|