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
@@ -0,0 +1,81 @@
1
+ # A proxy wrapper for a resource instance, which allows you to proxy through to its resource functionality
2
+ # while potentially overriding some of it with context-sensitive behaviour.
3
+ # Unlike the proxies in the stdlib's 'delegate' library, this will satisfy is_a?(Doze::Resource) for the proxy instance.
4
+ # Also note it only proxies the Resource interface, and doesn't do any method_missing magic to proxy additional methods
5
+ # on the underlying instance. If you need this you can access the target of the proxy with #target, or use 'try',
6
+ # which has been overridden sensibly.
7
+ #
8
+ # Also note the proxy is designed to do the sensible default with a nil target, however do make sure it's doing what
9
+ # you want it to do if using with a nil target
10
+ require 'doze/resource'
11
+ class Doze::Resource::Proxy
12
+ include Doze::Resource
13
+
14
+ def initialize(uri, target)
15
+ @uri = uri
16
+ @target = target
17
+ end
18
+
19
+ attr_reader :target
20
+
21
+ # Methods based around use try / respond_to? need special care when proxying:
22
+
23
+ def try(method, *args, &block)
24
+ if respond_to?(method)
25
+ send(method, *args, &block)
26
+ elsif target.respond_to?(method)
27
+ target && target.send(method, *args, &block)
28
+ end
29
+ end
30
+
31
+ def supports_method?(method)
32
+ supports_method = "supports_#{method}?"
33
+ if respond_to?(supports_method)
34
+ send(supports_method)
35
+ else
36
+ target && target.supports_method?(method)
37
+ end
38
+ end
39
+
40
+ def other_method(method_name, entity=nil)
41
+ if respond_to?(method_name)
42
+ send(method_name, entity)
43
+ else
44
+ target && target.other_method(method_name, entity)
45
+ end
46
+ end
47
+
48
+ def accepts_method_with_media_type?(resource_method, entity)
49
+ method_name = "accepts_#{resource_method}_with_media_type?"
50
+ if respond_to?(method_name)
51
+ send(method_name, entity)
52
+ else
53
+ target && target.accepts_method_with_media_type?(resource_method, entity)
54
+ end
55
+ end
56
+
57
+ # proxying post is a bit fiddly due to the (in retrospect perhaps a bit unadvised) convenience support for
58
+ # different method arities for post.
59
+ # maybe move to post_with_session(entity, session) vs post(entity) if this causes any more pain.
60
+ def post(entity, session)
61
+ t = target or return
62
+ if t.method(:post).arity.abs > 1
63
+ t.post(entity, session)
64
+ else
65
+ t.post(entity)
66
+ end
67
+ end
68
+
69
+ # Some methods which should return something other than nil by default for an empty target:
70
+
71
+ def authorize(user, method)
72
+ target ? target.authorize(user, method) : true
73
+ end
74
+
75
+ # Other methods which we can proxy generically
76
+
77
+ proxied_methods = Doze::Resource.public_instance_methods(true) - ['uri', 'uri_object'] - self.public_instance_methods(false)
78
+ proxied_methods.each do |method|
79
+ module_eval("def #{method}(*args, &block); t = target and t.__send__(:#{method}, *args, &block); end", __FILE__, __LINE__)
80
+ end
81
+ end
@@ -0,0 +1,193 @@
1
+ module Doze::Resource
2
+
3
+ # URIs and identity
4
+
5
+ # You would typically set @uri in your constructor; resources don't have to have a URI, but certain parts of the framework require a URI
6
+ # in order to create links to the object for Location headers, links etc.
7
+ # Also see Router (and note that a Resource can also act as a Router for its subresources)
8
+
9
+ # The URI path of this resource.
10
+ # #uri= may be used by propagate_static_routes when a resource which is also a router, is statically routed to.
11
+ # you can private :uri= if you don't want it writeable, however.
12
+ attr_accessor :uri
13
+
14
+ # Wraps up the URI path of this resource as a URI::Generic
15
+ def uri_object
16
+ uri && URI::Generic.build(:path => uri)
17
+ end
18
+
19
+
20
+ # Authorization / Authentication:
21
+ #
22
+ # Return true or false to allow or deny the given action on this resource for the given user (if user is nil, for the unauthenticated user).
23
+ # If you deny an action by the unauthenticated user, this will be taken as a requirement for authentication. So eg if authentication is all you require,
24
+ # you could just return !user.nil?
25
+ #
26
+ # method will be one of:
27
+ # * get, put, post, delete, or some other recognized_method where we supports_method?
28
+ #
29
+ # user will be the authenticated user, or nil if there is no authenticated user. The exact nature of the user object will depend on the middleware used
30
+ # to do authentication.
31
+ def authorize(user, method)
32
+ true
33
+ end
34
+
35
+ # You can return false here to make a resource instance act like it doesn't exist.
36
+ #
37
+ # You could use this if it's more convenient to have a router create an instance representing a potentially-extant resource,
38
+ # and to have the actual existence test run on the instance.
39
+ #
40
+ # Or you can use it if you want to support certain non-get methods on a resource, but appear non-existent in response to get requests.
41
+ def exists?
42
+ true
43
+ end
44
+
45
+ # A convenience which some libraries add to Kernel
46
+ def try(method, *args, &block)
47
+ send(method, *args, &block) if respond_to?(method)
48
+ end
49
+
50
+ def supports_method?(method)
51
+ try("supports_#{method}?")
52
+ end
53
+
54
+ def supports_get?
55
+ true
56
+ end
57
+
58
+
59
+ # Called to obtain one or more representations of the resource.
60
+ #
61
+ # Should be safe, that is, not have any side-effects visible to the caller. This also implies idempotency (which is weaker).
62
+ #
63
+ # You may return either:
64
+ #
65
+ # * A single entity representation, in the form of an instance of Doze::Entity
66
+ #
67
+ # * An array of multiple entity representations, instances of Doze::Entity with different media_types and/or languages.
68
+ # Content negotiation may be used to select an appropriate entity from the list.
69
+ # NB: if you return multiple entities we recommend using 'lazy' Doze::Entity instances constructed with a block -
70
+ # this way the entity data will not need to be generated for entities which aren't selected by content negotiation.
71
+ #
72
+ # * A resource representation, in the form of a Doze::Resource with a URI
73
+ # This would correspond to a redirect in HTTP.
74
+ #
75
+ # If you wish to indicate that the resource is missing, return false from exists?
76
+ def get
77
+ nil
78
+ end
79
+
80
+ # Called to update the entirity of the this resource to the resource represented by the given representation entity.
81
+ # entity will be a new entity representation whose media_type has been okayed by accepts_put_with_media_type?
82
+ #
83
+ # Should be idempotent; Subsequent to a successful put, the following should hold:
84
+ # * get should return the updated entity representation (or an alternative representation with the same resource-level semantics)
85
+ # * parent.resolve_subresource(additional_identifier_components) should return a resource for which the same holds.
86
+ #
87
+ # Need not return anything; success is assumed unless an error is raised. (or: should we have this return true/false?)
88
+ def put(entity)
89
+ nil
90
+ end
91
+
92
+ # Called to delete this resource.
93
+ #
94
+ # Should be idempotent. Subsequent to a successful delete, the following should hold:
95
+ # * exists? should return false, or get should return nil, or both
96
+ # * parent.resolve_subresource(additional_identifier_components) should return nil, or return a resource which "doesn't exist" in the same sense as above.
97
+ #
98
+ # Need not return anything; success is assumed unless an error is raised. (or: should we have this return true/false?)
99
+ def delete_resource
100
+ nil
101
+ end
102
+
103
+ # Intended to be called in order to:
104
+ # * Allocate an identifier for a new subresource, and create this subresource, based on the representation entity if given.
105
+ # (Note: use put_on_subresource instead if you know the desired identifier)
106
+ # * Create a new interal resource which isn't exposed to the caller (eg, log some data)
107
+ #
108
+ # May also be used for legacy reasons for some other wider purposes:
109
+ # * To annotate, append to or otherwise modify the current resource based on instructions contained in the given representation entity,
110
+ # potentially returning an anonymous resource describing the results
111
+ # * Otherwise process instructions contained in the given representation entity, returning an anonymous resource describing the results
112
+ # Note: these uses are a bit of a catch-all, not very REST-ful, and discouraged, see other_method.
113
+ #
114
+ # entity will be a entity whose media_type has been okayed by accepts_post_with_media_type?
115
+ #
116
+ # Does not need to be idempotent or safe, and should not be assumed to be.
117
+ #
118
+ # May return:
119
+ # * A Doze::Resource with identifier_components, which will be taken as a newly-created resource.
120
+ # This is what we're recommending as the primary intended semantics for post.
121
+ # * nil to indicate success without exposing a resulting resource
122
+ # * A Doze::Resource without identifier_components, which will be taken to be a returned description of the results of some arbitrary operation performed
123
+ # (see discouragement above)
124
+ # * A Doze::Entity, which will be taken to be a returned description of the results of some arbitrary operation performed
125
+ # (this one even more discouraged, but there if you need a quick way to make an arbitrary fixed response)
126
+ def post(entity)
127
+ nil
128
+ end
129
+
130
+ # This allows you to accept methods other than the standard get/put/post/delete from HTTP. When the resource is exposed over a protocol (or requested by a client)
131
+ # which doesn't support custom methods, some form of tunnelling will be used, typically ontop of POST.
132
+ #
133
+ # Semantics are up to you, but should not, and will not, be assumed to be idempotent or safe by any generic middleware.
134
+ #
135
+ # When should you consider using a custom method? as I understand it, the REST philosophy is that this is valid and to be encouraged if and only if:
136
+ # * Your proposed method has clearly-defined semantics which have the potential to apply generically to a wide variety of resources and media types
137
+ # * It doesn't overlap with the semantics of existing methods in a confusing or redundant way (exception: it may be used to replace legacy uses of post
138
+ # as described; see eg patch)
139
+ # * Requests with the method can safely be forwarded around by middleware which don't know anything about their semantics
140
+ # * You've given due consideration to the alternative: rephrasing the problem in terms of standard methods acting on a different resource structure.
141
+ # And you concluded that this would be significantly less elegant / would add excessive conceptual (or implementation) overhead
142
+ # * Ideally, some attempt has been made to standardize it
143
+ # The proposed 'patch' method (for selective update of a resource by a diff-like media type) is a good example of something which meets these criterea.
144
+ # http://greenbytes.de/tech/webdav/draft-dusseault-http-patch-11.html
145
+ #
146
+ # entity will be nil, or an entity whose media_type has been okayed by accepts_method_with_media_type?(method_name, ..)
147
+ #
148
+ # Return options are the same as for post, as are their interpretations, with the exception that a resource returned will not be assumed to be newly-created.
149
+ # (we're taking the stance that you should be using post or put for creation of new resources).
150
+ #
151
+ # By default it'll call a ruby method of the same name to call, as already happens for post/put/delete.
152
+ def other_method(method_name, entity=nil)
153
+ try(method_name, entity)
154
+ end
155
+
156
+ # Called to determine whether the request entity is of a suitable media type for the method in question (eg for a put or a post).
157
+ # The entity itself is passed in. If you return false, the method will never be called; if true then the method may be called with the entity.
158
+ def accepts_method_with_media_type?(resource_method, entity)
159
+ try("accepts_#{resource_method}_with_media_type?", entity)
160
+ end
161
+
162
+
163
+
164
+
165
+ # Caching and modification-related metadata
166
+
167
+ # May return a Time object inidicating the last modification date, or nil if not known
168
+ def last_modified
169
+ nil
170
+ end
171
+
172
+ # nil = no explicit policy, false = explicitly no caching, true = caching yes please
173
+ def cacheable?
174
+ nil
175
+ end
176
+
177
+ # false here (when cacheable? is true) implies that private caching only is desired.
178
+ def publicly_cacheable?
179
+ cacheable?
180
+ end
181
+
182
+ # Integer seconds, or nil for no explicitly-specified expiry period. Will only be checked if cacheable? is true.
183
+ # todo: a means to specify the equivalent of 'must-revalidate'
184
+ def cache_expiry_period
185
+ nil
186
+ end
187
+
188
+ # You can override public_cache_expiry_period to specify a longer or shorter period for public caches.
189
+ # Otherwise assumed same as general cache_expiry_period.
190
+ def public_cache_expiry_period
191
+ nil
192
+ end
193
+ end
@@ -0,0 +1,34 @@
1
+ class Doze::Responder::Error < Doze::Responder
2
+
3
+ def initialize(app, request, error)
4
+ super(app, request)
5
+ @error = error
6
+ end
7
+
8
+ # We use a resource class to represent for errors to enable content type negotiation for them.
9
+ # The class used is configurable but defaults to Doze::Resource::Error
10
+ def response
11
+ response = Doze::Response.new
12
+ if @app.config[:error_resource_class]
13
+ extras = {:error => @error}
14
+
15
+ if @app.config[:expose_exception_details] && @error.http_status >= 500
16
+ extras[:backtrace] = @error.backtrace
17
+ end
18
+
19
+ resource = @app.config[:error_resource_class].new(@error.http_status, @error.message, extras)
20
+ # we can't have it going STATUS_NOT_ACCEPTABLE in the middle of trying to return an error resource, so :ignore_unacceptable_accepts
21
+ responder = Doze::Responder::Resource.new(@app, @request, resource, :ignore_unacceptable_accepts => true)
22
+ entity = responder.get_preferred_representation(response)
23
+ response.entity = entity
24
+ else
25
+ response.headers['Content-Type'] = 'text/plain'
26
+ response.body = @error.message
27
+ end
28
+
29
+ response.head_only = true if @request.head?
30
+ response.status = @error.http_status
31
+ response.headers.merge!(@error.headers) if @error.headers
32
+ response
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ class Doze::Responder::Main < Doze::Responder
2
+
3
+ def response
4
+ resource = nil
5
+ route_to = @app.root
6
+ remaining_path = @request.routing_path
7
+ remaining_path = nil if remaining_path.empty? || remaining_path == '/'
8
+ session = @request.session
9
+ base_uri = ''
10
+
11
+ # main routing loop - results in either a final Resource which has been routed to, or nil
12
+ # todo: maybe something recursive would be a bit more readable here
13
+ while true
14
+ if remaining_path && route_to.is_a?(Doze::Router)
15
+ # Bail early with a 401 or 403 if the router refuses to authorize further routing
16
+ return auth_failed_response unless route_to.authorize_routing(@request.session)
17
+ route_to, base_uri, remaining_path = route_to.perform_routing(remaining_path, session, base_uri)
18
+ elsif !remaining_path && route_to.is_a?(Doze::Resource)
19
+ resource = route_to
20
+ break
21
+ else
22
+ break
23
+ end
24
+ end
25
+
26
+ if resource
27
+ Doze::Responder::Resource.new(@app, @request, resource).response
28
+ elsif @request.options?
29
+ if @request.raw_path_info == '*'
30
+ # Special response for "OPTIONS *" as in HTTP spec:
31
+ rec_methods = (@app.config[:recognized_methods] + [:head, :options]).join(', ').upcase
32
+ Doze::Response.new(STATUS_NO_CONTENT, 'Allow' => rec_methods)
33
+ else
34
+ # Special OPTIONS response for non-existent resource:
35
+ Doze::Response.new(STATUS_NO_CONTENT, 'Allow' => 'OPTIONS')
36
+ end
37
+ else
38
+ error_response(STATUS_NOT_FOUND)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,262 @@
1
+ class Doze::Responder::Resource < Doze::Responder
2
+ include Doze::Utils
3
+
4
+ attr_reader :resource, :options
5
+
6
+ def initialize(app, request, resource, options={})
7
+ super(app, request)
8
+ @resource = resource
9
+ @options = options
10
+ end
11
+
12
+
13
+ # Basic handling of method support and OPTIONS
14
+
15
+ def response
16
+ if @request.options?
17
+ Doze::Response.new(STATUS_NO_CONTENT, allow_header)
18
+ elsif !@resource.supports_method?(recognized_method)
19
+ error_response(STATUS_METHOD_NOT_ALLOWED, nil, allow_header)
20
+ else
21
+ response_to_supported_method
22
+ end
23
+ end
24
+
25
+ def allow_header
26
+ methods = @app.config[:recognized_methods].select {|m| @resource.supports_method?(m)}
27
+ # We support OPTIONS for free, and HEAD for free if GET is supported
28
+ methods << :head if methods.include?(:get)
29
+ methods << :options
30
+ {'Allow' => methods.map {|method| method.to_s.upcase}.join(', ')}
31
+ end
32
+
33
+
34
+
35
+ # Handling for supported methods
36
+
37
+ def response_to_supported_method
38
+ fail = authorization_fail_response(recognized_method) and return fail
39
+
40
+ exists = @resource.exists?
41
+
42
+ if @request.get_or_head?
43
+ return error_response(STATUS_NOT_FOUND) unless exists
44
+ response = resource_preconditions_fail_response || make_representation_of_resource_response
45
+ response.head_only = @request.head?
46
+ response
47
+ else
48
+ entity = @request.entity
49
+ if entity && recognized_method != :delete
50
+ fail = request_entity_media_type_fail_response(recognized_method, entity) and return fail
51
+ end
52
+
53
+ # Where the resource supports GET, we can use this to support preconditions (If-Match etc)
54
+ # on PUT/DELETE/POST operations based on the content that GET would return.
55
+ if exists && @resource.supports_get?
56
+ fail = resource_preconditions_fail_response and return fail
57
+ fail = entity_preconditions_fail_response and return fail
58
+ end
59
+
60
+ perform_non_get_action(entity, exists)
61
+ end
62
+ end
63
+
64
+ def perform_non_get_action(entity, existed_before)
65
+ case recognized_method
66
+ when :post
67
+ # Slightly hacky but for now we allow the session to be passed as an extra arg to post actions
68
+ # where the method has sufficient arity.
69
+ #
70
+ # This is only supported for POST at present; prefered approach (especially for GET/PUT/DELETE)
71
+ # is to use a session-specific route and construct the resource with the session context.
72
+ # Also, this should not be used for authorization logic - use the authorize method for that.
73
+ result = if @resource.method(:post).arity.abs > 1
74
+ @resource.post(entity, @request.session)
75
+ else
76
+ @resource.post(entity)
77
+ end
78
+
79
+ # 201 created is the default interpretation of a new resource with an identifier resulting from a post.
80
+ make_post_result_response(result)
81
+
82
+ when :put
83
+ if entity
84
+ result = @resource.put(entity)
85
+ Doze::Response.new_empty(existed_before ? STATUS_NO_CONTENT : STATUS_CREATED)
86
+ else
87
+ error_response(STATUS_BAD_REQUEST, "expected request body for PUT")
88
+ end
89
+
90
+ when :delete
91
+ result = @resource.delete_resource if existed_before
92
+ Doze::Response.new_empty
93
+
94
+ else
95
+ result = @resource.other_method(recognized_method, entity)
96
+ # For now we streat this pretty much as a POST
97
+ make_post_result_response(result)
98
+ end
99
+ end
100
+
101
+
102
+
103
+
104
+
105
+ # Precondition checkers
106
+
107
+ def request_entity_media_type_fail_response(resource_method, entity)
108
+ unless @resource.accepts_method_with_media_type?(resource_method, entity)
109
+ error_response(STATUS_UNSUPPORTED_MEDIA_TYPE)
110
+ end
111
+ end
112
+
113
+ def authorization_fail_response(action)
114
+ auth_failed_response unless @resource.authorize(@request.session, action)
115
+ end
116
+
117
+ def resource_preconditions_fail_response
118
+ last_modified = @resource.last_modified or return
119
+ if_modified_since = @request.env['HTTP_IF_MODIFIED_SINCE']
120
+ if_unmodified_since = @request.env['HTTP_IF_UNMODIFIED_SINCE']
121
+
122
+ if (if_unmodified_since && last_modified > Time.httpdate(if_unmodified_since))
123
+ # although technically an HTTP error response, we don't use error_response (and an error resource)
124
+ # to send STATUS_PRECONDITION_FAILED, since the precondition check was something the client specifically
125
+ # requested, so we assume they don't need a special error resource to make sense of it.
126
+ Doze::Response.new(STATUS_PRECONDITION_FAILED, 'Last-Modified' => last_modified.httpdate)
127
+ elsif (if_modified_since && last_modified <= Time.httpdate(if_modified_since))
128
+ if request.get_or_head?
129
+ Doze::Response.new(STATUS_NOT_MODIFIED, 'Last-Modified' => last_modified.httpdate)
130
+ else
131
+ Doze::Response.new(STATUS_PRECONDITION_FAILED, 'Last-Modified' => last_modified.httpdate)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Etag-based precondition checks. These are specific to the response entity that would be returned
137
+ # from a GET.
138
+ #
139
+ # Note: the default implementation of entity.etag just generates the entity body and hashes it,
140
+ # but you could return entities which lazily know their Etag without the body needing to be generated,
141
+ # and this code will take advantage of that
142
+ def entity_preconditions_fail_response(entity=nil)
143
+ if_match = @request.env['HTTP_IF_MATCH']
144
+ if_none_match = @request.env['HTTP_IF_NONE_MATCH']
145
+ return unless if_match || if_none_match
146
+
147
+ entity ||= get_preferred_representation
148
+ return unless entity.is_a?(Doze::Entity)
149
+
150
+ etag = entity.etag
151
+
152
+ # etag membership test is kinda crude at present, really we should parse the separate quoted etags out.
153
+ if (if_match && if_match != '*' && !(etag && if_match.include?(quote(etag))))
154
+ Doze::Response.new(STATUS_PRECONDITION_FAILED, 'Etag' => quote(etag))
155
+ elsif (if_none_match && (if_none_match == '*' || (etag && if_none_match.include?(quote(etag)))))
156
+ if @request.get_or_head?
157
+ Doze::Response.new(STATUS_NOT_MODIFIED, 'Etag' => quote(etag))
158
+ else
159
+ Doze::Response.new(STATUS_PRECONDITION_FAILED, 'Etag' => quote(etag))
160
+ end
161
+ end
162
+ end
163
+
164
+
165
+
166
+
167
+ # Response handling helpers
168
+
169
+ def get_preferred_representation(response=nil)
170
+ representation = @resource.get
171
+ if representation.is_a?(Array)
172
+ negotiator = @request.negotiator(@options[:ignore_unacceptable_accepts])
173
+
174
+ if response
175
+ # If the available representation entities differ by media type, add a Vary: Accept. similarly for language.
176
+ response.add_header_values('Vary', 'Accept') if not_all_equal?(representation.map {|e| e.media_type})
177
+ response.add_header_values('Vary', 'Accept-Language') if not_all_equal?(representation.map {|e| e.language})
178
+ end
179
+
180
+ # If we fail to find the requested media type when requested via a file extension (/foo.jpeg) the appropriate HTTP status
181
+ # is 404; at the HTTP level this is effectively a separate media-type-specific version of the resource at its own uri,
182
+ # which doesn't exist due to the particular media-type-specific version of the resource not being available.
183
+ # If we fail due to an Accept header not matching anything, of course the appropriate status is STATUS_NOT_ACCEPTABLE
184
+ negotiator.choose_entity(representation) or raise_error(@request.extension ? STATUS_NOT_FOUND : STATUS_NOT_ACCEPTABLE)
185
+ else
186
+ representation
187
+ end
188
+ end
189
+
190
+ def not_all_equal?(collection)
191
+ first = collection.first
192
+ collection.any? {|x| x != first}
193
+ end
194
+
195
+ def add_caching_headers(response)
196
+ # resource-level caching metadata headers
197
+ last_modified = @resource.last_modified and response.headers['Last-Modified'] = last_modified.httpdate
198
+ case @resource.cacheable?
199
+ when true
200
+ expiry_period = @resource.cache_expiry_period
201
+ if @resource.publicly_cacheable?
202
+ cache_control = 'public'
203
+ if expiry_period
204
+ cache_control << ", max-age=#{expiry_period}"
205
+ public_expiry_period = @resource.public_cache_expiry_period
206
+ cache_control << ", s-maxage=#{public_expiry_period}" if public_expiry_period
207
+ end
208
+ else
209
+ cache_control = 'private'
210
+ cache_control << ", max-age=#{expiry_period}" if expiry_period
211
+ end
212
+ response.headers['Expires'] = (Time.now + expiry_period).httpdate if expiry_period
213
+ response.headers['Cache-Control'] = cache_control
214
+ when false
215
+ response.headers['Expires'] = 'Thu, 01 Jan 1970 00:00:00 GMT' # Beginning of time woop woop
216
+ response.headers['Cache-Control'] = 'no-cache, max-age=0'
217
+ end
218
+ end
219
+
220
+ def make_representation_of_resource_response(include_location_with_status=nil)
221
+ response = Doze::Response.new
222
+ representation = get_preferred_representation(response)
223
+
224
+ case representation
225
+ when Doze::Resource
226
+ raise 'Resource representation must have a uri' unless representation.uri
227
+ response.set_redirect(representation, @request)
228
+ add_caching_headers(response)
229
+ response
230
+ when Doze::Entity
231
+ # preconditions on the representation only apply to the content that would be served up by a GET
232
+ fail_response = @request.get_or_head? && entity_preconditions_fail_response(representation)
233
+ response = fail_response || begin
234
+ response.entity = representation
235
+ response
236
+ end
237
+ add_caching_headers(response)
238
+
239
+ if include_location_with_status && @resource.uri
240
+ response.set_location(@resource, @request)
241
+ response.status = include_location_with_status
242
+ end
243
+
244
+ response
245
+ when Doze::Response
246
+ representation
247
+ end
248
+ end
249
+
250
+ def make_post_result_response(result)
251
+ case result
252
+ when Doze::Resource
253
+ Doze::Responder::Resource.new(@app, @request, result, @options).make_representation_of_resource_response(STATUS_CREATED)
254
+ when Doze::Entity
255
+ Doze::Response.new_from_entity(result)
256
+ when Doze::Response
257
+ result
258
+ when nil
259
+ Doze::Response.new_empty
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,58 @@
1
+ class Doze::Responder
2
+ include Doze::Utils
3
+
4
+ attr_reader :response, :request
5
+
6
+ def initialize(app, request)
7
+ @app = app
8
+ @request = request
9
+ end
10
+
11
+ def recognized_method
12
+ @recognized_method ||= begin
13
+ method = @request.normalized_request_method
14
+ ([:options] + @app.config[:recognized_methods]).find {|m| m.to_s == method} or raise_error(STATUS_NOT_IMPLEMENTED)
15
+ end
16
+ end
17
+
18
+ # for use within #response
19
+ def raise_error(status=STATUS_INTERNAL_SERVER_ERROR, message=nil, headers={})
20
+ raise Doze::Error.new(status, message, headers)
21
+ end
22
+
23
+ def error_response(status=STATUS_INTERNAL_SERVER_ERROR, message=nil, headers={}, backtrace=nil)
24
+ error_response_from_error(Doze::Error.new(status, message, headers, backtrace))
25
+ end
26
+
27
+ def error_response_from_error(error)
28
+ Doze::Responder::Error.new(@app, @request, error).response
29
+ end
30
+
31
+ def call
32
+ begin
33
+ response().finish
34
+ rescue Doze::Error => error
35
+ error_response_from_error(error).finish
36
+ rescue => exception
37
+ raise unless @app.config[:catch_application_errors]
38
+ lines = ["#{exception.class}: #{exception.message}", *exception.backtrace].join("\n")
39
+ @app.logger << lines
40
+ if @app.config[:expose_exception_details]
41
+ error_response(STATUS_INTERNAL_SERVER_ERROR, exception.message, {}, exception.backtrace).finish
42
+ else
43
+ error_response.finish
44
+ end
45
+ end
46
+ end
47
+
48
+ def response
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def auth_failed_response
53
+ error_response(@request.session_authenticated? ?
54
+ STATUS_FORBIDDEN : # this one, 403, really means 'unauthorized', ie
55
+ STATUS_UNAUTHORIZED # http status code 401 called 'unauthorized' but really used to mean 'unauthenticated'
56
+ )
57
+ end
58
+ end