cloudkit 0.10.1 → 0.11.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.
Files changed (58) hide show
  1. data/CHANGES +11 -0
  2. data/README +7 -6
  3. data/Rakefile +13 -6
  4. data/TODO +7 -5
  5. data/cloudkit.gemspec +23 -23
  6. data/doc/curl.html +2 -2
  7. data/doc/index.html +4 -6
  8. data/examples/5.ru +2 -3
  9. data/examples/TOC +1 -3
  10. data/lib/cloudkit.rb +17 -10
  11. data/lib/cloudkit/constants.rb +0 -6
  12. data/lib/cloudkit/exceptions.rb +10 -0
  13. data/lib/cloudkit/flash_session.rb +1 -3
  14. data/lib/cloudkit/oauth_filter.rb +8 -16
  15. data/lib/cloudkit/oauth_store.rb +9 -13
  16. data/lib/cloudkit/openid_filter.rb +25 -7
  17. data/lib/cloudkit/openid_store.rb +14 -17
  18. data/lib/cloudkit/request.rb +6 -1
  19. data/lib/cloudkit/service.rb +15 -15
  20. data/lib/cloudkit/store.rb +97 -284
  21. data/lib/cloudkit/store/memory_table.rb +105 -0
  22. data/lib/cloudkit/store/resource.rb +256 -0
  23. data/lib/cloudkit/store/response_helpers.rb +0 -1
  24. data/lib/cloudkit/uri.rb +88 -0
  25. data/lib/cloudkit/user_store.rb +7 -14
  26. data/spec/ext_spec.rb +76 -0
  27. data/spec/flash_session_spec.rb +20 -0
  28. data/spec/memory_table_spec.rb +86 -0
  29. data/spec/oauth_filter_spec.rb +326 -0
  30. data/spec/oauth_store_spec.rb +10 -0
  31. data/spec/openid_filter_spec.rb +64 -0
  32. data/spec/openid_store_spec.rb +101 -0
  33. data/spec/rack_builder_spec.rb +39 -0
  34. data/spec/request_spec.rb +185 -0
  35. data/spec/resource_spec.rb +291 -0
  36. data/spec/service_spec.rb +974 -0
  37. data/{test/helper.rb → spec/spec_helper.rb} +14 -2
  38. data/spec/store_spec.rb +10 -0
  39. data/spec/uri_spec.rb +93 -0
  40. data/spec/user_store_spec.rb +10 -0
  41. data/spec/util_spec.rb +11 -0
  42. metadata +37 -61
  43. data/examples/6.ru +0 -10
  44. data/lib/cloudkit/store/adapter.rb +0 -8
  45. data/lib/cloudkit/store/extraction_view.rb +0 -57
  46. data/lib/cloudkit/store/sql_adapter.rb +0 -36
  47. data/test/ext_test.rb +0 -76
  48. data/test/flash_session_test.rb +0 -22
  49. data/test/oauth_filter_test.rb +0 -331
  50. data/test/oauth_store_test.rb +0 -12
  51. data/test/openid_filter_test.rb +0 -60
  52. data/test/openid_store_test.rb +0 -12
  53. data/test/rack_builder_test.rb +0 -41
  54. data/test/request_test.rb +0 -197
  55. data/test/service_test.rb +0 -971
  56. data/test/store_test.rb +0 -93
  57. data/test/user_store_test.rb +0 -12
  58. data/test/util_test.rb +0 -13
@@ -1,42 +1,38 @@
1
1
  module CloudKit
2
2
 
3
3
  # An OAuthStore is a thin abstraction around CloudKit::Store, providing
4
- # consistent collection names, and allowing automatic migrations in later
4
+ # consistent collection names, and allowing automatic upgrades in later
5
5
  # releases if needed.
6
6
  class OAuthStore
7
7
  @@store = nil
8
8
 
9
9
  # Initialize a Store for use with OAuth middleware. Load the static consumer
10
10
  # resource if it does not exist.
11
- def initialize(uri=nil)
11
+ def initialize
12
12
  @@store = Store.new(
13
13
  :collections => [
14
14
  :cloudkit_oauth_nonces,
15
15
  :cloudkit_oauth_tokens,
16
16
  :cloudkit_oauth_request_tokens,
17
- :cloudkit_oauth_consumers],
18
- :adapter => SQLAdapter.new(uri)) unless @@store
17
+ :cloudkit_oauth_consumers]
18
+ ) unless @@store
19
19
  load_static_consumer
20
20
  end
21
21
 
22
22
  def get(uri, options={}) #:nodoc:
23
- @@store.get(uri, options)
23
+ @@store.get(CloudKit::URI.new(uri), options)
24
24
  end
25
25
 
26
26
  def post(uri, options={}) #:nodoc:
27
- @@store.post(uri, options)
27
+ @@store.post(CloudKit::URI.new(uri), options)
28
28
  end
29
29
 
30
30
  def put(uri, options={}) #:nodoc:
31
- @@store.put(uri, options)
31
+ @@store.put(CloudKit::URI.new(uri), options)
32
32
  end
33
33
 
34
34
  def delete(uri, options={}) #:nodoc:
35
- @@store.delete(uri, options)
36
- end
37
-
38
- def reset! #:nodoc:
39
- @@store.reset!
35
+ @@store.delete(CloudKit::URI.new(uri), options)
40
36
  end
