cloudkit 0.9.0
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/CHANGES +2 -0
- data/COPYING +20 -0
- data/README +55 -0
- data/Rakefile +35 -0
- data/TODO +22 -0
- data/cloudkit.gemspec +82 -0
- data/doc/curl.html +329 -0
- data/doc/images/example-code.gif +0 -0
- data/doc/images/json-title.gif +0 -0
- data/doc/images/oauth-discovery-logo.gif +0 -0
- data/doc/images/openid-logo.gif +0 -0
- data/doc/index.html +87 -0
- data/doc/main.css +151 -0
- data/doc/rest-api.html +358 -0
- data/examples/1.ru +3 -0
- data/examples/2.ru +3 -0
- data/examples/3.ru +6 -0
- data/examples/4.ru +5 -0
- data/examples/5.ru +10 -0
- data/examples/6.ru +10 -0
- data/examples/TOC +17 -0
- data/lib/cloudkit.rb +74 -0
- data/lib/cloudkit/flash_session.rb +22 -0
- data/lib/cloudkit/oauth_filter.rb +273 -0
- data/lib/cloudkit/oauth_store.rb +56 -0
- data/lib/cloudkit/openid_filter.rb +198 -0
- data/lib/cloudkit/openid_store.rb +101 -0
- data/lib/cloudkit/rack/builder.rb +120 -0
- data/lib/cloudkit/rack/router.rb +20 -0
- data/lib/cloudkit/request.rb +159 -0
- data/lib/cloudkit/service.rb +135 -0
- data/lib/cloudkit/store.rb +459 -0
- data/lib/cloudkit/store/adapter.rb +9 -0
- data/lib/cloudkit/store/extraction_view.rb +57 -0
- data/lib/cloudkit/store/response.rb +51 -0
- data/lib/cloudkit/store/response_helpers.rb +72 -0
- data/lib/cloudkit/store/sql_adapter.rb +36 -0
- data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
- data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
- data/lib/cloudkit/templates/oauth_meta.erb +8 -0
- data/lib/cloudkit/templates/openid_login.erb +31 -0
- data/lib/cloudkit/templates/request_authorization.erb +23 -0
- data/lib/cloudkit/templates/request_token_denied.erb +18 -0
- data/lib/cloudkit/user_store.rb +44 -0
- data/lib/cloudkit/util.rb +60 -0
- data/test/ext_test.rb +57 -0
- data/test/flash_session_test.rb +22 -0
- data/test/helper.rb +50 -0
- data/test/oauth_filter_test.rb +331 -0
- data/test/oauth_store_test.rb +12 -0
- data/test/openid_filter_test.rb +54 -0
- data/test/openid_store_test.rb +12 -0
- data/test/rack_builder_test.rb +41 -0
- data/test/request_test.rb +197 -0
- data/test/service_test.rb +718 -0
- data/test/store_test.rb +99 -0
- data/test/user_store_test.rb +12 -0
- data/test/util_test.rb +13 -0
- metadata +190 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A CloudKit Service is Rack middleware providing a REST/HTTP 1.1 interface to
|
4
|
+
# a Store. Its primary purpose is initialize and adapt a Store for use in a
|
5
|
+
# Rack middleware stack.
|
6
|
+
#
|
7
|
+
# ==Examples
|
8
|
+
#
|
9
|
+
# A rackup file exposing _items_ and _things_ as REST collections:
|
10
|
+
# require 'cloudkit'
|
11
|
+
# expose :items, :things
|
12
|
+
#
|
13
|
+
# The same as above, adding OpenID and OAuth/Discovery:
|
14
|
+
# require 'cloudkit'
|
15
|
+
# contain :items, :things
|
16
|
+
#
|
17
|
+
# An explicit setup, without using the Rack::Builder shortcuts:
|
18
|
+
# require 'cloudkit'
|
19
|
+
# use Rack::Session::Pool
|
20
|
+
# use CloudKit::OAuthFilter
|
21
|
+
# use CloudKit::OpenIDFilter
|
22
|
+
# use CloudKit::Service, :collections => [:items, :things]
|
23
|
+
# run lambda{|env| [200, {}, ['Hello']]}
|
24
|
+
#
|
25
|
+
# For more examples, including the use of different storage implementations,
|
26
|
+
# see the Table of Contents in the examples directory.
|
27
|
+
class Service
|
28
|
+
include Util
|
29
|
+
include ResponseHelpers
|
30
|
+
|
31
|
+
@@lock = Mutex.new
|
32
|
+
|
33
|
+
def initialize(app, options)
|
34
|
+
@app = app
|
35
|
+
@collections = options[:collections]
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(env)
|
39
|
+
@@lock.synchronize do
|
40
|
+
@store = Store.new(
|
41
|
+
:adapter => SQLAdapter.new(env[storage_uri_key]),
|
42
|
+
:collections => @collections)
|
43
|
+
end unless @store
|
44
|
+
|
45
|
+
request = Request.new(env)
|
46
|
+
unless bypass?(request)
|
47
|
+
return auth_config_error if (request.using_auth? && auth_missing?(request))
|
48
|
+
return not_implemented unless @store.implements?(request.request_method)
|
49
|
+
send(request.request_method.downcase, request) rescue internal_server_error
|
50
|
+
else
|
51
|
+
@app.call(env)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def get(request)
|
58
|
+
response = @store.get(
|
59
|
+
request.path_info,
|
60
|
+
{}.filter_merge!(
|
61
|
+
:remote_user => request.current_user,
|
62
|
+
:offset => request['offset'],
|
63
|
+
:limit => request['limit']))
|
64
|
+
response['Link'] = link_header(request) if @store.resource_uri?(request.path_info)
|
65
|
+
response.to_rack
|
66
|
+
end
|
67
|
+
|
68
|
+
def post(request)
|
69
|
+
if tunnel_methods.include?(request['_method'].try(:upcase))
|
70
|
+
return send(request['_method'].downcase)
|
71
|
+
end
|
72
|
+
@store.post(
|
73
|
+
request.path_info,
|
74
|
+
{:json => request.body.string}.filter_merge!(
|
75
|
+
:remote_user => request.current_user)).to_rack
|
76
|
+
end
|
77
|
+
|
78
|
+
def put(request)
|
79
|
+
@store.put(
|
80
|
+
request.path_info,
|
81
|
+
{:json => request.body.string}.filter_merge!(
|
82
|
+
:remote_user => request.current_user,
|
83
|
+
:etag => request.if_match)).to_rack
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete(request)
|
87
|
+
@store.delete(
|
88
|
+
request.path_info,
|
89
|
+
{}.filter_merge!(
|
90
|
+
:remote_user => request.current_user,
|
91
|
+
:etag => request.if_match)).to_rack
|
92
|
+
end
|
93
|
+
|
94
|
+
def head(request)
|
95
|
+
response = @store.head(
|
96
|
+
request.path_info,
|
97
|
+
{}.filter_merge!(
|
98
|
+
:remote_user => request.current_user,
|
99
|
+
:offset => request['offset'],
|
100
|
+
:limit => request['limit']))
|
101
|
+
response['Link'] = link_header(request) if @store.resource_uri?(request.path_info)
|
102
|
+
response.to_rack
|
103
|
+
end
|
104
|
+
|
105
|
+
def options(request)
|
106
|
+
@store.options(request.path_info).to_rack
|
107
|
+
end
|
108
|
+
|
109
|
+
def link_header(request)
|
110
|
+
base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{request.path_info}"
|
111
|
+
"<#{base_url}/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\""
|
112
|
+
end
|
113
|
+
|
114
|
+
def auth_missing?(request)
|
115
|
+
request.current_user == nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def tunnel_methods
|
119
|
+
['PUT', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE']
|
120
|
+
end
|
121
|
+
|
122
|
+
def not_implemented
|
123
|
+
json_error_response(501, 'not implemented').to_rack
|
124
|
+
end
|
125
|
+
|
126
|
+
def auth_config_error
|
127
|
+
json_error_response(500, 'server auth misconfigured').to_rack
|
128
|
+
end
|
129
|
+
|
130
|
+
def bypass?(request)
|
131
|
+
collection = @collections.detect{|type| request.path_info.match("/#{type.to_s}")}
|
132
|
+
!collection && !@store.meta_uri?(request.path_info)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,459 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A functional storage interface with HTTP semantics and pluggable adapters.
|
4
|
+
class Store
|
5
|
+
include CloudKit::Util
|
6
|
+
include ResponseHelpers
|
7
|
+
|
8
|
+
# Initialize a new Store, creating its schema if needed. All resources in a
|
9
|
+
# Store are automatically versioned.
|
10
|
+
#
|
11
|
+
# ===Options
|
12
|
+
# - :adapter - Optional. An instance of Adapter. Defaults to in-memory SQLite.
|
13
|
+
# - :collections - Array of resource collections to manage.
|
14
|
+
# - :views - Optional. Array of views to be updated based on JSON content.
|
15
|
+
#
|
16
|
+
# ===Example
|
17
|
+
# store = CloudKit::Store.new(:collections => [:foos, :bars])
|
18
|
+
#
|
19
|
+
# ===Example
|
20
|
+
# adapter = CloudKit::SQLAdapter.new('mysql://user:pass@localhost/my_db')
|
21
|
+
# fruit_color_view = CloudKit::ExtractionView.new(
|
22
|
+
# :fruits_by_color_and_season,
|
23
|
+
# :observe => :fruits,
|
24
|
+
# :extract => [:color, :season])
|
25
|
+
# store = CloudKit::Store.new(
|
26
|
+
# :adapter => adapter,
|
27
|
+
# :collections => [:foos, :fruits],
|
28
|
+
# :views => [fruit_color_view])
|
29
|
+
#
|
30
|
+
# See also: Adapter, ExtractionView, Response
|
31
|
+
#
|
32
|
+
def initialize(options)
|
33
|
+
@db = options[:adapter] || SQLAdapter.new
|
34
|
+
@collections = options[:collections]
|
35
|
+
@views = options[:views]
|
36
|
+
@views.each {|view| view.initialize_storage(@db)} if @views
|
37
|
+
end
|
38
|
+
|
39
|
+
# Retrieve a resource or collection of resources based on a URI.
|
40
|
+
#
|
41
|
+
# ===Parameters
|
42
|
+
# - uri - URI of the resource or collection to retrieve.
|
43
|
+
# - options - See below.
|
44
|
+
#
|
45
|
+
# ===Options
|
46
|
+
# - :remote_user - Optional. Scopes the dataset if provided.
|
47
|
+
# - :limit - Optional. Default is unlimited. Limit the number of records returned by a collection request.
|
48
|
+
# - :offset - Optional. Start the list of resources in a collection at offset (0-based).
|
49
|
+
# - :any - Optional. Not a literal ":any", but any key or keys defined as extrations from a view.
|
50
|
+
#
|
51
|
+
# ===URI Types
|
52
|
+
# /cloudkit-meta
|
53
|
+
# /{collection}
|
54
|
+
# /{collection}/{uuid}
|
55
|
+
# /{collection}/{uuid}/versions
|
56
|
+
# /{collection}/{uuid}/versions/{etag}
|
57
|
+
# /{view}
|
58
|
+
#
|
59
|
+
# ===Examples
|
60
|
+
# get('/cloudkit-meta')
|
61
|
+
# get('/foos')
|
62
|
+
# get('/foos', :remote_user => 'coltrane')
|
63
|
+
# get('/foos', :limit => 100, :offset => 200)
|
64
|
+
# get('/foos/123')
|
65
|
+
# get('/foos/123/versions')
|
66
|
+
# get('/foos/123/versions/abc')
|
67
|
+
# get('/shiny_foos', :color => 'green')
|
68
|
+
#
|
69
|
+
# See also: {REST API}[http://getcloudkit.com/rest-api.html]
|
70
|
+
#
|
71
|
+
def get(uri, options={})
|
72
|
+
return invalid_entity_type if !valid_collection_type?(collection_type(uri))
|
73
|
+
return meta if meta_uri?(uri)
|
74
|
+
return resource_collection(uri, options) if resource_collection_uri?(uri)
|
75
|
+
return resource(uri, options) if resource_uri?(uri)
|
76
|
+
return version_collection(uri, options) if version_collection_uri?(uri)
|
77
|
+
return resource_version(uri, options) if resource_version_uri?(uri)
|
78
|
+
return view(uri, options) if view_uri?(uri)
|
79
|
+
status_404
|
80
|
+
end
|
81
|
+
|
82
|
+
# Retrieve the same items as the get method, minus the content/body. Using
|
83
|
+
# this method on a single resource URI performs a slight optimization due
|
84
|
+
# to the way CloudKit stores its ETags and Last-Modified information on
|
85
|
+
# write.
|
86
|
+
def head(uri, options={})
|
87
|
+
return invalid_entity_type unless @collections.include?(collection_type(uri))
|
88
|
+
if resource_uri?(uri) || resource_version_uri?(uri)
|
89
|
+
# ETag and Last-Modified are already stored for single items, so a slight
|
90
|
+
# optimization can be made for HEAD requests.
|
91
|
+
result = @db[store_key].
|
92
|
+
select(:etag, :last_modified, :deleted).
|
93
|
+
filter(options.merge(:uri => uri))
|
94
|
+
if result.any?
|
95
|
+
result = result.first
|
96
|
+
return status_410.head if result[:deleted]
|
97
|
+
return response(200, '', result[:etag], result[:last_modified])
|
98
|
+
end
|
99
|
+
status_404.head
|
100
|
+
else
|
101
|
+
get(uri, options).head
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Update or create a resource at the specified URI. If the resource already
|
106
|
+
# exists, an :etag option is required.
|
107
|
+
def put(uri, options={})
|
108
|
+
methods = methods_for_uri(uri)
|
109
|
+
return status_405(methods) unless methods.include?('PUT')
|
110
|
+
return invalid_entity_type unless @collections.include?(collection_type(uri))
|
111
|
+
return data_required unless options[:json]
|
112
|
+
current_resource = resource(uri, options.excluding(:json, :etag, :remote_user))
|
113
|
+
return update_resource(uri, options) if current_resource.status == 200
|
114
|
+
create_resource(uri, options)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a resource in a given collection.
|
118
|
+
def post(uri, options={})
|
119
|
+
methods = methods_for_uri(uri)
|
120
|
+
return status_405(methods) unless methods.include?('POST')
|
121
|
+
return invalid_entity_type unless @collections.include?(collection_type(uri))
|
122
|
+
return data_required unless options[:json]
|
123
|
+
uri = "#{collection_uri_fragment(uri)}/#{UUID.generate}"
|
124
|
+
create_resource(uri, options)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Delete the resource specified by the URI. Requires the :etag option.
|
128
|
+
def delete(uri, options={})
|
129
|
+
methods = methods_for_uri(uri)
|
130
|
+
return status_405(methods) unless methods.include?('DELETE')
|
131
|
+
return invalid_entity_type unless @collections.include?(collection_type(uri))
|
132
|
+
return etag_required unless options[:etag]
|
133
|
+
original = @db[store_key].
|
134
|
+
filter(options.excluding(:etag).merge(:uri => uri))
|
135
|
+
if original.any?
|
136
|
+
item = original.first
|
137
|
+
return status_404 unless item[:remote_user] == options[:remote_user]
|
138
|
+
return status_410 if item[:deleted]
|
139
|
+
return status_412 if item[:etag] != options[:etag]
|
140
|
+
version_uri = ''
|
141
|
+
@db.transaction do
|
142
|
+
version_uri = "#{item[:uri]}/versions/#{item[:etag]}"
|
143
|
+
original.update(:uri => version_uri)
|
144
|
+
@db[store_key].insert(
|
145
|
+
:uri => item[:uri],
|
146
|
+
:collection_reference => item[:collection_reference],
|
147
|
+
:resource_reference => item[:resource_reference],
|
148
|
+
:remote_user => item[:remote_user],
|
149
|
+
:content => item[:content],
|
150
|
+
:deleted => true)
|
151
|
+
unmap(uri)
|
152
|
+
end
|
153
|
+
return json_meta_response(200, version_uri, item[:etag], item[:last_modified])
|
154
|
+
end
|
155
|
+
status_404
|
156
|
+
end
|
157
|
+
|
158
|
+
# Build a response containing the allowed methods for a given URI.
|
159
|
+
def options(uri)
|
160
|
+
methods = methods_for_uri(uri)
|
161
|
+
allow(methods)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Return a list of allowed methods for a given URI.
|
165
|
+
def methods_for_uri(uri)
|
166
|
+
if meta_uri?(uri)
|
167
|
+
meta_methods
|
168
|
+
elsif resource_collection_uri?(uri)
|
169
|
+
resource_collection_methods
|
170
|
+
elsif resource_uri?(uri)
|
171
|
+
resource_methods
|
172
|
+
elsif version_collection_uri?(uri)
|
173
|
+
version_collection_methods
|
174
|
+
elsif resource_version_uri?(uri)
|
175
|
+
resource_version_methods
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Return the list of methods allowed for the cloudkit-meta URI.
|
180
|
+
def meta_methods
|
181
|
+
@meta_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
182
|
+
end
|
183
|
+
|
184
|
+
# Return the list of methods allowed for a resource collection.
|
185
|
+
def resource_collection_methods
|
186
|
+
@resource_collection_methods ||= http_methods.excluding('PUT', 'DELETE')
|
187
|
+
end
|
188
|
+
|
189
|
+
# Return the list of methods allowed on an individual resource.
|
190
|
+
def resource_methods
|
191
|
+
@resource_methods ||= http_methods.excluding('POST')
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return the list of methods allowed on a version history collection.
|
195
|
+
def version_collection_methods
|
196
|
+
@version_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
197
|
+
end
|
198
|
+
|
199
|
+
# Return the list of methods allowed on a resource version.
|
200
|
+
def resource_version_methods
|
201
|
+
@resource_version_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
202
|
+
end
|
203
|
+
|
204
|
+
# Return true if this store implements a given HTTP method.
|
205
|
+
def implements?(http_method)
|
206
|
+
http_methods.include?(http_method.upcase)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Return the list of HTTP methods supported by this Store.
|
210
|
+
def http_methods
|
211
|
+
['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
212
|
+
end
|
213
|
+
|
214
|
+
# Return the resource collection URI fragment.
|
215
|
+
# Example: collection_uri_fragment('/foos/123') => '/foos
|
216
|
+
def collection_uri_fragment(uri)
|
217
|
+
"/#{uri_components(uri)[0]}" rescue nil
|
218
|
+
end
|
219
|
+
|
220
|
+
# Return the resource collection referenced by a URI.
|
221
|
+
# Example: collection_type('/foos/123') => :foos
|
222
|
+
def collection_type(uri)
|
223
|
+
uri_components(uri)[0].to_sym rescue nil
|
224
|
+
end
|
225
|
+
|
226
|
+
# Return the URI for the current version of a resource.
|
227
|
+
# Example: current_resource_uri('/foos/123/versions/abc') => '/foos/123'
|
228
|
+
def current_resource_uri(uri)
|
229
|
+
"/#{uri_components(uri)[0..1].join('/')}" rescue nil
|
230
|
+
end
|
231
|
+
|
232
|
+
# Splits a URI into its components
|
233
|
+
def uri_components(uri)
|
234
|
+
uri.split('/').reject{|x| x == '' || x == nil} rescue []
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns true if URI matches /cloudkit-meta
|
238
|
+
def meta_uri?(uri)
|
239
|
+
c = uri_components(uri)
|
240
|
+
return c.size == 1 && c[0] == 'cloudkit-meta'
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns true if URI matches /{collection}
|
244
|
+
def resource_collection_uri?(uri)
|
245
|
+
c = uri_components(uri)
|
246
|
+
return c.size == 1 && @collections.include?(c[0].to_sym)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns true if URI matches /{collection}/{uuid}
|
250
|
+
def resource_uri?(uri)
|
251
|
+
c = uri_components(uri)
|
252
|
+
return c.size == 2 && @collections.include?(c[0].to_sym)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns true if URI matches /{collection}/{uuid}/versions
|
256
|
+
def version_collection_uri?(uri)
|
257
|
+
c = uri_components(uri)
|
258
|
+
return c.size == 3 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns true if URI matches /{collection}/{uuid}/versions/{etag}
|
262
|
+
def resource_version_uri?(uri)
|
263
|
+
c = uri_components(uri)
|
264
|
+
return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns true if URI matches /{view}
|
268
|
+
def view_uri?(uri)
|
269
|
+
c = uri_components(uri)
|
270
|
+
return c.size == 1 && @views && @views.map{|v| v.name}.include?(c[0].to_sym)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Return an array containing the response for each URI in a list.
|
274
|
+
def resolve_uris(uris)
|
275
|
+
result = []
|
276
|
+
uris.each do |uri|
|
277
|
+
result << get(uri)
|
278
|
+
end
|
279
|
+
result
|
280
|
+
end
|
281
|
+
|
282
|
+
# Clear all contents of the store. Used mostly for testing.
|
283
|
+
def reset!
|
284
|
+
@db.schema.keys.each do |table|
|
285
|
+
@db[table].delete
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Return the version number of this Store.
|
290
|
+
def version; 1; end
|
291
|
+
|
292
|
+
protected
|
293
|
+
|
294
|
+
# Return the list of collections managed by this Store.
|
295
|
+
def meta
|
296
|
+
json = JSON.generate(:uris => @collections.map{|t| "/#{t}"})
|
297
|
+
response(200, json, build_etag(json))
|
298
|
+
end
|
299
|
+
|
300
|
+
# Return a list of resource URIs for the given collection URI.
|
301
|
+
def resource_collection(uri, options)
|
302
|
+
result = @db[store_key].
|
303
|
+
select(:uri, :last_modified).
|
304
|
+
filter(options.excluding(:offset, :limit).merge(:deleted => false)).
|
305
|
+
filter(:collection_reference => collection_uri_fragment(uri)).
|
306
|
+
filter('resource_reference = uri').
|
307
|
+
reverse_order(:id)
|
308
|
+
bundle_collection_result(uri, options, result)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Return the resource for the given URI. Return 404 if not found or if
|
312
|
+
# protected and unauthorized, 410 if authorized but deleted.
|
313
|
+
def resource(uri, options)
|
314
|
+
result = @db[store_key].
|
315
|
+
select(:content, :etag, :last_modified, :deleted).
|
316
|
+
filter(options.merge!(:uri => uri))
|
317
|
+
if result.any?
|
318
|
+
result = result.first
|
319
|
+
return status_410 if result[:deleted]
|
320
|
+
return response(200, result[:content], result[:etag], result[:last_modified])
|
321
|
+
end
|
322
|
+
status_404
|
323
|
+
end
|
324
|
+
|
325
|
+
# Return a collection of URIs for all versions of a resource including the
|
326
|
+
#current version. Sorted by Last-Modified date in descending order.
|
327
|
+
def version_collection(uri, options)
|
328
|
+
found = @db[store_key].
|
329
|
+
select(:uri).
|
330
|
+
filter(options.excluding(:offset, :limit).merge(
|
331
|
+
:uri => current_resource_uri(uri)))
|
332
|
+
return status_404 unless found.any?
|
333
|
+
result = @db[store_key].
|
334
|
+
select(:uri, :last_modified).
|
335
|
+
filter(:resource_reference => current_resource_uri(uri)).
|
336
|
+
filter(options.excluding(:offset, :limit).merge(:deleted => false)).
|
337
|
+
reverse_order(:id)
|
338
|
+
bundle_collection_result(uri, options, result)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Return a specific version of a resource.
|
342
|
+
def resource_version(uri, options)
|
343
|
+
result = @db[store_key].
|
344
|
+
select(:content, :etag, :last_modified).
|
345
|
+
filter(options.merge(:uri => uri))
|
346
|
+
return status_404 unless result.any?
|
347
|
+
result = result.first
|
348
|
+
response(200, result[:content], result[:etag], result[:last_modified])
|
349
|
+
end
|
350
|
+
|
351
|
+
# Return a list of URIs for all resources matching the list of key value
|
352
|
+
# pairs provided in the options arg.
|
353
|
+
def view(uri, options)
|
354
|
+
result = @db[collection_type(uri)].
|
355
|
+
select(:uri).
|
356
|
+
filter(options.excluding(:offset, :limit))
|
357
|
+
bundle_collection_result(uri, options, result)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Create a resource at the specified URI.
|
361
|
+
def create_resource(uri, options)
|
362
|
+
data = JSON.parse(options[:json]) rescue (return status_422)
|
363
|
+
etag = UUID.generate
|
364
|
+
last_modified = timestamp
|
365
|
+
@db[store_key].insert(
|
366
|
+
:uri => uri,
|
367
|
+
:collection_reference => collection_uri_fragment(uri),
|
368
|
+
:resource_reference => uri,
|
369
|
+
:etag => etag,
|
370
|
+
:last_modified => last_modified,
|
371
|
+
:remote_user => options[:remote_user],
|
372
|
+
:content => options[:json])
|
373
|
+
map(uri, data)
|
374
|
+
json_meta_response(201, uri, etag, last_modified)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Update the resource at the specified URI. Requires the :etag option.
|
378
|
+
def update_resource(uri, options)
|
379
|
+
data = JSON.parse(options[:json]) rescue (return status_422)
|
380
|
+
original = @db[store_key].
|
381
|
+
filter(options.excluding(:json, :etag).merge(:uri => uri))
|
382
|
+
if original.any?
|
383
|
+
item = original.first
|
384
|
+
return status_404 unless item[:remote_user] == options[:remote_user]
|
385
|
+
return etag_required unless options[:etag]
|
386
|
+
return status_412 unless options[:etag] == item[:etag]
|
387
|
+
etag = UUID.generate
|
388
|
+
last_modified = timestamp
|
389
|
+
@db.transaction do
|
390
|
+
original.update(:uri => "#{uri}/versions/#{item[:etag]}")
|
391
|
+
@db[store_key].insert(
|
392
|
+
:uri => uri,
|
393
|
+
:collection_reference => item[:collection_reference],
|
394
|
+
:resource_reference => item[:resource_reference],
|
395
|
+
:etag => etag,
|
396
|
+
:last_modified => last_modified,
|
397
|
+
:remote_user => options[:remote_user],
|
398
|
+
:content => options[:json])
|
399
|
+
end
|
400
|
+
map(uri, data)
|
401
|
+
return json_meta_response(200, uri, etag, last_modified)
|
402
|
+
end
|
403
|
+
status_404
|
404
|
+
end
|
405
|
+
|
406
|
+
# Bundle a collection of results as a list of URIs for the response.
|
407
|
+
def bundle_collection_result(uri, options, result)
|
408
|
+
total = result.count
|
409
|
+
offset = options[:offset].try(:to_i) || 0
|
410
|
+
max = options[:limit] ? offset + options[:limit].to_i : total
|
411
|
+
list = result.all[offset...max].map{|r| r[:uri]}
|
412
|
+
json = uri_list(list, total, offset)
|
413
|
+
last_modified = result.first[:last_modified] if result.any?
|
414
|
+
response(200, json, build_etag(json), last_modified)
|
415
|
+
end
|
416
|
+
|
417
|
+
# Generate a JSON URI list.
|
418
|
+
def uri_list(list, total, offset)
|
419
|
+
JSON.generate(:total => total, :offset => offset, :uris => list)
|
420
|
+
end
|
421
|
+
|
422
|
+
# Build an ETag for a collection. ETags are generated on write as an
|
423
|
+
# optimization for GETs. This method is used for collections of resources
|
424
|
+
# where the optimization is not practical.
|
425
|
+
def build_etag(data)
|
426
|
+
MD5::md5(data.to_s).hexdigest
|
427
|
+
end
|
428
|
+
|
429
|
+
# Returns true if the collection type represents a view.
|
430
|
+
def is_view?(collection_type)
|
431
|
+
@views && @views.map{|v| v.name}.include?(collection_type)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Returns true if the collection type is valid for this Store.
|
435
|
+
def valid_collection_type?(collection_type)
|
436
|
+
@collections.include?(collection_type) ||
|
437
|
+
is_view?(collection_type) ||
|
438
|
+
collection_type.to_s == 'cloudkit-meta'
|
439
|
+
end
|
440
|
+
|
441
|
+
# Delegates the mapping of data from a resource into a view.
|
442
|
+
def map(uri, data)
|
443
|
+
@views.each{|view| view.map(@db, collection_type(uri), uri, data)} if @views
|
444
|
+
end
|
445
|
+
|
446
|
+
# Delegates removal of view data.
|
447
|
+
def unmap(uri)
|
448
|
+
@views.each{|view| view.unmap(@db, collection_type(uri), uri)} if @views
|
449
|
+
end
|
450
|
+
|
451
|
+
# Return a HTTP date representing 'now.'
|
452
|
+
def timestamp
|
453
|
+
Time.now.httpdate
|
454
|
+
end
|
455
|
+
|
456
|
+
# Return the adapter instance used by this Store.
|
457
|
+
def db; @db; end
|
458
|
+
end
|
459
|
+
end
|