shark-on-lambda 1.0.1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/changelog.md +10 -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 +10 -58
  7. data/lib/shark_on_lambda/api_gateway_handler.rb +3 -55
  8. data/lib/shark_on_lambda/application.rb +72 -2
  9. data/lib/shark_on_lambda/base_controller.rb +29 -9
  10. data/lib/shark_on_lambda/configuration.rb +1 -65
  11. data/lib/shark_on_lambda/inferrers/serializer_inferrer.rb +10 -7
  12. data/lib/shark_on_lambda/jsonapi_renderer.rb +1 -2
  13. data/lib/shark_on_lambda/middleware/honeybadger.rb +5 -3
  14. data/lib/shark_on_lambda/middleware/jsonapi_rescuer.rb +21 -1
  15. data/lib/shark_on_lambda/middleware/lambda_logger.rb +5 -13
  16. data/lib/shark_on_lambda/rake_tasks.rb +16 -0
  17. data/lib/shark_on_lambda/request.rb +0 -3
  18. data/lib/shark_on_lambda/rspec/env_builder.rb +71 -37
  19. data/lib/shark_on_lambda/rspec/helpers.rb +5 -88
  20. data/lib/shark_on_lambda/rspec/request_helpers.rb +63 -0
  21. data/lib/shark_on_lambda/rspec/{jsonapi_helpers.rb → response_helpers.rb} +4 -10
  22. data/lib/shark_on_lambda/version.rb +1 -1
  23. data/shark-on-lambda.gemspec +7 -5
  24. metadata +32 -39
  25. data/lib/shark_on_lambda/concerns/resettable_singleton.rb +0 -18
  26. data/lib/shark_on_lambda/concerns/yaml_config_loader.rb +0 -28
  27. data/lib/shark_on_lambda/dispatcher.rb +0 -26
  28. data/lib/shark_on_lambda/inferrers/name_inferrer.rb +0 -66
  29. data/lib/shark_on_lambda/jsonapi_controller.rb +0 -32
  30. data/lib/shark_on_lambda/middleware/rescuer.rb +0 -37
  31. data/lib/shark_on_lambda/query.rb +0 -67
  32. data/lib/shark_on_lambda/rack_adapters/api_gateway.rb +0 -128
  33. 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