41
37
 
42
38
  # Return the version number for this store.
@@ -46,7 +42,7 @@ module CloudKit
46
42
  # See the OAuth Discovery spec for more info on static consumers.
47
43
  def load_static_consumer
48
44
  json = JSON.generate(:secret => '')
49
- @@store.put('/cloudkit_oauth_consumers/cloudkitconsumer', :json => json)
45
+ @@store.put(CloudKit::URI.new('/cloudkit_oauth_consumers/cloudkitconsumer'), :json => json)
50
46
  end
51
47
  end
52
48
  end
@@ -3,6 +3,11 @@ module CloudKit
3
3
  # An OpenIDFilter provides OpenID authentication, listening for upstream
4
4
  # OAuth authentication and bypassing if already authorized.
5
5
  #
6
+ # The root URI, "/", is always bypassed. More URIs can also be bypassed using
7
+ # the :allow option:
8
+ #
9
+ # use OpenIDFilter, :allow => ['/foo', '/bar']
10
+ #
6
11
  # Responds to the following URIs:
7
12
  # /login
8
13
  # /logout
@@ -15,14 +20,14 @@ module CloudKit
15
20
  @@store = nil
16
21
 
17
22
  def initialize(app, options={})
18
- @app = app; @options = options
23
+ @app = app
24
+ @options = options
19
25
  end
20
26
 
21
27
  def call(env)
22
28
  @@lock.synchronize do
23
- @@store = OpenIDStore.new(env[CLOUDKIT_STORAGE_URI])
24
- @users = UserStore.new(env[CLOUDKIT_STORAGE_URI])
25
- @@store.get_association('x') rescue nil # refresh sqlite3
29
+ @@store = OpenIDStore.new
30
+ @users = UserStore.new
26
31
  end unless @@store
27
32
 
28
33
  request = Request.new(env)
@@ -34,7 +39,7 @@ module CloudKit
34
39
  when r(:get, '/openid_complete'); complete_openid_login(request)
35
40
  when r(:post, request.logout_url); logout(request)
36
41
  else
37
- if (root_request?(request) || valid_auth_key?(request) || logged_in?(request))
42
+ if bypass?(request)
38
43
  @app.call(env)
39
44
  else
40
45
  if request.env[CLOUDKIT_AUTH_CHALLENGE]
@@ -103,7 +108,8 @@ module CloudKit
103
108
  end
104
109
 
105
110
  result = @users.get(
106
- '/cloudkit_login_view',
111
+ '/cloudkit_users',
112
+ # '/cloudkit_login_view',
107
113
  :identity_url => idp_response.endpoint.claimed_id)
108
114
  user_uris = result.parsed_content['uris']
109
115
 
@@ -183,7 +189,8 @@ module CloudKit
183
189
  def valid_remember_me_token?(request)
184
190
  return false unless token = request.cookies['remember_me']
185
191
 
186
- result = @users.get('/cloudkit_login_view', :remember_me_token => token)
192
+ # result = @users.get('/cloudkit_login_view', :remember_me_token => token)
193
+ result = @users.get('/cloudkit_users', :remember_me_token => token)
187
194
  return false unless result.status == 200
188
195
 
189
196
  user_uris = result.parsed_content['uris']
@@ -204,5 +211,16 @@ module CloudKit
204
211
  def two_weeks_from_now
205
212
  Time.now.to_i+1209600
206
213
  end
214
+
215
+ def allow?(uri)
216
+ @options[:allow] && @options[:allow].include?(uri)
217
+ end
218
+
219
+ def bypass?(request)
220
+ root_request?(request) ||
221
+ allow?(request.path_info) ||
222
+ valid_auth_key?(request) ||
223
+ logged_in?(request)
224
+ end
207
225
  end
208
226
  end
@@ -6,24 +6,18 @@ module CloudKit
6
6
  class OpenIDStore < OpenID::Store::Interface
7
7
  @@store = nil
8
8
 
9
- # Initialize an OpenIDStore and its required views.
10
- def initialize(uri=nil)
9
+ # Initialize an OpenIDStore.
10
+ def initialize
11
11
  unless @@store
12
- association_view = ExtractionView.new(
13
- :cloudkit_openid_server_handles,
14
- :observe => :cloudkit_openid_associations,
15
- :extract => [:server_url, :handle])
16
12
  @@store = Store.new(
17
- :collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces],
18
- :views => [association_view],
19
- :adapter => SQLAdapter.new(uri))
13
+ :collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces])
20
14
  end
21
15
  end
22
16
 
23
17
  def get_association(server_url, handle=nil) #:nodoc:
24
18
  options = {:server_url => server_url}
25
19
  options.merge!(:handle => Base64.encode64(handle)) if (handle && handle != '')
26
- result = @@store.get('/cloudkit_openid_server_handles', options)
20
+ result = @@store.get(CloudKit::URI.new('/cloudkit_openid_associations'), options)
27
21
  return nil unless result.status == 200
28
22
  return nil if result.parsed_content['total'] == 0
29
23
 
@@ -42,7 +36,7 @@ module CloudKit
42
36
 
43
37
  def remove_association(server_url, handle) #:nodoc:
