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