shark-on-lambda 0.0.0 → 0.6.7
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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.gitlab-ci.yml +13 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +9 -2
- data/README.md +184 -18
- data/Rakefile +2 -0
- data/bin/console +4 -9
- data/changelog.md +17 -0
- data/gems.locked +92 -0
- data/{Gemfile → gems.rb} +2 -1
- data/lib/shark-on-lambda.rb +1 -5
- data/lib/shark_on_lambda.rb +104 -1
- data/lib/shark_on_lambda/api_gateway/base_controller.rb +76 -0
- data/lib/shark_on_lambda/api_gateway/base_handler.rb +82 -0
- data/lib/shark_on_lambda/api_gateway/concerns/http_response_validation.rb +61 -0
- data/lib/shark_on_lambda/api_gateway/errors.rb +49 -0
- data/lib/shark_on_lambda/api_gateway/headers.rb +37 -0
- data/lib/shark_on_lambda/api_gateway/jsonapi_controller.rb +77 -0
- data/lib/shark_on_lambda/api_gateway/jsonapi_parameters.rb +68 -0
- data/lib/shark_on_lambda/api_gateway/jsonapi_renderer.rb +105 -0
- data/lib/shark_on_lambda/api_gateway/parameters.rb +18 -0
- data/lib/shark_on_lambda/api_gateway/query.rb +69 -0
- data/lib/shark_on_lambda/api_gateway/request.rb +148 -0
- data/lib/shark_on_lambda/api_gateway/response.rb +82 -0
- data/lib/shark_on_lambda/api_gateway/serializers/base_error_serializer.rb +20 -0
- data/lib/shark_on_lambda/concerns/filter_actions.rb +82 -0
- data/lib/shark_on_lambda/concerns/resettable_singleton.rb +18 -0
- data/lib/shark_on_lambda/concerns/yaml_config_loader.rb +28 -0
- data/lib/shark_on_lambda/configuration.rb +71 -0
- data/lib/shark_on_lambda/inferrers/name_inferrer.rb +66 -0
- data/lib/shark_on_lambda/inferrers/serializer_inferrer.rb +45 -0
- data/lib/shark_on_lambda/secrets.rb +43 -0
- data/lib/shark_on_lambda/tasks.rb +3 -0
- data/lib/shark_on_lambda/tasks/build.rake +146 -0
- data/lib/{shark-on-lambda → shark_on_lambda}/version.rb +1 -1
- data/shark-on-lambda.gemspec +21 -6
- metadata +158 -20
- data/Gemfile.lock +0 -35
@@ -0,0 +1,68 @@
|
|
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
|
@@ -0,0 +1,105 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
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
|
@@ -0,0 +1,69 @@
|
|
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.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
|
@@ -0,0 +1,148 @@
|
|
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
|
@@ -0,0 +1,82 @@
|
|
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
|