44
38
  result = @@store.get(
45
- '/cloudkit_openid_server_handles',
39
+ CloudKit::URI.new('/cloudkit_openid_associations'),
46
40
  :server_url => server_url,
47
41
  :handle => Base64.encode64(handle))
48
42
  return nil unless result.status == 200
@@ -52,7 +46,7 @@ module CloudKit
52
46
 
53
47
  uris = result.parsed_content['uris']
54
48
  responses.each_with_index do |r, index|
55
- @@store.delete(uris[index], :etag => r.etag)
49
+ @@store.delete(CloudKit::URI.new(uris[index]), :etag => r.etag)
56
50
  end
57
51
  end
58
52
 
@@ -65,18 +59,18 @@ module CloudKit
65
59
  :issued => association.issued.to_i,
66
60
  :lifetime => association.lifetime,
67
61
  :assoc_type => association.assoc_type)
68
- result = @@store.post('/cloudkit_openid_associations', :json => json)
62
+ result = @@store.post(CloudKit::URI.new('/cloudkit_openid_associations'), :json => json)
69
63
  return (result.status == 201)
70
64
  end
71
65
 
72
66
  def use_nonce(server_url, timestamp, salt) #:nodoc:
73
67
  return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
74
68
 
75
- fragment = URI.escape(
69
+ fragment = ::URI.escape(
76
70
  [server_url, timestamp, salt].join('-'),
77
- Regexp.union(URI::REGEXP::UNSAFE, '/', ':'))
71
+ Regexp.union(::URI::REGEXP::UNSAFE, '/', ':'))
78
72
  uri = "/cloudkit_openid_nonces/#{fragment}"
79
- result = @@store.put(uri, :json => '{}')
73
+ result = @@store.put(CloudKit::URI.new(uri), :json => '{}')
80
74
  return (result.status == 201)
81
75
  end
82
76
 
@@ -92,10 +86,13 @@ module CloudKit
92
86
  # TODO
93
87
  end
94
88
 
89
+ # Return the version number for this store.
90
+ def version; 1; end
91
+
95
92
  protected
96
93
 
97
94
  def resolve_associations(parsed_content) #:nodoc:
98
- uri_list = parsed_content['uris']
95
+ uri_list = parsed_content['uris'].map! { |u| CloudKit::URI.new(u) }
99
96
  association_responses = @@store.resolve_uris(uri_list)
100
97
  return association_responses, association_responses.map{|a| a.parsed_content}
101
98
  end
@@ -20,6 +20,11 @@ module CloudKit
20
20
  self.body.read
21
21
  end
22
22
 
23
+ # Return a CloudKit::URI instance representing the rack request's path info.
24
+ def uri
25
+ @uri ||= CloudKit::URI.new(self.path_info)
26
+ end
27
+
23
28
  # Return true if method, path, and required_params match.
24
29
  def match?(method, path, required_params=[])
25
30
  (request_method == method) &&
@@ -66,7 +71,7 @@ module CloudKit
66
71
 
67
72
  # Unescape a value according to the OAuth spec.
68
73
  def unescape(value)
69
- URI.unescape(value.gsub('+', '%2B'))
74
+ ::URI.unescape(value.gsub('+', '%2B'))
70
75
  end
71
76
 
72
77
  # Return the last path element in the request URI.
@@ -29,6 +29,8 @@ module CloudKit
29
29
 
30
30
  @@lock = Mutex.new
31
31
 
32
+ attr_reader :store
33
+
32
34
  def initialize(app, options)
33
35
  @app = app
34
36
  @collections = options[:collections]
@@ -36,9 +38,7 @@ module CloudKit
36
38
 
37
39
  def call(env)
38
40
  @@lock.synchronize do
39
- @store = Store.new(
40
- :adapter => SQLAdapter.new(env[CLOUDKIT_STORAGE_URI]),
41
- :collections => @collections)
41
+ @store = Store.new(:collections => @collections)
42
42
  end unless @store
43
43
 
44
44
  request = Request.new(env)
@@ -55,7 +55,7 @@ module CloudKit
55
55
 
56
56
  def get(request)
