shark-on-lambda 0.0.0 → 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1 +1,104 @@
1
- require 'shark-on-lambda'
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'ostruct'
5
+ require 'pathname'
6
+ require 'singleton'
7
+
8
+ require 'active_support/all'
9
+ require 'jsonapi/deserializable'
10
+ require 'jsonapi/serializable'
11
+ require 'rack/utils'
12
+ require 'yaml'
13
+
14
+ require 'shark_on_lambda/version'
15
+
16
+ require 'shark_on_lambda/concerns/filter_actions'
17
+ require 'shark_on_lambda/concerns/resettable_singleton'
18
+ require 'shark_on_lambda/concerns/yaml_config_loader'
19
+
20
+ require 'shark_on_lambda/configuration'
21
+ require 'shark_on_lambda/secrets'
22
+
23
+ require 'shark_on_lambda/inferrers/name_inferrer'
24
+ require 'shark_on_lambda/inferrers/serializer_inferrer'
25
+
26
+ require 'shark_on_lambda/api_gateway/serializers/base_error_serializer'
27
+ require 'shark_on_lambda/api_gateway/concerns/http_response_validation'
28
+ require 'shark_on_lambda/api_gateway/base_controller'
29
+ require 'shark_on_lambda/api_gateway/base_handler'
30
+ require 'shark_on_lambda/api_gateway/errors'
31
+ require 'shark_on_lambda/api_gateway/headers'
32
+ require 'shark_on_lambda/api_gateway/jsonapi_controller'
33
+ require 'shark_on_lambda/api_gateway/jsonapi_parameters'
34
+ require 'shark_on_lambda/api_gateway/jsonapi_renderer'
35
+ require 'shark_on_lambda/api_gateway/parameters'
36
+ require 'shark_on_lambda/api_gateway/query'
37
+ require 'shark_on_lambda/api_gateway/request'
38
+ require 'shark_on_lambda/api_gateway/response'
39
+
40
+ # Top-level module for this gem.
41
+ module SharkOnLambda
42
+ class << self
43
+ extend ::Forwardable
44
+
45
+ attr_writer :logger
46
+
47
+ def_instance_delegators :config, :root, :stage
48
+
49
+ def config
50
+ Configuration.instance
51
+ end
52
+
53
+ def configure
54
+ yield(config, secrets)
55
+ end
56
+
57
+ def initialize!
58
+ yield(config, secrets)
59
+
60
+ Configuration.load(stage)
61
+ Secrets.load(stage)
62
+ run_initializers
63
+
64
+ true
65
+ end
66
+
67
+ def load_configuration
68
+ Configuration.load(stage)
69
+ Secrets.load(stage)
70
+
71
+ true
72
+ end
73
+
74
+ def logger
75
+ @logger ||= ::Logger.new(STDOUT)
76
+ end
77
+
78
+ def reset_configuration
79
+ known_stage = config.stage
80
+ known_root = config.root
81
+
82
+ Configuration.reset
83
+ Secrets.reset
84
+
85
+ config.root = known_root
86
+ config.stage = known_stage
87
+
88
+ true
89
+ end
90
+
91
+ def secrets
92
+ Secrets.instance
93
+ end
94
+
95
+ protected
96
+
97
+ def run_initializers
98
+ initializers_path = root.join('config', 'initializers')
99
+ Dir.glob(initializers_path.join('*.rb')).each do |path|
100
+ load path
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module ApiGateway
5
+ class BaseController
6
+ include SharkOnLambda::Concerns::FilterActions
7
+ include Concerns::HttpResponseValidation
8
+
9
+ attr_reader :action_name, :event, :context
10
+
11
+ def initialize(event:, context:)
12
+ @event = event
13
+ @context = context
14
+ end
15
+
16
+ def call(method)
17
+ @action_name = method.to_s
18
+ call_with_filter_actions(method)
19
+ response.to_h
20
+ end
21
+
22
+ def params
23
+ @params ||= Parameters.new(request)
24
+ end
25
+
26
+ def redirect_to(url, status: 307, message: nil)
27
+ status = status.to_i
28
+ validate_url!(url)
29
+ validate_redirection_status!(status)
30
+
31
+ uri = URI.parse(url)
32
+ response.set_header('Location', uri.to_s)
33
+ body = message.presence || "You are being redirected to: #{url}"
34
+
35
+ render(body, status: status)
36
+ end
37
+
38
+ def render(object, status: 200)
39
+ update_response(status: status, body: object)
40
+ respond!
41
+ end
42
+
43
+ def request
44
+ @request ||= Request.new(event: event, context: context)
45
+ end
46
+
47
+ def response
48
+ @response ||= Response.new
49
+ end
50
+
51
+ protected
52
+
53
+ def respond!
54
+ if responded?
55
+ raise Errors[500],
56
+ '#render or #redirect_to was called more than once.'
57
+ end
58
+
59
+ @responded = true
60
+ response
61
+ end
62
+
63
+ def responded?
64
+ @responded.present?
65
+ end
66
+
67
+ def update_response(status:, body: nil)
68
+ status = status.to_i
69
+ validate_response_status!(status)
70
+
71
+ response.status = status
72
+ response.body = body.to_s.presence
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module ApiGateway
5
+ class BaseHandler
6
+ class << self
7
+ attr_writer :controller_class
8
+
9
+ def controller_class
10
+ return @controller_class if defined?(@controller_class)
11
+
12
+ name_inferrer = Inferrers::NameInferrer.from_handler_name(name)
13
+ controller_class_name = name_inferrer.controller
14
+ @controller_class = controller_class_name.safe_constantize
15
+ end
16
+
17
+ protected
18
+
19
+ def known_actions
20
+ controller_class.public_instance_methods(false)
21
+ end
22
+
23
+ def method_missing(name, *args, &block)
24
+ return super unless respond_to_missing?(name)
25
+
26
+ instance = new
27
+ instance.call(name, *args, &block)
28
+ end
29
+
30
+ def respond_to_missing?(name, _include_all = false)
31
+ known_actions.include?(name)
32
+ end
33
+ end
34
+
35
+ def call(action, event:, context:)
36
+ controller_class = self.class.controller_class
37
+ controller = controller_class.new(event: event, context: context)
38
+ controller.call(action)
39
+ rescue StandardError => e
40
+ handle_error(e)
41
+ end
42
+
43
+ protected
44
+
45
+ def client_error?(error)
46
+ error.is_a?(Errors::Base) && (400..499).cover?(error.status)
47
+ end
48
+
49
+ def error_body(status, message)
50
+ {
51
+ errors: [{
52
+ status: status.to_s,
53
+ title: ::Rack::Utils::HTTP_STATUS_CODES[status],
54
+ detail: message
55
+ }]
56
+ }.to_json
57
+ end
58
+
59
+ def error_response(error)
60
+ status = error.try(:status) || 500
61
+
62
+ {
63
+ statusCode: status,
64
+ headers: {
65
+ 'Content-Type' => 'application/vnd.api+json'
66
+ },
67
+ body: error_body(status, error.message)
68
+ }
69
+ end
70
+
71
+ def handle_error(error)
72
+ unless client_error?(error)
73
+ SharkOnLambda.logger.error(error.message)
74
+ SharkOnLambda.logger.error(error.backtrace.join("\n"))
75
+ ::Honeybadger.notify(error) if defined?(::Honeybadger)
76
+ end
77
+
78
+ error_response(error)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module ApiGateway
5
+ module Concerns
6
+ # Provides methods for HTTP-related validation, e. g. for status codes
7
+ # and URIs. If validation fails, question mark methods return *false*,
8
+ # exclamation mark methods raise an exception.
9
+ #
10
+ # TODO: Specs for this module.
11
+ module HttpResponseValidation
12
+ # Validates a URL.
13
+ #
14
+ # @param url [String] The URL to validate.
15
+ # @return [TrueClass] If the URL is valid and present.
16
+ # @return [FalseClass] If the URL is invalid or blank.
17
+ def valid_url?(url)
18
+ URI.parse(url.to_s)
19
+ url.present?
20
+ rescue URI::InvalidURIError
21
+ false
22
+ end
23
+
24
+ # Validates a HTTP redirection status code.
25
+ #
26
+ # @param status [Number] The HTTP status code to validate.
27
+ # @return [NilClass] Returns *nil* if *status* is valid.
28
+ # @raise [Errors::InternalServerError] If the status code does not
29
+ # represent a HTTP redirection.
30
+ def validate_redirection_status!(status)
31
+ validate_response_status!(status)
32
+ return if (300..399).cover?(status)
33
+
34
+ raise Errors[500], 'HTTP redirections must have a 3xx status code.'
35
+ end
36
+
37
+ # Validates a HTTP status code.
38
+ #
39
+ # @param status [Number] The HTTP status code to validate.
40
+ # @return [NilClass] Returns *nil* if *status* is valid.
41
+ # @raise [Errors::InternalServerError] If the status code is unknown.
42
+ def validate_response_status!(status)
43
+ return if ::Rack::Utils::HTTP_STATUS_CODES[status].present?
44
+
45
+ raise Errors[500], 'Unknown response status code.'
46
+ end
47
+
48
+ # Validates a URL.
49
+ #
50
+ # @param url [String] The URL to validate.
51
+ # @return [NilClass] Returns *nil* if *url* is valid.
52
+ # @raise [Errors::InternalServerError] If the URL is invalid.
53
+ def validate_url!(url)
54
+ return if valid_url?(url)
55
+
56
+ raise Errors[500], "`#{url}' is not a valid URL."
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module ApiGateway
5
+ module Errors
6
+ class Base < ::StandardError
7
+ attr_accessor :id, :code, :meta, :pointer, :parameter
8
+ attr_writer :detail
9
+
10
+ def self.status(status_code)
11
+ define_method :status do
12
+ status_code
13
+ end
14
+ end
15
+
16
+ def detail
17
+ return @detail if @detail.present?
18
+ return nil if message == self.class.name
19
+
20
+ message
21
+ end
22
+
23
+ def title
24
+ ::Rack::Utils::HTTP_STATUS_CODES[status]
25
+ end
26
+ end
27
+
28
+ def self.[](status_code)
29
+ @errors[status_code]
30
+ end
31
+
32
+ @errors = ::Rack::Utils::HTTP_STATUS_CODES.map do |status_code, message|
33
+ next unless (400..599).cover?(status_code) && message.present?
34
+
35
+ error_class = Class.new(Base) do
36
+ status status_code
37
+ end
38
+ class_name_parts = message.to_s.split(/\s+/)
39
+ class_name_parts.map! { |word| word.gsub(/[^a-z]/i, '').capitalize }
40
+ class_name = class_name_parts.join('')
41
+ const_set(class_name, error_class)
42
+
43
+ [status_code, error_class]
44
+ end
45
+ @errors.compact!
46
+ @errors = @errors.to_h
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharkOnLambda
4
+ module ApiGateway
5
+ class Headers
6
+ def initialize
7
+ @headers = {}
8
+ end
9
+
10
+ def [](key)
11
+ @headers[normalized_key(key)]
12
+ end
13
+
14
+ def []=(key, value)
15
+ @headers[normalized_key(key)] = value.to_s
16
+ end
17
+
18
+ def delete(key)
19
+ @headers.delete(normalized_key(key))
20
+ end
21
+
22
+ def key?(key)
23
+ @headers.key?(normalized_key(key))
24
+ end
25
+
26
+ def to_h
27
+ @headers.dup
28
+ end
29
+
30
+ protected
31
+
32
+ def normalized_key(key)
33
+ key.to_s.downcase
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,77 @@
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