crepe 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ module Crepe
2
+ module Middleware
3
+
4
+ autoload :ContentNegotiation, 'crepe/middleware/content_negotiation'
5
+ autoload :Head, 'crepe/middleware/head'
6
+ autoload :RestfulStatus, 'crepe/middleware/restful_status'
7
+
8
+ end
9
+ 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