57
57
  response = @store.get(
58
- request.path_info,
58
+ request.uri,
59
59
  {}.filter_merge!(
60
60
  :remote_user => request.current_user,
61
61
  :offset => request['offset'],
@@ -69,14 +69,14 @@ module CloudKit
69
69
  return send(request['_method'].downcase)
70
70
  end
71
71
  @store.post(
72
- request.path_info,
72
+ request.uri,
73
73
  {:json => request.json}.filter_merge!(
74
74
  :remote_user => request.current_user)).to_rack
75
75
  end
76
76
 
77
77
  def put(request)
78
78
  @store.put(
79
- request.path_info,
79
+ request.uri,
80
80
  {:json => request.json}.filter_merge!(
81
81
  :remote_user => request.current_user,
82
82
  :etag => request.if_match)).to_rack
@@ -84,7 +84,7 @@ module CloudKit
84
84
 
85
85
  def delete(request)
86
86
  @store.delete(
87
- request.path_info,
87
+ request.uri,
88
88
  {}.filter_merge!(
89
89
  :remote_user => request.current_user,
90
90
  :etag => request.if_match)).to_rack
@@ -92,7 +92,7 @@ module CloudKit
92
92
 
93
93
  def head(request)
94
94
  response = @store.head(
95
- request.path_info,
95
+ request.uri,
96
96
  {}.filter_merge!(
97
97
  :remote_user => request.current_user,
98
98
  :offset => request['offset'],
@@ -102,15 +102,15 @@ module CloudKit
102
102
  end
103
103
 
104
104
  def options(request)
105
- @store.options(request.path_info).to_rack
105
+ @store.options(request.uri).to_rack
106
106
  end
107
107
 
108
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)
109
+ response['Link'] = versions_link_header(request) if request.uri.resource_uri?
110
+ response['Link'] = resolved_link_header(request) if request.uri.resource_collection_uri?
111
+ response['Link'] = index_link_header(request) if request.uri.resolved_resource_collection_uri?
112
+ response['Link'] = resolved_link_header(request) if request.uri.version_collection_uri?
113
+ response['Link'] = index_link_header(request) if request.uri.resolved_version_collection_uri?
114
114
  end
115
115
 
116
116
  def versions_link_header(request)
@@ -147,7 +147,7 @@ module CloudKit
147
147
 
148
148
  def bypass?(request)
149
149
  collection = @collections.detect{|type| request.path_info.match("/#{type.to_s}")}
150
- !collection && !@store.meta_uri?(request.path_info)
150
+ !collection && !request.uri.meta_uri?
151
151
  end
152
152
  end
153
153
  end
@@ -3,36 +3,22 @@ module CloudKit
3
3
  # A functional storage interface with HTTP semantics and pluggable adapters.
4
4
  class Store
5
5
  include ResponseHelpers
6
+ include CloudKit::Util
6
7
 
7
- # Initialize a new Store, creating its schema if needed. All resources in a
8
- # Store are automatically versioned.
8
+ # Initialize a new Store. All resources in a Store are automatically
9
+ # versioned.
9
10
  #
10
11
  # ===Options
11
- # - :adapter - Optional. An instance of Adapter. Defaults to in-memory SQLite.
12
12
  # - :collections - Array of resource collections to manage.
13
- # - :views - Optional. Array of views to be updated based on JSON content.
14
13
  #
15
14
  # ===Example
16
15
  # store = CloudKit::Store.new(:collections => [:foos, :bars])
17
16
  #
18
- # ===Example
19
- # adapter = CloudKit::SQLAdapter.new('mysql://user:pass@localhost/my_db')
20
- # fruit_color_view = CloudKit::ExtractionView.new(
21
- # :fruits_by_color_and_season,
22
- # :observe => :fruits,
23
- # :extract => [:color, :season])
24
- # store = CloudKit::Store.new(
25
- # :adapter => adapter,
26
- # :collections => [:foos, :fruits],
27
- # :views => [fruit_color_view])
28
- #
29
- # See also: Adapter, ExtractionView, Response
17
+ # See also: Response
30
18
  #
31
19
  def initialize(options)
32
- @db = options[:adapter] || SQLAdapter.new
20
+ CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
33
21
  @collections = options[:collections]
34
- @views = options[:views]
35
- @views.each {|view| view.initialize_storage(@db)} if @views
36
22
  end
37
23
 
38
24
  # Retrieve a resource or collection of resources based on a URI.
@@ -45,7 +31,7 @@ module CloudKit
45
31
  # - :remote_user - Optional. Scopes the dataset if provided.
46
32
  # - :limit - Optional. Default is unlimited. Limit the number of records returned by a collection request.
47
33
  # - :offset - Optional. Start the list of resources in a collection at offset (0-based).
48
- # - :any - Optional. Not a literal ":any", but any key or keys defined as extrations from a view.
34
+ # - :any - Optional. Not a literal ":any", but any key or keys that are top level JSON keys. This is a starting point for future JSONPath/JSONQuery support.
49
35
  #
50
36
  # ===URI Types
51
37
  # /cloudkit-meta
@@ -55,7 +41,6 @@ module CloudKit
55
41
  # /{collection}/{uuid}/versions
56
42
  # /{collection}/{uuid}/versions/_resolved
57
43
  # /{collection}/{uuid}/versions/{etag}
58
- # /{view}
59
44
  #
60
45
  # ===Examples
61
46
  # get('/cloudkit-meta')
@@ -65,20 +50,18 @@ module CloudKit
65
50
  # get('/foos/123')
66
51
  # get('/foos/123/versions')
67
52
  # get('/foos/123/versions/abc')
68
- # get('/shiny_foos', :color => 'green')
69
53
  #
70
54
  # See also: {REST API}[http://getcloudkit.com/rest-api.html]
71
55
  #
72
56
  def get(uri, options={})
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)
57
+ return invalid_entity_type if !valid_collection_type?(uri.collection_type)
58
+ return meta if uri.meta_uri?
59
+ return resource_collection(uri, options) if uri.resource_collection_uri?
60
+ return resolved_resource_collection(uri, options) if uri.resolved_resource_collection_uri?
61
+ return resource(uri, options) if uri.resource_uri?
62
+ return version_collection(uri, options) if uri.version_collection_uri?
63
+ return resolved_version_collection(uri, options) if uri.resolved_version_collection_uri?
64
+ return resource_version(uri, options) if uri.resource_version_uri?
82
65
  status_404
83
66
  end
84
67
 
@@ -87,19 +70,14 @@ module CloudKit
87
70
  # to the way CloudKit stores its ETags and Last-Modified information on
88
71
  # write.
89
72
  def head(uri, options={})
90
- return invalid_entity_type unless @collections.include?(collection_type(uri))
91
- if resource_uri?(uri) || resource_version_uri?(uri)
73
+ return invalid_entity_type unless @collections.include?(uri.collection_type)
74
+ if uri.resource_uri? || uri.resource_version_uri?
92
75
  # ETag and Last-Modified are already stored for single items, so a slight
93
76
  # optimization can be made for HEAD requests.
94
- result = @db[CLOUDKIT_STORE].
95
- select(:etag, :last_modified, :deleted).
96
- filter(options.merge(:uri => uri))
97
- if result.any?
98
- result = result.first
99
- return status_410.head if result[:deleted]
100
- return response(200, '', result[:etag], result[:last_modified])
101
- end
102
- status_404.head
77
+ result = CloudKit::Resource.first(options.merge(:uri => uri.string))
78
+ return status_404.head unless result
79
+ return status_410.head if result.deleted?
80
+ return response(200, '', result.etag, result.last_modified)
103
81
  else
104
82
  get(uri, options).head
105
83
  end
@@ -110,7 +88,7 @@ module CloudKit
110
88
  def put(uri, options={})
111
89
  methods = methods_for_uri(uri)
112
90
  return status_405(methods) unless methods.include?('PUT')
113
- return invalid_entity_type unless @collections.include?(collection_type(uri))
91
+ return invalid_entity_type unless @collections.include?(uri.collection_type)
114
92
  return data_required unless options[:json]
115
93
  current_resource = resource(uri, options.excluding(:json, :etag, :remote_user))
116
94
  return update_resource(uri, options) if current_resource.status == 200
@@ -122,9 +100,8 @@ module CloudKit
122
100
  def post(uri, options={})
123
101
  methods = methods_for_uri(uri)
124
102
  return status_405(methods) unless methods.include?('POST')
125
- return invalid_entity_type unless @collections.include?(collection_type(uri))
103
+ return invalid_entity_type unless @collections.include?(uri.collection_type)
126
104
  return data_required unless options[:json]
127
- uri = "#{collection_uri_fragment(uri)}/#{UUID.generate}"
128
105
  create_resource(uri, options)
129
106
  end
130
107
 
@@ -132,31 +109,15 @@ module CloudKit
132
109
  def delete(uri, options={})
133
110
  methods = methods_for_uri(uri)
134
111
  return status_405(methods) unless methods.include?('DELETE')
135
- return invalid_entity_type unless @collections.include?(collection_type(uri))
112
+ return invalid_entity_type unless @collections.include?(uri.collection_type)
136
113
  return etag_required unless options[:etag]
137
- original = @db[CLOUDKIT_STORE].
138
- filter(options.excluding(:etag).merge(:uri => uri))
139
- if original.any?
140
- item = original.first
141
- return status_404 unless item[:remote_user] == options[:remote_user]
142
- return status_410 if item[:deleted]
143
- return status_412 if item[:etag] != options[:etag]
144
- version_uri = ''
145
- @db.transaction do
146
- version_uri = "#{item[:uri]}/versions/#{item[:etag]}"
147
- original.update(:uri => version_uri)
148
- @db[CLOUDKIT_STORE].insert(
149
- :uri => item[:uri],
150
- :collection_reference => item[:collection_reference],
151
- :resource_reference => item[:resource_reference],
152
- :remote_user => item[:remote_user],
153
- :content => item[:content],
154
- :deleted => true)
155
- unmap(uri)
156
- end
157
- return json_meta_response(200, version_uri, item[:etag], item[:last_modified])
158
- end
159
- status_404
114
+ resource = CloudKit::Resource.first(options.excluding(:etag).merge(:uri => uri.string))
115
+ return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
116
+ return status_410 if resource.deleted?
117
+ return status_412 if resource.etag != options[:etag]
118
+
119
+ resource.delete
120
+ return json_meta_response(200, resource.uri.string, resource.etag, resource.last_modified)
160
121
  end
161
122
 
162
123
  # Build a response containing the allowed methods for a given URI.
@@ -167,13 +128,13 @@ module CloudKit
167
128
 
168
129
  # Return a list of allowed methods for a given URI.
169
130
  def methods_for_uri(uri)
170
- return meta_methods if meta_uri?(uri)
171
- return resource_collection_methods if resource_collection_uri?(uri)
172
- return resolved_resource_collection_methods if resolved_resource_collection_uri?(uri)
173
- return resource_methods if resource_uri?(uri)
174
- return version_collection_methods if version_collection_uri?(uri)
175
- return resolved_version_collection_methods if resolved_version_collection_uri?(uri)
176
- return resource_version_methods if resource_version_uri?(uri)
131
+ return meta_methods if uri.meta_uri?
132
+ return resource_collection_methods if uri.resource_collection_uri?
133
+ return resolved_resource_collection_methods if uri.resolved_resource_collection_uri?
134
+ return resource_methods if uri.resource_uri?
135
+ return version_collection_methods if uri.version_collection_uri?
136
+ return resolved_version_collection_methods if uri.resolved_version_collection_uri?
137
+ return resource_version_methods if uri.resource_version_uri?
177
138
  end
178
139
 
179
140
  # Return the list of methods allowed for the cloudkit-meta URI.
@@ -221,91 +182,17 @@ module CloudKit
221
182
  ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']
222
183
  end
223
184
 
224
- # Return the resource collection URI fragment.
225
- # Example: collection_uri_fragment('/foos/123') => '/foos
226
- def collection_uri_fragment(uri)
227
- "/#{uri_components(uri)[0]}" rescue nil
228
- end
229
-
230
- # Return the resource collection referenced by a URI.
231
- # Example: collection_type('/foos/123') => :foos
232
- def collection_type(uri)
233
- uri_components(uri)[0].to_sym rescue nil
234
- end
235
-
236
- # Return the URI for the current version of a resource.
237
- # Example: current_resource_uri('/foos/123/versions/abc') => '/foos/123'
238
- def current_resource_uri(uri)
239
- "/#{uri_components(uri)[0..1].join('/')}" rescue nil
240
- end
241
-
242
- # Splits a URI into its components
243
- def uri_components(uri)
244
- uri.split('/').reject{|x| x == '' || x == nil} rescue []
245
- end
246
-
247
- # Returns true if URI matches /cloudkit-meta
248
- def meta_uri?(uri)
249
- c = uri_components(uri)
250
- return c.size == 1 && c[0] == 'cloudkit-meta'
251
- end
252
-
253
- # Returns true if URI matches /{collection}
254
- def resource_collection_uri?(uri)
255
- c = uri_components(uri)
256
- return c.size == 1 && @collections.include?(c[0].to_sym)
257
- end
258
-
259
- # Returns true if URI matches /{collection}/_resolved
260
- def resolved_resource_collection_uri?(uri)
261
- c = uri_components(uri)
262
- return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] == '_resolved'
263
- end
264
-
265
- # Returns true if URI matches /{collection}/{uuid}
266
- def resource_uri?(uri)
267
- c = uri_components(uri)
268
- return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] != '_resolved'
269
- end
270
-
271
- # Returns true if URI matches /{collection}/{uuid}/versions
272
- def version_collection_uri?(uri)
273
- c = uri_components(uri)
274
- return c.size == 3 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
275
- end
276
-
277
- # Returns true if URI matches /{collection}/{uuid}/versions/_resolved
278
- def resolved_version_collection_uri?(uri)
279
- c = uri_components(uri)
280
- return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] == '_resolved'
281
- end
282
-
283
- # Returns true if URI matches /{collection}/{uuid}/versions/{etag}
284
- def resource_version_uri?(uri)
285
- c = uri_components(uri)
286
- return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] != '_resolved'
287
- end
288
-
289
- # Returns true if URI matches /{view}
290
- def view_uri?(uri)
291
- c = uri_components(uri)
292
- return c.size == 1 && @views && @views.map{|v| v.name}.include?(c[0].to_sym)
293
- end
294
-
295
185
  # Return an array containing the response for each URI in a list.
