doze 0.0.11
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.
- 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
|