shark-on-lambda 1.0.1 → 2.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/changelog.md +12 -0
  4. data/doc/upgrade-from-0.6.x-to-1.x.md +3 -3
  5. data/lib/shark-on-lambda.rb +9 -0
  6. data/lib/shark_on_lambda.rb +14 -54
  7. data/lib/shark_on_lambda/api_gateway_handler.rb +3 -55
  8. data/lib/shark_on_lambda/application.rb +74 -2
  9. data/lib/shark_on_lambda/base_controller.rb +29 -9
  10. data/lib/shark_on_lambda/cacheable.rb +21 -0
  11. data/lib/shark_on_lambda/configuration.rb +1 -65
  12. data/lib/shark_on_lambda/inferrers/serializer_inferrer.rb +10 -7
  13. data/lib/shark_on_lambda/jsonapi_renderer.rb +1 -2
  14. data/lib/shark_on_lambda/middleware/honeybadger.rb +5 -3
  15. data/lib/shark_on_lambda/middleware/jsonapi_rescuer.rb +21 -1
  16. data/lib/shark_on_lambda/middleware/lambda_logger.rb +5 -13
  17. data/lib/shark_on_lambda/rake_tasks.rb +16 -0
  18. data/lib/shark_on_lambda/request.rb +0 -3
  19. data/lib/shark_on_lambda/rspec/env_builder.rb +71 -37
  20. data/lib/shark_on_lambda/rspec/helpers.rb +5 -88
  21. data/lib/shark_on_lambda/rspec/request_helpers.rb +63 -0
  22. data/lib/shark_on_lambda/rspec/{jsonapi_helpers.rb → response_helpers.rb} +4 -10
  23. data/lib/shark_on_lambda/version.rb +1 -1
  24. data/shark-on-lambda.gemspec +7 -5
  25. metadata +37 -37
  26. data/lib/shark_on_lambda/concerns/resettable_singleton.rb +0 -18
  27. data/lib/shark_on_lambda/concerns/yaml_config_loader.rb +0 -28
  28. data/lib/shark_on_lambda/dispatcher.rb +0 -26
  29. data/lib/shark_on_lambda/inferrers/name_inferrer.rb +0 -66
  30. data/lib/shark_on_lambda/jsonapi_controller.rb +0 -32
  31. data/lib/shark_on_lambda/middleware/rescuer.rb +0 -37
  32. data/lib/shark_on_lambda/query.rb +0 -67
  33. data/lib/shark_on_lambda/rack_adapters/api_gateway.rb +0 -128
  34. data/lib/shark_on_lambda/secrets.rb +0 -43
@@ -31,14 +31,17 @@ module SharkOnLambda
31
31
  def serializer_class_names
32
32
  return @serializer_class_names if defined?(@serializer_class_names)
33
33
 
34
- @serializer_class_names = object_class.ancestors.map do |ancestor|
35
- ancestor_name = ancestor.name
36
- next if ancestor_name.blank?
34
+ @serializer_class_names =
35
+ object_class.ancestors.reduce([]) do |result, ancestor|
36
+ ancestor_name = ancestor.name
37
+ next result if ancestor_name.blank?
37
38
 
38
- name_inferrer = NameInferrer.from_model_name(ancestor_name)
39
- name_inferrer.serializer
40
- end
41
- @serializer_class_names.compact! || @serializer_class_names
39
+ result << serializer_name_from_model_name(ancestor_name)
40
+ end
41
+ end
42
+
43
+ def serializer_name_from_model_name(model_name)
44
+ "#{model_name.underscore}_serializer".camelize
42
45
  end
43
46
  end
44
47
  end
@@ -100,7 +100,7 @@ module SharkOnLambda
100
100
  def transform_active_model_errors(errors)
101
101
  return errors unless active_model_error?(errors)
102
102
 
103
- result = errors.messages.map do |attribute, attribute_errors|
103
+ errors.messages.flat_map do |attribute, attribute_errors|
104
104
  attribute_errors.map do |attribute_error|
105
105
  error_message = "`#{attribute_name(attribute)}' #{attribute_error}"
106
106
  Errors[422].new(error_message).tap do |error|
@@ -108,7 +108,6 @@ module SharkOnLambda
108
108
  end
109
109
  end
110
110
  end
111
- result.flatten! || result
112
111
  end
113
112
 
114
113
  def unrenderable_objects(object, options)
@@ -26,12 +26,14 @@ module SharkOnLambda
26
26
  end
27
27
 