296
- def resolve_uris(uris)
186
+ def resolve_uris(uris) # TODO - remove if no longer needed
297
187
  result = []
298
188
  uris.each do |uri|
299
189
  result << get(uri)
300
190
  end
301
191
  result
302
192
  end
303
-
304
- # Clear all contents of the store. Used mostly for testing.
305
- def reset!
306
- @db.schema.keys.each do |table|
307
- @db[table.gsub('`','').to_sym].delete
308
- end
193
+
194
+ def storage_adapter
195
+ CloudKit.storage_adapter
309
196
  end
310
197
 
311
198
  # Return the version number of this Store.
@@ -322,145 +209,96 @@ module CloudKit
322
209
  # Return a list of resource URIs for the given collection URI. Sorted by
323
210
  # Last-Modified date in descending order.
324
211
  def resource_collection(uri, options)
325
- result = @db[CLOUDKIT_STORE].
326
- select(:uri, :last_modified).
327
- filter(options.excluding(:offset, :limit).merge(:deleted => false)).
328
- filter(:collection_reference => collection_uri_fragment(uri)).
329
- filter('resource_reference = uri').
330
- reverse_order(:id)
331
- bundle_collection_result(uri, options, result)
212
+ filter = options.excluding(:offset, :limit).merge(
213
+ :deleted => false,
214
+ :collection_reference => uri.collection_uri_fragment,
215
+ :archived => false)
216
+ result = CloudKit::Resource.current(filter)
217
+ bundle_collection_result(uri.string, options, result)
332
218
  end
