doze 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
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