belt 0.0.1 → 0.0.6
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +25 -0
- data/README.md +266 -4
- data/certs/stowzilla.pem +26 -0
- data/lib/belt/action_router.rb +166 -0
- data/lib/belt/helpers/cors_origin.rb +55 -0
- data/lib/belt/helpers/error_logging.rb +55 -0
- data/lib/belt/helpers/response.rb +70 -0
- data/lib/belt/lambda_handler.rb +96 -0
- data/lib/belt/observability.rb +64 -0
- data/lib/belt/parameters.rb +177 -0
- data/lib/belt/version.rb +3 -1
- data/lib/belt.rb +54 -1
- data/lib/belt_controller/base.rb +282 -0
- data.tar.gz.sig +2 -0
- metadata +57 -5
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
require_relative '../belt/parameters'
|
|
6
|
+
require_relative '../belt/helpers/response'
|
|
7
|
+
require_relative '../belt/helpers/error_logging'
|
|
8
|
+
require_relative '../belt/helpers/cors_origin'
|
|
9
|
+
|
|
10
|
+
module BeltController
|
|
11
|
+
class Base
|
|
12
|
+
include Belt::Helpers::Response
|
|
13
|
+
|
|
14
|
+
attr_reader :event, :body
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def rescue_handlers
|
|
18
|
+
@rescue_handlers ||= {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rescue_from(*exceptions, with:)
|
|
22
|
+
exceptions.each { |e| rescue_handlers[e] = with }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def before_actions
|
|
26
|
+
@before_actions ||= []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def before_action(method_name, only: nil, except: nil)
|
|
30
|
+
before_actions << { method: method_name, only: only&.map(&:to_sym), except: except&.map(&:to_sym) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def after_actions
|
|
34
|
+
@after_actions ||= []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def after_action(method_name, only: nil, except: nil)
|
|
38
|
+
after_actions << { method: method_name, only: only&.map(&:to_sym), except: except&.map(&:to_sym) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def skipped_before_actions
|
|
42
|
+
@skipped_before_actions ||= []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def skip_before_action(method_name, only: nil, except: nil)
|
|
46
|
+
skipped_before_actions << { method: method_name, only: only&.map(&:to_sym), except: except&.map(&:to_sym) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def all_before_actions
|
|
50
|
+
if superclass.respond_to?(:all_before_actions)
|
|
51
|
+
superclass.all_before_actions + before_actions
|
|
52
|
+
else
|
|
53
|
+
before_actions
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def all_after_actions
|
|
58
|
+
if superclass.respond_to?(:all_after_actions)
|
|
59
|
+
superclass.all_after_actions + after_actions
|
|
60
|
+
else
|
|
61
|
+
after_actions
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def all_skipped_before_actions
|
|
66
|
+
if superclass.respond_to?(:all_skipped_before_actions)
|
|
67
|
+
superclass.all_skipped_before_actions + skipped_before_actions
|
|
68
|
+
else
|
|
69
|
+
skipped_before_actions
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def all_rescue_handlers
|
|
74
|
+
if superclass.respond_to?(:all_rescue_handlers)
|
|
75
|
+
superclass.all_rescue_handlers.merge(rescue_handlers)
|
|
76
|
+
else
|
|
77
|
+
rescue_handlers
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
rescue_from ArgumentError, with: :handle_argument_error
|
|
83
|
+
rescue_from Belt::RecordNotFound, with: :handle_not_found
|
|
84
|
+
rescue_from Belt::AuthenticationError, with: :handle_authentication_error
|
|
85
|
+
rescue_from Belt::ActionNotFound, with: :handle_action_not_found
|
|
86
|
+
rescue_from ActionController::ParameterMissing, with: :handle_parameter_missing
|
|
87
|
+
rescue_from ActionController::UnpermittedParameters, with: :handle_unpermitted_parameters
|
|
88
|
+
|
|
89
|
+
rescue_from ActiveItem::RecordNotFound, with: :handle_not_found if defined?(ActiveItem::RecordNotFound)
|
|
90
|
+
|
|
91
|
+
rescue_from ActiveModel::ValidationError, with: :handle_validation_error if defined?(ActiveModel::ValidationError)
|
|
92
|
+
|
|
93
|
+
def initialize(event:, body:)
|
|
94
|
+
@event = event
|
|
95
|
+
@raw_body = deep_transform_keys_to_snake_case(body || {})
|
|
96
|
+
@params = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def params
|
|
100
|
+
@params ||= build_params
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def body
|
|
104
|
+
@raw_body
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def dispatch(action_name)
|
|
108
|
+
action_sym = action_name.to_sym
|
|
109
|
+
@current_action = action_sym
|
|
110
|
+
|
|
111
|
+
unless respond_to?(action_sym)
|
|
112
|
+
raise Belt::ActionNotFound, "The action '#{action_sym}' could not be found for #{self.class.name}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
run_before_actions(action_sym)
|
|
116
|
+
result = send(action_sym)
|
|
117
|
+
run_after_actions(action_sym)
|
|
118
|
+
result
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
handle_exception(e)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def authenticate!
|
|
124
|
+
user_id = event.dig('requestContext', 'authorizer', 'claims', 'sub')
|
|
125
|
+
raise Belt::AuthenticationError, 'Authentication required' unless user_id
|
|
126
|
+
|
|
127
|
+
@current_user_id = user_id
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def current_user_id
|
|
131
|
+
@current_user_id ||= event.dig('requestContext', 'authorizer', 'claims', 'sub')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def user_groups
|
|
135
|
+
@user_groups ||= extract_user_groups
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def admin?
|
|
139
|
+
user_groups.include?('Admin')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def employee?
|
|
143
|
+
user_groups.include?('Employee')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def action_name
|
|
147
|
+
@current_action
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def run_before_actions(action_sym)
|
|
153
|
+
self.class.all_before_actions.each do |callback|
|
|
154
|
+
next if callback[:only] && !callback[:only].include?(action_sym)
|
|
155
|
+
next if callback[:except]&.include?(action_sym)
|
|
156
|
+
next if should_skip_callback?(callback[:method], action_sym)
|
|
157
|
+
|
|
158
|
+
send(callback[:method])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def run_after_actions(action_sym)
|
|
163
|
+
self.class.all_after_actions.each do |callback|
|
|
164
|
+
next if callback[:only] && !callback[:only].include?(action_sym)
|
|
165
|
+
next if callback[:except]&.include?(action_sym)
|
|
166
|
+
|
|
167
|
+
send(callback[:method])
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def should_skip_callback?(method_name, action_sym)
|
|
172
|
+
self.class.all_skipped_before_actions.any? do |skip|
|
|
173
|
+
next false unless skip[:method] == method_name
|
|
174
|
+
|
|
175
|
+
if skip[:only]
|
|
176
|
+
skip[:only].include?(action_sym)
|
|
177
|
+
elsif skip[:except]
|
|
178
|
+
!skip[:except].include?(action_sym)
|
|
179
|
+
else
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def handle_exception(exception, context = {})
|
|
186
|
+
if @current_action
|
|
187
|
+
controller_name = self.class.name.split('::').last.sub('Controller', '').downcase
|
|
188
|
+
context[:action] ||= "#{controller_name}##{@current_action}"
|
|
189
|
+
end
|
|
190
|
+
context[:resource_id] ||= params['id'] if params['id']
|
|
191
|
+
|
|
192
|
+
handler = find_rescue_handler(exception.class)
|
|
193
|
+
if handler
|
|
194
|
+
send(handler, exception, context)
|
|
195
|
+
else
|
|
196
|
+
handle_error_and_respond(exception, 'Internal server error', context, 500)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def find_rescue_handler(exception_class)
|
|
201
|
+
handlers = self.class.all_rescue_handlers
|
|
202
|
+
return handlers[exception_class] if handlers[exception_class]
|
|
203
|
+
|
|
204
|
+
exception_class.ancestors.each do |ancestor|
|
|
205
|
+
break if ancestor == Object
|
|
206
|
+
return handlers[ancestor] if handlers[ancestor]
|
|
207
|
+
end
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def handle_argument_error(exception, _context = {})
|
|
212
|
+
error_response(exception.message, 400)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_validation_error(exception, _context = {})
|
|
216
|
+
error_response(exception.model.errors.full_messages.join(', '), 400)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def handle_not_found(exception, _context = {})
|
|
220
|
+
error_response(exception.message.to_s.empty? ? 'Record not found' : exception.message, 404)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def handle_action_not_found(exception, _context = {})
|
|
224
|
+
error_response(exception.message, 404)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def handle_authentication_error(exception, _context = {})
|
|
228
|
+
error_response(exception.message.to_s.empty? ? 'Authentication required' : exception.message, 401)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def handle_parameter_missing(exception, _context = {})
|
|
232
|
+
error_response("Missing required parameter: #{exception.param}", 400)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_unpermitted_parameters(exception, _context = {})
|
|
236
|
+
error_response("Unpermitted parameters: #{exception.params.join(', ')}", 400)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def build_params
|
|
240
|
+
path_params = extract_path_params(@event)
|
|
241
|
+
query_params = @event['queryStringParameters'] || {}
|
|
242
|
+
merged = query_params.merge(@raw_body).merge(path_params)
|
|
243
|
+
ActionController::Parameters.new(merged)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def extract_path_params(event)
|
|
247
|
+
(event['pathParameters'] || {}).transform_keys { |key| key.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') }
|
|
248
|
+
.transform_values { |v| CGI.unescape(v.to_s) }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def deep_transform_keys_to_snake_case(value)
|
|
252
|
+
case value
|
|
253
|
+
when Hash
|
|
254
|
+
value.transform_keys { |key| key.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '') }
|
|
255
|
+
.transform_values { |v| deep_transform_keys_to_snake_case(v) }
|
|
256
|
+
when Array
|
|
257
|
+
value.map { |item| deep_transform_keys_to_snake_case(item) }
|
|
258
|
+
else
|
|
259
|
+
value
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def extract_user_groups
|
|
264
|
+
groups = event.dig('requestContext', 'authorizer', 'claims', 'cognito:groups')
|
|
265
|
+
return parse_groups(groups) if groups
|
|
266
|
+
|
|
267
|
+
[]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def parse_groups(groups)
|
|
271
|
+
return groups if groups.is_a?(Array)
|
|
272
|
+
return [] unless groups.is_a?(String)
|
|
273
|
+
|
|
274
|
+
begin
|
|
275
|
+
parsed = JSON.parse(groups)
|
|
276
|
+
return parsed if parsed.is_a?(Array)
|
|
277
|
+
rescue JSON::ParserError
|
|
278
|
+
end
|
|
279
|
+
groups.split(',').map(&:strip)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
data.tar.gz.sig
ADDED
metadata
CHANGED
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
bindir: bin
|
|
9
|
-
cert_chain:
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIEdDCCAtygAwIBAgIBATANBgkqhkiG9w0BAQsFADBAMQ4wDAYDVQQDDAVhZ2Vu
|
|
13
|
+
dDEZMBcGCgmSJomT8ixkARkWCXN0b3d6aWxsYTETMBEGCgmSJomT8ixkARkWA2Nv
|
|
14
|
+
bTAeFw0yNjA2MDgxOTExNTlaFw0yNzA2MDgxOTExNTlaMEAxDjAMBgNVBAMMBWFn
|
|
15
|
+
ZW50MRkwFwYKCZImiZPyLGQBGRYJc3Rvd3ppbGxhMRMwEQYKCZImiZPyLGQBGRYD
|
|
16
|
+
Y29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAupBquKI/4WvXOgND
|
|
17
|
+
pXyqH2GllZs1wG4TWWdn/DoMg45UoCwD+AWEuGrIdInBCpPN8vEJNJWPoM/RrU+b
|
|
18
|
+
xRBZT4uUk00bnZRW2SYh5GJSqBoBR+rWc2DGkXyGfdRU2sQvkB0+is6ChgQ61WMM
|
|
19
|
+
33LE9+loBlVsZ6EVtrc18Uh2OW0mJpe0hN2nmBrxZqqOZigxC4DKRMFHvpRkxSb6
|
|
20
|
+
mD4kit1AcwX9NEWJsXxrPaetL/SB/VbXaEZX93XAvp6USaXvCWt4slkDS2mIvqtn
|
|
21
|
+
9DtGC43LFC7SDGbnsG9PVenQgVCi8UWFPUAab0PqZSlmi3Qlbhw8qTGPp5Cbv4vz
|
|
22
|
+
qjC2UGPOQigA/7lbbGRhCohMrjOVHMAQwkcgiIqtolUoYlnvPMIy+m3pdvgDv/PH
|
|
23
|
+
bsZGvXQ7i0458xsmp1vaKthZocVAR+GboHbuIiYPUnO45ccXUQ00x6365tTe7mZi
|
|
24
|
+
NvmUYdAGbQmVvFqyxF7IYA6sF74L2Lstu0knSfss557bAe1HAgMBAAGjeTB3MAkG
|
|
25
|
+
A1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSnxTL/lNBCeLqpeVIX6AUY
|
|
26
|
+
kel4zjAeBgNVHREEFzAVgRNhZ2VudEBzdG93emlsbGEuY29tMB4GA1UdEgQXMBWB
|
|
27
|
+
E2FnZW50QHN0b3d6aWxsYS5jb20wDQYJKoZIhvcNAQELBQADggGBACm9Fjit/UCv
|
|
28
|
+
FxlKqeiCTIG94cIx+QrWAOJSx9knKydwUec1u04D/DbfZjTn3C2Bj227QgxeUn+6
|
|
29
|
+
if3e2v7zAk1896hLmGYzML0+nxQPb0vmtdLR7HETUlSKTVabcv1fbwLyjsuGrBvk
|
|
30
|
+
y51vOEzUEZ508a9yepLYqrQu1kOju4d57c9oA5l3H0mMKWz7av9tFj0B+STvuaWk
|
|
31
|
+
HRYDWc5HgOEVTyV+w0uFt2Kw4OCb8C42uSvC5RfYYtw78MSP+5Ru+LXJ7XOtmuN0
|
|
32
|
+
E6GVmofQ17ig9O3rgfFbMendSInrRmvPIGswvM1yivq9NOllFbdck2OJKPx6FCJF
|
|
33
|
+
7SJIkXQfc9P4B5iASIV1d1FsE0YX+g3jHXPJK/4mGL5bAyBKzpMfQB/mg6vQBzkh
|
|
34
|
+
aOKPwcreFj7TznBl89R5tNS9wZQfPVR98zgPyocddWhK18eQNMSBUnv4eeJ8PPbk
|
|
35
|
+
DovL+G8ajHDZ9fjH/+GVYHEMuiVdLarXrKJpHC1VfGTTUAp4NSEpUQ==
|
|
36
|
+
-----END CERTIFICATE-----
|
|
10
37
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
38
|
+
dependencies:
|
|
39
|
+
- !ruby/object:Gem::Dependency
|
|
40
|
+
name: lambda_loadout
|
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.0'
|
|
46
|
+
type: :runtime
|
|
47
|
+
prerelease: false
|
|
48
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.0'
|
|
12
53
|
description: Belt provides a collection of lightweight utilities for Ruby applications.
|
|
13
54
|
email:
|
|
14
55
|
- andy@stowzilla.com
|
|
@@ -17,17 +58,28 @@ executables: []
|
|
|
17
58
|
extensions: []
|
|
18
59
|
extra_rdoc_files: []
|
|
19
60
|
files:
|
|
61
|
+
- CHANGELOG.md
|
|
20
62
|
- LICENSE.txt
|
|
21
63
|
- README.md
|
|
64
|
+
- certs/stowzilla.pem
|
|
22
65
|
- lib/belt.rb
|
|
66
|
+
- lib/belt/action_router.rb
|
|
67
|
+
- lib/belt/helpers/cors_origin.rb
|
|
68
|
+
- lib/belt/helpers/error_logging.rb
|
|
69
|
+
- lib/belt/helpers/response.rb
|
|
70
|
+
- lib/belt/lambda_handler.rb
|
|
71
|
+
- lib/belt/observability.rb
|
|
72
|
+
- lib/belt/parameters.rb
|
|
23
73
|
- lib/belt/version.rb
|
|
74
|
+
- lib/belt_controller/base.rb
|
|
24
75
|
homepage: https://github.com/stowzilla/belt
|
|
25
76
|
licenses:
|
|
26
77
|
- MIT
|
|
27
78
|
metadata:
|
|
28
79
|
homepage_uri: https://github.com/stowzilla/belt
|
|
29
80
|
source_code_uri: https://github.com/stowzilla/belt
|
|
30
|
-
changelog_uri: https://github.com/stowzilla/belt/blob/
|
|
81
|
+
changelog_uri: https://github.com/stowzilla/belt/blob/master/CHANGELOG.md
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
31
83
|
rdoc_options: []
|
|
32
84
|
require_paths:
|
|
33
85
|
- lib
|
|
@@ -42,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
42
94
|
- !ruby/object:Gem::Version
|
|
43
95
|
version: '0'
|
|
44
96
|
requirements: []
|
|
45
|
-
rubygems_version:
|
|
97
|
+
rubygems_version: 3.6.9
|
|
46
98
|
specification_version: 4
|
|
47
99
|
summary: Belt - a utility toolkit for Ruby applications
|
|
48
100
|
test_files: []
|
metadata.gz.sig
ADDED
|
Binary file
|