cloudkit 0.10.1 → 0.11.0

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