cloudkit 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,12 +20,11 @@ module CloudKit
20
20
  # use CloudKit::OAuthFilter
21
21
  # use CloudKit::OpenIDFilter
22
22
  # use CloudKit::Service, :collections => [:items, :things]
23
- # run lambda{|env| [200, {}, ['Hello']]}
23
+ # run lambda{|env| [200, {'Content-Type' => 'text/html'}, ['Hello']]}
24
24
  #
25
25
  # For more examples, including the use of different storage implementations,
26
26
  # see the Table of Contents in the examples directory.
27
27
  class Service
28
- include Util
29
28
  include ResponseHelpers
30
29
 
31
30
  @@lock = Mutex.new
@@ -38,7 +37,7 @@ module CloudKit
38
37
  def call(env)
39
38
  @@lock.synchronize do
40
39
  @store = Store.new(
41
- :adapter => SQLAdapter.new(env[storage_uri_key]),
40
+ :adapter => SQLAdapter.new(env[CLOUDKIT_STORAGE_URI]),
42
41
  :collections => @collections)
43
42
  end unless @store
44
43
 
@@ -61,7 +60,7 @@ module CloudKit
61
60
  :remote_user => request.current_user,
62
61
  :offset => request['offset'],
63
62
  :limit => request['limit']))
64
- response['Link'] = link_header(request) if @store.resource_uri?(request.path_info)
63
+ inject_link_headers(request, response)
65
64
  response.to_rack
66
65
  end
67
66
 
@@ -71,14 +70,14 @@ module CloudKit
71
70
  end
72
71
  @store.post(
73
72
  request.path_info,
74
- {:json => request.body.string}.filter_merge!(
73
+ {:json => request.json}.filter_merge!(
75
74
  :remote_user => request.current_user)).to_rack
76
75
  end
77
76
 
78
77
  def put(request)
79
78
  @store.put(
80
79
  request.path_info,
81
- {:json => request.body.string}.filter_merge!(
80
+ {:json => request.json}.filter_merge!(
82
81
  :remote_user => request.current_user,
83
82
  :etag => request.if_match)).to_rack
84
83
  end
@@ -98,7 +97,7 @@ module CloudKit
98
97
  :remote_user => request.current_user,
99
98
  :offset => request['offset'],
100
99
  :limit => request['limit']))
101
- response['Link'] = link_header(request) if @store.resource_uri?(request.path_info)
100
+ inject_link_headers(request, response)
102
101
  response.to_rack
103
102
  end
104
103
 
@@ -106,11 +105,30 @@ module CloudKit
106
105
  @store.options(request.path_info).to_rack
107
106
  end
108
107
 
109
- def link_header(request)
108
+ def inject_link_headers(request, response)
109
+ response['Link'] = versions_link_header(request) if @store.resource_uri?(request.path_info)
110
+ response['Link'] = resolved_link_header(request) if @store.resource_collection_uri?(request.path_info)
111
+ response['Link'] = index_link_header(request) if @store.resolved_resource_collection_uri?(request.path_info)
112
+ response['Link'] = resolved_link_header(request) if @store.version_collection_uri?(request.path_info)
113
+ response['Link'] = index_link_header(request) if @store.resolved_version_collection_uri?(request.path_info)
114
+ end
115
+
116
+ def versions_link_header(request)
110
117
  base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{request.path_info}"
111
118
  "<#{base_url}/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\""
112
119
  end
113
120
 
121
+ def resolved_link_header(request)
122
+ base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{request.path_info}"
123
+ "<#{base_url}/_resolved>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/resolved\""
124
+ end
125
+
126
+ def index_link_header(request)
127
+ index_path = request.path_info.sub(/\/_resolved(\/)*$/, '')
128
+ base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{index_path}"
129
+ "<#{base_url}>; rel=\"index\""
130
+ end
131
+
114
132
  def auth_missing?(request)
115
133
  request.current_user == nil
116
134
  end
@@ -2,7 +2,6 @@ module CloudKit
2
2
 
3
3
  # A functional storage interface with HTTP semantics and pluggable adapters.
4
4
  class Store