333
219
 
334
220
  # Return all documents and their associated metadata for the given
335
221
  # collection URI.
336
222
  def resolved_resource_collection(uri, options)
337
- result = @db[CLOUDKIT_STORE].
338
- filter(options.excluding(:offset, :limit).merge(:deleted => false)).
339
- filter(:collection_reference => collection_uri_fragment(uri)).
340
- filter('resource_reference = uri').
341
- reverse_order(:id)
223
+ result = CloudKit::Resource.current(
224
+ options.excluding(:offset, :limit).merge(
225
+ :collection_reference => uri.collection_uri_fragment))
342
226
  bundle_resolved_collection_result(uri, options, result)
343
227
  end
344
228
 
345
229
  # Return the resource for the given URI. Return 404 if not found or if
346
230
  # protected and unauthorized, 410 if authorized but deleted.
347
231
  def resource(uri, options)
348
- result = @db[CLOUDKIT_STORE].
349
- select(:content, :etag, :last_modified, :deleted).
350
- filter(options.merge!(:uri => uri))
351
- if result.any?
352
- result = result.first
353
- return status_410 if result[:deleted]
354
- return response(200, result[:content], result[:etag], result[:last_modified])
232
+ if resource = CloudKit::Resource.first(options.merge!(:uri => uri.string))
233
+ return status_410 if resource.deleted?
234
+ return response(200, resource.json, resource.etag, resource.last_modified)
355
235
  end
