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.
@@ -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