28
28
  def notify(error, env)
29
+ params = env.fetch('action_dispatch.request.parameters', {})
30
+
29
31
  ::Honeybadger.notify(
30
32
  error,
31
33
  tags: tags,
32
- controller: env['shark.controller'],
33
- action: env['shark.action'],
34
- parameters: env['action_dispatch.request.parameters']
34
+ controller: params[:controller],
35
+ action: params[:action],
36
+ parameters: params
35
37
  )
36
38
  end
37
39
 
@@ -2,7 +2,15 @@
2
2
 
3
3
  module SharkOnLambda
4
4
  module Middleware
5
- class JsonapiRescuer < Rescuer
5
+ class JsonapiRescuer < Base
6
+ def call!(env)
7
+ app.call(env)
8
+ rescue Errors::Base => e
9
+ rescue_shark_error(e)
10
+ rescue StandardError => e
11
+ rescue_standard_error(e)
12
+ end
13
+
6
14
  private
7
15
 
8
16
  def error_object(status, message)
@@ -26,6 +34,18 @@ module SharkOnLambda
26
34
 
27
35
  [status, headers, response_body]
28
36
  end
37
+
38
+ def rescue_shark_error(error)
39
+ status = error.status || 500
40
+ error_response(status, {}, error.message)
41
+ end
42
+
43
+ def rescue_standard_error(error)
44
+ SharkOnLambda.logger.error(error.message)
45
+ SharkOnLambda.logger.error(error.backtrace.join("\n"))
46
+
47
+ error_response(500, {}, error.message)
48
+ end
29
49
  end
30
50
  end
31
51
  end
@@ -13,13 +13,10 @@ module SharkOnLambda
13
13
  def call!(env)
14
14
  start_time = Time.now
15
15
  response = app.call(env)
16
- end_time = Time.now
16
+ duration = duration_in_ms(start_time, Time.now)
17
17
 
18
18
  if logger.info?
19
- log_request(env: env,
20
- response: response,
21
- start_time: start_time,
22
- end_time: end_time)
19
+ log_request(env: env, response: response, duration: duration)
23
20
  end
24
21
 
25
22
  response
@@ -33,14 +30,14 @@ module SharkOnLambda
33
30
  size
34
31
  end
35
32
 
36
- def log_request(env:, response:, start_time:, end_time:)
33
+ def log_request(env:, response:, duration:)
37
34
  log_object = {
38
35
  url: env['PATH_INFO'],
39
36
  method: env['REQUEST_METHOD'],
40
- params: params(env),
37
+ params: env.fetch('action_dispatch.request.parameters', {}),
41
38
  status: response[0],
42
39
  length: body_size(response[2]),
43
- duration: "#{duration_in_ms(start_time, end_time)} ms"
40
+ duration: "#{duration} ms"
44
41
  }
45
42
  logger.info log_object.to_json
46
43
  end
@@ -49,11 +46,6 @@ module SharkOnLambda
49
46
  duration = (end_time - start_time) * 1000
50
47
  duration.abs.floor(3)
51
48
  end
52
-
53
- def params(env)
54
- query_params = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
55
- query_params.merge(env['shark.path_parameters'] || {})
56
- end
57
49
  end
58
50
  end
59
51
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_dispatch/routing/inspector'
4
+ require 'rake'
5
+ require 'shark_on_lambda'
6
+
7
+ namespace :'shark-on-lambda' do
8
+ desc 'Print out all defined routes in match order, with names'
9
+ task :routes do
10
+ routes = SharkOnLambda.application.routes.routes
11
+ inspector = ActionDispatch::Routing::RoutesInspector.new(routes)
12
+ formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet.new
13
+
14
+ puts inspector.format(formatter)
15
+ end
16
+ end
@@ -2,8 +2,5 @@
2
2
 
3
3
  module SharkOnLambda
4
4
  class Request < ActionDispatch::Request
5
- def path_parameters
6
- super.merge(env['shark.path_parameters'] || {})
7
- end
8
5
  end
9
6
  end
@@ -3,25 +3,14 @@
3
3
  module SharkOnLambda
4
4
  module RSpec
5
5
  class EnvBuilder
