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.
- 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
data/lib/shark_on_lambda.rb
CHANGED
@@ -1 +1,104 @@
|
|
1
|
-
|
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
|