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.
Files changed (50) hide show
  1. data/README +6 -0
  2. data/lib/doze/application.rb +92 -0
  3. data/lib/doze/collection/object.rb +14 -0
  4. data/lib/doze/entity.rb +62 -0
  5. data/lib/doze/error.rb +75 -0
  6. data/lib/doze/media_type.rb +135 -0
  7. data/lib/doze/negotiator.rb +107 -0
  8. data/lib/doze/request.rb +119 -0
  9. data/lib/doze/resource/error.rb +21 -0
  10. data/lib/doze/resource/proxy.rb +81 -0
  11. data/lib/doze/resource.rb +193 -0
  12. data/lib/doze/responder/error.rb +34 -0
  13. data/lib/doze/responder/main.rb +41 -0
  14. data/lib/doze/responder/resource.rb +262 -0
  15. data/lib/doze/responder.rb +58 -0
  16. data/lib/doze/response.rb +78 -0
  17. data/lib/doze/router/anchored_route_set.rb +68 -0
  18. data/lib/doze/router/route.rb +88 -0
  19. data/lib/doze/router/route_set.rb +34 -0
  20. data/lib/doze/router.rb +100 -0
  21. data/lib/doze/serialization/entity.rb +34 -0
  22. data/lib/doze/serialization/form_data_helpers.rb +40 -0
  23. data/lib/doze/serialization/html.rb +116 -0
  24. data/lib/doze/serialization/json.rb +29 -0
  25. data/lib/doze/serialization/multipart_form_data.rb +162 -0
  26. data/lib/doze/serialization/resource.rb +30 -0
  27. data/lib/doze/serialization/resource_proxy.rb +14 -0
  28. data/lib/doze/serialization/www_form_encoded.rb +42 -0
  29. data/lib/doze/serialization/yaml.rb +25 -0
  30. data/lib/doze/uri_template.rb +220 -0
  31. data/lib/doze/utils.rb +53 -0
  32. data/lib/doze/version.rb +3 -0
  33. data/lib/doze.rb +5 -0
  34. data/test/functional/auth_test.rb +69 -0
  35. data/test/functional/base.rb +159 -0
  36. data/test/functional/cache_header_test.rb +76 -0
  37. data/test/functional/direct_response_test.rb +16 -0
  38. data/test/functional/error_handling_test.rb +131 -0
  39. data/test/functional/get_and_conneg_test.rb +182 -0
  40. data/test/functional/media_type_extensions_test.rb +102 -0
  41. data/test/functional/media_type_test.rb +40 -0
  42. data/test/functional/method_support_test.rb +49 -0
  43. data/test/functional/non_get_method_test.rb +173 -0
  44. data/test/functional/precondition_test.rb +84 -0
  45. data/test/functional/raw_path_info_test.rb +69 -0
  46. data/test/functional/resource_representation_test.rb +14 -0
  47. data/test/functional/router_test.rb +196 -0
  48. data/test/functional/serialization_test.rb +142 -0
  49. data/test/functional/uri_template_test.rb +51 -0
  50. metadata +221 -0
data/README ADDED
@@ -0,0 +1,6 @@
1
+ # Doze
2
+
3
+ RESTful resource-oriented API framework
4
+
5
+ # Example application
6
+
@@ -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
@@ -0,0 +1,14 @@
1
+ module Doze::Collection::Object
2
+
3
+ include Doze::Serialization::Resource
4
+
5
+ def get_data
6
+ end
7
+
8
+ def get_property(property)
9
+ end
10
+
11
+ def put_property(property, data)
12
+ end
13
+
14
+ end
@@ -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
@@ -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