cloudkit-jruby 0.11.2
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 +47 -0
- data/COPYING +20 -0
- data/README +84 -0
- data/Rakefile +42 -0
- data/TODO +21 -0
- data/cloudkit.gemspec +89 -0
- data/doc/curl.html +388 -0
- data/doc/images/example-code.gif +0 -0
- data/doc/images/json-title.gif +0 -0
- data/doc/images/oauth-discovery-logo.gif +0 -0
- data/doc/images/openid-logo.gif +0 -0
- data/doc/index.html +90 -0
- data/doc/main.css +151 -0
- data/doc/rest-api.html +467 -0
- data/examples/1.ru +3 -0
- data/examples/2.ru +3 -0
- data/examples/3.ru +6 -0
- data/examples/4.ru +5 -0
- data/examples/5.ru +9 -0
- data/examples/6.ru +11 -0
- data/examples/TOC +17 -0
- data/lib/cloudkit.rb +92 -0
- data/lib/cloudkit/constants.rb +34 -0
- data/lib/cloudkit/exceptions.rb +10 -0
- data/lib/cloudkit/flash_session.rb +20 -0
- data/lib/cloudkit/oauth_filter.rb +266 -0
- data/lib/cloudkit/oauth_store.rb +48 -0
- data/lib/cloudkit/openid_filter.rb +236 -0
- data/lib/cloudkit/openid_store.rb +100 -0
- data/lib/cloudkit/rack/builder.rb +120 -0
- data/lib/cloudkit/rack/router.rb +20 -0
- data/lib/cloudkit/request.rb +177 -0
- data/lib/cloudkit/service.rb +162 -0
- data/lib/cloudkit/store.rb +349 -0
- data/lib/cloudkit/store/memory_table.rb +99 -0
- data/lib/cloudkit/store/resource.rb +269 -0
- data/lib/cloudkit/store/response.rb +52 -0
- data/lib/cloudkit/store/response_helpers.rb +84 -0
- data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
- data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
- data/lib/cloudkit/templates/oauth_meta.erb +8 -0
- data/lib/cloudkit/templates/openid_login.erb +31 -0
- data/lib/cloudkit/templates/request_authorization.erb +23 -0
- data/lib/cloudkit/templates/request_token_denied.erb +18 -0
- data/lib/cloudkit/uri.rb +88 -0
- data/lib/cloudkit/user_store.rb +37 -0
- data/lib/cloudkit/util.rb +25 -0
- 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 +81 -0
- data/spec/openid_store_spec.rb +101 -0
- data/spec/rack_builder_spec.rb +39 -0
- data/spec/request_spec.rb +191 -0
- data/spec/resource_spec.rb +310 -0
- data/spec/service_spec.rb +1039 -0
- data/spec/spec_helper.rb +32 -0
- 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 +180 -0
@@ -0,0 +1,99 @@
|
|
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
|
+
protected
|
63
|
+
|
64
|
+
def valid?(record)
|
65
|
+
return false unless record.is_a?(Hash)
|
66
|
+
record.keys.all? { |k| k.is_a?(String) && record[k].is_a?(String) }
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# MemoryQuery is used internally by MemoryTable to configure a query and run
|
72
|
+
# it against the store.
|
73
|
+
class MemoryQuery
|
74
|
+
|
75
|
+
# Initialize a new MemoryQuery.
|
76
|
+
def initialize
|
77
|
+
@conditions = []
|
78
|
+
end
|
79
|
+
|
80
|
+
# Run a query against the provided table using the conditions stored in this
|
81
|
+
# MemoryQuery instance. Returns all records that match all conditions.
|
82
|
+
# Conditions are added using #add_condition.
|
83
|
+
def run(table)
|
84
|
+
table.keys.inject([]) do |result, key|
|
85
|
+
if @conditions.all? { |condition| table[key][condition[0]] == condition[2] }
|
86
|
+
result << table[key].merge(:pk => key)
|
87
|
+
else
|
88
|
+
result
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Add a condition to this query. The operator parameter is ignored at this
|
94
|
+
# time, assuming only equality.
|
95
|
+
def add_condition(key, operator, value)
|
96
|
+
@conditions << [key, operator, value]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,269 @@
|
|
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 its URI, ETag,
|
5
|
+
# Last-Modified date, remote user, and 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
|
+
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 inserting 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
|
+
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
|
+
when Array, Hash
|
227
|
+
JSON.generate(value) # temporary bug fix prior to JSONQuery support
|
228
|
+
else
|
229
|
+
value
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def unescape(value)
|
234
|
+
self.class.unescape(value)
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.unescape(value)
|
238
|
+
case value
|
239
|
+
when "true"
|
240
|
+
true
|
241
|
+
when "false"
|
242
|
+
false
|
243
|
+
when "null"
|
244
|
+
nil
|
245
|
+
else
|
246
|
+
value
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def symbolize_keys(hash)
|
251
|
+
hash.inject({}) { |memo, pair| memo.merge({pair[0].to_sym => pair[1]}) }
|
252
|
+
end
|
253
|
+
|
254
|
+
def escape_values(hash)
|
255
|
+
hash.inject({}) { |memo, pair| memo.merge({pair[0] => escape(pair[1])}) }
|
256
|
+
end
|
257
|
+
|
258
|
+
def transaction
|
259
|
+
open('.lock', 'w+') do |f|
|
260
|
+
f.flock(File::LOCK_EX)
|
261
|
+
begin
|
262
|
+
yield
|
263
|
+
ensure
|
264
|
+
f.flock(File::LOCK_UN)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A response wrapper for CloudKit::Store
|
4
|
+
class Response
|
5
|
+
include Util
|
6
|
+
|
7
|
+
attr_reader :status, :meta, :content
|
8
|
+
|
9
|
+
# Create an instance of a Response.
|
10
|
+
def initialize(status, meta, content='')
|
11
|
+
@status = status; @meta = meta; @content = content
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return the header value specified by key.
|
15
|
+
def [](key)
|
16
|
+
meta[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the header specified by key to value.
|
20
|
+
def []=(key, value)
|
21
|
+
meta[key] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Translate to the standard Rack representation: [status, headers, content]
|
25
|
+
def to_rack
|
26
|
+
meta['Content-Length'] = content.length.to_s
|
27
|
+
[status, meta, [content.to_s]]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Parse and return the JSON content
|
31
|
+
def parsed_content
|
32
|
+
JSON.parse(content)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Clear only the content of the response. Useful for HEAD requests.
|
36
|
+
def clear_content
|
37
|
+
@content = ''
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return a response suitable for HEAD requests.
|
41
|
+
def head
|
42
|
+
response = self.dup
|
43
|
+
response.clear_content
|
44
|
+
response
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the ETag for this response without the surrounding quotes.
|
48
|
+
def etag
|
49
|
+
unquote(meta['ETag'])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# A set of mixins for building CloudKit::Response objects.
|
2
|
+
module CloudKit::ResponseHelpers
|
3
|
+
def status_404
|
4
|
+
json_error_response(404, 'not found')
|
5
|
+
end
|
6
|
+
|
7
|
+
def status_405(methods)
|
8
|
+
response = json_error_response(405, 'method not allowed')
|
9
|
+
response['Allow'] = methods.join(', ')
|
10
|
+
response
|
11
|
+
end
|
12
|
+
|
13
|
+
def status_410
|
14
|
+
json_error_response(410, 'entity previously deleted')
|
15
|
+
end
|
16
|
+
|
17
|
+
def status_412
|
18
|
+
json_error_response(412, 'precondition failed')
|
19
|
+
end
|
20
|
+
|
21
|
+
def status_422
|
22
|
+
json_error_response(422, 'unprocessable entity')
|
23
|
+
end
|
24
|
+
|
25
|
+
def internal_server_error
|
26
|
+
json_error_response(500, 'unknown server error')
|
27
|
+
end
|
28
|
+
|
29
|
+
def data_required
|
30
|
+
json_error_response(400, 'data required')
|
31
|
+
end
|
32
|
+
|
33
|
+
def invalid_entity_type
|
34
|
+
json_error_response(400, 'valid entity type required')
|
35
|
+
end
|
36
|
+
|
37
|
+
def etag_required
|
38
|
+
json_error_response(400, 'etag required')
|
39
|
+
end
|
40
|
+
|
41
|
+
def allow(methods)
|
42
|
+
CloudKit::Response.new(
|
43
|
+
200,
|
44
|
+
{'Allow' => methods.join(', '), 'Content-Type' => 'application/json'})
|
45
|
+
end
|
46
|
+
|
47
|
+
def response(status, content='', etag=nil, last_modified=nil, options={})
|
48
|
+
cache_control = options[:cache] == false ? 'no-cache' : 'proxy-revalidate'
|
49
|
+
etag = "\"#{etag}\"" if etag
|
50
|
+
headers = {}.filter_merge!(
|
51
|
+
'Content-Type' => 'application/json',
|
52
|
+
'Cache-Control' => cache_control,
|
53
|
+
'Last-Modified' => last_modified,
|
54
|
+
'Location' => options[:location],
|
55
|
+
'ETag' => etag)
|
56
|
+
CloudKit::Response.new(status, headers, content)
|
57
|
+
end
|
58
|
+
|
59
|
+
def json_meta_response(uri, etag, last_modified)
|
60
|
+
json = json_metadata(uri, etag, last_modified)
|
61
|
+
response(200, json, nil, nil, :cache => false)
|
62
|
+
end
|
63
|
+
|
64
|
+
def json_create_response(uri, etag, last_modified)
|
65
|
+
json = json_metadata(uri, etag, last_modified)
|
66
|
+
response(201, json, nil, nil, {:cache => false, :location => uri})
|
67
|
+
end
|
68
|
+
|
69
|
+
def json_metadata(uri, etag, last_modified)
|
70
|
+
JSON.generate(
|
71
|
+
:ok => true,
|
72
|
+
:uri => uri,
|
73
|
+
:etag => etag,
|
74
|
+
:last_modified => last_modified)
|
75
|
+
end
|
76
|
+
|
77
|
+
def json_error(message)
|
78
|
+
"{\"error\":\"#{message}\"}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def json_error_response(status, message)
|
82
|
+
response(status, json_error(message), nil, nil, :cache => false)
|
83
|
+
end
|
84
|
+
end
|