shark-on-lambda 0.6.10 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class JsonapiController < BaseController
6
- # TODO: Evaluate if deserialisation should happen in here, happen
7
- # somewhere else in SharkOnLambda, or if it should be something
8
- # the user has to take care of entirely.
9
- #
10
- # class << self
11
- # attr_writer :deserializer_class
12
- #
13
- # def deserializer_class
14
- # return @deserializer_class if defined?(@deserializer_class)
15
- #
16
- # name_inferrer = Inferrers::NameInferrer.from_controller_name(name)
17
- # @deserializer_class = name_inferrer.deserializer.safe_constantize
18
- # end
19
- # end
20
- #
21
- # def payload
22
- # if request.raw_post.blank?
23
- # raise Errors[400], "The request body can't be empty."
24
- # end
25
- #
26
- # deserialize(request.request_parameters[:data])
27
- # end
28
-
29
- def redirect_to(url, options = {})
30
- status = options[:status] || 307
31
- validate_url!(url)
32
- validate_redirection_status!(status)
33
-
34
- uri = URI.parse(url)
35
- response.set_header('Location', uri.to_s)
36
- render(nil, status: status)
37
- end
38
-
39
- def render(object, options = {})
40
- status = options.delete(:status) || 200
41
- renderer_options = jsonapi_params.to_h.merge(options)
42
-
43
- body = serialize(object, renderer_options)
44
- update_response(status: status, body: body)
45
-
46
- respond!
47
- end
48
-
49
- protected
50
-
51
- # def deserialize(data)
52
- # deserializer_class = self.class.deserializer_class
53
- # if deserializer_class.nil?
54
- # raise Errors[500], 'Could not find a deserializer class.'
55
- # end
56
- #
57
- # deserializer = deserializer_class.new(data)
58
- # deserializer.to_h
59
- # end
60
-
61
- def jsonapi_params
62
- @jsonapi_params ||= JsonapiParameters.new(params)
63
- end
64
-
65
- def jsonapi_renderer
66
- @jsonapi_renderer ||= JsonapiRenderer.new
67
- end
68
-
69
- def serialize(object, options = {})
70
- return { data: {} }.to_json if object.nil?
71
-
72
- jsonapi_hash = jsonapi_renderer.render(object, options)
73
- jsonapi_hash.to_json
74
- end
75
- end
76
- end
77
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class JsonapiParameters
6
- def initialize(params = {})
7
- @class = default_serializer_classes
8
- @fields = HashWithIndifferentAccess.new
9
- @include = []
10
-
11
- parse_params(params) if params.present?
12
- end
13
-
14
- def classes(serializer_classes = {})
15
- @class = default_serializer_classes.merge(serializer_classes)
16
- end
17
-
18
- def fields(serialized_fields = {})
19
- @fields = serialized_fields.with_indifferent_access
20
- end
21
-
22
- def includes(*includes_list)
23
- @include = includes_list
24
- end
25
-
26
- def to_h
27
- {
28
- class: @class,
29
- fields: @fields,
30
- include: @include
31
- }
32
- end
33
- alias to_hash to_h
34
-
35
- protected
36
-
37
- def default_serializer_classes
38
- HashWithIndifferentAccess.new do |hash, key|
39
- serializer_service = Inferrers::SerializerInferrer.new(key)
40
- serializer_class = serializer_service.serializer_class
41
- hash[key] = serializer_class
42
- end
43
- end
44
-
45
- def parse_fields_params(fields_params)
46
- return if fields_params.blank?
47
-
48
- serialized_fields = fields_params.transform_values do |fields_param|
49
- fields = fields_param.split(',')
50
- fields.map!(&:strip)
51
- fields.map!(&:to_sym)
52
- end
53
- fields(serialized_fields)
54
- end
55
-
56
- def parse_include_params(include_params)
57
- include_params = ::JSONAPI::IncludeDirective.new(include_params)
58
- include_params = include_params.to_hash
59
- includes(include_params.with_indifferent_access)
60
- end
61
-
62
- def parse_params(params)
63
- parse_fields_params(params[:fields])
64
- parse_include_params(params[:include])
65
- end
66
- end
67
- end
68
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class JsonapiRenderer
6
- def initialize(renderer: nil)
7
- @renderer = renderer || ::JSONAPI::Serializable::Renderer.new
8
- end
9
-
10
- def render(object, options = {})
11
- object = transform_active_model_errors(object)
12
-
13
- unless renderable?(object, options)
14
- return handle_unrenderable_objects(object, options)
15
- end
16
-
17
- if error?(object)
18
- render_errors(object, options)
19
- else
20
- render_success(object, options)
21
- end
22
- end
23
-
24
- protected
25
-
26
- attr_reader :renderer
27
-
28
- def active_model_error?(error)
29
- return false unless defined?(::ActiveModel::Errors)
30
-
31
- error.is_a?(::ActiveModel::Errors)
32
- end
33
-
34
- def error?(object)
35
- if object.respond_to?(:to_ary)
36
- object.to_ary.any? { |item| item.is_a?(StandardError) }
37
- else
38
- object.is_a?(StandardError)
39
- end
40
- end
41
-
42
- def handle_unrenderable_objects(object, options)
43
- objects_without_serializer = unrenderable_objects(object, options)
44
- classes_without_serializer = objects_without_serializer.map(&:class)
45
- classes_without_serializer.uniq!
46
- errors = classes_without_serializer.map do |item|
47
- Errors[500].new("Could not find serializer for: #{item.name}.")
48
- end
49
-
50
- render_errors(errors, options)
51
- end
52
-
53
- def render_errors(error, options)
54
- renderer.render_errors(Array(error), options)
55
- end
56
-
57
- def render_success(object, options)
58
- renderer.render(object, options)
59
- end
60
-
61
- def renderable?(object, options)
62
- serializers = serializer_classes(options)
63
-
64
- if object.respond_to?(:to_ary)
65
- object.to_ary.all? { |item| serializers[item.class.name].present? }
66
- else
67
- serializers[object.class.name].present?
68
- end
69
- end
70
-
71
- def serializer_classes(options)
72
- return @serializer_classes if defined?(@serializer_classes)
73
-
74
- @serializer_classes = HashWithIndifferentAccess.new do |hash, key|
75
- serializer_inferrer = Inferrers::SerializerInferrer.new(key)
76
- serializer_class = serializer_inferrer.serializer_class
77
- hash[key] = serializer_class
78
- end
79
- @serializer_classes.merge!(options[:class])
80
- end
81
-
82
- def transform_active_model_errors(errors)
83
- return errors unless active_model_error?(errors)
84
-
85
- result = errors.messages.map do |attribute, attribute_errors|
86
- attribute_errors.map do |attribute_error|
87
- error_message = "`#{attribute}' #{attribute_error}"
88
- Errors[422].new(error_message).tap do |error|
89
- error.pointer = "/data/attributes/#{attribute}"
90
- end
91
- end
92
- end
93
- result.flatten! || result
94
- end
95
-
96
- def unrenderable_objects(object, options)
97
- if object.respond_to?(:to_ary)
98
- object.to_ary.reject { |item| renderable?(item, options) }
99
- else
100
- renderable?(object, options) ? [] : [object]
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class Parameters
6
- extend ::Forwardable
7
-
8
- def initialize(request)
9
- @params = ::HashWithIndifferentAccess.new
10
- @params = @params.merge(request.request_parameters)
11
- @params = @params.merge(request.query_parameters)
12
- @params = @params.merge(request.path_parameters)
13
- end
14
-
15
- def_instance_delegators :@params, :[], :as_json
16
- end
17
- end
18
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- # The `multiValueQueryStringParameters` object from the API Gateway event
6
- # keeps _all_ values in an array, regardless of the actual size of the array
7
- # and regardless of the "intent" of the query string parameter.
8
- #
9
- # In order to normalise this behaviour, we treat the query strings
10
- #
11
- # `key=value1&key=value2`
12
- #
13
- # and
14
- #
15
- # `key[]=value1&key[]=value2`
16
- #
17
- # the same. Both are to be serialised to the query string
18
- #
19
- # `key[]=value1&key[]=value2`
20
- #
21
- # However, the query strings
22
- #
23
- # `key=value`
24
- #
25
- # and
26
- #
27
- # `key[]=value`
28
- #
29
- # are _not_ to be treated the same.
30
- class Query
31
- def initialize
32
- @params = HashWithIndifferentAccess.new
33
- end
34
-
35
- def add(key, values)
36
- if key.to_s.end_with?('[]')
37
- actual_key = key[0..-3]
38
- add_list(actual_key, values)
39
- else
40
- values.each { |value| add_item(key, value) }
41
- end
42
- end
43
-
44
- def to_h
45
- @params
46
- end
47
-
48
- def to_s
49
- Rack::Utils.build_nested_query(to_h)
50
- end
51
-
52
- private
53
-
54
- def add_item(key, value)
55
- if @params[key].nil?
56
- @params[key] = value
57
- else
58
- @params[key] = Array(@params[key])
59
- @params[key] << value
60
- end
61
- end
62
-
63
- def add_list(key, value)
64
- @params[key] ||= []
65
- @params[key].concat(value)
66
- end
67
- end
68
- end
69
- end
@@ -1,148 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class Request
6
- attr_reader :event, :context
7
-
8
- LOCALHOST = Regexp.union([/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
9
- /^::1$/,
10
- /^0:0:0:0:0:0:0:1(%.*)?$/]).freeze
11
-
12
- def initialize(event:, context:)
13
- @event = event
14
- @context = context
15
- @headers = Headers.new
16
-
17
- event['headers']&.each { |key, value| @headers[key] = value }
18
- end
19
-
20
- def authorization
21
- @headers['authorization'] || @headers['x-authorization']
22
- end
23
-
24
- def body
25
- StringIO.new(raw_post)
26
- end
27
-
28
- def content_length
29
- raw_post.present? ? raw_post.bytesize : 0
30
- end
31
-
32
- def form_data?
33
- raw_post.present?
34
- end
35
-
36
- # This method does not necessarily return the full path the client sent
37
- # for the request: AWS API Gateway does not provide such an attribute.
38
- # We therefore put together the query string using the
39
- # event['multiValueQueryStringParameters'] property and return a path
40
- # that is for almost all purposes equivalent to the client's request path.
41
- # But keep in mind it is NOT the same!
42
- def fullpath
43
- return @fullpath if defined?(@fullpath)
44
-
45
- uri = URI.parse(event['requestContext']['path'])
46
- uri.query = query_string if query_string.present?
47
- @fullpath = uri.to_s
48
- end
49
- alias original_fullpath fullpath
50
-
51
- def headers
52
- @headers.to_h
53
- end
54
-
55
- def ip
56
- event['requestContext']['identity']['sourceIp']
57
- end
58
- alias remote_ip ip
59
-
60
- def key?(key)
61
- @headers.key?(key)
62
- end
63
-
64
- def local?
65
- (LOCALHOST =~ remote_ip).present?
66
- end
67
-
68
- def media_type
69
- @headers['content-type']
70
- end
71
-
72
- def method
73
- event['httpMethod']
74
- end
75
- alias request_method method
76
-
77
- def method_symbol
78
- return nil if method.blank?
79
-
80
- method.downcase.to_sym
81
- end
82
- alias request_method_symbol method_symbol
83
-
84
- def original_url
85
- hostname = event['requestContext']['domainName']
86
- URI.join("https://#{hostname}", original_fullpath).to_s
87
- end
88
-
89
- def path_parameters
90
- return @path_parameters if defined?(@path_parameters)
91
-
92
- @path_parameters = HashWithIndifferentAccess.new
93
- @path_parameters = @path_parameters.merge(event['pathParameters'] || {})
94
- end
95
-
96
- def query_parameters
97
- return @query_parameters if defined?(@query_parameters)
98
-
99
- # We have to jump through the Rack::Utils hoops here, because the event
100
- # object from the AWS Gateway deserialises the query string in a "wrong"
101
- # way, so we need to put it back together and deserialise it with
102
- # Rack::Utils.parse_nested_query.
103
- data = Rack::Utils.parse_nested_query(query_string)
104
- @query_parameters = HashWithIndifferentAccess.new
105
- @query_parameters = @query_parameters.merge(data)
106
- end
107
- alias GET query_parameters
108
-
109
- def raw_post
110
- return @raw_post if defined?(@raw_post)
111
-
112
- @raw_post = event['body']
113
- @raw_post = Base64.decode64(@raw_post) if event['isBase64Encoded']
114
- @raw_post
115
- end
116
-
117
- def request_parameters
118
- return @request_parameters if defined?(@request_parameters)
119
- return {} if raw_post.blank?
120
-
121
- data = JSON.parse(raw_post)
122
- @request_parameters = HashWithIndifferentAccess.new.merge(data)
123
- rescue JSON::ParserError => e
124
- raise Errors[400], e.message
125
- rescue StandardError
126
- raise Errors[400], 'The request body must be empty or a JSON object.'
127
- end
128
- alias POST request_parameters
129
-
130
- def xml_http_request?
131
- (headers['x-requested-with'] =~ /XMLHttpRequest/i).present?
132
- end
133
- alias xhr? xml_http_request?
134
-
135
- protected
136
-
137
- def query_string
138
- return @query_string if defined?(@query_string)
139
-
140
- query = Query.new
141
- event['multiValueQueryStringParameters']&.each_pair do |key, value|
142
- query.add(key, value)
143
- end
144
- @query_string = query.to_s
145
- end
146
- end
147
- end
148
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- class Response
6
- attr_accessor :body, :charset, :content_type
7
- attr_reader :headers
8
- attr_writer :status
9
-
10
- STATUS_WITH_NO_ENTITY_BODY = ((100..199).to_a + [204, 304]).freeze
11
-
12
- def self.default_charset
13
- 'utf-8'
14
- end
15
-
16
- def self.default_content_type
17
- 'application/vnd.api+json'
18
- end
19
-
20
- def initialize(headers: nil)
21
- @body = nil
22
- @charset = self.class.default_charset
23
- @content_type = self.class.default_content_type
24
- @headers = headers || Headers.new
25
- @headers['content-type'] = self.class.default_content_type
26
- @status = 200
27
- end
28
-
29
- def delete_header(key)
30
- @headers.delete(key)
31
- end
32
-
33
- def get_header(key)
34
- @headers[key]
35
- end
36
-
37
- # rubocop:disable Naming/PredicateName
38
- def has_header?(key)
39
- @headers.key?(key)
40
- end
41
- # rubocop:enable Naming/PredicateName
42
-
43
- def message
44
- ::Rack::Utils::HTTP_STATUS_CODES[response_code]
45
- end
46
- alias status_message message
47
-
48
- def response_code
49
- @status
50
- end
51
-
52
- def set_header(key, value)
53
- @headers[key] = value
54
- end
55
-
56
- def to_h
57
- {
58
- statusCode: response_status_code,
59
- headers: headers.to_h,
60
- body: response_body
61
- }
62
- end
63
-
64
- protected
65
-
66
- def response_status_code
67
- return response_code.to_i if response_body.present?
68
- return 204 if (200..299).cover?(response_code.to_i)
69
- return 304 if (300..399).cover?(response_code.to_i)
70
-
71
- response_code.to_i
72
- end
73
-
74
- def response_body
75
- return if STATUS_WITH_NO_ENTITY_BODY.include?(response_code)
76
- return if body.blank?
77
-
78
- body.is_a?(String) ? body : body.to_json
79
- end
80
- end
81
- end
82
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module ApiGateway
5
- module Errors
6
- class BaseSerializer < ::JSONAPI::Serializable::Error
7
- id { @object.id }
8
- status { @object.status }
9
- code { @object.code }
10
- title { @object.title }
11
- detail { @object.detail }
12
- meta { @object.meta }
13
- source do
14
- pointer @object.pointer if @object.pointer.present?
15
- parameter @object.parameter if @object.parameter.present?
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SharkOnLambda
4
- module Concerns
5
- module FilterActions
6
- def self.included(base)
7
- base.extend(ClassMethods)
8
- end
9
-
10
- module ClassMethods
11
- def after_action(symbol, only: [], except: [])
12
- @after_actions ||= []
13
- @after_actions << {
14
- symbol: symbol,
15
- only: Array(only),
16
- except: Array(except)
17
- }
18
- end
19
-
20
- def after_actions
21
- collect_from_ancestors(:after_actions) + Array(@after_actions)
22
- end
23
-
24
- def before_action(symbol, only: [], except: [])
25
- @before_actions ||= []
26
- @before_actions << {
27
- symbol: symbol,
28
- only: Array(only),
29
- except: Array(except)
30
- }
31
- end
32
-
33
- def before_actions
34
- collect_from_ancestors(:before_actions) + Array(@before_actions)
35
- end
36
-
37
- private
38
-
39
- def collect_from_ancestors(method)
40
- ancestors_without_self = ancestors[1..-1]
41
- return [] if ancestors_without_self.empty?
42
-
43
- ancestors_without_self.map! { |ancestor| ancestor.try(method) }
44
- ancestors_without_self.compact!
45
- ancestors_without_self.reduce([], &:+)
46
- end
47
- end
48
-
49
- def call_with_filter_actions(method, *args)
50
- run_before_actions(method)
51
- result = send(method, *args)
52
- run_after_actions(method)
53
- result
54
- end
55
-
56
- protected
57
-
58
- def after_actions
59
- self.class.after_actions
60
- end
61
-
62
- def before_actions
63
- self.class.before_actions
64
- end
65
-
66
- def run_actions(method, actions)
67
- actions.each do |action|
68
- next if skip_filter_action?(method, action)
69
-
70
- send(action[:symbol])
71
- end
72
- end
73
-
74
- def run_after_actions(method)
75
- run_actions(method, after_actions)
76
- end
77
-
78
- def run_before_actions(method)
79
- run_actions(method, before_actions)
80
- end
81
-
82
- def skip_filter_action?(method, filter_action)
83
- only = filter_action[:only]
84
- except = filter_action[:except]
85
-
86
- return true if only.any? && !only.include?(method)
87
- return true if except.any? && except.include?(method)
88
-
89
- false
90
- end
91
- end
92
- end
93
- end