cloudkit 0.9.1 → 0.10.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.
@@ -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