6
- attr_reader :action, :controller, :headers, :method
7
- attr_reader :params, :path_parameters
8
-
9
- def initialize(options = {})
10
- @method = options.fetch(:method).to_s.upcase
11
- @controller = options.fetch(:controller)
12
- @action = options.fetch(:action)
13
-
14
- @headers = (options[:headers] || {}).deep_stringify_keys
15
- @headers.transform_keys!(&:downcase)
16
- @params = options[:params] || {}
17
- @path_parameters = options[:path_parameters] || {}
18
-
19
- initialize_env
20
- add_headers
21
- add_request_body
6
+ def initialize(**options)
7
+ @options = options
22
8
  end
23
9
 
24
10
  def build
11
+ initialize_env
12
+ add_headers
13
+ add_request_body_as_json if body? && jsonable_params? && json_request?
25
14
  env.deep_stringify_keys
26
15
  end
27
16
 
@@ -29,43 +18,88 @@ module SharkOnLambda
29
18
 
30
19
  attr_reader :env
31
20
 
32
- def add_header(name, value)
33
- name = name.upcase.tr('-', '_')
34
- key = case name
35
- when 'CONTENT_LENGTH', 'CONTENT_TYPE' then name
36
- else "HTTP_#{name}"
37
- end
38
- @env[key] = value.to_s
21
+ def action
22
+ @options.fetch(:action)
39
23
  end
40
24
 
41
25
  def add_headers
42
- headers.each_pair { |name, value| add_header(name, value) }
26
+ headers.each_pair do |name, value|
27
+ name = name.upcase.tr('-', '_')
28
+ key = case name
29
+ when 'CONTENT_LENGTH', 'CONTENT_TYPE' then name
30
+ else "HTTP_#{name}"
31
+ end
32
+ env[key] = value.to_s
33
+ end
43
34
  end
44
35
 
45
- def add_request_body
46
- return if %w[GET HEAD OPTIONS].include?(env['REQUEST_METHOD'])
47
- return unless params.is_a?(Hash)
48
-
36
+ def add_request_body_as_json
49
37
  body = params.to_json
50
38
 
51
39
  env['rack.input'] = StringIO.new(body).set_encoding(Encoding::BINARY)
52
- set_content_type_and_content_length
40
+ env['CONTENT_TYPE'] = headers['content-type']
41
+ env['CONTENT_LENGTH'] = env['rack.input'].length.to_s
42
+ end
43
+
44
+ def as
45
+ @options.fetch(:as, :json)
46
+ end
47
+
48
+ def body?
49
+ !%w[GET HEAD OPTIONS].include?(env['REQUEST_METHOD'])
53
50
  end
54
51
 
55
52
  def initialize_env
56
53
  @env = Rack::MockRequest.env_for(
57
- 'https://localhost:9292',
54
+ request_uri.to_s,
58
55
  method: method,
59
- params: params,
60
- 'shark.controller' => controller,
61
- 'shark.action' => action,
62
- 'shark.path_parameters' => path_parameters
56
+ params: params
63
57
  )
64
58
  end
65
59
 
66
- def set_content_type_and_content_length
67
- env['CONTENT_TYPE'] = headers['content-type']
68
- env['CONTENT_LENGTH'] = env['rack.input'].length.to_s
60
+ def controller
61
+ @options.fetch(:controller, nil)
62
+ end
63
+
64
+ def headers
65
+ return @headers if defined?(@headers)
66
+
67
+ @headers = @options.fetch(:headers, {}).deep_stringify_keys
68
+ @headers.transform_keys!(&:downcase)
69
+ @headers
70
+ end
71
+
72
+ def json_request?
73
+ as == :json
74
+ end
75
+
76
+ def jsonable_params?
77
+ params.is_a?(Hash)
78
+ end
79
+
80
+ def method
81
+ @options.fetch(:method).to_s.upcase
82
+ end
83
+
84
+ def params
85
+ @options.fetch(:params, {}).deep_stringify_keys
86
+ end
87
+
88
+ def path_from_routes
89
+ path_params = {
90
+ controller: controller.name.underscore.sub(/_controller$/, ''),
91
+ action: action,
92
+ only_path: true
93
+ }
94
+ url = SharkOnLambda.application.routes.url_for(path_params, nil)
95
+ URI.parse(url).path
96
+ end
97
+
98
+ def request_uri
99
+ return @request_uri if defined?(@request_uri)
100
+
101
+ path = action.is_a?(String) ? action : path_from_routes
102
+ @request_uri = URI.join('https://localhost:9292', path)
69
103
  end
70
104
  end
71
105
  end
@@ -3,95 +3,12 @@
3
3
  module SharkOnLambda
4
4
  module RSpec
5
5
  module Helpers
