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
@@ -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