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
@@ -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