crepe 0.0.1.pre
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 +7 -0
- data/lib/crepe.rb +10 -0
- data/lib/crepe/api.rb +242 -0
- data/lib/crepe/endpoint.rb +173 -0
- data/lib/crepe/endpoint/filter.rb +10 -0
- data/lib/crepe/endpoint/filter/acceptance.rb +34 -0
- data/lib/crepe/endpoint/filter/parser.rb +43 -0
- data/lib/crepe/endpoint/renderer.rb +19 -0
- data/lib/crepe/endpoint/renderer/base.rb +95 -0
- data/lib/crepe/endpoint/renderer/simple.rb +21 -0
- data/lib/crepe/endpoint/renderer/tilt.rb +87 -0
- data/lib/crepe/endpoint/request.rb +59 -0
- data/lib/crepe/middleware.rb +9 -0
- data/lib/crepe/middleware/content_negotiation.rb +110 -0
- data/lib/crepe/middleware/head.rb +30 -0
- data/lib/crepe/middleware/restful_status.rb +33 -0
- data/lib/crepe/params.rb +74 -0
- data/lib/crepe/util.rb +73 -0
- data/lib/crepe/util/chained_include.rb +78 -0
- data/lib/crepe/util/hash_stack.rb +57 -0
- data/lib/crepe/version.rb +10 -0
- metadata +207 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module Crepe
|
4
|
+
class Endpoint
|
5
|
+
module Filter
|
6
|
+
# A default before filter that parses the body of an incoming request.
|
7
|
+
class Parser
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def filter endpoint
|
12
|
+
endpoint.instance_eval do
|
13
|
+
body = request.body
|
14
|
+
return if body.blank?
|
15
|
+
|
16
|
+
input = env['crepe.input'] = case request.content_type
|
17
|
+
when %r{application/json}
|
18
|
+
begin
|
19
|
+
MultiJson.load body
|
20
|
+
rescue MultiJson::DecodeError
|
21
|
+
error! :bad_request, "Invalid JSON"
|
22
|
+
end
|
23
|
+
when %r{application/xml}
|
24
|
+
begin
|
25
|
+
MultiXml.parse body
|
26
|
+
rescue MultiXml::ParseError
|
27
|
+
error! :bad_request, "Invalid XML"
|
28
|
+
end
|
29
|
+
else
|
30
|
+
error! :unsupported_media_type,
|
31
|
+
%(Content-type "#{request.content_type}" not supported)
|
32
|
+
end
|
33
|
+
|
34
|
+
@params = @params.merge input if input.is_a? Hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Crepe
|
2
|
+
class Endpoint
|
3
|
+
module Renderer
|
4
|
+
|
5
|
+
# A RenderError can be used to indicate that rendering has failed for
|
6
|
+
# some reason. More specific errors in a renderer should inherit from
|
7
|
+
# this class so that a Crepe::API class can rescue all errors within
|
8
|
+
# rendering by rescuing Endpoint::Renderer::RenderError.
|
9
|
+
class RenderError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
autoload :Base, 'crepe/endpoint/renderer/base'
|
13
|
+
autoload :Simple, 'crepe/endpoint/renderer/simple'
|
14
|
+
autoload :Tilt, 'crepe/endpoint/renderer/tilt'
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
require 'active_support/core_ext/object/to_query'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Crepe
|
7
|
+
class Endpoint
|
8
|
+
module Renderer
|
9
|
+
# A base renderer class that sets pagination headers.
|
10
|
+
class Base
|
11
|
+
|
12
|
+
# Generates pagination links based on provided page, limit, and total.
|
13
|
+
class Links < Struct.new :page, :per_page, :count
|
14
|
+
|
15
|
+
def render request
|
16
|
+
uri = URI request.url
|
17
|
+
params = request.query_parameters.except 'page'
|
18
|
+
|
19
|
+
links = {
|
20
|
+
first: first, prev: prev, next: self.next, last: last
|
21
|
+
}
|
22
|
+
links = links.map do |rel, query|
|
23
|
+
next unless query
|
24
|
+
%(<#{uri + "?#{params.merge(query).to_query}"}>; rel="#{rel}")
|
25
|
+
end
|
26
|
+
|
27
|
+
links.compact.join ', '
|
28
|
+
end
|
29
|
+
|
30
|
+
def first
|
31
|
+
return if page == 1
|
32
|
+
{} # page=1
|
33
|
+
end
|
34
|
+
|
35
|
+
def prev
|
36
|
+
return if page == 1
|
37
|
+
prev = page.pred
|
38
|
+
prev > 1 ? { page: prev } : {} # page=1
|
39
|
+
end
|
40
|
+
|
41
|
+
def next
|
42
|
+
if count.nil? || page * per_page < count
|
43
|
+
{ page: page.next } unless page == last[:page]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def last
|
48
|
+
last = (count.to_f / per_page).ceil
|
49
|
+
{ page: last } unless page == last
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
PER_PAGE = 20
|
55
|
+
|
56
|
+
attr_reader :endpoint
|
57
|
+
|
58
|
+
def initialize endpoint
|
59
|
+
@endpoint = endpoint
|
60
|
+
end
|
61
|
+
|
62
|
+
def render resource, options = {}
|
63
|
+
if resource.respond_to? :paginate
|
64
|
+
count = resource.count if resource.respond_to? :count
|
65
|
+
endpoint.headers['Count'] = count.to_s if count
|
66
|
+
|
67
|
+
params = endpoint.params.slice :page, :per_page
|
68
|
+
page = validate_param params, :page, 1
|
69
|
+
per_page = resource.per_page if resource.respond_to? :per_page
|
70
|
+
per_page = validate_param params, :per_page, per_page || PER_PAGE
|
71
|
+
links = Links.new page, per_page, count
|
72
|
+
endpoint.headers['Link'] = links.render endpoint.request
|
73
|
+
|
74
|
+
resource = resource.paginate params
|
75
|
+
end
|
76
|
+
|
77
|
+
throw :head if endpoint.request.head?
|
78
|
+
|
79
|
+
resource
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def validate_param params, name, default
|
85
|
+
value = Integer params.fetch(name, default.to_s), 10
|
86
|
+
raise ArgumentError if value < 1
|
87
|
+
value
|
88
|
+
rescue ArgumentError
|
89
|
+
endpoint.error! 400, "Invalid value #{params[name]} for #{name}"
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Crepe
|
2
|
+
class Endpoint
|
3
|
+
module Renderer
|
4
|
+
# The simplest renderer delegates rendering to the resource itself.
|
5
|
+
class Simple < Base
|
6
|
+
|
7
|
+
def render resource, options = {}
|
8
|
+
resource = super
|
9
|
+
format = options.fetch :format, endpoint.format
|
10
|
+
|
11
|
+
if resource.respond_to? "to_#{format}"
|
12
|
+
resource.__send__("to_#{format}")
|
13
|
+
else
|
14
|
+
resource.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Crepe
|
2
|
+
class Endpoint
|
3
|
+
module Renderer
|
4
|
+
# Sends a resource and template to [Tilt][] for rendering, falling back
|
5
|
+
# to {Renderer::Simple} if no template name is provided (or can be
|
6
|
+
# derived). Template names are derived by the resource class's ability to
|
7
|
+
# return a {.model_name}.
|
8
|
+
#
|
9
|
+
# [Tilt]: https://github.com/rtomayko/tilt
|
10
|
+
class Tilt < Base
|
11
|
+
|
12
|
+
# Raised when a template name is derived but cannot be found in the
|
13
|
+
# template path.
|
14
|
+
class MissingTemplate < RenderError
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
def configure
|
20
|
+
yield self
|
21
|
+
end
|
22
|
+
|
23
|
+
def template_path
|
24
|
+
@template_path ||= 'app/views'
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_writer :template_path
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def render resource, options = {}
|
32
|
+
resource = super
|
33
|
+
|
34
|
+
format = options.fetch :format, endpoint.format
|
35
|
+
handlers = options.fetch :handlers, [:rabl, :erb, :*]
|
36
|
+
template_name = options.fetch :template, model_name(resource)
|
37
|
+
|
38
|
+
unless template_name
|
39
|
+
return Simple.new(endpoint).render resource, options
|
40
|
+
end
|
41
|
+
|
42
|
+
path_options = { format: format, handlers: handlers }
|
43
|
+
unless template = find_template(template_name, path_options)
|
44
|
+
raise MissingTemplate,
|
45
|
+
"Missing template #{template_name} with #{path_options}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# FIXME: this is only needed for Rabl, which doesn't support Tilt
|
49
|
+
# locals properly. Can probably move into a Renderer::Rabl.
|
50
|
+
endpoint.instance_variable_set :"@#{template_name}", resource
|
51
|
+
endpoint.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
52
|
+
attr_reader :#{template_name}
|
53
|
+
RUBY
|
54
|
+
|
55
|
+
template.render endpoint
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def model_name resource
|
61
|
+
if resource.respond_to? :model_name
|
62
|
+
resource.model_name.tableize
|
63
|
+
elsif resource.class.respond_to? :model_name
|
64
|
+
resource.class.model_name.underscore
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_template relative_path, path_options
|
69
|
+
path_query = File.join self.class.template_path, relative_path
|
70
|
+
|
71
|
+
format, handlers = path_options.values
|
72
|
+
path_query << '.{%{format}.{%{handlers}},{%{handlers}}}' % {
|
73
|
+
format: format, handlers: handlers.join(',')
|
74
|
+
}
|
75
|
+
|
76
|
+
template_path = Dir[path_query].reject { |path|
|
77
|
+
ext = File.basename(path).split('.').last
|
78
|
+
File.directory?(path) || ::Tilt.mappings[ext].nil?
|
79
|
+
}.first
|
80
|
+
|
81
|
+
template_path && ::Tilt.new(template_path)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
|
3
|
+
module Crepe
|
4
|
+
class Endpoint
|
5
|
+
# A thin wrapper over {Rack::Request} that provides helper methods to
|
6
|
+
# better access request attributes.
|
7
|
+
class Request < Rack::Request
|
8
|
+
|
9
|
+
@@env_keys = Hash.new { |h, k| h[k] = "HTTP_#{k.upcase.tr '-', '_'}" }
|
10
|
+
|
11
|
+
def method
|
12
|
+
@method ||= env['crepe.original_request_method'] || request_method
|
13
|
+
end
|
14
|
+
|
15
|
+
def head?
|
16
|
+
method == 'HEAD'
|
17
|
+
end
|
18
|
+
|
19
|
+
def path
|
20
|
+
@path ||= Util.normalize_path! super
|
21
|
+
end
|
22
|
+
|
23
|
+
def headers
|
24
|
+
@headers ||= Hash.new { |h, k| h.fetch @@env_keys[k], nil }.update env
|
25
|
+
end
|
26
|
+
|
27
|
+
alias query_parameters GET
|
28
|
+
|
29
|
+
def POST
|
30
|
+
env['crepe.input'] || super
|
31
|
+
end
|
32
|
+
alias request_parameters POST
|
33
|
+
|
34
|
+
def path_parameters
|
35
|
+
@path_parameters ||= env['rack.routing_args'] || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def parameters
|
39
|
+
@parameters ||= path_parameters.merge self.GET.merge self.POST
|
40
|
+
end
|
41
|
+
alias params parameters
|
42
|
+
|
43
|
+
def body
|
44
|
+
env['crepe.input'] || begin
|
45
|
+
body = super
|
46
|
+
body.respond_to?(:read) ? body.read : body
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def credentials
|
51
|
+
@credentials ||= begin
|
52
|
+
request = Rack::Auth::Basic::Request.new env
|
53
|
+
request.provided? ? request.credentials : []
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'active_support/core_ext/object/to_query'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'crepe/util'
|
4
|
+
|
5
|
+
module Crepe
|
6
|
+
module Middleware
|
7
|
+
# Negotiates API content type and version from an Accept header.
|
8
|
+
#
|
9
|
+
# Given an Accept header with a vendor-specific mime type, it will
|
10
|
+
# transform the Rack environment: prefixing a version and postfixing
|
11
|
+
# an extension to the path, and removing the vendor-specific parts of the
|
12
|
+
# Accept header.
|
13
|
+
#
|
14
|
+
# E.g., the following request:
|
15
|
+
#
|
16
|
+
# GET /users
|
17
|
+
# Accept: application/vnd.crepe-v2+json
|
18
|
+
#
|
19
|
+
# Will pass through to the next middleware as this:
|
20
|
+
#
|
21
|
+
# GET /v2/users.json
|
22
|
+
# Accept: application/json
|
23
|
+
#
|
24
|
+
# The vendor name is stored as <tt>crepe.vendor</tt> in the Rack
|
25
|
+
# environment.
|
26
|
+
#
|
27
|
+
# env['crepe.vendor'] # => "crepe"
|
28
|
+
#--
|
29
|
+
# TODO: Support Accept headers with multiple mime types:
|
30
|
+
#
|
31
|
+
# Accept: application/vnd.crepe-v2+xml, application/vnd.crepe-v1+xml;q=0.7
|
32
|
+
#
|
33
|
+
# XXX: Should the env be modified more? Should we store version
|
34
|
+
# somewhere? As is, this middleware depends heavily on Crepe and
|
35
|
+
# Rack::Mount to be useful.
|
36
|
+
#++
|
37
|
+
class ContentNegotiation
|
38
|
+
|
39
|
+
# Matches an `type`, `vendor`, `version`, and `format` (subtype) given
|
40
|
+
# an accept header.
|
41
|
+
ACCEPT_HEADER = %r{
|
42
|
+
(?<type>[^/;,\s]+)
|
43
|
+
/
|
44
|
+
(?:
|
45
|
+
(?:
|
46
|
+
(?:vnd\.)(?<vendor>[^/;,\s\.+-]+)
|
47
|
+
(?:-(?<version>[^/;,\s\.+-]+))?
|
48
|
+
(?:\+(?<format>[^/;,\s\.+-]+))?
|
49
|
+
)
|
50
|
+
|
|
51
|
+
(?<format>[^/;,\s\.+]+)
|
52
|
+
)
|
53
|
+
}ix
|
54
|
+
|
55
|
+
MIME_TYPES = {
|
56
|
+
'application/json' => :json,
|
57
|
+
'application/pdf' => :pdf,
|
58
|
+
'application/xml' => :xml,
|
59
|
+
'text/html' => :html,
|
60
|
+
'text/plain' => :txt
|
61
|
+
}
|
62
|
+
|
63
|
+
def initialize app
|
64
|
+
@app = app
|
65
|
+
end
|
66
|
+
|
67
|
+
def call env
|
68
|
+
accept = ACCEPT_HEADER.match(env['HTTP_ACCEPT']) || {}
|
69
|
+
path = env['crepe.original_path_info'] = env['PATH_INFO']
|
70
|
+
|
71
|
+
env['crepe.vendor'] = accept[:vendor] if accept[:vendor]
|
72
|
+
|
73
|
+
version = accept[:version] || query_string_version(env)
|
74
|
+
if version && !path.start_with?("/#{version}")
|
75
|
+
path = ::File.join '/', version, path
|
76
|
+
end
|
77
|
+
|
78
|
+
if accept[:format]
|
79
|
+
env['crepe.original_http_accept'] = env['HTTP_ACCEPT'].dup
|
80
|
+
content_type = [accept[:type], accept[:format]].join '/'
|
81
|
+
|
82
|
+
env['HTTP_ACCEPT'][ACCEPT_HEADER] = content_type
|
83
|
+
extension = MIME_TYPES.fetch content_type, accept[:format]
|
84
|
+
|
85
|
+
if ::File.extname(path) != ".#{extension}"
|
86
|
+
path += ".#{extension}" unless extension == '*'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
env['PATH_INFO'] = Util.normalize_path path
|
91
|
+
|
92
|
+
@app.call env
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def query_string_version env
|
98
|
+
env['crepe.original_query_string'] = env['QUERY_STRING']
|
99
|
+
query = Rack::Utils.parse_nested_query env['QUERY_STRING']
|
100
|
+
|
101
|
+
version = query.delete('v')
|
102
|
+
if version
|
103
|
+
env['QUERY_STRING'] = query.to_query
|
104
|
+
version
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|