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