356
236
  status_404
357
237
  end
358
238
 
359
239
  # Return a collection of URIs for all versions of a resource including the
360
- #current version. Sorted by Last-Modified date in descending order.
240
+ # current version. Sorted by Last-Modified date in descending order.
361
241
  def version_collection(uri, options)
362
- found = @db[CLOUDKIT_STORE].
363
- select(:uri).
364
- filter(options.excluding(:offset, :limit).merge(
365
- :uri => current_resource_uri(uri)))
366
- return status_404 unless found.any?
367
- result = @db[CLOUDKIT_STORE].
368
- select(:uri, :last_modified).
369
- filter(:resource_reference => current_resource_uri(uri)).
370
- filter(options.excluding(:offset, :limit).merge(:deleted => false)).
371
- reverse_order(:id)
372
- bundle_collection_result(uri, options, result)
242
+ found = CloudKit::Resource.first(
243
+ options.excluding(:offset, :limit).merge(
244
+ :uri => uri.current_resource_uri))
245
+ return status_404 unless found
246
+ result = CloudKit::Resource.all( # TODO - just use found.versions
247
+ options.excluding(:offset, :limit).merge(
248
+ :resource_reference => uri.current_resource_uri,
249
+ :deleted => false))
250
+ bundle_collection_result(uri.string, options, result)
373
251
  end
374
252
 
375
253
  # Return all document versions and their associated metadata for a given
376
254
  # resource including the current version. Sorted by Last-Modified date in
377
255
  # descending order.
378
256
  def resolved_version_collection(uri, options)
379
- found = @db[CLOUDKIT_STORE].
380
- select(:uri).
381
- filter(options.excluding(:offset, :limit).merge(
382
- :uri => current_resource_uri(uri)))
383
- return status_404 unless found.any?
384
- result = @db[CLOUDKIT_STORE].
385
- filter(:resource_reference => current_resource_uri(uri)).
386
- filter(options.excluding(:offset, :limit).merge(:deleted => false)).
387
- reverse_order(:id)
257
+ found = CloudKit::Resource.first(
258
+ options.excluding(:offset, :limit).merge(
259
+ :uri => uri.current_resource_uri))
260
+ return status_404 unless found
261
+ result = CloudKit::Resource.all(
262
+ options.excluding(:offset, :limit).merge(
263
+ :resource_reference => uri.current_resource_uri,
264
+ :deleted => false))
388
265
  bundle_resolved_collection_result(uri, options, result)
389
266
  end
390
267
 
391
268
  # Return a specific version of a resource.
392
269
  def resource_version(uri, options)
393
- result = @db[CLOUDKIT_STORE].
394
- select(:content, :etag, :last_modified).
395
- filter(options.merge(:uri => uri))
396
- return status_404 unless result.any?
397
- result = result.first
398
- response(200, result[:content], result[:etag], result[:last_modified])
399
- end
400
-
401
- # Return a list of URIs for all resources matching the list of key value
402
- # pairs provided in the options arg.
403
- def view(uri, options)
404
- result = @db[collection_type(uri)].
405
- select(:uri).
406
- filter(options.excluding(:offset, :limit))
407
- bundle_collection_result(uri, options, result)
270
+ result = CloudKit::Resource.first(options.merge(:uri => uri.string))
271
+ return status_404 unless result
272
+ response(200, result.json, result.etag, result.last_modified)
408
273
  end
409
274
 
410
275
  # Create a resource at the specified URI.
411
276
  def create_resource(uri, options)
412
- data = JSON.parse(options[:json]) rescue (return status_422)
413
- etag = UUID.generate
414
- last_modified = timestamp
415
- @db[CLOUDKIT_STORE].insert(
416
- :uri => uri,
417
- :collection_reference => collection_uri_fragment(uri),
418
- :resource_reference => uri,
419
- :etag => etag,
420
- :last_modified => last_modified,
421
- :remote_user => options[:remote_user],
422
- :content => options[:json])
423
- map(uri, data)
424
- json_meta_response(201, uri, etag, last_modified)
277
+ JSON.parse(options[:json]) rescue (return status_422)
278
+ resource = CloudKit::Resource.create(uri, options[:json], options[:remote_user])
279
+ json_meta_response(201, resource.uri.string, resource.etag, resource.last_modified)
425
280
  end
426
281
 
427
282
  # Update the resource at the specified URI. Requires the :etag option.
428
283
  def update_resource(uri, options)