5
- include CloudKit::Util
6
5
  include ResponseHelpers
7
6
 
8
7
  # Initialize a new Store, creating its schema if needed. All resources in a
@@ -51,8 +50,10 @@ module CloudKit
51
50
  # ===URI Types
52
51
  # /cloudkit-meta
53
52
  # /{collection}
53
+ # /{collection}/_resolved
54
54
  # /{collection}/{uuid}
55
55
  # /{collection}/{uuid}/versions
56
+ # /{collection}/{uuid}/versions/_resolved
56
57
  # /{collection}/{uuid}/versions/{etag}
57
58
  # /{view}
58
59
  #
@@ -69,13 +70,15 @@ module CloudKit
69
70
  # See also: {REST API}[http://getcloudkit.com/rest-api.html]
70
71
  #
71
72
  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)
73
+ return invalid_entity_type if !valid_collection_type?(collection_type(uri))
74
+ return meta if meta_uri?(uri)
75
+ return resource_collection(uri, options) if resource_collection_uri?(uri)
76
+ return resolved_resource_collection(uri, options) if resolved_resource_collection_uri?(uri)
77
+ return resource(uri, options) if resource_uri?(uri)
78
+ return version_collection(uri, options) if version_collection_uri?(uri)
79
+ return resolved_version_collection(uri, options) if resolved_version_collection_uri?(uri)
80
+ return resource_version(uri, options) if resource_version_uri?(uri)
81
+ return view(uri, options) if view_uri?(uri)
79
82
  status_404
80
83
  end
81
84
 
@@ -88,7 +91,7 @@ module CloudKit
88
91
  if resource_uri?(uri) || resource_version_uri?(uri)
89
92
  # ETag and Last-Modified are already stored for single items, so a slight
90
93
  # optimization can be made for HEAD requests.
91
- result = @db[store_key].
94
+ result = @db[CLOUDKIT_STORE].
92
95
  select(:etag, :last_modified, :deleted).
93
96
  filter(options.merge(:uri => uri))
94
97
  if result.any?
@@ -130,7 +133,7 @@ module CloudKit
130
133
  return status_405(methods) unless methods.include?('DELETE')
131
134
  return invalid_entity_type unless @collections.include?(collection_type(uri))
132
135
  return etag_required unless options[:etag]
133
- original = @db[store_key].
136
+ original = @db[CLOUDKIT_STORE].
134
137
  filter(options.excluding(:etag).merge(:uri => uri))
135
138
  if original.any?
136
139
  item = original.first
@@ -141,7 +144,7 @@ module CloudKit
141
144
  @db.transaction do
142
145
  version_uri = "#{item[:uri]}/versions/#{item[:etag]}"
143
146
  original.update(:uri => version_uri)
144
- @db[store_key].insert(
147
+ @db[CLOUDKIT_STORE].insert(
145
148
  :uri => item[:uri],
146
149
  :collection_reference => item[:collection_reference],
147
150
  :resource_reference => item[:resource_reference],
@@ -161,19 +164,15 @@ module CloudKit
161
164
  allow(methods)
162
165
  end
163
166
 
164
- # Return a list of allowed methods for a given URI.
167
+ # Return a list of allowed methods for a given URI.
165
168
  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
169
+ return meta_methods if meta_uri?(uri)
170
+ return resource_collection_methods if resource_collection_uri?(uri)
171
+ return resolved_resource_collection_methods if resolved_resource_collection_uri?(uri)
172
+ return resource_methods if resource_uri?(uri)
173
+ return version_collection_methods if version_collection_uri?(uri)
174
+ return resolved_version_collection_methods if resolved_version_collection_uri?(uri)
175
+ return resource_version_methods if resource_version_uri?(uri)
177
176
  end
178
177
 
179
178
  # Return the list of methods allowed for the cloudkit-meta URI.
@@ -186,6 +185,11 @@ module CloudKit
186
185
  @resource_collection_methods ||= http_methods.excluding('PUT', 'DELETE')
187
186
  end
188
187
 
188
+ # Return the list of methods allowed on a resolved resource collection.
189
+ def resolved_resource_collection_methods
190
+ @resolved_resource_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
191
+ end
192
+
189
193
  # Return the list of methods allowed on an individual resource.
190
194
  def resource_methods
191
195
  @resource_methods ||= http_methods.excluding('POST')
@@ -196,6 +200,11 @@ module CloudKit
196
200
  @version_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
197
201
  end
198
202
 
203
+ # Return the list of methods allowed on a resolved version history collection.
204
+ def resolved_version_collection_methods
205
+ @resolved_version_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
206
+ end
207
+
199
208
  # Return the list of methods allowed on a resource version.
200
209
  def resource_version_methods
201
210
  @resource_version_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
@@ -246,10 +255,16 @@ module CloudKit
246
255
  return c.size == 1 && @collections.include?(c[0].to_sym)
247
256
  end
248
257
 
258
+ # Returns true if URI matches /{collection}/_resolved
259
+ def resolved_resource_collection_uri?(uri)
260
+ c = uri_components(uri)
261
+ return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] == '_resolved'
262
+ end
263
+
249
264
  # Returns true if URI matches /{collection}/{uuid}
250
265
  def resource_uri?(uri)
251
266
  c = uri_components(uri)
252
- return c.size == 2 && @collections.include?(c[0].to_sym)
267
+ return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] != '_resolved'
253
268
  end
