toast 0.9.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +171 -86
- data/config/routes.rb +1 -27
- data/lib/generators/toast/USAGE +1 -0
- data/lib/generators/toast/templates/toast-api.rb.erb +10 -0
- data/lib/generators/toast/toast_generator.rb +9 -0
- data/lib/toast/canonical_request.rb +179 -0
- data/lib/toast/collection_request.rb +161 -0
- data/lib/toast/config_dsl/association.rb +72 -0
- data/lib/toast/config_dsl/base.rb +50 -0
- data/lib/toast/config_dsl/collection.rb +45 -0
- data/lib/toast/config_dsl/common.rb +38 -0
- data/lib/toast/config_dsl/default_handlers.rb +83 -0
- data/lib/toast/config_dsl/expose.rb +176 -0
- data/lib/toast/config_dsl/settings.rb +35 -0
- data/lib/toast/config_dsl/single.rb +15 -0
- data/lib/toast/config_dsl/via_verb.rb +49 -0
- data/lib/toast/config_dsl.rb +60 -225
- data/lib/toast/engine.rb +19 -30
- data/lib/toast/errors.rb +41 -0
- data/lib/toast/http_range.rb +17 -0
- data/lib/toast/plural_assoc_request.rb +285 -0
- data/lib/toast/rack_app.rb +133 -0
- data/lib/toast/request_helpers.rb +134 -0
- data/lib/toast/single_request.rb +66 -0
- data/lib/toast/singular_assoc_request.rb +207 -0
- data/lib/toast/version.rb +2 -2
- data/lib/toast.rb +100 -1
- metadata +83 -116
- data/app/controller/toast_controller.rb +0 -103
- data/lib/toast/active_record_extensions.rb +0 -85
- data/lib/toast/association.rb +0 -219
- data/lib/toast/collection.rb +0 -139
- data/lib/toast/record.rb +0 -123
- data/lib/toast/resource.rb +0 -175
- data/lib/toast/single.rb +0 -89
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'toast/request_helpers'
|
2
|
+
require 'toast/http_range'
|
3
|
+
require 'link_header'
|
4
|
+
|
5
|
+
class Toast::PluralAssocRequest
|
6
|
+
include Toast::RequestHelpers
|
7
|
+
include Toast::Errors
|
8
|
+
|
9
|
+
def initialize id, config, base_config, auth, request
|
10
|
+
@id = id
|
11
|
+
@config = config
|
12
|
+
@base_config = base_config
|
13
|
+
@selected_attributes = request.query_parameters.delete(:toast_select).try(:split,',')
|
14
|
+
@uri_params = request.query_parameters
|
15
|
+
@base_uri = base_uri(request)
|
16
|
+
@verb = request.request_method.downcase
|
17
|
+
@auth = auth
|
18
|
+
@request = request
|
19
|
+
end
|
20
|
+
|
21
|
+
def respond
|
22
|
+
if @verb.in? %w(get post link unlink)
|
23
|
+
self.send(@verb)
|
24
|
+
else
|
25
|
+
response :method_not_allowed,
|
26
|
+
headers: {'Allow' => allowed_methods(@config)},
|
27
|
+
msg: "method #{@verb.upcase} not supported for association URIs"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def get
|
34
|
+
|
35
|
+
if @config.via_get.nil?
|
36
|
+
response :method_not_allowed,
|
37
|
+
headers: {'Allow' => allowed_methods(@config)},
|
38
|
+
msg: "GET not configured"
|
39
|
+
else
|
40
|
+
begin
|
41
|
+
|
42
|
+
target_config = get_config(@config.target_model_class)
|
43
|
+
|
44
|
+
requested_range = Toast::HttpRange.new(@request.env['HTTP_RANGE'])
|
45
|
+
|
46
|
+
range_start = requested_range.start
|
47
|
+
window = if (requested_range.size.nil? || requested_range.size > @config.max_window)
|
48
|
+
@config.max_window
|
49
|
+
else
|
50
|
+
requested_range.size
|
51
|
+
end
|
52
|
+
|
53
|
+
source = @base_config.model_class.find(@id) # may raise ActiveRecord::RecordNotFound
|
54
|
+
relation = call_handler(@config.via_get.handler, source, @uri_params) # may raise HandlerError
|
55
|
+
|
56
|
+
unless relation.is_a? ActiveRecord::Relation and relation.model == @config.target_model_class
|
57
|
+
return response :internal_server_error,
|
58
|
+
msg: "plural association handler returned `#{relation.class}', expected `ActiveRecord::Relation' (#{@config.target_model_class})"
|
59
|
+
end
|
60
|
+
|
61
|
+
call_allow(@config.via_get.permissions, @auth, source, @uri_params) # may raise NotAllowed, AllowError
|
62
|
+
|
63
|
+
|
64
|
+
# count = relation.count doesn't always work
|
65
|
+
# fix problematic select extensions for counting (-> { select(...) })
|
66
|
+
# this fails if the where clause depends on the the extended select
|
67
|
+
count = relation.count_by_sql relation.to_sql.sub(/SELECT.+FROM/,'SELECT COUNT(*) FROM')
|
68
|
+
headers = {"Content-Type" => @config.media_type}
|
69
|
+
|
70
|
+
if count > 0
|
71
|
+
range_end = if (range_start + window - 1) > (count - 1) # behind last
|
72
|
+
count - 1
|
73
|
+
else
|
74
|
+
(range_start + window - 1)
|
75
|
+
end
|
76
|
+
|
77
|
+
headers[ "Content-Range"] = "items=#{range_start}-#{range_end}/#{count}"
|
78
|
+
end
|
79
|
+
|
80
|
+
response :ok,
|
81
|
+
headers: headers,
|
82
|
+
body: represent(relation.limit(window).offset(range_start), target_config),
|
83
|
+
msg: "sent #{count} of #{target_config.model_class}"
|
84
|
+
|
85
|
+
|
86
|
+
rescue ActiveRecord::RecordNotFound
|
87
|
+
return response :not_found,
|
88
|
+
msg: "#{@config.model_class.name}##{@config.assoc_name} not found"
|
89
|
+
|
90
|
+
rescue AllowError => error
|
91
|
+
return response :internal_server_error,
|
92
|
+
msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
|
93
|
+
|
94
|
+
rescue BadRequest => error
|
95
|
+
response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
|
96
|
+
|
97
|
+
rescue HandlerError => error
|
98
|
+
return response :internal_server_error,
|
99
|
+
msg: "exception raised in via_get handler: `#{error.orig_error.message}' in #{error.source_location}"
|
100
|
+
rescue NotAllowed => error
|
101
|
+
return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
|
102
|
+
|
103
|
+
rescue ConfigNotFound => error
|
104
|
+
return response :internal_server_error,
|
105
|
+
msg: "no API configuration found for model `#{@config.target_model_class.name}'"
|
106
|
+
|
107
|
+
rescue => error
|
108
|
+
return response :internal_server_error,
|
109
|
+
msg: "exception raised: #{error} \n#{error.backtrace[0..5].join("\n")}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def post
|
115
|
+
if @config.via_post.nil?
|
116
|
+
response :method_not_allowed,
|
117
|
+
headers: {'Allow' => allowed_methods(@config)},
|
118
|
+
msg: "POST not configured"
|
119
|
+
else
|
120
|
+
begin
|
121
|
+
payload = JSON.parse(@request.body.read)
|
122
|
+
target_config = get_config(@config.target_model_class)
|
123
|
+
|
124
|
+
# remove all attributes not in writables from payload
|
125
|
+
payload.delete_if do |attr,val|
|
126
|
+
unless attr.to_sym.in?(target_config.writables)
|
127
|
+
Toast.logger.warn "<POST #{@request.fullpath}> received attribute `#{attr}' is not writable or unknown"
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
source = @config.base_model_class.find(@id)
|
133
|
+
|
134
|
+
call_allow(@config.via_post.permissions,
|
135
|
+
@auth, source, @uri_params)
|
136
|
+
|
137
|
+
new_instance = call_handler(@config.via_post.handler,
|
138
|
+
source, payload, @uri_params)
|
139
|
+
|
140
|
+
if new_instance.persisted?
|
141
|
+
response :created,
|
142
|
+
headers: {"Content-Type" => target_config.media_type},
|
143
|
+
body: represent(new_instance, target_config ),
|
144
|
+
msg: "created #{new_instance.class}##{new_instance.id}"
|
145
|
+
else
|
146
|
+
message = new_instance.errors.count > 0 ?
|
147
|
+
": " + new_instance.errors.full_messages.join(',') : ''
|
148
|
+
|
149
|
+
response :conflict,
|
150
|
+
msg: "creation of #{new_instance.class} aborted#{message}"
|
151
|
+
end
|
152
|
+
|
153
|
+
rescue ActiveRecord::RecordNotFound
|
154
|
+
response :not_found, msg: "#{@config.base_model_class.name}##{@id} not found"
|
155
|
+
|
156
|
+
rescue JSON::ParserError => error
|
157
|
+
return response :internal_server_error, msg: "expect JSON body"
|
158
|
+
|
159
|
+
rescue AllowError => error
|
160
|
+
return response :internal_server_error,
|
161
|
+
msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
|
162
|
+
|
163
|
+
rescue BadRequest => error
|
164
|
+
response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
|
165
|
+
|
166
|
+
rescue HandlerError => error
|
167
|
+
return response :internal_server_error,
|
168
|
+
msg: "exception raised in via_post handler: `#{error.orig_error.message}' in #{error.source_location}"
|
169
|
+
rescue NotAllowed => error
|
170
|
+
return response :unauthorized,
|
171
|
+
msg: "not authorized by allow block in: #{error.source_location}"
|
172
|
+
|
173
|
+
rescue ConfigNotFound => error
|
174
|
+
return response :internal_server_error,
|
175
|
+
msg: "no API configuration found for model `#{@config.target_model_class.name}'"
|
176
|
+
|
177
|
+
rescue => error
|
178
|
+
return response :internal_server_error,
|
179
|
+
msg: "exception raised: #{error} \n#{error.backtrace[0..5].join("\n")}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def link
|
185
|
+
if @config.via_link.nil?
|
186
|
+
response :method_not_allowed,
|
187
|
+
headers: {'Allow' => allowed_methods(@config)},
|
188
|
+
msg: "LINK not configured"
|
189
|
+
else
|
190
|
+
begin
|
191
|
+
source = @base_config.model_class.find(@id)
|
192
|
+
link = LinkHeader.parse(@request.headers['Link']).find_link(['rel','related'])
|
193
|
+
|
194
|
+
if link.nil? or URI(link.href).path.nil?
|
195
|
+
return response :bad_request, msg: "Link header missing or invalid"
|
196
|
+
end
|
197
|
+
|
198
|
+
name, target_id = URI(link.href).path.split('/')[1..-1]
|
199
|
+
target_model_class = name.singularize.classify.constantize
|
200
|
+
|
201
|
+
unless is_active_record? target_model_class
|
202
|
+
return response :not_found, msg: "target class `#{target_model_class.name}' is not an `ActiveRecord'"
|
203
|
+
end
|
204
|
+
|
205
|
+
target = target_model_class.find(target_id)
|
206
|
+
|
207
|
+
call_allow(@config.via_link.permissions, @auth, source, @uri_params)
|
208
|
+
call_handler(@config.via_link.handler, source, target, @uri_params)
|
209
|
+
|
210
|
+
response :ok,
|
211
|
+
msg: "linked #{target_model_class.name}##{@id} with #{source.class}##{source.id}.#{@config.assoc_name}"
|
212
|
+
|
213
|
+
rescue ActiveRecord::RecordNotFound => error
|
214
|
+
response :not_found, msg: error.message
|
215
|
+
|
216
|
+
rescue BadRequest => error
|
217
|
+
response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
|
218
|
+
|
219
|
+
rescue AllowError => error
|
220
|
+
return response :internal_server_error,
|
221
|
+
msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
|
222
|
+
rescue HandlerError => error
|
223
|
+
return response :internal_server_error,
|
224
|
+
msg: "exception raised in via_link handler: `#{error.orig_error.message}' in #{error.source_location}"
|
225
|
+
rescue NotAllowed => error
|
226
|
+
return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
|
227
|
+
|
228
|
+
rescue => error
|
229
|
+
response :internal_server_error,
|
230
|
+
msg: "exception from via_link handler #{error.message}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def unlink
|
236
|
+
if @config.via_unlink.nil?
|
237
|
+
response :method_not_allowed,
|
238
|
+
headers: {'Allow' => allowed_methods(@config)},
|
239
|
+
msg: "UNLINK not configured"
|
240
|
+
else
|
241
|
+
begin
|
242
|
+
source = @base_config.model_class.find(@id)
|
243
|
+
link = LinkHeader.parse(@request.headers['Link']).find_link(['rel','related'])
|
244
|
+
|
245
|
+
if link.nil? or URI(link.href).nil?
|
246
|
+
return response :bad_request, msg: "Link header missing or invalid"
|
247
|
+
end
|
248
|
+
|
249
|
+
name, id = URI(link.href).path.split('/')[1..-1]
|
250
|
+
target_model_class = name.singularize.classify.constantize
|
251
|
+
|
252
|
+
unless is_active_record? target_model_class
|
253
|
+
return response :not_found, msg: "target class `#{target_model_class.name}' is not an `ActiveRecord'"
|
254
|
+
end
|
255
|
+
|
256
|
+
call_allow(@config.via_unlink.permissions, @auth, source, @uri_params)
|
257
|
+
call_handler(@config.via_unlink.handler, source, target_model_class.find(id), @uri_params)
|
258
|
+
|
259
|
+
response :ok,
|
260
|
+
msg: "unlinked #{target_model_class.name}##{id} from #{source.class}##{source.id}.#{@config.assoc_name}"
|
261
|
+
|
262
|
+
rescue ActiveRecord::RecordNotFound => error
|
263
|
+
response :not_found, msg: error.message
|
264
|
+
|
265
|
+
rescue AllowError => error
|
266
|
+
return response :internal_server_error,
|
267
|
+
msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
|
268
|
+
|
269
|
+
rescue BadRequest => error
|
270
|
+
response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
|
271
|
+
|
272
|
+
rescue HandlerError => error
|
273
|
+
return response :internal_server_error,
|
274
|
+
msg: "exception raised in via_unlink handler: `#{error.orig_error.message}' in #{error.source_location}"
|
275
|
+
rescue NotAllowed => error
|
276
|
+
return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
|
277
|
+
|
278
|
+
rescue => error
|
279
|
+
response :internal_server_error,
|
280
|
+
msg: "exception from via_unlink handler: " + error.message
|
281
|
+
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'toast/collection_request'
|
2
|
+
require 'toast/canonical_request'
|
3
|
+
require 'toast/single_request'
|
4
|
+
require 'toast/singular_assoc_request'
|
5
|
+
require 'toast/plural_assoc_request'
|
6
|
+
require 'toast/errors'
|
7
|
+
|
8
|
+
class Toast::RackApp
|
9
|
+
# NOTE: the RackApp object is shared in threads of concurrent requests
|
10
|
+
# (e.g. when using Puma server, but not in Passenger (single-threded, multi-process)).
|
11
|
+
# Anyays, don't use any instance vars (@ variables in #call).
|
12
|
+
# It causes chaos
|
13
|
+
|
14
|
+
include Toast::RequestHelpers
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
|
18
|
+
request = ActionDispatch::Request.new(env)
|
19
|
+
|
20
|
+
Toast.logger.info "[#{Thread.current.object_id}] processing: <#{URI.decode(request.fullpath)}>"
|
21
|
+
|
22
|
+
# Authentication: respond with 401 on exception or falsy return value:
|
23
|
+
begin
|
24
|
+
unless (auth = Toast::ConfigDSL::Settings::AuthenticateContext.new.
|
25
|
+
instance_exec(request, &Toast.settings.authenticate))
|
26
|
+
return response :unauthorized, msg: "authentication failed"
|
27
|
+
end
|
28
|
+
rescue Toast::Errors::CustomAuthFailure => caf
|
29
|
+
return response(caf.response_data[:status] || :unauthorized,
|
30
|
+
msg: caf.response_data[:body],
|
31
|
+
headers: caf.response_data[:headers])
|
32
|
+
rescue => error
|
33
|
+
return response :unauthorized, msg: "authentication failed: `#{error.message}'"
|
34
|
+
end
|
35
|
+
|
36
|
+
path = request.path_parameters[:toast_path].split('/')
|
37
|
+
|
38
|
+
# strip path prefixes
|
39
|
+
# get model class
|
40
|
+
begin
|
41
|
+
if path.first.singularize == path.first # not in plural form?
|
42
|
+
raise NameError
|
43
|
+
end
|
44
|
+
model_class = path.first.singularize.classify.constantize
|
45
|
+
raise NameError unless is_active_record?(model_class)
|
46
|
+
rescue NameError
|
47
|
+
return response :not_found,
|
48
|
+
msg: "resource at /#{path.join('/')} not found"
|
49
|
+
end
|
50
|
+
|
51
|
+
# select base configuration
|
52
|
+
|
53
|
+
## This is unused. Do we need it at all?
|
54
|
+
# preferred_type = Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT']).prefered || "application/json"
|
55
|
+
|
56
|
+
# the base_config is the configuration for the model corresponding to the first part of the URI path
|
57
|
+
begin
|
58
|
+
base_config = get_config(model_class)
|
59
|
+
rescue Toast::Errors::ConfigNotFound => error
|
60
|
+
return response :internal_server_error,
|
61
|
+
msg: "no API configuration found for model `#{model_class.name}'"
|
62
|
+
end
|
63
|
+
|
64
|
+
toast_request =
|
65
|
+
case path.length
|
66
|
+
when 1 # root collection: /apples
|
67
|
+
|
68
|
+
if base_config.collections[:all].nil?
|
69
|
+
return response :not_found, msg: "collection `/#{path[0]}' not configured"
|
70
|
+
else
|
71
|
+
# root collection
|
72
|
+
Toast::CollectionRequest.new( base_config.collections[:all],
|
73
|
+
base_config,
|
74
|
+
auth,
|
75
|
+
request)
|
76
|
+
end
|
77
|
+
when 2 # canonical, single or collection: /apples/10 , /apples/first, /apples/red_ones
|
78
|
+
if path.second =~ /\A\d+\z/
|
79
|
+
Toast::CanonicalRequest.new( path.second.to_i,
|
80
|
+
base_config,
|
81
|
+
auth,
|
82
|
+
request )
|
83
|
+
else
|
84
|
+
if col_config = base_config.collections[path.second.to_sym]
|
85
|
+
Toast::CollectionRequest.new(col_config,
|
86
|
+
base_config,
|
87
|
+
auth,
|
88
|
+
request)
|
89
|
+
|
90
|
+
elsif sin_config = base_config.singles[path.second.to_sym]
|
91
|
+
Toast::SingleRequest.new(sin_config,
|
92
|
+
base_config,
|
93
|
+
auth,
|
94
|
+
request)
|
95
|
+
else
|
96
|
+
return response :not_found,
|
97
|
+
msg: "collection or single `#{path.second}' not configured in: #{base_config.source_location}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
when 3 # association: /apples/10/tree, /tree/5/apples
|
102
|
+
|
103
|
+
if assoc_config = base_config.associations[path.third.to_sym]
|
104
|
+
if assoc_config.singular
|
105
|
+
|
106
|
+
Toast::SingularAssocRequest.new( path.second.to_i,
|
107
|
+
assoc_config,
|
108
|
+
base_config,
|
109
|
+
auth,
|
110
|
+
request )
|
111
|
+
|
112
|
+
else
|
113
|
+
# process_plural_association assoc_config, path.second.to_i
|
114
|
+
Toast::PluralAssocRequest.new( path.second.to_i,
|
115
|
+
assoc_config,
|
116
|
+
base_config,
|
117
|
+
auth,
|
118
|
+
request )
|
119
|
+
end
|
120
|
+
else
|
121
|
+
return response :not_found,
|
122
|
+
msg: "association `#{model_class.name}##{path.third}' not configured"
|
123
|
+
end
|
124
|
+
|
125
|
+
else
|
126
|
+
return response :not_found,
|
127
|
+
msg: "#{request.url} not found (invalid path)"
|
128
|
+
end
|
129
|
+
|
130
|
+
toast_request.respond
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'toast/errors'
|
2
|
+
|
3
|
+
module Toast::RequestHelpers
|
4
|
+
|
5
|
+
def get_config model_class
|
6
|
+
Toast.expositions.detect do |exp|
|
7
|
+
exp.model_class == model_class
|
8
|
+
end || raise(Toast::Errors::ConfigNotFound)
|
9
|
+
end
|
10
|
+
|
11
|
+
# this is hard when behind a proxy
|
12
|
+
# relies on HTTP_X_FORWARDED* headers
|
13
|
+
def base_uri request
|
14
|
+
port = ":#{request.port}" unless request.port.in?([80,443])
|
15
|
+
path = request.path.sub(request.path_parameters[:toast_path]+'/','')
|
16
|
+
(request.protocol + request.host + port.to_s + path).chomp('/')
|
17
|
+
end
|
18
|
+
|
19
|
+
def represent_one record, config
|
20
|
+
result = {}
|
21
|
+
|
22
|
+
(config.readables + config.writables).each do |attr|
|
23
|
+
result[attr.to_s] = record.send(attr) if attr_selected?(attr)
|
24
|
+
end
|
25
|
+
|
26
|
+
model_uri = "#{@base_uri}/#{record.class.name.underscore.pluralize}"
|
27
|
+
result['self'] = "#{model_uri}/#{record.id}" if attr_selected?('self')
|
28
|
+
|
29
|
+
# add associations, collections and singles
|
30
|
+
config.associations.each do |name, config|
|
31
|
+
result[name.to_s] = "#{model_uri}/#{record.id}/#{name}" if attr_selected?(name)
|
32
|
+
end
|
33
|
+
|
34
|
+
config.singles.each do |name, config|
|
35
|
+
result[name.to_s] = "#{model_uri}/#{name}" if attr_selected?(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
config.collections.each do |name, config|
|
39
|
+
if attr_selected?(name)
|
40
|
+
result[name.to_s] = if name == :all
|
41
|
+
"#{model_uri}"
|
42
|
+
else
|
43
|
+
"#{model_uri}/#{name}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def represent record_or_enum, config
|
52
|
+
result = if record_or_enum.is_a? Enumerable
|
53
|
+
record_or_enum.map do |record|
|
54
|
+
represent_one(record, config)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
represent_one(record_or_enum, config)
|
58
|
+
end
|
59
|
+
|
60
|
+
result.to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
def allowed_methods(config)
|
64
|
+
["DELETE", "GET", "LINK", "PATCH", "POST", "UNLINK"].select{|m|
|
65
|
+
!config.send("via_#{m.downcase}").nil?
|
66
|
+
}.join(", ")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Builds a Rack conform response tripel. Should be called at the end of the
|
70
|
+
# request processing.
|
71
|
+
#
|
72
|
+
# Params:
|
73
|
+
# - status_sym [Symbol] A status code name like :ok, :unauthorized, etc.
|
74
|
+
# - headers: [Hash] HTTP headers, defaults to empty Hash
|
75
|
+
# - msg: [String] A Message for Toast log file, will be included in the body,
|
76
|
+
# if body is no set and app is in non-production modes
|
77
|
+
# - body: [String] The repsosne body text, default to nil
|
78
|
+
#
|
79
|
+
# Return: Rack conform response
|
80
|
+
def response status_sym, headers: {}, msg: nil, body: nil
|
81
|
+
Toast.logger.info "[#{Thread.current.object_id}] done: #{msg}"
|
82
|
+
|
83
|
+
unless Rails.env == 'production'
|
84
|
+
# put message in body, too, if body is free
|
85
|
+
body ||= msg
|
86
|
+
end
|
87
|
+
|
88
|
+
[ Rack::Utils::SYMBOL_TO_STATUS_CODE[status_sym],
|
89
|
+
headers,
|
90
|
+
[body] ]
|
91
|
+
end
|
92
|
+
|
93
|
+
def call_allow procs, *args
|
94
|
+
procs.each do |proc|
|
95
|
+
# call all procs, break if proc returns false and raise
|
96
|
+
begin
|
97
|
+
result = Object.new.instance_exec *args, &proc
|
98
|
+
rescue => error
|
99
|
+
raise Toast::Errors::AllowError.new(error, proc.source_location.join(':'))
|
100
|
+
end
|
101
|
+
|
102
|
+
if result == false
|
103
|
+
raise Toast::Errors::NotAllowed.new(proc.source_location.join(':'))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def call_handler proc, *args
|
109
|
+
result = nil
|
110
|
+
|
111
|
+
begin
|
112
|
+
context = Object.new
|
113
|
+
context.define_singleton_method(:bad_request) do |message|
|
114
|
+
raise Toast::Errors::BadRequest.new message, caller.first.sub(/:in.*/,'')
|
115
|
+
end
|
116
|
+
|
117
|
+
result = context.instance_exec *args, &proc
|
118
|
+
|
119
|
+
rescue Toast::Errors::BadRequest
|
120
|
+
raise # re-raise
|
121
|
+
rescue => error
|
122
|
+
raise Toast::Errors::HandlerError.new(error, error.backtrace.first.sub(/:in.*/,''))
|
123
|
+
end
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
def is_active_record? klass
|
128
|
+
klass.include? ActiveRecord::Core
|
129
|
+
end
|
130
|
+
|
131
|
+
def attr_selected? name
|
132
|
+
(@selected_attributes.nil? or @selected_attributes.include?(name.to_s))
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'toast/request_helpers'
|
2
|
+
|
3
|
+
class Toast::SingleRequest
|
4
|
+
include Toast::RequestHelpers
|
5
|
+
include Toast::Errors
|
6
|
+
|
7
|
+
def initialize config, base_config, auth, request
|
8
|
+
@config = config
|
9
|
+
@base_config = base_config
|
10
|
+
@selected_attributes = request.query_parameters.delete(:toast_select).try(:split,',')
|
11
|
+
@uri_params = request.query_parameters
|
12
|
+
@base_uri = base_uri(request)
|
13
|
+
@verb = request.request_method.downcase
|
14
|
+
@auth = auth
|
15
|
+
@path = request.path_parameters[:toast_path]#.split('/')
|
16
|
+
@request = request
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond
|
20
|
+
|
21
|
+
if @config.via_get.nil?
|
22
|
+
# not declared
|
23
|
+
response :method_not_allowed,
|
24
|
+
headers: {'Allow' => allowed_methods(@config)},
|
25
|
+
msg: "GET not configured"
|
26
|
+
else
|
27
|
+
begin
|
28
|
+
model = call_handler(@config.via_get.handler, @uri_params)
|
29
|
+
call_allow(@config.via_get.permissions, @auth, model, @uri_params)
|
30
|
+
|
31
|
+
case model
|
32
|
+
when @base_config.model_class
|
33
|
+
response :ok,
|
34
|
+
headers: {"Content-Type" => @base_config.media_type},
|
35
|
+
body: represent(model, @base_config)
|
36
|
+
when nil
|
37
|
+
response :not_found, msg: "resource not found at /#{@path}"
|
38
|
+
else
|
39
|
+
# wrong class/model_class
|
40
|
+
response :internal_server_error,
|
41
|
+
msg: "single method returned `#{model.class}', expected `#{@base_config.model_class}'"
|
42
|
+
end
|
43
|
+
|
44
|
+
rescue ActiveRecord::RecordNotFound => error
|
45
|
+
response :not_found, msg: error.message
|
46
|
+
|
47
|
+
rescue AllowError => error
|
48
|
+
return response :internal_server_error,
|
49
|
+
msg: "exception raised in allow block: `#{error.orig_error.message}' in #{error.source_location}"
|
50
|
+
|
51
|
+
rescue BadRequest => error
|
52
|
+
response :bad_request, msg: "`#{error.message}' in: #{error.source_location}"
|
53
|
+
|
54
|
+
rescue HandlerError => error
|
55
|
+
return response :internal_server_error,
|
56
|
+
msg: "exception raised in handler: `#{error.orig_error.message}' in #{error.source_location}"
|
57
|
+
rescue NotAllowed => error
|
58
|
+
return response :unauthorized, msg: "not authorized by allow block in: #{error.source_location}"
|
59
|
+
|
60
|
+
rescue => error
|
61
|
+
return response :internal_server_error,
|
62
|
+
msg: "exception raised: #{error} \n#{error.backtrace[0..5].join("\n")}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|