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