doze 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/README +6 -0
- data/lib/doze/application.rb +92 -0
- data/lib/doze/collection/object.rb +14 -0
- data/lib/doze/entity.rb +62 -0
- data/lib/doze/error.rb +75 -0
- data/lib/doze/media_type.rb +135 -0
- data/lib/doze/negotiator.rb +107 -0
- data/lib/doze/request.rb +119 -0
- data/lib/doze/resource/error.rb +21 -0
- data/lib/doze/resource/proxy.rb +81 -0
- data/lib/doze/resource.rb +193 -0
- data/lib/doze/responder/error.rb +34 -0
- data/lib/doze/responder/main.rb +41 -0
- data/lib/doze/responder/resource.rb +262 -0
- data/lib/doze/responder.rb +58 -0
- data/lib/doze/response.rb +78 -0
- data/lib/doze/router/anchored_route_set.rb +68 -0
- data/lib/doze/router/route.rb +88 -0
- data/lib/doze/router/route_set.rb +34 -0
- data/lib/doze/router.rb +100 -0
- data/lib/doze/serialization/entity.rb +34 -0
- data/lib/doze/serialization/form_data_helpers.rb +40 -0
- data/lib/doze/serialization/html.rb +116 -0
- data/lib/doze/serialization/json.rb +29 -0
- data/lib/doze/serialization/multipart_form_data.rb +162 -0
- data/lib/doze/serialization/resource.rb +30 -0
- data/lib/doze/serialization/resource_proxy.rb +14 -0
- data/lib/doze/serialization/www_form_encoded.rb +42 -0
- data/lib/doze/serialization/yaml.rb +25 -0
- data/lib/doze/uri_template.rb +220 -0
- data/lib/doze/utils.rb +53 -0
- data/lib/doze/version.rb +3 -0
- data/lib/doze.rb +5 -0
- data/test/functional/auth_test.rb +69 -0
- data/test/functional/base.rb +159 -0
- data/test/functional/cache_header_test.rb +76 -0
- data/test/functional/direct_response_test.rb +16 -0
- data/test/functional/error_handling_test.rb +131 -0
- data/test/functional/get_and_conneg_test.rb +182 -0
- data/test/functional/media_type_extensions_test.rb +102 -0
- data/test/functional/media_type_test.rb +40 -0
- data/test/functional/method_support_test.rb +49 -0
- data/test/functional/non_get_method_test.rb +173 -0
- data/test/functional/precondition_test.rb +84 -0
- data/test/functional/raw_path_info_test.rb +69 -0
- data/test/functional/resource_representation_test.rb +14 -0
- data/test/functional/router_test.rb +196 -0
- data/test/functional/serialization_test.rb +142 -0
- data/test/functional/uri_template_test.rb +51 -0
- metadata +221 -0
data/README
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'time' # httpdate
|
2
|
+
require 'doze/utils'
|
3
|
+
require 'doze/error'
|
4
|
+
require 'doze/uri_template'
|
5
|
+
require 'doze/request'
|
6
|
+
require 'doze/router'
|
7
|
+
require 'doze/resource'
|
8
|
+
require 'doze/entity'
|
9
|
+
require 'doze/resource/error'
|
10
|
+
require 'doze/resource/proxy'
|
11
|
+
require 'doze/request'
|
12
|
+
require 'doze/response'
|
13
|
+
require 'doze/responder'
|
14
|
+
require 'doze/responder/main'
|
15
|
+
require 'doze/responder/error'
|
16
|
+
require 'doze/responder/resource'
|
17
|
+
require 'doze/negotiator'
|
18
|
+
|
19
|
+
class Doze::Application
|
20
|
+
include Doze::Utils
|
21
|
+
|
22
|
+
DEFAULT_CONFIG = {
|
23
|
+
:error_resource_class => Doze::Resource::Error,
|
24
|
+
|
25
|
+
# Setting this to false is useful for testing, so an exception can make a test fail via
|
26
|
+
# the normal channels rather than having to check and parse it out of a response.
|
27
|
+
:catch_application_errors => true,
|
28
|
+
|
29
|
+
# useful for development
|
30
|
+
:expose_exception_details => true,
|
31
|
+
|
32
|
+
# Methods not included here will be rejected with 'method not implemented'
|
33
|
+
# before any resource is called. (methods included here may still be rejected
|
34
|
+
# as not supported by individual resources via supports_method).
|
35
|
+
# Note: HEAD is supported as part of GET support, and OPTIONS comes for free.
|
36
|
+
:recognized_methods => [:get, :post, :put, :delete],
|
37
|
+
|
38
|
+
# You might need to change this depending on what rack middleware you use to
|
39
|
+
# authenticate / identify users. Eg could use
|
40
|
+
# env['rack.session'] for use with Rack::Session (the default)
|
41
|
+
# env['REMOTE_USER'] for use with Rack::Auth::Basic / Digest, and direct via Apache and some other front-ends that do http auth
|
42
|
+
# env['rack.auth.openid'] for use with Rack::Auth::OpenID
|
43
|
+
# This is used to look up a session or user object in the rack environment
|
44
|
+
:session_from_rack_env => proc {|env| env['rack.session']},
|
45
|
+
|
46
|
+
# Used to determine whether the user/session object obtained via :session_from_rack_env is to be treated as authenticated or not.
|
47
|
+
# By default this looks for a key :user within a session object.
|
48
|
+
# For eg env['REMOTE_USER'] the session is the authenticated username and you probably just want to test for !session.nil?
|
49
|
+
:session_authenticated => proc {|session| !session[:user].nil?},
|
50
|
+
|
51
|
+
# Doze has a special facility to use a file extension style suffix on the URI.
|
52
|
+
# Instead of passing this file extension through the routing process, it is dropped from the routed URI
|
53
|
+
# and handled specially, effectively overriding the Accept header for that request and forcing a response
|
54
|
+
# with the media type in question.
|
55
|
+
# Relies on media types being registered by extension, see Doze::MediaType.
|
56
|
+
# NB: if you enable this setting, be aware that extensions are stripped prior to routing, so you will
|
57
|
+
# lose the ability to route based on file extensions, and must be careful to escape the extension delimiter (".")
|
58
|
+
# when putting a text fragment for matching at the end of a uri.
|
59
|
+
:media_type_extensions => false
|
60
|
+
}
|
61
|
+
|
62
|
+
attr_reader :config, :root, :logger
|
63
|
+
|
64
|
+
# root may be a Router, a Resource, or both.
|
65
|
+
# If a resource, its uri should return '/'
|
66
|
+
def initialize(root, config={})
|
67
|
+
@config = DEFAULT_CONFIG.merge(config)
|
68
|
+
@logger = @config[:logger] || STDOUT
|
69
|
+
@root = root
|
70
|
+
|
71
|
+
# This is done on application initialization to ensure that statically-known
|
72
|
+
# information about routing paths is propagated as far as possible, so that
|
73
|
+
# resource instances can know their uris without necessarily having been
|
74
|
+
# a part of the routing chain for the current request.
|
75
|
+
# TODO use SCRIPT_NAME from the rack env of the first request we get, rather than ''
|
76
|
+
@root.propagate_static_routes('') if @root.respond_to?(:propagate_static_routes)
|
77
|
+
end
|
78
|
+
|
79
|
+
def call(env)
|
80
|
+
begin
|
81
|
+
request = Doze::Request.new(self, env)
|
82
|
+
responder = Doze::Responder::Main.new(self, request)
|
83
|
+
responder.call
|
84
|
+
rescue => exception
|
85
|
+
raise unless config[:catch_application_errors]
|
86
|
+
lines = ['500 response via error resource failed', "#{exception.class}: #{exception.message}", *exception.backtrace]
|
87
|
+
@logger << lines.join("\n")
|
88
|
+
body = config[:expose_exception_details] ? lines : [lines.first]
|
89
|
+
[STATUS_INTERNAL_SERVER_ERROR, {}, [body.join("\n")]]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/doze/entity.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
# A simple wrapper class for an entity, which is essentially a lump of binary data
|
4
|
+
# together with some metadata about it, most importantly a MediaType, but also
|
5
|
+
# potentially a character encoding and a content language.
|
6
|
+
#
|
7
|
+
# A key feature is that the binary data may be specified lazily via a block.
|
8
|
+
# This is so that content negotiation can demand the data only once it's decided
|
9
|
+
# which (if any) of many proferred entities it wants to respond with.
|
10
|
+
#
|
11
|
+
# TODO: handle character encodings here in a nicer 1.9-compatible way
|
12
|
+
# TODO: maybe allow a stream for lazy_binary_data too
|
13
|
+
class Doze::Entity
|
14
|
+
DEFAULT_TEXT_ENCODING = 'iso-8859-1'
|
15
|
+
|
16
|
+
attr_reader :media_type, :media_type_params, :encoding, :language, :binary_data_length
|
17
|
+
|
18
|
+
# Used when constructing a HTTP response from this entity
|
19
|
+
# For when you need to specify extra entity-content-specific HTTP headers to be included when
|
20
|
+
# responding with this entity.
|
21
|
+
# A teensy bit of an abstraction leak, but helpful for awkward cases.
|
22
|
+
attr_reader :extra_content_headers
|
23
|
+
|
24
|
+
class << self; alias :new_from_binary_data :new; end
|
25
|
+
|
26
|
+
def initialize(media_type, options={}, &lazy_binary_data)
|
27
|
+
@binary_data = options[:binary_data]
|
28
|
+
@binary_data_stream = options[:binary_data_stream]
|
29
|
+
@binary_data_length = options[:binary_data_length]
|
30
|
+
@lazy_binary_data = options[:lazy_binary_data] || lazy_binary_data
|
31
|
+
|
32
|
+
@media_type = media_type
|
33
|
+
@media_type_params = options[:media_type_params] || {}
|
34
|
+
@encoding = options[:encoding] || (DEFAULT_TEXT_ENCODING if @media_type.major == 'text')
|
35
|
+
@language = options[:language]
|
36
|
+
@extra_content_headers = options[:extra_content_headers] || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def binary_data_stream
|
40
|
+
@binary_data_stream ||= if @binary_data
|
41
|
+
StringIO.new(@binary_data)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def binary_data
|
46
|
+
@binary_data ||= if @lazy_binary_data
|
47
|
+
@lazy_binary_data.call
|
48
|
+
elsif @binary_data_stream
|
49
|
+
@binary_data_stream.rewind if @binary_data_stream.respond_to?(:rewind)
|
50
|
+
@binary_data_stream.read
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This is a 'strong' etag in that it's sensitive to the exact bytes of the entity.
|
55
|
+
# Note that etags are per-entity, not per-resource. (even weak etags, which we don't yet support;
|
56
|
+
# 'weak' appears to refer to time-based equivalence for the same entity, rather than equivalence of all entity representations of a resource.)
|
57
|
+
#
|
58
|
+
# May return nil. Default implementation is an MD5 digest of the entity data.
|
59
|
+
def etag
|
60
|
+
Digest::MD5.hexdigest(binary_data)
|
61
|
+
end
|
62
|
+
end
|
data/lib/doze/error.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Doze::Error wraps the data required to send an HTTP error response as an exception which Application and Responder infrastructure can raise.
|
2
|
+
class Doze::Error < StandardError
|
3
|
+
def initialize(http_status=Doze::Utils::STATUS_INTERNAL_SERVER_ERROR, message='', headers={}, backtrace=nil)
|
4
|
+
@http_status = http_status
|
5
|
+
@headers = headers
|
6
|
+
@backtrace = backtrace
|
7
|
+
super(Rack::Utils::HTTP_STATUS_CODES[http_status] + (message ? ": #{message}" : ''))
|
8
|
+
end
|
9
|
+
|
10
|
+
def backtrace
|
11
|
+
@backtrace || super
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :http_status, :headers
|
15
|
+
end
|
16
|
+
|
17
|
+
# Errors intended to be raised within resource or entity code to indicate a client error.
|
18
|
+
# Currently this is a subclass of the internally-used Doze::Error class, but could
|
19
|
+
# equally be a separate exception class intended for Resource-level use which is caught and
|
20
|
+
# re-raised by the internal code.
|
21
|
+
class Doze::ClientError < Doze::Error; end
|
22
|
+
|
23
|
+
# An error parsing a submitted Entity representation. Should typically only be raised within Entity code
|
24
|
+
class Doze::ClientEntityError < Doze::ClientError
|
25
|
+
def initialize(message=nil)
|
26
|
+
super(Doze::Utils::STATUS_BAD_REQUEST, message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Unbeknownst at the time of routing, the resource is not actually there.
|
31
|
+
class Doze::ResourceNotFoundError < Doze::ClientError
|
32
|
+
def initialize(message=nil)
|
33
|
+
super(Doze::Utils::STATUS_NOT_FOUND, message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Can be used for any error at the resource level which is caused by client error.
|
38
|
+
# Should relate to a problem processing the resource-level semantics of a request,
|
39
|
+
# rather than a syntactic error in a submitted entity representation.
|
40
|
+
# see http://tools.ietf.org/html/rfc4918#section-11.2
|
41
|
+
class Doze::ClientResourceError < Doze::ClientError
|
42
|
+
def initialize(message=nil)
|
43
|
+
super(Doze::Utils::STATUS_UNPROCESSABLE_ENTITY, message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Can be used if you want to deny an action, but you couldn't do it at the time
|
48
|
+
# of routing (which you could have done with Router#authorize_routing) or if you
|
49
|
+
# want to include an error reason with the response
|
50
|
+
class Doze::UnauthorizedError < Doze::ClientError
|
51
|
+
def initialize(reason='unauthorized')
|
52
|
+
super(Doze::Utils::STATUS_UNAUTHORIZED, reason)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
class Doze::ForbiddenError < Doze::ClientError
|
56
|
+
def initialize(reason='forbidden')
|
57
|
+
super(Doze::Utils::STATUS_FORBIDDEN, reason)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# You can raise this if there is some problem internally that can't be handled
|
62
|
+
# by the resource
|
63
|
+
class Doze::ServerError < Doze::Error; end
|
64
|
+
|
65
|
+
# The resource might exist, but for some reason the requested operation
|
66
|
+
# can not be performed at this moment in time. This type of error would
|
67
|
+
# indicate that this is a temporary situation and is due, like the error says,
|
68
|
+
# to the resource, or some dependency of it, being unavailable.
|
69
|
+
# This translates to a 503, innit.
|
70
|
+
class Doze::ResourceUnavailableError < Doze::ServerError
|
71
|
+
def initialize(message=nil)
|
72
|
+
super(Doze::Utils::STATUS_SERVICE_UNAVAILABLE, message)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
class Doze::MediaType
|
2
|
+
NAME_LOOKUP = {}
|
3
|
+
BY_EXTENSION = {}
|
4
|
+
|
5
|
+
# Names for this media type.
|
6
|
+
# Names should uniquely identify the media type, so eg [audio/x-mpeg3, audio/mp3] might both be names of one
|
7
|
+
# media type, but application/xml is not a name of application/xhtml+xml; see matches_names
|
8
|
+
attr_reader :names
|
9
|
+
|
10
|
+
# The primary name for this media type.
|
11
|
+
def name; @names.first; end
|
12
|
+
|
13
|
+
# Media type strings which this media type matches.
|
14
|
+
# Matching means this media type is acceptable in reponse to a request for the media type string in question.
|
15
|
+
# Eg application/xhtml+xml is acceptable in response to a request for application/xml or text/xml,
|
16
|
+
# so text/xml and application/xml may be listed under the matches_names of application/xhtml+xml
|
17
|
+
attr_reader :matches_names
|
18
|
+
|
19
|
+
# The name used to describe this media type to clients (sometimes we want to use a more
|
20
|
+
# detailed media type internally). Defaults to name
|
21
|
+
def output_name
|
22
|
+
@output_name || @names.first
|
23
|
+
end
|
24
|
+
|
25
|
+
# Media types may be configured to use a different entity class to the default (Doze::Entity) for an
|
26
|
+
# entity of that media type
|
27
|
+
attr_reader :entity_class
|
28
|
+
|
29
|
+
# Some serialization media types have a plus suffix which can be used to create derived types, eg
|
30
|
+
# application/xml, with plus_suffix 'xml', could have application/xhtml+xml as a derived type
|
31
|
+
# see register_derived_type
|
32
|
+
attr_reader :plus_suffix
|
33
|
+
|
34
|
+
# Media type may be associated with a particular file extension, eg image/jpeg with .jpeg
|
35
|
+
# Registered media types may be looked up by extension, eg this is used when :media_type_extensions
|
36
|
+
# is enabled on the application.
|
37
|
+
#
|
38
|
+
# If you register more than one media type with the same extension the most recent one will
|
39
|
+
# take priority, ie probably best not to do this.
|
40
|
+
attr_reader :extension
|
41
|
+
|
42
|
+
# Creates and registers a media_type instance by its names for lookup via [].
|
43
|
+
# This means this instance will be used when a client submits an entity with any of the given
|
44
|
+
# names.
|
45
|
+
# You're recommended to register any media types that are frequently used as well,
|
46
|
+
# even if you don't need any special options or methods for them.
|
47
|
+
def self.register(name, options={})
|
48
|
+
new(name, options).register!
|
49
|
+
end
|
50
|
+
|
51
|
+
def register!
|
52
|
+
names.each do |n|
|
53
|
+
raise "Attempt to register media_type name #{n} twice" if NAME_LOOKUP.has_key?(n)
|
54
|
+
NAME_LOOKUP[n] = self
|
55
|
+
end
|
56
|
+
register_extension!
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def register_extension!
|
61
|
+
BY_EXTENSION[@extension] = self if @extension
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.[](name)
|
65
|
+
NAME_LOOKUP[name] || new(name)
|
66
|
+
end
|
67
|
+
|
68
|
+
# name: primary name for the media type
|
69
|
+
# options:
|
70
|
+
# :aliases :: extra names to add to #names
|
71
|
+
# :output_name :: defaults to name
|
72
|
+
# :also_matches :: extra names to add to matches_names, in addition to names and output_name
|
73
|
+
# :entity_class
|
74
|
+
# :plus_suffix
|
75
|
+
# :extension
|
76
|
+
def initialize(name, options={})
|
77
|
+
@names = [name]
|
78
|
+
@names.push(*options[:aliases]) if options[:aliases]
|
79
|
+
|
80
|
+
@output_name = options[:output_name]
|
81
|
+
|
82
|
+
@matches_names = @names.dup
|
83
|
+
@matches_names << @output_name if @output_name
|
84
|
+
@matches_names.push(*options[:also_matches]) if options[:also_matches]
|
85
|
+
@matches_names.uniq!
|
86
|
+
|
87
|
+
@entity_class = options[:entity_class] || Doze::Entity
|
88
|
+
@plus_suffix = options[:plus_suffix]
|
89
|
+
|
90
|
+
@extension = options[:extension]
|
91
|
+
end
|
92
|
+
|
93
|
+
def major
|
94
|
+
@major ||= name.split('/', 2)[0]
|
95
|
+
end
|
96
|
+
|
97
|
+
def minor
|
98
|
+
@major ||= name.split('/', 2)[1]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Helper to derive eg application/vnd.foo+json from application/json and name_prefix application/vnd.foo
|
102
|
+
def register_derived_type(name_prefix, options={})
|
103
|
+
options = {
|
104
|
+
:also_matches => [],
|
105
|
+
:entity_class => @entity_class
|
106
|
+
}.merge!(options)
|
107
|
+
options[:also_matches].push(*self.matches_names)
|
108
|
+
name = @plus_suffix ? "#{name_prefix}+#{plus_suffix}" : name_prefix
|
109
|
+
self.class.register(name, options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Create a new entity of this media_type. Uses entity_class
|
113
|
+
def new_entity(options, &b)
|
114
|
+
@entity_class.new(self, options, &b)
|
115
|
+
end
|
116
|
+
|
117
|
+
def subtype?(other)
|
118
|
+
@matches_names.include?(other.name)
|
119
|
+
end
|
120
|
+
|
121
|
+
def matches_prefix?(prefix)
|
122
|
+
@matches_names.any? {|name| name.start_with?(prefix)}
|
123
|
+
end
|
124
|
+
|
125
|
+
# Equality override to help in case multiple temporary instances of a media type of a given name are compared.
|
126
|
+
def ==(other)
|
127
|
+
super || (other.is_a?(Doze::MediaType) && other.name == name)
|
128
|
+
end
|
129
|
+
|
130
|
+
alias :eql? :==
|
131
|
+
|
132
|
+
def hash
|
133
|
+
name.hash
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# A Negotiator handles content negotiation on behalf of a client request.
|
2
|
+
# It will choose the entity it prefers from a list of options offered to it.
|
3
|
+
# You can ask it to give you a quality value, or to choose from a list of options.
|
4
|
+
# It'll choose from media_types, languages, or combinations of the two.
|
5
|
+
class Doze::Negotiator
|
6
|
+
def initialize(request, ignore_unacceptable_accepts=false)
|
7
|
+
@ignore_unacceptable_accepts = ignore_unacceptable_accepts
|
8
|
+
|
9
|
+
@media_type_criterea = if (ext = request.extension)
|
10
|
+
# if the request extension requests a specific media type, this overrides any Accept header and is
|
11
|
+
# interpreted as a demand for this media type and this one only.
|
12
|
+
media_type = Doze::MediaType::BY_EXTENSION[ext]
|
13
|
+
if media_type
|
14
|
+
[[media_type.name, 2, 1.0]]
|
15
|
+
else
|
16
|
+
# if there's a request extension but we can't find a media type for it, we interpret this as an
|
17
|
+
# 'impossible demand' that will match nothing
|
18
|
+
[]
|
19
|
+
end
|
20
|
+
|
21
|
+
elsif (accept_header = request.env['HTTP_ACCEPT'])
|
22
|
+
parse_accept_header(accept_header) {|range| matcher_from_media_range(range)}.sort_by {|matcher,specificity,q| -specificity}
|
23
|
+
|
24
|
+
else
|
25
|
+
# No Accept header - anything will do
|
26
|
+
[[Object, 0, 1.0]]
|
27
|
+
end
|
28
|
+
|
29
|
+
accept_language_header = request.env['HTTP_ACCEPT_LANGUAGE']
|
30
|
+
@language_criterea = if accept_language_header
|
31
|
+
parse_accept_header(accept_language_header) {|range| matcher_from_language_range(range)}.sort_by {|matcher,specificity,q| -specificity} + [[nil, 0, 0.001]]
|
32
|
+
# When language_criterea are given, we allow a low-specificity tiny-but-nonzero match for a language of 'nil', ie entities with no
|
33
|
+
# language, even though the accept header may appear to require a particular language. Because it makes no sense to apply Accept-Language
|
34
|
+
# criterea to resources whose representations aren't language-specific.
|
35
|
+
else
|
36
|
+
[[Object, 0, 1.0]]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def media_type_quality(media_type)
|
41
|
+
@media_type_criterea.each {|matcher,specificity,quality| return quality if media_type.matches_names.any? {|name| matcher === name}}; 0
|
42
|
+
end
|
43
|
+
|
44
|
+
def language_quality(language)
|
45
|
+
@language_criterea.each {|matcher,specificity,quality| return quality if matcher === language}; 0
|
46
|
+
end
|
47
|
+
|
48
|
+
# Combined quality value for a (media_type, language) pair
|
49
|
+
def quality(media_type, language)
|
50
|
+
media_type_quality(media_type)*language_quality(language)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Choose from a list of Doze::Entity
|
54
|
+
def choose_entity(entities)
|
55
|
+
max_by_non_zero(entities) {|a| quality(a.media_type, a.language)}
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
# Given an http-style media-range, language-range, charset-range etc string, return a ruby object which answers to ===(string)
|
60
|
+
# for whether or not that string matches the range given. (note: these are useful in combination with Enumerable#grep)
|
61
|
+
# together with a priority value for the priority of this matcher (most specific has highest priority)
|
62
|
+
# Example input: *, text/*, text/html, en, en-gb, utf-8
|
63
|
+
def matcher_from_media_range(range_string)
|
64
|
+
case range_string
|
65
|
+
when '*', '*/*'
|
66
|
+
# Object === 'anything'
|
67
|
+
[Object, 0]
|
68
|
+
when /^(.*?\/)\*$/
|
69
|
+
# media type range eg text/*
|
70
|
+
[/^#{Regexp.escape($1)}/, 1]
|
71
|
+
else
|
72
|
+
[range_string, 2]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def matcher_from_language_range(range_string)
|
77
|
+
case range_string
|
78
|
+
when '*'
|
79
|
+
# Object === 'anything'
|
80
|
+
[Object, 0]
|
81
|
+
else
|
82
|
+
# en matches en, en-gb, en-whatever-else-after-a-hyphen, with longer strings more specific
|
83
|
+
[/^#{Regexp.escape(range_string)}(-|$)/, range_string.length]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_accept_header(accept_header_value)
|
88
|
+
accept_header_value.split(/,\s*/).map do |part|
|
89
|
+
/^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) or next # From WEBrick via Rack
|
90
|
+
q = ($2 || 1.0).to_f
|
91
|
+
matcher, specificity = yield($1)
|
92
|
+
[matcher, specificity, q]
|
93
|
+
end.compact
|
94
|
+
end
|
95
|
+
|
96
|
+
def max_by_non_zero(array)
|
97
|
+
max_quality = 0; max_item = nil
|
98
|
+
array.each do |item|
|
99
|
+
quality = yield(item)
|
100
|
+
if quality > max_quality
|
101
|
+
max_quality = quality
|
102
|
+
max_item = item
|
103
|
+
end
|
104
|
+
end
|
105
|
+
max_item || (@ignore_unacceptable_accepts && array.first)
|
106
|
+
end
|
107
|
+
end
|
data/lib/doze/request.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'doze/error'
|
2
|
+
require 'doze/utils'
|
3
|
+
|
4
|
+
# Some helpers for Rack::Request
|
5
|
+
class Doze::Request < Rack::Request
|
6
|
+
def initialize(app, env)
|
7
|
+
@app = app
|
8
|
+
super(env)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :app
|
12
|
+
|
13
|
+
# this delibarately ignores the HEAD vs GET distinction; use head? to check
|
14
|
+
def normalized_request_method
|
15
|
+
method = @env["REQUEST_METHOD"]
|
16
|
+
method == 'HEAD' ? 'get' : method.downcase
|
17
|
+
end
|
18
|
+
|
19
|
+
# At this stage, we only care that the servlet spec says PATH_INFO is decoded so special case
|
20
|
+
# it. There might be others needed, but webrick and thin return an encoded PATH_INFO so this'll
|
21
|
+
# do for now.
|
22
|
+
# http://bulknews.typepad.com/blog/2009/09/path_info-decoding-horrors.html
|
23
|
+
# http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/http/HttpServletRequest.html#getPathInfo%28%29
|
24
|
+
def raw_path_info
|
25
|
+
((servlet_request = @env['java.servlet_request']) &&
|
26
|
+
raw_path_info_from_servlet_request(servlet_request)) || path_info
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_or_head?
|
30
|
+
method = @env["REQUEST_METHOD"]
|
31
|
+
method == "GET" || method == "HEAD"
|
32
|
+
end
|
33
|
+
|
34
|
+
def options?
|
35
|
+
@env["REQUEST_METHOD"] == 'OPTIONS'
|
36
|
+
end
|
37
|
+
|
38
|
+
def entity
|
39
|
+
return @entity if defined?(@entity)
|
40
|
+
@entity = if media_type
|
41
|
+
media_type.new_entity(
|
42
|
+
:binary_data_stream => env['rack.input'],
|
43
|
+
:binary_data_length => content_length && content_length.to_i,
|
44
|
+
:encoding => content_charset,
|
45
|
+
:media_type_params => media_type_params
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def media_type
|
51
|
+
@mt ||= (mt = super and Doze::MediaType[mt])
|
52
|
+
end
|
53
|
+
|
54
|
+
# For now, to do authentication you need some (rack) middleware that sets one of these env's.
|
55
|
+
# See :session_from_rack_env under Doze::Application config
|
56
|
+
def session
|
57
|
+
@session ||= @app.config[:session_from_rack_env].call(@env)
|
58
|
+
end
|
59
|
+
|
60
|
+
def session_authenticated?
|
61
|
+
@session_authenticated ||= (session && @app.config[:session_authenticated].call(session))
|
62
|
+
end
|
63
|
+
|
64
|
+
EXTENSION_REGEXP = /\.([a-z0-9\-_]+)$/
|
65
|
+
|
66
|
+
# splits the raw_path_info into a routing path, and an optional file extension for use with
|
67
|
+
# special media-type-specific file extension handling (where :media_type_extensions => true
|
68
|
+
# configured on the app)
|
69
|
+
def routing_path_and_extension
|
70
|
+
@routing_path_and_extension ||= begin
|
71
|
+
path = raw_path_info
|
72
|
+
extension = nil
|
73
|
+
|
74
|
+
if @app.config[:media_type_extensions] && (match = EXTENSION_REGEXP.match(path))
|
75
|
+
path = match.pre_match
|
76
|
+
extension = match[1]
|
77
|
+
end
|
78
|
+
|
79
|
+
[path, extension]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def routing_path
|
84
|
+
routing_path_and_extension[0]
|
85
|
+
end
|
86
|
+
|
87
|
+
def extension
|
88
|
+
routing_path_and_extension[1]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Makes a Doze::Negotiator to do content negotiation on behalf of this request
|
92
|
+
def negotiator(ignore_unacceptable_accepts=false)
|
93
|
+
Doze::Negotiator.new(self, ignore_unacceptable_accepts)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
URL_CHUNK = /^\/[^\/\?]+/
|
99
|
+
URL_UP_TO_URI = /^(\w)+:\/\/[\w0-9\-\.]+(:[0-9]+)?/
|
100
|
+
URL_SEARCHPART = /\?.+$/
|
101
|
+
|
102
|
+
# FIXME - This doesn't do anything with the script name, which will cause trouble
|
103
|
+
# if one is specified
|
104
|
+
def raw_path_info_from_servlet_request(servlet_request)
|
105
|
+
# servlet spec decodes the path info, we want an unencoded version
|
106
|
+
# fortunately getRequestURL is unencoded, but includes extra stuff - chop it off
|
107
|
+
sb = servlet_request.getRequestURL.toString
|
108
|
+
# chomp off the proto, host and optional port
|
109
|
+
sb = sb.gsub(URL_UP_TO_URI, "")
|
110
|
+
|
111
|
+
# chop off context path if one is specified - not sure if this is desired behaviour
|
112
|
+
# but conforms to servlet spec and then remove the search part
|
113
|
+
if servlet_request.getContextPath == ""
|
114
|
+
sb
|
115
|
+
else
|
116
|
+
sb.gsub(URL_CHUNK, "")
|
117
|
+
end.gsub(URL_SEARCHPART, "")
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A special resource class used to represent errors which are available in different media_types etc.
|
2
|
+
# Used by the framework to render 5xx / 4xx etc
|
3
|
+
#
|
4
|
+
# The framework supplies this, based on Doze::Serialization::Resource, as the default implementation
|
5
|
+
# but you may specify your own error resource class in the app config.
|
6
|
+
require 'doze/resource'
|
7
|
+
require 'doze/serialization/resource'
|
8
|
+
class Doze::Resource::Error
|
9
|
+
include Doze::Resource
|
10
|
+
include Doze::Serialization::Resource
|
11
|
+
|
12
|
+
def initialize(status=Doze::Utils::STATUS_INTERNAL_SERVER_ERROR, message=Rack::Utils::HTTP_STATUS_CODES[status], extras={})
|
13
|
+
@status = status
|
14
|
+
@message = message
|
15
|
+
@extra_properties = extras
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_data
|
19
|
+
@extra_properties.merge(:status => @status, :message => @message)
|
20
|
+
end
|
21
|
+
end
|