shark-on-lambda 0.6.10 → 1.0.0.rc1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push-gem.yml +67 -0
  3. data/.github/workflows/tests.yml +32 -0
  4. data/README.md +10 -12
  5. data/changelog.md +11 -0
  6. data/gems.locked +73 -23
  7. data/lib/shark_on_lambda/api_gateway_handler.rb +61 -0
  8. data/lib/shark_on_lambda/application.rb +17 -0
  9. data/lib/shark_on_lambda/base_controller.rb +50 -0
  10. data/lib/shark_on_lambda/configuration.rb +9 -1
  11. data/lib/shark_on_lambda/dispatcher.rb +26 -0
  12. data/lib/shark_on_lambda/errors/base.rb +47 -0
  13. data/lib/shark_on_lambda/errors/base_serializer.rb +18 -0
  14. data/lib/shark_on_lambda/jsonapi_controller.rb +29 -0
  15. data/lib/shark_on_lambda/jsonapi_parameters.rb +66 -0
  16. data/lib/shark_on_lambda/jsonapi_renderer.rb +115 -0
  17. data/lib/shark_on_lambda/middleware/base.rb +23 -0
  18. data/lib/shark_on_lambda/middleware/jsonapi_rescuer.rb +31 -0
  19. data/lib/shark_on_lambda/middleware/rescuer.rb +38 -0
  20. data/lib/shark_on_lambda/query.rb +67 -0
  21. data/lib/shark_on_lambda/rack_adapters/api_gateway.rb +127 -0
  22. data/lib/shark_on_lambda/request.rb +9 -0
  23. data/lib/shark_on_lambda/response.rb +6 -0
  24. data/lib/shark_on_lambda/rspec/env_builder.rb +64 -0
  25. data/lib/shark_on_lambda/rspec/helpers.rb +84 -0
  26. data/lib/shark_on_lambda/rspec/jsonapi_helpers.rb +27 -0
  27. data/lib/shark_on_lambda/version.rb +1 -1
  28. data/lib/shark_on_lambda.rb +25 -28
  29. data/shark-on-lambda.gemspec +8 -3
  30. metadata +104 -24
  31. data/.gitlab-ci.yml +0 -39
  32. data/.travis.yml +0 -14
  33. data/lib/shark_on_lambda/api_gateway/base_controller.rb +0 -76
  34. data/lib/shark_on_lambda/api_gateway/base_handler.rb +0 -82
  35. data/lib/shark_on_lambda/api_gateway/concerns/http_response_validation.rb +0 -61
  36. data/lib/shark_on_lambda/api_gateway/errors.rb +0 -49
  37. data/lib/shark_on_lambda/api_gateway/headers.rb +0 -37
  38. data/lib/shark_on_lambda/api_gateway/jsonapi_controller.rb +0 -77
  39. data/lib/shark_on_lambda/api_gateway/jsonapi_parameters.rb +0 -68
  40. data/lib/shark_on_lambda/api_gateway/jsonapi_renderer.rb +0 -105
  41. data/lib/shark_on_lambda/api_gateway/parameters.rb +0 -18
  42. data/lib/shark_on_lambda/api_gateway/query.rb +0 -69
  43. data/lib/shark_on_lambda/api_gateway/request.rb +0 -148
  44. data/lib/shark_on_lambda/api_gateway/response.rb +0 -82
  45. data/lib/shark_on_lambda/api_gateway/serializers/base_error_serializer.rb +0 -20
  46. data/lib/shark_on_lambda/concerns/filter_actions.rb +0 -93
  47. data/lib/shark_on_lambda/tasks/build.rake +0 -146
  48. data/lib/shark_on_lambda/tasks.rb +0 -3
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ class JsonapiParameters
5
+ def initialize(params = {})
6
+ @class = default_serializer_classes
7
+ @fields = HashWithIndifferentAccess.new
8
+ @include = []
9
+
10
+ parse_params(params) if params.present?
11
+ end
12
+
13
+ def classes(serializer_classes = {})
14
+ @class = default_serializer_classes.merge(serializer_classes)
15
+ end
16
+
17
+ def fields(serialized_fields = {})
18
+ @fields = serialized_fields.with_indifferent_access
19
+ end
20
+
21
+ def includes(*includes_list)
22
+ @include = includes_list
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ class: @class,
28
+ fields: @fields,
29
+ include: @include
30
+ }
31
+ end
32
+ alias to_hash to_h
33
+
34
+ protected
35
+
36
+ def default_serializer_classes
37
+ HashWithIndifferentAccess.new do |hash, key|
38
+ serializer_service = Inferrers::SerializerInferrer.new(key)
39
+ serializer_class = serializer_service.serializer_class
40
+ hash[key] = serializer_class
41
+ end
42
+ end
43
+
44
+ def parse_fields_params(fields_params)
45
+ return if fields_params.blank?
46
+
47
+ serialized_fields = fields_params.transform_values do |fields_param|
48
+ fields = fields_param.split(',')
49
+ fields.map!(&:strip)
50
+ fields.map!(&:to_sym)
51
+ end
52
+ fields(serialized_fields)
53
+ end
54
+
55
+ def parse_include_params(include_params)
56
+ include_params = JSONAPI::IncludeDirective.new(include_params)
57
+ include_params = include_params.to_hash
58
+ includes(include_params.with_indifferent_access)
59
+ end
60
+
61
+ def parse_params(params)
62
+ parse_fields_params(params[:fields])
63
+ parse_include_params(params[:include])
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ class JsonapiRenderer
5
+ def initialize(renderer: nil)
6
+ @renderer = renderer || JSONAPI::Serializable::Renderer.new
7
+ end
8
+
9
+ def render(object, options = {})
10
+ object = transform_active_model_errors(object)
11
+
12
+ unless renderable?(object, options)
13
+ return handle_unrenderable_objects(object, options)
14
+ end
15
+
16
+ if error?(object)
17
+ render_errors(object, options).to_json
18
+ else
19
+ render_success(object, options).to_json
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ attr_reader :renderer
26
+
27
+ def active_model_error?(error)
28
+ return false unless defined?(::ActiveModel::Errors)
29
+
30
+ error.is_a?(::ActiveModel::Errors)
31
+ end
32
+
33
+ def attribute_name(attribute)
34
+ File.basename(attribute_path(attribute))
35
+ end
36
+
37
+ def attribute_path(attribute)
38
+ attribute.to_s.tr('.', '/').gsub(/\[(\d+)\]/, '/\1')
39
+ end
40
+
41
+ def error?(object)
42
+ if object.respond_to?(:to_ary)
43
+ object.to_ary.any? { |item| item.is_a?(StandardError) }
44
+ else
45
+ object.is_a?(StandardError)
46
+ end
47
+ end
48
+
49
+ def handle_unrenderable_objects(object, options)
50
+ objects_without_serializer = unrenderable_objects(object, options)
51
+ classes_without_serializer = objects_without_serializer.map(&:class)
52
+ classes_without_serializer.uniq!
53
+ errors = classes_without_serializer.map do |item|
54
+ Errors[500].new("Could not find serializer for: #{item.name}.")
55
+ end
56
+
57
+ render_errors(errors, options)
58
+ end
59
+
60
+ def render_errors(error, options)
61
+ renderer.render_errors(Array(error), options)
62
+ end
63
+
64
+ def render_success(object, options)
65
+ return { data: {} } if object.nil?
66
+
67
+ renderer.render(object, options)
68
+ end
69
+
70
+ def renderable?(object, options)
71
+ return true if object.nil?
72
+
73
+ serializers = serializer_classes(options)
74
+
75
+ if object.respond_to?(:to_ary)
76
+ object.to_ary.all? { |item| serializers[item.class.name].present? }
77
+ else
78
+ serializers[object.class.name].present?
79
+ end
80
+ end
81
+
82
+ def serializer_classes(options)
83
+ return @serializer_classes if defined?(@serializer_classes)
84
+
85
+ @serializer_classes = HashWithIndifferentAccess.new do |hash, key|
86
+ serializer_inferrer = Inferrers::SerializerInferrer.new(key)
87
+ serializer_class = serializer_inferrer.serializer_class
88
+ hash[key] = serializer_class
89
+ end
90
+ @serializer_classes.merge!(options[:class])
91
+ end
92
+
93
+ def transform_active_model_errors(errors)
94
+ return errors unless active_model_error?(errors)
95
+
96
+ result = errors.messages.map do |attribute, attribute_errors|
97
+ attribute_errors.map do |attribute_error|
98
+ error_message = "`#{attribute_name(attribute)}' #{attribute_error}"
99
+ Errors[422].new(error_message).tap do |error|
100
+ error.pointer = "/data/attributes/#{attribute_path(attribute)}"
101
+ end
102
+ end
103
+ end
104
+ result.flatten! || result
105
+ end
106
+
107
+ def unrenderable_objects(object, options)
108
+ if object.respond_to?(:to_ary)
109
+ object.to_ary.reject { |item| renderable?(item, options) }
110
+ else
111
+ renderable?(object, options) ? [] : [object]
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module Middleware
5
+ class Base
6
+ attr_reader :app
7
+
8
+ def initialize(app = nil)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ dup.send('_call', env)
14
+ end
15
+
16
+ private
17
+
18
+ def _call(_env)
19
+ raise NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module Middleware
5
+ class JsonapiRescuer < Rescuer
6
+ private
7
+
8
+ def error_object(status, message)
9
+ {
10
+ status: status.to_s,
11
+ title: Rack::Utils::HTTP_STATUS_CODES[status],
12
+ detail: message
13
+ }
14
+ end
15
+
16
+ def error_response(status, headers, message)
17
+ body = {
18
+ errors: [
19
+ error_object(status, message)
20
+ ]
21
+ }.to_json
22
+
23
+ response_body = Rack::BodyProxy.new([body]) do
24
+ message.close if message.respond_to?(:close)
25
+ end
26
+
27
+ [status, headers, response_body]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module Middleware
5
+ class Rescuer < Base
6
+ private
7
+
8
+ def _call(env)
9
+ app.call(env)
10
+ rescue Errors::Base => e
11
+ rescue_shark_error(e)
12
+ rescue StandardError => e
13
+ rescue_standard_error(e)
14
+ end
15
+
16
+ def error_response(status, headers, message)
17
+ response_body = Rack::BodyProxy.new([message]) do
18
+ message.close if message.respond_to?(:close)
19
+ end
20
+
21
+ [status, headers, response_body]
22
+ end
23
+
24
+ def rescue_shark_error(error)
25
+ status = error.status || 500
26
+ error_response(status, {}, error.message)
27
+ end
28
+
29
+ def rescue_standard_error(error)
30
+ SharkOnLambda.logger.error(error.message)
31
+ SharkOnLambda.logger.error(error.backtrace.join("\n"))
32
+ Honeybadger.notify(error) if defined?(Honeybadger)
33
+
34
+ error_response(500, {}, error.message)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ # The `multiValueQueryStringParameters` object from the API Gateway event
5
+ # keeps _all_ values in an array, regardless of the actual size of the array
6
+ # and regardless of the "intent" of the query string parameter.
7
+ #
8
+ # In order to normalise this behaviour, we treat the query strings
9
+ #
10
+ # `key=value1&key=value2`
11
+ #
12
+ # and
13
+ #
14
+ # `key[]=value1&key[]=value2`
15
+ #
16
+ # the same. Both are to be serialised to the query string
17
+ #
18
+ # `key[]=value1&key[]=value2`
19
+ #
20
+ # However, the query strings
21
+ #
22
+ # `key=value`
23
+ #
24
+ # and
25
+ #
26
+ # `key[]=value`
27
+ #
28
+ # are _not_ to be treated the same.
29
+ class Query
30
+ def initialize(data = {})
31
+ @params = HashWithIndifferentAccess.new.merge(data)
32
+ end
33
+
34
+ def add(key, values)
35
+ if key.to_s.end_with?('[]')
36
+ actual_key = key[0..-3]
37
+ add_list(actual_key, values)
38
+ else
39
+ values.each { |value| add_item(key, value) }
40
+ end
41
+ end
42
+
43
+ def to_h
44
+ @params
45
+ end
46
+
47
+ def to_s
48
+ Rack::Utils.build_nested_query(to_h)
49
+ end
50
+
51
+ private
52
+
53
+ def add_item(key, value)
54
+ if @params[key].nil?
55
+ @params[key] = value
56
+ else
57
+ @params[key] = Array(@params[key])
58
+ @params[key] << value
59
+ end
60
+ end
61
+
62
+ def add_list(key, value)
63
+ @params[key] ||= []
64
+ @params[key].concat(value)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module RackAdapters
5
+ class ApiGateway
6
+ attr_reader :context, :event
7
+
8
+ def initialize(context:, event:)
9
+ @context = context
10
+ @event = event.deep_stringify_keys
11
+ end
12
+
13
+ def build_response(status, headers, body)
14
+ body_content = ''
15
+ body.each { |line| body_content += line.to_s }
16
+ response = {
17
+ 'statusCode' => status,
18
+ 'headers' => headers,
19
+ 'body' => body_content
20
+ }
21
+ response['isBase64Encoded'] = false if elb?
22
+ response
23
+ end
24
+
25
+ def env
26
+ default_env.merge(env_request_metadata)
27
+ .merge(env_headers)
28
+ .merge(env_params)
29
+ .merge(env_body)
30
+ end
31
+
32
+ private
33
+
34
+ def default_env
35
+ {
36
+ 'SCRIPT_NAME' => '',
37
+ 'rack.version' => Rack::VERSION,
38
+ 'rack.errors' => $stderr,
39
+ 'rack.multithread' => true,
40
+ 'rack.multiprocess' => true,
41
+ 'rack.run_once' => false
42
+ }
43
+ end
44
+
45
+ def elb?
46
+ return false if event['requestContext'].nil?
47
+
48
+ event['requestContext'].key?('elb')
49
+ end
50
+
51
+ def env_body
52
+ result = event['body'] || ''
53
+ result = Base64.decode64(result) if event['isBase64Encoded']
54
+
55
+ {
56
+ 'rack.input' => StringIO.new(result).set_encoding(Encoding::BINARY)
57
+ }
58
+ end
59
+
60
+ def env_headers
61
+ result = {}
62
+ http_headers.each_pair do |header, value|
63
+ key = key_for_header(header)
64
+ result[key] = value.to_s
65
+ end
66
+ result
67
+ end
68
+
69
+ def env_params
70
+ {
71
+ 'QUERY_STRING' => query_string,
72
+ 'shark.path_parameters' => event['pathParameters']
73
+ }
74
+ end
75
+
76
+ def env_request_metadata
77
+ {
78
+ 'REQUEST_METHOD' => event['httpMethod'],
79
+ 'PATH_INFO' => path_info,
80
+ 'SERVER_NAME' => server_name,
81
+ 'SERVER_PORT' => server_port.to_s,
82
+ 'rack.url_scheme' => url_scheme
83
+ }
84
+ end
85
+
86
+ def http_headers
87
+ event['headers'] || {}
88
+ end
89
+
90
+ def key_for_header(header)
91
+ key = header.upcase.tr('-', '_')
92
+ case key
93
+ when 'CONTENT_LENGTH', 'CONTENT_TYPE' then key
94
+ else "HTTP_#{key}"
95
+ end
96
+ end
97
+
98
+ def path_info
99
+ event['path'] || ''
100
+ end
101
+
102
+ def query_string
103
+ return @query_string if defined?(@query_string)
104
+
105
+ query = Query.new
106
+ event['multiValueQueryStringParameters']&.each_pair do |key, value|
107
+ query.add(key, value)
108
+ end
109
+ @query_string = query.to_s
110
+ end
111
+
112
+ def server_name
113
+ http_headers['Host'] || 'localhost'
114
+ end
115
+
116
+ def server_port
117
+ http_headers['X-Forwarded-Port'] || 443
118
+ end
119
+
120
+ def url_scheme
121
+ http_headers['CloudFront-Forwarded-Proto'] ||
122
+ http_headers['X-Forwarded-Proto'] ||
123
+ 'https'
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ class Request < ActionDispatch::Request
5
+ def path_parameters
6
+ super.merge(env['shark.path_parameters'] || {})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ class Response < ActionDispatch::Response
5
+ end
6
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module RSpec
5
+ class EnvBuilder
6
+ attr_reader :action, :controller, :headers, :method, :params
7
+
8
+ def initialize(options = {})
9
+ @method = options.fetch(:method).to_s.upcase
10
+ @controller = options.fetch(:controller)
11
+ @action = options.fetch(:action)
12
+
13
+ @headers = (options[:headers] || {}).deep_stringify_keys
14
+ @params = options[:params]
15
+
16
+ initialize_env
17
+ add_headers
18
+ add_request_body
19
+ end
20
+
21
+ def build
22
+ env.deep_stringify_keys
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :env
28
+
29
+ def add_header(name, value)
30
+ name = name.upcase.tr('-', '_')
31
+ key = case name
32
+ when 'CONTENT_LENGTH', 'CONTENT_TYPE' then name
33
+ else "HTTP_#{name}"
34
+ end
35
+ @env[key] = value.to_s
36
+ end
37
+
38
+ def add_headers
39
+ headers.each_pair { |name, value| add_header(name, value) }
40
+ end
41
+
42
+ def add_request_body
43
+ return if %w[GET HEAD OPTIONS].include?(env['REQUEST_METHOD'])
44
+ return unless params.is_a?(Hash)
45
+
46
+ body = params.to_json
47
+
48
+ env['rack.input'] = StringIO.new(body).set_encoding(Encoding::BINARY)
49
+ env['CONTENT_TYPE'] = 'application/json'
50
+ env['CONTENT_LENGTH'] = body.bytesize.to_s
51
+ end
52
+
53
+ def initialize_env
54
+ @env = Rack::MockRequest.env_for(
55
+ 'https://localhost:9292',
56
+ method: method,
57
+ params: params,
58
+ 'shark.controller' => controller,
59
+ 'shark.action' => action
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module RSpec
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
+ )
44
+ env_builder.build
45
+ end
46
+
47
+ def controller?
48
+ controller_name.present?
49
+ end
50
+
51
+ def controller_name
52
+ self.class.ancestors.find do |klass|
53
+ klass.name.end_with?('Controller')
54
+ end&.description
55
+ end
56
+
57
+ def dispatch_request(env, skip_middleware: false)
58
+ return SharkOnLambda.application.call(env) unless skip_middleware
59
+
60
+ controller_class = env['shark.controller'].constantize
61
+ action = env['shark.action']
62
+
63
+ request = Request.new(env)
64
+ response = Response.new
65
+ controller_class.dispatch(action, request, response)
66
+ response.prepare!
67
+ end
68
+
69
+ def make_request(method, action, options = {})
70
+ raise ArgumentError, 'Cannot find controller name.' unless controller?
71
+
72
+ options = options.with_indifferent_access
73
+ env = build_env(method, action, options)
74
+
75
+ status, headers, body = dispatch_request(
76
+ env,
77
+ skip_middleware: options[:skip_middleware]
78
+ )
79
+ errors = env['rack.errors']
80
+ @response = Rack::MockResponse.new(status, headers, body, errors)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module RSpec
5
+ module JsonapiHelpers
6
+ include Helpers
7
+
8
+ def jsonapi_attributes
9
+ jsonapi_data[:attributes] || {}
10
+ end
11
+
12
+ def jsonapi_data
13
+ parsed_body[:data] || {}
14
+ end
15
+
16
+ def jsonapi_errors
17
+ parsed_body[:errors] || []
18
+ end
19
+
20
+ private
21
+
22
+ def parsed_body
23
+ @parsed_body ||= JSON.parse(response.body).with_indifferent_access
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharkOnLambda
4
- VERSION = '0.6.10'
4
+ VERSION = '1.0.0.rc1'
5
5
  end