6
- def delete(controller_method, options = {})
7
- make_request('DELETE', controller_method, options)
8
- end
9
-
10
- def get(controller_method, options = {})
11
- make_request('GET', controller_method, options)
12
- end
13
-
14
- def patch(controller_method, options = {})
15
- make_request('PATCH', controller_method, options)
16
- end
17
-
18
- def post(controller_method, options = {})
19
- make_request('POST', controller_method, options)
20
- end
21
-
22
- def put(controller_method, options = {})
23
- make_request('PUT', controller_method, options)
24
- end
25
-
26
- def response
27
- if @response.nil?
28
- raise 'You must make a request before you can request a response.'
29
- end
30
-
31
- @response
32
- end
33
-
34
- private
35
-
36
- def build_env(method, action, options = {})
37
- env_builder = EnvBuilder.new(
38
- method: method,
39
- controller: controller_name,
40
- action: action,
41
- headers: options[:headers],
42
- params: options[:params],
43
- path_parameters: options[:path_parameters]
44
- )
45
- env_builder.build
46
- end
47
-
48
- def controller?
49
- controller_name.present?
50
- end
51
-
52
- def controller_name
53
- self.class.ancestors.find do |klass|
54
- klass.name.end_with?('Controller')
55
- end&.description
56
- end
57
-
58
- def default_content_type
59
- 'application/json'
60
- end
61
-
62
- def dispatch_request(env, skip_middleware: false)
63
- return SharkOnLambda.application.call(env) unless skip_middleware
64
-
65
- controller_class = env['shark.controller'].constantize
66
- action = env['shark.action']
67
-
68
- request = Request.new(env)
69
- response = Response.new
70
- controller_class.dispatch(action, request, response)
71
- response.prepare!
72
- end
73
-
74
- def headers_with_content_type(headers)
75
- headers ||= {}
76
- headers.transform_keys! { |key| key.to_s.downcase }
77
- headers['content-type'] ||= default_content_type
78
- headers
79
- end
80
-
81
- def make_request(method, action, options = {})
82
- raise ArgumentError, 'Cannot find controller name.' unless controller?
83
-
84
- options = options.with_indifferent_access
85
- options[:headers] = headers_with_content_type(options[:headers])
86
-
87
- env = build_env(method, action, options)
6
+ extend ActiveSupport::Concern
7
+ include RequestHelpers
8
+ include ResponseHelpers
88
9
 
89
- status, headers, body = dispatch_request(
90
- env,
91
- skip_middleware: options[:skip_middleware]
92
- )
93
- errors = env['rack.errors']
94
- @response = Rack::MockResponse.new(status, headers, body, errors)
10
+ included do
11
+ include SharkOnLambda.application.routes.url_helpers
95
12
  end
96
13
  end
97
14
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module RSpec
5
+ module RequestHelpers
6
+ attr_writer :app
7
+
8
+ SUPPORTED_HTTP_METHODS = %w[
9
+ DELETE GET HEAD OPTIONS PATCH POST PUT
10
+ ].freeze
11
+
12
+ SUPPORTED_HTTP_METHODS.each do |http_method|
13
+ define_method(http_method.underscore) do |action, **options|
14
+ make_request(http_method, action, **options)
15
+ end
16
+ end
17
+
18
+ def app
19
+ @app ||= SharkOnLambda.application
20
+ end
21
+
22
+ def response
23
+ if @response.nil?
24
+ raise 'You must make a request before you can request a response.'
25
+ end
26
+
27
+ @response
28
+ end
29
+
30
+ private
31
+
32
+ def build_env(method, action, **options)
33
+ headers = options.fetch(:headers, {})
34
+ env_builder = EnvBuilder.new(
35
+ method: method,
36
+ controller: described_class,
37
+ action: action,
38
+ headers: normalized_headers(headers),
39
+ params: options.fetch(:params, {})
40
+ )
41
+ env_builder.build
42
+ end
43
+
44
+ def default_content_type
45
+ 'application/vnd.api+json'
46
+ end
47
+
48
+ def make_request(method, action, **options)
49
+ env = build_env(method, action, **options)
50
+
51
+ status, headers, body = app.call(env)
52
+ errors = env['rack.errors']
53
+ @response = Rack::MockResponse.new(status, headers, body, errors)
54
+ end
55
+
56
+ def normalized_headers(headers)
57
+ headers.transform_keys! { |key| key.to_s.downcase }
58
+ headers['content-type'] ||= default_content_type
59
+ headers
60
+ end
61
+ end
62
+ end
63
+ end