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