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
@@ -0,0 +1,105 @@
1
+ module CloudKit
2
+
3
+ # A MemoryTable implements the essential pieces of the Rufus Tokyo Table API
4
+ # required for CloudKit's operation. It is basically a hash of hashes with
5
+ # querying capabilities. None of the data is persisted to disk nor is it
6
+ # designed with production use in mind. The primary purpose is to enable
7
+ # testing and development with CloudKit without depending on binary Tokyo
8
+ # Cabinet dependencies.
9
+ #
10
+ # Implementing a new adapter for CloudKit means writing an adapter that
11
+ # passes the specs for this one.
12
+ class MemoryTable
13
+
14
+ # Create a new MemoryTable instance.
15
+ def initialize
16
+ @serial_id = 0
17
+ clear
18
+ end
19
+
20
+ # Create a hash record for the given key. Returns the record if valid or nil
21
+ # otherwise. Records are valid if they are hashses with both string keys and
22
+ # string values.
23
+ def []=(key, record)
24
+ if valid?(record)
25
+ @keys << key unless @hash[key]
26
+ return @hash[key] = record
27
+ end
28
+ nil
29
+ end
30
+
31
+ # Retrieve the hash record for a given key.
32
+ def [](key)
33
+ @hash[key]
34
+ end
35
+
36
+ # Clear the contents of the store.
37
+ def clear
38
+ @hash = {}
39
+ @keys = []
40
+ end
41
+
42
+ # Return an ordered set of all keys in the store.
43
+ def keys
44
+ @keys
45
+ end
46
+
47
+ # Generate a unique ID within the scope of this store.
48
+ def generate_unique_id
49
+ @serial_id += 1
50
+ end
51
+
52
+ # Run a query configured by the provided block. If no block is provided, all
53
+ # records are returned. Each record contains the original hash key/value
54
+ # pairs, plus the primary key (indexed by :pk => value).
55
+ def query(&block)
56
+ return @keys.map { |key| @hash[key].merge(:pk => key) } unless block
57
+ q = MemoryQuery.new
58
+ block.call(q)
59
+ q.run(self)
60
+ end
61
+
62
+ # Simulate a transaction. This development-mode transaction merely yields
63
+ # to its block.
64
+ def transaction
65
+ yield
66
+ end
67
+
68
+ protected
69
+
70
+ def valid?(record)
71
+ return false unless record.is_a?(Hash)
72
+ record.keys.all? { |k| k.is_a?(String) && record[k].is_a?(String) }
73
+ end
74
+
75
+ end
76
+
77
+ # MemoryQuery is used internally by MemoryTable to configure a query and run
78
+ # it against the store.
79
+ class MemoryQuery
80
+
81
+ # Initialize a new MemoryQuery.
82
+ def initialize
83
+ @conditions = []
84
+ end
85
+
86
+ # Run a query against the provided table using the conditions stored in this
87
+ # MemoryQuery instance. Returns all records that match all conditions.
88
+ # Conditions are added using #add_condition.
89
+ def run(table)
90
+ table.keys.inject([]) do |result, key|
91
+ if @conditions.all? { |condition| table[key][condition[0]] == condition[2] }
92
+ result << table[key].merge(:pk => key)
93
+ else
94
+ result
95
+ end
96
+ end
97
+ end
98
+
99
+ # Add a condition to this query. The operator parameter is ignored at this
100
+ # time, assuming only equality.
101
+ def add_condition(key, operator, value)
102
+ @conditions << [key, operator, value]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,256 @@
1
+ module CloudKit
2
+
3
+ # A CloudKit::Resource represents a "resource" in the REST/HTTP sense of the
4
+ # word. It encapsulates a JSON document and its metadata such as it URI, ETag,
5
+ # Last-Modified date, remote user, and its historical versions.
6
+ class Resource
7
+
8
+ attr_reader :uri, :etag, :last_modified, :json, :remote_user
9
+
10
+ # Initialize a new instance of a resource.
11
+ #
12
+ # === Parameters
13
+ # - uri - A CloudKit::URI for the resource or its parent collection.
14
+ # - json - A string representing a valid JSON object.
15
+ # - remote_user - Optional. The URI for the user creating the resource.
16
+ # - options - Optional. A hash of other internal properties to set, mostly for internal use.
17
+ def initialize(uri, json, remote_user=nil, options={})
18
+ load_from_options(options.merge(
19
+ :uri => uri,
20
+ :json => json,
21
+ :remote_user => remote_user))
22
+ end
23
+
24
+ # Save the resource. If this is a new resource with only a resource
25
+ # collection URI, its full URI will be generated. ETags and Last-Modified
26
+ # dates are also generated upon saving. No manual reloads are required.
27
+ def save
28
+ @id ||= '%064d' % CloudKit.storage_adapter.generate_unique_id
29
+ @etag = UUID.generate unless @deleted
30
+ @last_modified = Time.now.httpdate
31
+
32
+ CloudKit.storage_adapter[@id] = {
33
+ 'uri' => @uri.cannonical_uri_string,
34
+ 'etag' => escape(@etag),
35
+ 'last_modified' => @last_modified,
36
+ 'json' => @json,
37
+ 'deleted' => escape(@deleted),
38
+ 'archived' => escape(@archived),
39
+ 'remote_user' => escape(@remote_user),
40
+ 'collection_reference' => @collection_reference ||= @uri.collection_uri_fragment,
41
+ 'resource_reference' => @resource_reference ||= @uri.cannonical_uri_string
42
+ }.merge(escape_values(parsed_json))
43
+ reload
44
+ end
45
+
46
+ # Update the json and optionally the remote_user for the current resource.
47
+ # Automatically archives the previous version, generating new ETag and
48
+ # Last-Modified dates. Raises HistoricalIntegrityViolation for attempts to
49
+ # modify resources that are not current.
50
+ def update(json, remote_user=nil)
51
+ raise HistoricalIntegrityViolation unless current?
52
+ CloudKit.storage_adapter.transaction do
53
+ record = CloudKit.storage_adapter[@id]
54
+ record['uri'] = "#{@uri.string}/versions/#{@etag}"
55
+ record['archived'] = escape(true)
56
+ CloudKit.storage_adapter[@id] = record
57
+ self.class.create(@uri, json, remote_user || @remote_user)
58
+ end
59
+ reload
60
+ end
61
+
62
+ # Delete the given resource. This is a soft delete, archiving the previous
63
+ # resource and inserted a deleted resource placeholder at the old URI.
64
+ # Raises HistoricalIntegrityViolation for attempts to delete resources that
65
+ # are not current.
66
+ def delete
67
+ raise HistoricalIntegrityViolation unless current?
68
+ CloudKit.storage_adapter.transaction do
69
+ original_uri = @uri
70
+ record = CloudKit.storage_adapter[@id]
71
+ record['uri'] = "#{@uri.string}/versions/#{@etag}"
72
+ record['archived'] = escape(true)
73
+ @uri = wrap_uri(record['uri'])
74
+ @archived = unescape(record['archived'])
75
+ CloudKit.storage_adapter[@id] = record
76
+ self.class.new(original_uri, @json, @remote_user, {:deleted => true}).save
77
+ end
78
+ reload
79
+ end
80
+
81
+ # Returns all versions of the given resource, reverse ordered by
82
+ # Last-Modified date, including the current version of the resource.
83
+ def versions
84
+ # TODO make this a collection proxy, only loading the first, then the
85
+ # rest as needed during iteration (possibly in chunks)
86
+ return nil if @archived
87
+ @versions ||= [self].concat(CloudKit.storage_adapter.query { |q|
88
+ q.add_condition('resource_reference', :eql, @resource_reference)
89
+ q.add_condition('archived', :eql, 'true')
90
+ }.reverse.map { |hash| self.class.build_from_hash(hash) })
91
+ end
92
+
93
+ # Returns all previous versions of a resource, reverse ordered by
94
+ # Last-Modified date.
95
+ def previous_versions
96
+ @previous_versions ||= versions[1..-1] rescue []
97
+ end
98
+
99
+ # Returns the most recent previous version of the given resource.
100
+ def previous_version
101
+ @previous_version ||= previous_versions[0]
102
+ end
103
+
104
+ # Returns true if the resource has been deleted.
105
+ def deleted?
106
+ @deleted
107
+ end
108
+
109
+ # Returns true if the resource is archived i.e. not the current version.
110
+ def archived?
111
+ @archived
112
+ end
113
+
114
+ # Returns true if the resource is not archived and not deleted.
115
+ def current?
116
+ !@deleted && !@archived
117
+ end
118
+
119
+ # Returns and caches the parsed JSON representation for this resource.
120
+ def parsed_json
121
+ @parsed_json ||= JSON.parse(@json)
122
+ end
123
+
124
+ # Create a new resource. Intializes and saves in one step.
125
+ #
126
+ # === Parameters
127
+ # - uri - A CloudKit::URI for the resource or its parent collection.
128
+ # - json - A string representing a valid JSON object.
129
+ # - remote_user - Optional. The URI for the user creating the resource.
130
+ def self.create(uri, json, remote_user=nil)
131
+ resource = new(uri, json, remote_user)
132
+ resource.save
133
+ resource
134
+ end
135
+
136
+ # Find all current resources with the given properties. Expectes a hash
137
+ # specifying the search parameters. Returns an array of Resources.
138
+ def self.current(spec={})
139
+ all({:deleted => false, :archived => false}.merge(spec))
140
+ end
141
+
142
+ # Find all resources with the given properties. Expects a hash specifying
143
+ # the search parameters. Returns an array of Resources.
144
+ def self.all(spec={})
145
+ CloudKit.storage_adapter.query { |q|
146
+ spec.keys.each { |k|
147
+ q.add_condition(k.to_s, :eql, escape(spec[k]))
148
+ }
149
+ }.reverse.map { |hash| build_from_hash(hash) }
150
+ end
151
+
152
+ # Find the first matching resource or nil. Expects a hash specifying the
153
+ # search parameters.
154
+ def self.first(spec)
155
+ all(spec)[0]
156
+ end
157
+
158
+ protected
159
+
160
+ def load_from_options(opts)
161
+ options = symbolize_keys(opts)
162
+
163
+ @uri = wrap_uri(options[:uri])
164
+ @json = options[:json]
165
+ @last_modified = options[:last_modified]
166
+ @resource_reference = options[:resource_reference]
167
+ @collection_reference = options[:collection_reference]
168
+ @id = options[:id] || options[:pk] || nil
169
+ @etag = unescape(options[:etag])
170
+ @remote_user = unescape(options[:remote_user])
171
+ @archived = unescape(options[:archived]) || false
172
+ @deleted = unescape(options[:deleted]) || false
173
+ end
174
+
175
+ def reload
176
+ result = CloudKit.storage_adapter.query { |q|
177
+ q.add_condition('uri', :eql, @resource_reference)
178
+ }
179
+ load_from_options(result[0])
180
+ end
181
+
182
+ def self.build_from_hash(data)
183
+ new(
184
+ data['uri'],
185
+ data['json'],
186
+ data['remote_user'],
187
+ {}.filter_merge!(
188
+ :etag => data['etag'],
189
+ :last_modified => data['last_modified'],
190
+ :resource_reference => data['resource_reference'],
191
+ :collection_reference => data['collection_reference'],
192
+ :id => data[:pk],
193
+ :deleted => data['deleted'],
194
+ :archived => data['archived']))
195
+ end
196
+
197
+ def wrap_uri(uri)
198
+ self.class.wrap_uri(uri)
199
+ end
200
+
201
+ def self.wrap_uri(uri)
202
+ case uri
203
+ when CloudKit::URI; uri
204
+ else CloudKit::URI.new(uri)
205
+ end
206
+ end
207
+
208
+ def scope(key)
209
+ "#{@id}:#{key}"
210
+ end
211
+
212
+ def escape(value)
213
+ self.class.escape(value)
214
+ end
215
+
216
+ def self.escape(value)
217
+ case value
218
+ when TrueClass
219
+ "true"
220
+ when FalseClass
221
+ "false"
222
+ when NilClass
223
+ "null"
224
+ when Fixnum, Bignum, Float
225
+ value.to_s
226
+ else
227
+ value
228
+ end
229
+ end
230
+
231
+ def unescape(value)
232
+ self.class.unescape(value)
233
+ end
234
+
235
+ def self.unescape(value)
236
+ case value
237
+ when "true"
238
+ true
239
+ when "false"
240
+ false
241
+ when "null"
242
+ nil
243
+ else
244
+ value
245
+ end
246
+ end
247
+
248
+ def symbolize_keys(hash)
249
+ hash.inject({}) { |memo, pair| memo.merge({pair[0].to_sym => pair[1]}) }
250
+ end
251
+
252
+ def escape_values(hash)
253
+ hash.inject({}) { |memo, pair| memo.merge({pair[0] => escape(pair[1])}) }
254
+ end
255
+ end
256
+ end
@@ -68,7 +68,6 @@ module CloudKit::ResponseHelpers
68
68
  end
