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.
@@ -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
@@ -0,0 +1,2 @@
1
+ �PYJeIu+B���G��*��?L�v��õ,Bz�y���z�s�CYR����o�&.Uj�l#[�/�x2*�x�vKո0���a�'�K�Xi�ӦDV!"@��4_d��DțA�
2
+ ��P%��qE��-��X-,�Ssv$=����쩞�p�~Қ�¬�4=�
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.1
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/main/CHANGELOG.md
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: 4.0.11
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