254
269
 
255
270
  # Returns true if URI matches /{collection}/{uuid}/versions
@@ -258,10 +273,16 @@ module CloudKit
258
273
  return c.size == 3 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
259
274
  end
260
275
 
276
+ # Returns true if URI matches /{collection}/{uuid}/versions/_resolved
277
+ def resolved_version_collection_uri?(uri)
278
+ c = uri_components(uri)
279
+ return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] == '_resolved'
280
+ end
281
+
261
282
  # Returns true if URI matches /{collection}/{uuid}/versions/{etag}
262
283
  def resource_version_uri?(uri)
263
284
  c = uri_components(uri)
264
- return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
285
+ return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] != '_resolved'
265
286
  end
266
287
 
267
288
  # Returns true if URI matches /{view}
@@ -297,9 +318,10 @@ module CloudKit
297
318
  response(200, json, build_etag(json))
298
319
  end
299
320
 
300
- # Return a list of resource URIs for the given collection URI.
321
+ # Return a list of resource URIs for the given collection URI. Sorted by
322
+ # Last-Modified date in descending order.
301
323
  def resource_collection(uri, options)
302
- result = @db[store_key].
324
+ result = @db[CLOUDKIT_STORE].
303
325
  select(:uri, :last_modified).
304
326
  filter(options.excluding(:offset, :limit).merge(:deleted => false)).
305
327
  filter(:collection_reference => collection_uri_fragment(uri)).
@@ -308,10 +330,21 @@ module CloudKit
308
330
  bundle_collection_result(uri, options, result)
309
331
  end
310
332
 
333
+ # Return all documents and their associated metadata for the given
334
+ # collection URI.
335
+ def resolved_resource_collection(uri, options)
336
+ result = @db[CLOUDKIT_STORE].
337
+ filter(options.excluding(:offset, :limit).merge(:deleted => false)).
338
+ filter(:collection_reference => collection_uri_fragment(uri)).
339
+ filter('resource_reference = uri').
340
+ reverse_order(:id)
341
+ bundle_resolved_collection_result(uri, options, result)
342
+ end
343
+
311
344
  # Return the resource for the given URI. Return 404 if not found or if
312
345
  # protected and unauthorized, 410 if authorized but deleted.
313
346
  def resource(uri, options)
314
- result = @db[store_key].
347
+ result = @db[CLOUDKIT_STORE].
315
348
  select(:content, :etag, :last_modified, :deleted).
316
349
  filter(options.merge!(:uri => uri))
317
350
  if result.any?
@@ -325,12 +358,12 @@ module CloudKit
325
358
  # Return a collection of URIs for all versions of a resource including the
326
359
  #current version. Sorted by Last-Modified date in descending order.
327
360
  def version_collection(uri, options)
328
- found = @db[store_key].
361
+ found = @db[CLOUDKIT_STORE].
329
362
  select(:uri).
