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.
- data/CHANGES +11 -0
- data/README +7 -6
- data/Rakefile +13 -6
- data/TODO +7 -5
- data/cloudkit.gemspec +23 -23
- data/doc/curl.html +2 -2
- data/doc/index.html +4 -6
- data/examples/5.ru +2 -3
- data/examples/TOC +1 -3
- data/lib/cloudkit.rb +17 -10
- data/lib/cloudkit/constants.rb +0 -6
- data/lib/cloudkit/exceptions.rb +10 -0
- data/lib/cloudkit/flash_session.rb +1 -3
- data/lib/cloudkit/oauth_filter.rb +8 -16
- data/lib/cloudkit/oauth_store.rb +9 -13
- data/lib/cloudkit/openid_filter.rb +25 -7
- data/lib/cloudkit/openid_store.rb +14 -17
- data/lib/cloudkit/request.rb +6 -1
- data/lib/cloudkit/service.rb +15 -15
- data/lib/cloudkit/store.rb +97 -284
- data/lib/cloudkit/store/memory_table.rb +105 -0
- data/lib/cloudkit/store/resource.rb +256 -0
- data/lib/cloudkit/store/response_helpers.rb +0 -1
- data/lib/cloudkit/uri.rb +88 -0
- data/lib/cloudkit/user_store.rb +7 -14
- data/spec/ext_spec.rb +76 -0
- data/spec/flash_session_spec.rb +20 -0
- data/spec/memory_table_spec.rb +86 -0
- data/spec/oauth_filter_spec.rb +326 -0
- data/spec/oauth_store_spec.rb +10 -0
- data/spec/openid_filter_spec.rb +64 -0
- data/spec/openid_store_spec.rb +101 -0
- data/spec/rack_builder_spec.rb +39 -0
- data/spec/request_spec.rb +185 -0
- data/spec/resource_spec.rb +291 -0
- data/spec/service_spec.rb +974 -0
- data/{test/helper.rb → spec/spec_helper.rb} +14 -2
- data/spec/store_spec.rb +10 -0
- data/spec/uri_spec.rb +93 -0
- data/spec/user_store_spec.rb +10 -0
- data/spec/util_spec.rb +11 -0
- metadata +37 -61
- data/examples/6.ru +0 -10
- data/lib/cloudkit/store/adapter.rb +0 -8
- data/lib/cloudkit/store/extraction_view.rb +0 -57
- data/lib/cloudkit/store/sql_adapter.rb +0 -36
- data/test/ext_test.rb +0 -76
- data/test/flash_session_test.rb +0 -22
- data/test/oauth_filter_test.rb +0 -331
- data/test/oauth_store_test.rb +0 -12
- data/test/openid_filter_test.rb +0 -60
- data/test/openid_store_test.rb +0 -12
- data/test/rack_builder_test.rb +0 -41
- data/test/request_test.rb +0 -197
- data/test/service_test.rb +0 -971
- data/test/store_test.rb +0 -93
- data/test/user_store_test.rb +0 -12
- 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
|
data/lib/cloudkit/uri.rb
ADDED
@@ -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
|