cloudkit-jruby 0.11.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,20 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
|
3
|
+
# A minimal router providing just what is needed for the OAuth and OpenID
|
4
|
+
# filters.
|
5
|
+
class Router
|
6
|
+
|
7
|
+
# Create an instance of Router to match on method, path and params.
|
8
|
+
def initialize(method, path, params=[])
|
9
|
+
@method = method.to_s.upcase; @path = path; @params = params
|
10
|
+
end
|
11
|
+
|
12
|
+
# By overriding the case comparison operator, we can match routes in a case
|
13
|
+
# statement.
|
14
|
+
#
|
15
|
+
# See also: CloudKit::Util#r, CloudKit::Request#match?
|
16
|
+
def ===(request)
|
17
|
+
request.match?(@method, @path, @params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A subclass of Rack::Request providing CloudKit-specific features.
|
4
|
+
class Request < Rack::Request
|
5
|
+
include CloudKit::Util
|
6
|
+
alias_method :cloudkit_params, :params
|
7
|
+
|
8
|
+
def initialize(env)
|
9
|
+
super(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Return a merged set of both standard params and OAuth header params.
|
13
|
+
def params
|
14
|
+
@cloudkit_params ||= cloudkit_params.merge(oauth_header_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return the JSON content from the request body
|
18
|
+
def json
|
19
|
+
self.body.rewind
|
20
|
+
raw = self.body.read
|
21
|
+
# extract the json from the body to avoid tunneled _method param from being parsed as json
|
22
|
+
(matches = raw.match(/(\{.*\})/)) ? matches[1] : raw
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return a CloudKit::URI instance representing the rack request's path info.
|
26
|
+
def uri
|
27
|
+
@uri ||= CloudKit::URI.new(self.path_info)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return true if method, path, and required_params match.
|
31
|
+
def match?(method, path, required_params=[])
|
32
|
+
(request_method == method) &&
|
33
|
+
path_info.match(path.gsub(':id', '*')) && # just enough to work for now
|
34
|
+
param_match?(required_params)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return true of the array of required params match the request params. If
|
38
|
+
# a hash in passed in for a param, its value is also used in the match.
|
39
|
+
def param_match?(required_params)
|
40
|
+
required_params.all? do |required_param|
|
41
|
+
case required_param
|
42
|
+
when Hash
|
43
|
+
key = required_param.keys.first
|
44
|
+
return false unless params.has_key? key
|
45
|
+
return false unless params[key] == required_param[key]
|
46
|
+
when String
|
47
|
+
return false unless params.has_key? required_param
|
48
|
+
else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return OAuth header params in a hash.
|
56
|
+
def oauth_header_params
|
57
|
+
# This is a copy of the same method from the OAuth gem.
|
58
|
+
# TODO: Refactor the OAuth gem so that this method is available via a
|
59
|
+
# mixin, outside of the request proxy context.
|
60
|
+
%w( X-HTTP_AUTHORIZATION Authorization HTTP_AUTHORIZATION ).each do |header|
|
61
|
+
next unless @env.include?(header)
|
62
|
+
header = @env[header]
|
63
|
+
next unless header[0,6] == 'OAuth '
|
64
|
+
oauth_param_string = header[6,header.length].split(/[,=]/)
|
65
|
+
oauth_param_string.map!{|v| unescape(v.strip)}
|
66
|
+
oauth_param_string.map!{|v| v =~ /^\".*\"$/ ? v[1..-2] : v}
|
67
|
+
oauth_params = Hash[*oauth_param_string.flatten]
|
68
|
+
oauth_params.reject!{|k,v| k !~ /^oauth_/}
|
69
|
+
return oauth_params
|
70
|
+
end
|
71
|
+
return {}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Unescape a value according to the OAuth spec.
|
75
|
+
def unescape(value)
|
76
|
+
::URI.unescape(value.gsub('+', '%2B'))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return the last path element in the request URI.
|
80
|
+
def last_path_element
|
81
|
+
path_element(-1)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return a specific path element
|
85
|
+
def path_element(index)
|
86
|
+
path_info.split('/')[index] rescue nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return an array containing one entry for each piece of upstream
|
90
|
+
# middleware. This is in the same spirit as Via headers in HTTP, but does
|
91
|
+
# not use the header because the transition from one piece of middleware to
|
92
|
+
# the next does not use HTTP.
|
93
|
+
def via
|
94
|
+
@env[CLOUDKIT_VIA].split(', ') rescue []
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return parsed contents of an If-Match header.
|
98
|
+
#
|
99
|
+
# Note: Only a single ETag is useful in the context of CloudKit, so a list
|
100
|
+
# is treated as one ETag; the result of using the wrong ETag or a list of
|
101
|
+
# ETags is the same in the context of PUT and DELETE where If-Match
|
102
|
+
# headers are required.
|
103
|
+
def if_match
|
104
|
+
etag = @env['HTTP_IF_MATCH']
|
105
|
+
return nil unless etag
|
106
|
+
etag.strip!
|
107
|
+
etag = unquote(etag)
|
108
|
+
return nil if etag == '*'
|
109
|
+
etag
|
110
|
+
end
|
111
|
+
|
112
|
+
# Add a via entry to the Rack environment.
|
113
|
+
def inject_via(key)
|
114
|
+
items = via << key
|
115
|
+
@env[CLOUDKIT_VIA] = items.join(', ')
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return the current user URI.
|
119
|
+
def current_user
|
120
|
+
return nil unless @env[CLOUDKIT_AUTH_KEY] && @env[CLOUDKIT_AUTH_KEY] != ''
|
121
|
+
@env[CLOUDKIT_AUTH_KEY]
|
122
|
+
end
|
123
|
+
|
124
|
+
# Set the current user URI.
|
125
|
+
def current_user=(user)
|
126
|
+
@env[CLOUDKIT_AUTH_KEY] = user
|
127
|
+
end
|
128
|
+
|
129
|
+
# Return true if authentication is being used.
|
130
|
+
def using_auth?
|
131
|
+
@env[CLOUDKIT_AUTH_PRESENCE] != nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Report to downstream middleware that authentication is in use.
|
135
|
+
def announce_auth(via)
|
136
|
+
inject_via(via)
|
137
|
+
@env[CLOUDKIT_AUTH_PRESENCE] = 1
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return the session associated with this request.
|
141
|
+
def session
|
142
|
+
@env['rack.session']
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return the login URL for this request. This is stashed in the Rack
|
146
|
+
# environment so the OpenID and OAuth middleware can cooperate during the
|
147
|
+
# token authorization step in the OAuth flow.
|
148
|
+
def login_url
|
149
|
+
@env[CLOUDKIT_LOGIN_URL] || '/login'
|
150
|
+
end
|
151
|
+
|
152
|
+
# Set the login url for this request.
|
153
|
+
def login_url=(url)
|
154
|
+
@env[CLOUDKIT_LOGIN_URL] = url
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return the logout URL for this request.
|
158
|
+
def logout_url
|
159
|
+
@env[CLOUDKIT_LOGOUT_URL] || '/logout'
|
160
|
+
end
|
161
|
+
|
162
|
+
# Set the logout URL for this request.
|
163
|
+
def logout_url=(url)
|
164
|
+
@env[CLOUDKIT_LOGOUT_URL] = url
|
165
|
+
end
|
166
|
+
|
167
|
+
# Return the flash session for this request.
|
168
|
+
def flash
|
169
|
+
session[CLOUDKIT_FLASH] ||= CloudKit::FlashSession.new
|
170
|
+
end
|
171
|
+
|
172
|
+
# Return the host and scheme
|
173
|
+
def domain_root
|
174
|
+
"#{scheme}://#{@env['HTTP_HOST']}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A CloudKit Service is Rack middleware providing a REST/HTTP 1.1 interface to
|
4
|
+
# a Store. Its primary purpose is to initialize and adapt a Store for use in a
|
5
|
+
# Rack middleware stack.
|
6
|
+
#
|
7
|
+
# ==Examples
|
8
|
+
#
|
9
|
+
# A rackup file exposing _items_ and _things_ as REST collections:
|
10
|
+
# require 'cloudkit'
|
11
|
+
# expose :items, :things
|
12
|
+
#
|
13
|
+
# The same as above, adding OpenID and OAuth/Discovery:
|
14
|
+
# require 'cloudkit'
|
15
|
+
# contain :items, :things
|
16
|
+
#
|
17
|
+
# An explicit setup, without using the Rack::Builder shortcuts:
|
18
|
+
# require 'cloudkit'
|
19
|
+
# use Rack::Session::Pool
|
20
|
+
# use CloudKit::OAuthFilter
|
21
|
+
# use CloudKit::OpenIDFilter
|
22
|
+
# use CloudKit::Service, :collections => [:items, :things]
|
23
|
+
# run lambda{|env| [200, {'Content-Type' => 'text/html'}, ['Hello']]}
|
24
|
+
#
|
25
|
+
# For more examples, including the use of different storage implementations,
|
26
|
+
# see the Table of Contents in the examples directory.
|
27
|
+
class Service
|
28
|
+
include ResponseHelpers
|
29
|
+
|
30
|
+
@@lock = Mutex.new
|
31
|
+
|
32
|
+
attr_reader :store
|
33
|
+
|
34
|
+
def initialize(app, options)
|
35
|
+
@app = app
|
36
|
+
@collections = options[:collections]
|
37
|
+
end
|
38
|
+
|
39
|
+
def call(env)
|
40
|
+
@@lock.synchronize do
|
41
|
+
@store = Store.new(:collections => @collections)
|
42
|
+
end unless @store
|
43
|
+
|
44
|
+
request = Request.new(env)
|
45
|
+
unless bypass?(request)
|
46
|
+
return auth_config_error if (request.using_auth? && auth_missing?(request))
|
47
|
+
return not_implemented unless @store.implements?(request.request_method)
|
48
|
+
send(request.request_method.downcase, request) rescue internal_server_error.to_rack
|
49
|
+
else
|
50
|
+
@app.call(env)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def get(request)
|
57
|
+
response = @store.get(
|
58
|
+
request.uri,
|
59
|
+
{}.filter_merge!(
|
60
|
+
:remote_user => request.current_user,
|
61
|
+
:offset => request['offset'],
|
62
|
+
:limit => request['limit']))
|
63
|
+
inject_link_headers(request, response)
|
64
|
+
response.to_rack
|
65
|
+
end
|
66
|
+
|
67
|
+
def post(request)
|
68
|
+
if tunnel_methods.include?(request['_method'].try(:upcase))
|
69
|
+
return send(request['_method'].downcase, request)
|
70
|
+
end
|
71
|
+
response = @store.post(
|
72
|
+
request.uri,
|
73
|
+
{:json => request.json}.filter_merge!(
|
74
|
+
:remote_user => request.current_user))
|
75
|
+
update_location_header(request, response)
|
76
|
+
response.to_rack
|
77
|
+
end
|
78
|
+
|
79
|
+
def put(request)
|
80
|
+
response = @store.put(
|
81
|
+
request.uri,
|
82
|
+
{:json => request.json}.filter_merge!(
|
83
|
+
:remote_user => request.current_user,
|
84
|
+
:etag => request.if_match))
|
85
|
+
update_location_header(request, response)
|
86
|
+
response.to_rack
|
87
|
+
end
|
88
|
+
|
89
|
+
def delete(request)
|
90
|
+
@store.delete(
|
91
|
+
request.uri,
|
92
|
+
{}.filter_merge!(
|
93
|
+
:remote_user => request.current_user,
|
94
|
+
:etag => request.if_match)).to_rack
|
95
|
+
end
|
96
|
+
|
97
|
+
def head(request)
|
98
|
+
response = @store.head(
|
99
|
+
request.uri,
|
100
|
+
{}.filter_merge!(
|
101
|
+
:remote_user => request.current_user,
|
102
|
+
:offset => request['offset'],
|
103
|
+
:limit => request['limit']))
|
104
|
+
inject_link_headers(request, response)
|
105
|
+
response.to_rack
|
106
|
+
end
|
107
|
+
|
108
|
+
def options(request)
|
109
|
+
@store.options(request.uri).to_rack
|
110
|
+
end
|
111
|
+
|
112
|
+
def inject_link_headers(request, response)
|
113
|
+
response['Link'] = versions_link_header(request) if request.uri.resource_uri?
|
114
|
+
response['Link'] = resolved_link_header(request) if request.uri.resource_collection_uri?
|
115
|
+
response['Link'] = index_link_header(request) if request.uri.resolved_resource_collection_uri?
|
116
|
+
response['Link'] = resolved_link_header(request) if request.uri.version_collection_uri?
|
117
|
+
response['Link'] = index_link_header(request) if request.uri.resolved_version_collection_uri?
|
118
|
+
end
|
119
|
+
|
120
|
+
def versions_link_header(request)
|
121
|
+
base_url = "#{request.domain_root}#{request.path_info}"
|
122
|
+
"<#{base_url}/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\""
|
123
|
+
end
|
124
|
+
|
125
|
+
def resolved_link_header(request)
|
126
|
+
base_url = "#{request.domain_root}#{request.path_info}"
|
127
|
+
"<#{base_url}/_resolved>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/resolved\""
|
128
|
+
end
|
129
|
+
|
130
|
+
def index_link_header(request)
|
131
|
+
index_path = request.path_info.sub(/\/_resolved(\/)*$/, '')
|
132
|
+
base_url = "#{request.domain_root}#{index_path}"
|
133
|
+
"<#{base_url}>; rel=\"index\""
|
134
|
+
end
|
135
|
+
|
136
|
+
def update_location_header(request, response)
|
137
|
+
return unless response['Location']
|
138
|
+
response['Location'] = "#{request.domain_root}#{response['Location']}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def auth_missing?(request)
|
142
|
+
request.current_user == nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def tunnel_methods
|
146
|
+
['PUT', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE']
|
147
|
+
end
|
148
|
+
|
149
|
+
def not_implemented
|
150
|
+
json_error_response(501, 'not implemented').to_rack
|
151
|
+
end
|
152
|
+
|
153
|
+
def auth_config_error
|
154
|
+
json_error_response(500, 'server auth misconfigured').to_rack
|
155
|
+
end
|
156
|
+
|
157
|
+
def bypass?(request)
|
158
|
+
collection = @collections.detect{|type| request.path_info.match("/#{type.to_s}")}
|
159
|
+
!collection && !request.uri.meta_uri?
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A functional storage interface with HTTP semantics and pluggable adapters.
|
4
|
+
class Store
|
5
|
+
include ResponseHelpers
|
6
|
+
include CloudKit::Util
|
7
|
+
|
8
|
+
# Initialize a new Store. All resources in a Store are automatically
|
9
|
+
# versioned.
|
10
|
+
#
|
11
|
+
# ===Options
|
12
|
+
# - :collections - Array of resource collections to manage.
|
13
|
+
#
|
14
|
+
# ===Example
|
15
|
+
# store = CloudKit::Store.new(:collections => [:foos, :bars])
|
16
|
+
#
|
17
|
+
# See also: Response
|
18
|
+
#
|
19
|
+
def initialize(options)
|
20
|
+
CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
|
21
|
+
@collections = options[:collections]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve a resource or collection of resources based on a URI.
|
25
|
+
#
|
26
|
+
# ===Parameters
|
27
|
+
# - uri - URI of the resource or collection to retrieve.
|
28
|
+
# - options - See below.
|
29
|
+
#
|
30
|
+
# ===Options
|
31
|
+
# - :remote_user - Optional. Scopes the dataset if provided.
|
32
|
+
# - :limit - Optional. Default is unlimited. Limit the number of records returned by a collection request.
|
33
|
+
# - :offset - Optional. Start the list of resources in a collection at offset (0-based).
|
34
|
+
# - :any - Optional. Not a literal ":any", but any key or keys that are top level JSON keys. This is a starting point for future JSONPath/JSONQuery support.
|
35
|
+
#
|
36
|
+
# ===URI Types
|
37
|
+
# /cloudkit-meta
|
38
|
+
# /{collection}
|
39
|
+
# /{collection}/_resolved
|
40
|
+
# /{collection}/{uuid}
|
41
|
+
# /{collection}/{uuid}/versions
|
42
|
+
# /{collection}/{uuid}/versions/_resolved
|
43
|
+
# /{collection}/{uuid}/versions/{etag}
|
44
|
+
#
|
45
|
+
# ===Examples
|
46
|
+
# get('/cloudkit-meta')
|
47
|
+
# get('/foos')
|
48
|
+
# get('/foos', :remote_user => 'coltrane')
|
49
|
+
# get('/foos', :limit => 100, :offset => 200)
|
50
|
+
# get('/foos/123')
|
51
|
+
# get('/foos/123/versions')
|
52
|
+
# get('/foos/123/versions/abc')
|
53
|
+
#
|
54
|
+
# See also: {REST API}[http://getcloudkit.com/rest-api.html]
|
55
|
+
#
|
56
|
+
def get(uri, options={})
|
57
|
+
return invalid_entity_type if !valid_collection_type?(uri.collection_type)
|
58
|
+
return meta if uri.meta_uri?
|
59
|
+
return resource_collection(uri, options) if uri.resource_collection_uri?
|
60
|
+
return resolved_resource_collection(uri, options) if uri.resolved_resource_collection_uri?
|
61
|
+
return resource(uri, options) if uri.resource_uri?
|
62
|
+
return version_collection(uri, options) if uri.version_collection_uri?
|
63
|
+
return resolved_version_collection(uri, options) if uri.resolved_version_collection_uri?
|
64
|
+
return resource_version(uri, options) if uri.resource_version_uri?
|
65
|
+
status_404
|
66
|
+
end
|
67
|
+
|
68
|
+
# Retrieve the same items as the get method, minus the content/body. Using
|
69
|
+
# this method on a single resource URI performs a slight optimization due
|
70
|
+
# to the way CloudKit stores its ETags and Last-Modified information on
|
71
|
+
# write.
|
72
|
+
def head(uri, options={})
|
73
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
74
|
+
if uri.resource_uri? || uri.resource_version_uri?
|
75
|
+
# ETag and Last-Modified are already stored for single items, so a slight
|
76
|
+
# optimization can be made for HEAD requests.
|
77
|
+
result = CloudKit::Resource.first(options.merge(:uri => uri.string))
|
78
|
+
return status_404.head unless result
|
79
|
+
return status_410.head if result.deleted?
|
80
|
+
return response(200, '', result.etag, result.last_modified)
|
81
|
+
else
|
82
|
+
get(uri, options).head
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Update or create a resource at the specified URI. If the resource already
|
87
|
+
# exists, an :etag option is required.
|
88
|
+
def put(uri, options={})
|
89
|
+
methods = methods_for_uri(uri)
|
90
|
+
return status_405(methods) unless methods.include?('PUT')
|
91
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
92
|
+
return data_required unless options[:json]
|
93
|
+
current_resource = resource(uri, options.excluding(:json, :etag, :remote_user))
|
94
|
+
return update_resource(uri, options) if current_resource.status == 200
|
95
|
+
return current_resource if current_resource.status == 410
|
96
|
+
create_resource(uri, options)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a resource in a given collection.
|
100
|
+
def post(uri, options={})
|
101
|
+
methods = methods_for_uri(uri)
|
102
|
+
return status_405(methods) unless methods.include?('POST')
|
103
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
104
|
+
return data_required unless options[:json]
|
105
|
+
create_resource(uri, options)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Delete the resource specified by the URI. Requires the :etag option.
|
109
|
+
def delete(uri, options={})
|
110
|
+
methods = methods_for_uri(uri)
|
111
|
+
return status_405(methods) unless methods.include?('DELETE')
|
112
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
113
|
+
return etag_required unless options[:etag]
|
114
|
+
resource = CloudKit::Resource.first(options.excluding(:etag).merge(:uri => uri.string))
|
115
|
+
return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
|
116
|
+
return status_410 if resource.deleted?
|
117
|
+
return status_412 if resource.etag != options[:etag]
|
118
|
+
|
119
|
+
resource.delete
|
120
|
+
archived_resource = resource.previous_version
|
121
|
+
return json_meta_response(archived_resource.uri.string, archived_resource.etag, resource.last_modified)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Build a response containing the allowed methods for a given URI.
|
125
|
+
def options(uri)
|
126
|
+
methods = methods_for_uri(uri)
|
127
|
+
allow(methods)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return a list of allowed methods for a given URI.
|
131
|
+
def methods_for_uri(uri)
|
132
|
+
return meta_methods if uri.meta_uri?
|
133
|
+
return resource_collection_methods if uri.resource_collection_uri?
|
134
|
+
return resolved_resource_collection_methods if uri.resolved_resource_collection_uri?
|
135
|
+
return resource_methods if uri.resource_uri?
|
136
|
+
return version_collection_methods if uri.version_collection_uri?
|
137
|
+
return resolved_version_collection_methods if uri.resolved_version_collection_uri?
|
138
|
+
return resource_version_methods if uri.resource_version_uri?
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return the list of methods allowed for the cloudkit-meta URI.
|
142
|
+
def meta_methods
|
143
|
+
@meta_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return the list of methods allowed for a resource collection.
|
147
|
+
def resource_collection_methods
|
148
|
+
@resource_collection_methods ||= http_methods.excluding('PUT', 'DELETE')
|
149
|
+
end
|
150
|
+
|
151
|
+
# Return the list of methods allowed on a resolved resource collection.
|
152
|
+
def resolved_resource_collection_methods
|
153
|
+
@resolved_resource_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return the list of methods allowed on an individual resource.
|
157
|
+
def resource_methods
|
158
|
+
@resource_methods ||= http_methods.excluding('POST')
|
159
|
+
end
|
160
|
+
|
161
|
+
# Return the list of methods allowed on a version history collection.
|
162
|
+
def version_collection_methods
|
163
|
+
@version_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
164
|
+
end
|
165
|
+
|
166
|
+
# Return the list of methods allowed on a resolved version history collection.
|
167
|
+
def resolved_version_collection_methods
|
168
|
+
@resolved_version_collection_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
169
|
+
end
|
170
|
+
|
171
|
+
# Return the list of methods allowed on a resource version.
|
172
|
+
def resource_version_methods
|
173
|
+
@resource_version_methods ||= http_methods.excluding('POST', 'PUT', 'DELETE')
|
174
|
+
end
|
175
|
+
|
176
|
+
# Return true if this store implements a given HTTP method.
|
177
|
+
def implements?(http_method)
|
178
|
+
http_methods.include?(http_method.upcase)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Return the list of HTTP methods supported by this Store.
|
182
|
+
def http_methods
|
183
|
+
['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return an array containing the response for each URI in a list.
|
187
|
+
def resolve_uris(uris) # TODO - remove if no longer needed
|
188
|
+
result = []
|
189
|
+
uris.each do |uri|
|
190
|
+
result << get(uri)
|
191
|
+
end
|
192
|
+
result
|
193
|
+
end
|
194
|
+
|
195
|
+
def storage_adapter
|
196
|
+
CloudKit.storage_adapter
|
197
|
+
end
|
198
|
+
|
199
|
+
# Return the version number of this Store.
|
200
|
+
def version; 1; end
|
201
|
+
|
202
|
+
protected
|
203
|
+
|
204
|
+
# Return the list of collections managed by this Store.
|
205
|
+
def meta
|
206
|
+
json = JSON.generate(:uris => @collections.map{|t| "/#{t}"})
|
207
|
+
response(200, json, build_etag(json))
|
208
|
+
end
|
209
|
+
|
210
|
+
# Return a list of resource URIs for the given collection URI. Sorted by
|
211
|
+
# Last-Modified date in descending order.
|
212
|
+
def resource_collection(uri, options)
|
213
|
+
filter = options.excluding(:offset, :limit).merge(
|
214
|
+
:deleted => false,
|
215
|
+
:collection_reference => uri.collection_uri_fragment,
|
216
|
+
:archived => false)
|
217
|
+
result = CloudKit::Resource.current(filter)
|
218
|
+
bundle_collection_result(uri.string, options, result)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return all documents and their associated metadata for the given
|
222
|
+
# collection URI.
|
223
|
+
def resolved_resource_collection(uri, options)
|
224
|
+
result = CloudKit::Resource.current(
|
225
|
+
options.excluding(:offset, :limit).merge(
|
226
|
+
:collection_reference => uri.collection_uri_fragment))
|
227
|
+
bundle_resolved_collection_result(uri, options, result)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Return the resource for the given URI. Return 404 if not found or if
|
231
|
+
# protected and unauthorized, 410 if authorized but deleted.
|
232
|
+
def resource(uri, options)
|
233
|
+
if resource = CloudKit::Resource.first(options.merge!(:uri => uri.string))
|
234
|
+
return status_410 if resource.deleted?
|
235
|
+
return response(200, resource.json, resource.etag, resource.last_modified)
|
236
|
+
end
|
237
|
+
status_404
|
238
|
+
end
|
239
|
+
|
240
|
+
# Return a collection of URIs for all versions of a resource including the
|
241
|
+
# current version. Sorted by Last-Modified date in descending order.
|
242
|
+
def version_collection(uri, options)
|
243
|
+
found = CloudKit::Resource.first(
|
244
|
+
options.excluding(:offset, :limit).merge(
|
245
|
+
:uri => uri.current_resource_uri))
|
246
|
+
return status_404 unless found
|
247
|
+
result = CloudKit::Resource.all( # TODO - just use found.versions
|
248
|
+
options.excluding(:offset, :limit).merge(
|
249
|
+
:resource_reference => uri.current_resource_uri,
|
250
|
+
:deleted => false))
|
251
|
+
bundle_collection_result(uri.string, options, result)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Return all document versions and their associated metadata for a given
|
255
|
+
# resource including the current version. Sorted by Last-Modified date in
|
256
|
+
# descending order.
|
257
|
+
def resolved_version_collection(uri, options)
|
258
|
+
found = CloudKit::Resource.first(
|
259
|
+
options.excluding(:offset, :limit).merge(
|
260
|
+
:uri => uri.current_resource_uri))
|
261
|
+
return status_404 unless found
|
262
|
+
result = CloudKit::Resource.all(
|
263
|
+
options.excluding(:offset, :limit).merge(
|
264
|
+
:resource_reference => uri.current_resource_uri,
|
265
|
+
:deleted => false))
|
266
|
+
bundle_resolved_collection_result(uri, options, result)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Return a specific version of a resource.
|
270
|
+
def resource_version(uri, options)
|
271
|
+
result = CloudKit::Resource.first(options.merge(:uri => uri.string))
|
272
|
+
return status_404 unless result
|
273
|
+
response(200, result.json, result.etag, result.last_modified)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Create a resource at the specified URI.
|
277
|
+
def create_resource(uri, options)
|
278
|
+
JSON.parse(options[:json]) rescue (return status_422)
|
279
|
+
resource = CloudKit::Resource.create(uri, options[:json], options[:remote_user])
|
280
|
+
json_create_response(resource.uri.string, resource.etag, resource.last_modified)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Update the resource at the specified URI. Requires the :etag option.
|
284
|
+
def update_resource(uri, options)
|
285
|
+
JSON.parse(options[:json]) rescue (return status_422)
|
286
|
+
resource = CloudKit::Resource.first(
|
287
|
+
options.excluding(:json, :etag).merge(:uri => uri.string))
|
288
|
+
return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
|
289
|
+
return etag_required unless options[:etag]
|
290
|
+
return status_412 unless options[:etag] == resource.etag
|
291
|
+
resource.update(options[:json])
|
292
|
+
return json_meta_response(uri.string, resource.etag, resource.last_modified)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Bundle a collection of results as a list of URIs for the response.
|
296
|
+
def bundle_collection_result(uri, options, result)
|
297
|
+
total = result.size
|
298
|
+
offset = options[:offset].try(:to_i) || 0
|
299
|
+
max = options[:limit] ? offset + options[:limit].to_i : total
|
300
|
+
list = result.to_a[offset...max].map{|r| r.uri}
|
301
|
+
json = uri_list(list, total, offset)
|
302
|
+
last_modified = result.first.try(:last_modified) if result.any?
|
303
|
+
response(200, json, build_etag(json), last_modified)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Bundle a collection of results as a list of documents and the associated
|
307
|
+
# metadata (last_modified, uri, etag) that would have accompanied a response
|
308
|
+
# to their singular request.
|
309
|
+
def bundle_resolved_collection_result(uri, options, result)
|
310
|
+
total = result.size
|
311
|
+
offset = options[:offset].try(:to_i) || 0
|
312
|
+
max = options[:limit] ? offset + options[:limit].to_i : total
|
313
|
+
list = result.to_a[offset...max]
|
314
|
+
json = resource_list(list, total, offset)
|
315
|
+
last_modified = result.first.last_modified if result.any?
|
316
|
+
response(200, json, build_etag(json), last_modified)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Generate a JSON URI list.
|
320
|
+
def uri_list(list, total, offset)
|
321
|
+
JSON.generate(:total => total, :offset => offset, :uris => list.map { |u| u.string })
|
322
|
+
end
|
323
|
+
|
324
|
+
# Generate a JSON document list.
|
325
|
+
def resource_list(list, total, offset)
|
326
|
+
results = []
|
327
|
+
list.each do |resource|
|
328
|
+
results << {
|
329
|
+
:uri => resource.uri.string,
|
330
|
+
:etag => resource.etag,
|
331
|
+
:last_modified => resource.last_modified,
|
332
|
+
:document => resource.json}
|
333
|
+
end
|
334
|
+
JSON.generate(:total => total, :offset => offset, :documents => results)
|
335
|
+
end
|
336
|
+
|
337
|
+
# Build an ETag for a collection. ETags are generated on write as an
|
338
|
+
# optimization for GETs. This method is used for collections of resources
|
339
|
+
# where the optimization is not practical.
|
340
|
+
def build_etag(data)
|
341
|
+
Digest::MD5.hexdigest(data.to_s)
|
342
|
+
end
|
343
|
+
|
344
|
+
# Returns true if the collection type is valid for this Store.
|
345
|
+
def valid_collection_type?(collection_type)
|
346
|
+
@collections.include?(collection_type) || collection_type.to_s == 'cloudkit-meta'
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|