429
- data = JSON.parse(options[:json]) rescue (return status_422)
430
- original = @db[CLOUDKIT_STORE].
431
- filter(options.excluding(:json, :etag).merge(:uri => uri))
432
- if original.any?
433
- item = original.first
434
- return status_404 unless item[:remote_user] == options[:remote_user]
435
- return etag_required unless options[:etag]
436
- return status_412 unless options[:etag] == item[:etag]
437
- etag = UUID.generate
438
- last_modified = timestamp
439
- @db.transaction do
440
- original.update(:uri => "#{uri}/versions/#{item[:etag]}")
441
- @db[CLOUDKIT_STORE].insert(
442
- :uri => uri,
443
- :collection_reference => item[:collection_reference],
444
- :resource_reference => item[:resource_reference],
445
- :etag => etag,
446
- :last_modified => last_modified,
447
- :remote_user => options[:remote_user],
448
- :content => options[:json])
449
- end
450
- map(uri, data)
451
- return json_meta_response(200, uri, etag, last_modified)
452
- end
453
- status_404
284
+ JSON.parse(options[:json]) rescue (return status_422)
285
+ resource = CloudKit::Resource.first(
286
+ options.excluding(:json, :etag).merge(:uri => uri.string))
287
+ return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
288
+ return etag_required unless options[:etag]
289
+ return status_412 unless options[:etag] == resource.etag
290
+ resource.update(options[:json])
291
+ return json_meta_response(200, uri.string, resource.etag, resource.last_modified)
454
292
  end
455
293
 
456
294
  # Bundle a collection of results as a list of URIs for the response.
457
295
  def bundle_collection_result(uri, options, result)
458
- total = result.count
296
+ total = result.size
459
297
  offset = options[:offset].try(:to_i) || 0
460
298
  max = options[:limit] ? offset + options[:limit].to_i : total
461
- list = result.all[offset...max].map{|r| r[:uri]}
299
+ list = result.to_a[offset...max].map{|r| r.uri}
462
300
  json = uri_list(list, total, offset)
463
- last_modified = result.first[:last_modified] if result.any?
301
+ last_modified = result.first.try(:last_modified) if result.any?
464
302
  response(200, json, build_etag(json), last_modified)
465
303
  end
466
304
 
@@ -468,18 +306,18 @@ module CloudKit
468
306
  # metadata (last_modified, uri, etag) that would have accompanied a response
469
307
  # to their singular request.
470
308
  def bundle_resolved_collection_result(uri, options, result)
471
- total = result.count
309
+ total = result.size
472
310
  offset = options[:offset].try(:to_i) || 0
473
311
  max = options[:limit] ? offset + options[:limit].to_i : total
474
- list = result.all[offset...max]
312
+ list = result.to_a[offset...max]
475
313
  json = resource_list(list, total, offset)
476
- last_modified = result.first[:last_modified] if result.any?
314
+ last_modified = result.first.last_modified if result.any?
477
315
  response(200, json, build_etag(json), last_modified)
478
316
  end
479
317
 
480
318
  # Generate a JSON URI list.
481
319
  def uri_list(list, total, offset)
482
- JSON.generate(:total => total, :offset => offset, :uris => list)
320
+ JSON.generate(:total => total, :offset => offset, :uris => list.map { |u| u.string })
483
321
  end
484
322
 
485
323
  # Generate a JSON document list.
@@ -487,10 +325,10 @@ module CloudKit
487
325
  results = []
488
326
  list.each do |resource|
489
327
  results << {
490
- :uri => resource[:uri],
491
- :etag => resource[:etag],
492
- :last_modified => resource[:last_modified],
493
- :document => resource[:content]}
328
+ :uri => resource.uri.string,
329
+ :etag => resource.etag,
330
+ :last_modified => resource.last_modified,
331
+ :document => resource.json}
494
332
  end
495
333
  JSON.generate(:total => total, :offset => offset, :documents => results)
496
334
  end
@@ -502,34 +340,9 @@ module CloudKit
502
340
  MD5::md5(data.to_s).hexdigest
503
341
  end
504
342
 
505
- # Returns true if the collection type represents a view.
506
- def is_view?(collection_type)
507
- @views && @views.map{|v| v.name}.include?(collection_type)
508
- end
509
-
510
343
  # Returns true if the collection type is valid for this Store.
511
344
  def valid_collection_type?(collection_type)
512
- @collections.include?(collection_type) ||
513
- is_view?(collection_type) ||
514
- collection_type.to_s == 'cloudkit-meta'
515
- end
516
-
517
- # Delegates the mapping of data from a resource into a view.
518
- def map(uri, data)
519
- @views.each{|view| view.map(@db, collection_type(uri), uri, data)} if @views
345
+ @collections.include?(collection_type) || collection_type.to_s == 'cloudkit-meta'
520
346
  end
521
-
522
- # Delegates removal of view data.
523
- def unmap(uri)
524
- @views.each{|view| view.unmap(@db, collection_type(uri), uri)} if @views
525
- end
526
-
527
- # Return a HTTP date representing 'now.'
528
- def timestamp
529
- Time.now.httpdate
530
- end
531
-
532
- # Return the adapter instance used by this Store.
533
- def db; @db; end
534
347
  end
535
348
  end