330
363
  filter(options.excluding(:offset, :limit).merge(
331
364
  :uri => current_resource_uri(uri)))
332
365
  return status_404 unless found.any?
333
- result = @db[store_key].
366
+ result = @db[CLOUDKIT_STORE].
334
367
  select(:uri, :last_modified).
335
368
  filter(:resource_reference => current_resource_uri(uri)).
336
369
  filter(options.excluding(:offset, :limit).merge(:deleted => false)).
@@ -338,9 +371,25 @@ module CloudKit
338
371
  bundle_collection_result(uri, options, result)
339
372
  end
340
373
 
374
+ # Return all document versions and their associated metadata for a given
375
+ # resource including the current version. Sorted by Last-Modified date in
376
+ # descending order.
377
+ def resolved_version_collection(uri, options)
378
+ found = @db[CLOUDKIT_STORE].
379
+ select(:uri).
380
+ filter(options.excluding(:offset, :limit).merge(
381
+ :uri => current_resource_uri(uri)))
382
+ return status_404 unless found.any?
383
+ result = @db[CLOUDKIT_STORE].
384
+ filter(:resource_reference => current_resource_uri(uri)).
385
+ filter(options.excluding(:offset, :limit).merge(:deleted => false)).
386
+ reverse_order(:id)
387
+ bundle_resolved_collection_result(uri, options, result)
388
+ end
389
+
341
390
  # Return a specific version of a resource.
342
391
  def resource_version(uri, options)
343
- result = @db[store_key].
392
+ result = @db[CLOUDKIT_STORE].
344
393
  select(:content, :etag, :last_modified).
345
394
  filter(options.merge(:uri => uri))
346
395
  return status_404 unless result.any?
@@ -362,7 +411,7 @@ module CloudKit
362
411
  data = JSON.parse(options[:json]) rescue (return status_422)
363
412
  etag = UUID.generate
364
413
  last_modified = timestamp
