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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.gitlab-ci.yml +13 -0
  4. data/.rubocop.yml +13 -0
  5. data/.travis.yml +9 -2
  6. data/README.md +184 -18
  7. data/Rakefile +2 -0
  8. data/bin/console +4 -9
  9. data/changelog.md +17 -0
  10. data/gems.locked +92 -0
  11. data/{Gemfile → gems.rb} +2 -1
  12. data/lib/shark-on-lambda.rb +1 -5
  13. data/lib/shark_on_lambda.rb +104 -1
  14. data/lib/shark_on_lambda/api_gateway/base_controller.rb +76 -0
  15. data/lib/shark_on_lambda/api_gateway/base_handler.rb +82 -0
  16. data/lib/shark_on_lambda/api_gateway/concerns/http_response_validation.rb +61 -0
  17. data/lib/shark_on_lambda/api_gateway/errors.rb +49 -0
  18. data/lib/shark_on_lambda/api_gateway/headers.rb +37 -0
  19. data/lib/shark_on_lambda/api_gateway/jsonapi_controller.rb +77 -0
  20. data/lib/shark_on_lambda/api_gateway/jsonapi_parameters.rb +68 -0
  21. data/lib/shark_on_lambda/api_gateway/jsonapi_renderer.rb +105 -0
  22. data/lib/shark_on_lambda/api_gateway/parameters.rb +18 -0
  23. data/lib/shark_on_lambda/api_gateway/query.rb +69 -0
  24. data/lib/shark_on_lambda/api_gateway/request.rb +148 -0
  25. data/lib/shark_on_lambda/api_gateway/response.rb +82 -0
  26. data/lib/shark_on_lambda/api_gateway/serializers/base_error_serializer.rb +20 -0
  27. data/lib/shark_on_lambda/concerns/filter_actions.rb +82 -0
  28. data/lib/shark_on_lambda/concerns/resettable_singleton.rb +18 -0
  29. data/lib/shark_on_lambda/concerns/yaml_config_loader.rb +28 -0
  30. data/lib/shark_on_lambda/configuration.rb +71 -0
  31. data/lib/shark_on_lambda/inferrers/name_inferrer.rb +66 -0
  32. data/lib/shark_on_lambda/inferrers/serializer_inferrer.rb +45 -0
  33. data/lib/shark_on_lambda/secrets.rb +43 -0
  34. data/lib/shark_on_lambda/tasks.rb +3 -0
  35. data/lib/shark_on_lambda/tasks/build.rake +146 -0
  36. data/lib/{shark-on-lambda → shark_on_lambda}/version.rb +1 -1
  37. data/shark-on-lambda.gemspec +21 -6
  38. metadata +158 -20
  39. 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