toast 0.9.5 → 1.0.0
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.
- 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
|