365
- @db[store_key].insert(
414
+ @db[CLOUDKIT_STORE].insert(
366
415
  :uri => uri,
367
416
  :collection_reference => collection_uri_fragment(uri),
368
417
  :resource_reference => uri,
@@ -377,7 +426,7 @@ module CloudKit
377
426
  # Update the resource at the specified URI. Requires the :etag option.
378
427
  def update_resource(uri, options)
379
428
  data = JSON.parse(options[:json]) rescue (return status_422)
380
- original = @db[store_key].
429
+ original = @db[CLOUDKIT_STORE].
381
430
  filter(options.excluding(:json, :etag).merge(:uri => uri))
382
431
  if original.any?
383
432
  item = original.first
@@ -388,7 +437,7 @@ module CloudKit
388
437
  last_modified = timestamp
389
438
  @db.transaction do
390
439
  original.update(:uri => "#{uri}/versions/#{item[:etag]}")
391
- @db[store_key].insert(
440
+ @db[CLOUDKIT_STORE].insert(
392
441
  :uri => uri,
393
442
  :collection_reference => item[:collection_reference],
394
443
  :resource_reference => item[:resource_reference],
@@ -414,11 +463,37 @@ module CloudKit
414
463
  response(200, json, build_etag(json), last_modified)
415
464
  end
416
465
 
466
+ # Bundle a collection of results as a list of documents and the associated
467
+ # metadata (last_modified, uri, etag) that would have accompanied a response
468
+ # to their singular request.
469
+ def bundle_resolved_collection_result(uri, options, result)
470
+ total = result.count
471
+ offset = options[:offset].try(:to_i) || 0
472
+ max = options[:limit] ? offset + options[:limit].to_i : total
473
+ list = result.all[offset...max]
474
+ json = resource_list(list, total, offset)
475
+ last_modified = result.first[:last_modified] if result.any?
476
+ response(200, json, build_etag(json), last_modified)
477
+ end
478
+
417
479
  # Generate a JSON URI list.
418
480
  def uri_list(list, total, offset)
419
481
  JSON.generate(:total => total, :offset => offset, :uris => list)
420
482
  end
421
483
 
484
+ # Generate a JSON document list.
485
+ def resource_list(list, total, offset)
486
+ results = []
487
+ list.each do |resource|
488
+ results << {
489
+ :uri => resource[:uri],
490
+ :etag => resource[:etag],
491
+ :last_modified => resource[:last_modified],
492
+ :document => resource[:content]}
493
+ end
494
+ JSON.generate(:total => total, :offset => offset, :documents => results)
495
+ end
496
+
422
497
  # Build an ETag for a collection. ETags are generated on write as an
423
498
  # optimization for GETs. This method is used for collections of resources
424
499
  # where the optimization is not practical.
@@ -2,7 +2,6 @@ module CloudKit
2
2
 
3
3
  # A common interface for pluggable storage adapters
4
4
  class Adapter
5
- include Util
6
5
 
7
6
  # TODO extract from SQLAdapter
8
7
  end
@@ -23,6 +23,7 @@ module CloudKit
23
23
 
24
24
  # Translate to the standard Rack representation: [status, headers, content]
25
25
  def to_rack
26
+ meta['Content-Length'] = content.length.to_s
26
27
  [status, meta, [content]]
27
28
  end
28
29
 
@@ -20,7 +20,7 @@ module CloudKit
20
20
 
21
21
  # Initialize the HTTP-oriented storage if it does not exist.
22
22
  def initialize_storage
23
- @db.create_table store_key do
23
+ @db.create_table CLOUDKIT_STORE do
24
24
  primary_key :id
25
25
  varchar :uri, :unique => true
26
26
  varchar :etag
@@ -30,7 +30,7 @@ module CloudKit
30
30
  varchar :remote_user
31
31
  text :content
32
32
  boolean :deleted, :default => false
33
- end unless @db.table_exists?(store_key)
33
+ end unless @db.table_exists?(CLOUDKIT_STORE)
34
34
  end
35
35
  end
36
36
  end
@@ -8,7 +8,8 @@ module CloudKit
8
8
  'templates',
9
9
  "#{template.to_s}.erb"))
10
10
  template = ERB.new(template_file.read)
11
- [status, headers, [template.result(binding)]]
11
+ result = template.result(binding)
12
+ Rack::Response.new(result, status, headers).finish
12
13
  end
13
14
 
14
15
  # Build a Rack::Router instance
@@ -20,41 +21,5 @@ module CloudKit
20
21
  def unquote(text)
21
22
  (text =~ /^\".*\"$/) ? text[1..-2] : text
22
23
  end
23
-
24
- # Return the key used to store the authenticated user.
25
- def auth_key; 'cloudkit.user'; end
26
-
27
- # Return the key used to indicate the presence of auth in a stack.
28
- def auth_presence_key; 'cloudkit.auth'; end
29
-
30
- # Return the key used to store auth challenge headers shared between
31
- # OpenID and OAuth middleware.
32
- def challenge_key; 'cloudkit.challenge'; end
33
-
34
- # Return the 'via' key used to announce and track upstream middleware.
35
- def via_key; 'cloudkit.via'; end
36
-
37
- # Return the key used to store the 'flash' in the session.
38
- def flash_key; 'cloudkit.flash'; end
39
-
40
- # Return the 'via' key for the OAuth filter.
41
- def oauth_filter_key; 'cloudkit.filter.oauth'; end
42
-
43
- # Return the 'via' key for the OpenID filter.
44
- def openid_filter_key; 'cloudkit.filter.openid'; end
45
-
46
- # Return the key used to store the shared storage URI for the stack.
47
- def storage_uri_key; 'cloudkit.storage.uri'; end
48
-
49
- # Return the key for the login URL used in OpenID and OAuth middleware
50
- # components.
51
- def login_url_key; 'cloudkit.filter.openid.url.login'; end
52
-
53
- # Return the key for the logout URL used in OpenID and OAuth middleware
54
- # components.
55
- def logout_url_key; 'cloudkit.filter.openid.url.logout'; end
56
-
57
- # Return the outer namespace key for the JSON store.
58
- def store_key; :cloudkit_json_store; end
59
24
  end
60
25
  end