69
69
 
70
70
  def json_error_response(status, message)
71
- "trying to throw a json error message for #{status} #{message}"
72
71
  response(status, json_error(message), nil, nil, :cache => false)
73
72
  end
74
73
  end
@@ -0,0 +1,88 @@
1
+ module CloudKit
2
+
3
+ # A CloudKit::URI wraps a URI string, added methods useful for routing
4
+ # in CloudKit as well as caching URI components for future comparisons.
5
+ class URI
6
+
7
+ # The string form of a URI.
8
+ attr_reader :string
9
+
10
+ # Create a new URI with the given string.
11
+ def initialize(string)
12
+ @string = string
13
+ end
14
+
15
+ # Return the resource collection URI fragment.
16
+ # Example: URI.new('/foos/123').collection_uri_fragment => '/foos
17
+ def collection_uri_fragment
18
+ "/#{components[0]}" rescue nil
19
+ end
20
+
21
+ # Splits a URI into its components
22
+ def components
23
+ @components ||= @string.split('/').reject{|x| x == '' || x == nil} rescue []
24
+ end
25
+
26
+ # Return the resource collection referenced by a URI.
27
+ # Example: URI.new('/foos/123').collection_type => :foos
28
+ def collection_type
29
+ components[0].to_sym rescue nil
30
+ end
31
+
32
+ # Return the URI for the current version of a resource.
33
+ # Example: URI.new('/foos/123/versions/abc').current_resource_uri => '/foos/123'
34
+ def current_resource_uri
35
+ "/#{components[0..1].join('/')}" rescue nil
36
+ end
37
+
38
+ # Returns true if URI matches /cloudkit-meta
39
+ def meta_uri?
40
+ return components.size == 1 && components[0] == 'cloudkit-meta'
41
+ end
42
+
43
+ # Returns true if URI matches /{collection}
44
+ def resource_collection_uri?
45
+ return components.size == 1
46
+ end
47
+
48
+ # Returns true if URI matches /{collection}/_resolved
49
+ def resolved_resource_collection_uri?
50
+ return components.size == 2 && components[1] == '_resolved'
51
+ end
52
+
53
+ # Returns true if URI matches /{collection}/{uuid}
54
+ def resource_uri?
55
+ return components.size == 2 && components[1] != '_resolved'
56
+ end
57
+
58
+ # Returns true if URI matches /{collection}/{uuid}/versions
59
+ def version_collection_uri?
60
+ return components.size == 3 && components[2] == 'versions'
61
+ end
62
+
63
+ # Returns true if URI matches /{collection}/{uuid}/versions/_resolved
64
+ def resolved_version_collection_uri?
65
+ return components.size == 4 && components[2] == 'versions' && components[3] == '_resolved'
66
+ end
67
+
68
+ # Returns true if URI matches /{collection}/{uuid}/versions/{etag}
69
+ def resource_version_uri?
70
+ return components.size == 4 && components[2] == 'versions' && components[3] != '_resolved'
71
+ end
72
+
73
+ # Returns a cannonical URI for a given URI/URI fragment, generating it if
74
+ # required.
75
+ # Example: URI.new('/items/123').cannoncal_uri_string => /items/123
76
+ #
77
+ # Example: URI.new('/items').cannonical_uri_string => /items/some-new-uuid
78
+ def cannonical_uri_string
79
+ @cannonical_uri_string ||= if resource_collection_uri?
80
+ "#{@string}/#{UUID.generate}"
81
+ elsif resource_uri?
82
+ @string
83
+ else
84
+ raise CloudKit::InvalidURIFormat
85
+ end
86
+ end
87
+ end
88
+ end