jets 1.1.5 → 1.2.0
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/CHANGELOG.md +7 -0
- data/Gemfile.lock +10 -6
- data/README/testing.md +5 -1
- data/jets.gemspec +1 -0
- data/lib/jets.rb +5 -1
- data/lib/jets/application.rb +39 -19
- data/lib/jets/aws_services.rb +16 -10
- data/lib/jets/aws_services/stack_status.rb +7 -0
- data/lib/jets/booter.rb +6 -2
- data/lib/jets/builders/code_builder.rb +14 -0
- data/lib/jets/builders/handler_generator.rb +15 -0
- data/lib/jets/builders/shim_vars/app.rb +4 -3
- data/lib/jets/builders/shim_vars/shared.rb +8 -4
- data/lib/jets/builders/templates/shim.js +7 -3
- data/lib/jets/camelizer.rb +2 -1
- data/lib/jets/cfn/builders.rb +0 -1
- data/lib/jets/cfn/builders/api_deployment_builder.rb +27 -0
- data/lib/jets/cfn/builders/api_gateway_builder.rb +22 -2
- data/lib/jets/cfn/ship.rb +38 -6
- data/lib/jets/commands/call.rb +0 -1
- data/lib/jets/commands/call/guesser.rb +0 -3
- data/lib/jets/commands/clean/log.rb +18 -0
- data/lib/jets/commands/console.rb +1 -1
- data/lib/jets/commands/import/sequence.rb +2 -3
- data/lib/jets/commands/runner.rb +1 -1
- data/lib/jets/commands/sequence.rb +0 -1
- data/lib/jets/commands/templates/skeleton/config/application.rb.tt +11 -0
- data/lib/jets/commands/url.rb +32 -7
- data/lib/jets/controller/base.rb +21 -5
- data/lib/jets/controller/layout.rb +0 -3
- data/lib/jets/controller/middleware/local/api_gateway.rb +2 -5
- data/lib/jets/controller/middleware/local/mimic_aws_call.rb +2 -2
- data/lib/jets/controller/params.rb +42 -10
- data/lib/jets/controller/rack/adapter.rb +5 -2
- data/lib/jets/controller/rack/env.rb +17 -8
- data/lib/jets/controller/renderers/rack_renderer.rb +1 -1
- data/lib/jets/controller/rendering.rb +4 -1
- data/lib/jets/core.rb +8 -16
- data/lib/jets/internal/app/functions/jets/base_path.rb +153 -0
- data/lib/jets/klass.rb +38 -5
- data/lib/jets/lambda/dsl.rb +0 -2
- data/lib/jets/mega/request.rb +44 -13
- data/lib/jets/mega/request/source.rb +21 -0
- data/lib/jets/middleware/configurator.rb +1 -1
- data/lib/jets/middleware/default_stack.rb +2 -2
- data/lib/jets/resource.rb +1 -0
- data/lib/jets/resource/api_gateway.rb +5 -3
- data/lib/jets/resource/api_gateway/base_path.rb +5 -0
- data/lib/jets/resource/api_gateway/base_path/function.rb +42 -0
- data/lib/jets/resource/api_gateway/base_path/mapping.rb +44 -0
- data/lib/jets/resource/api_gateway/base_path/role.rb +76 -0
- data/lib/jets/resource/api_gateway/cors.rb +1 -1
- data/lib/jets/resource/api_gateway/deployment.rb +9 -5
- data/lib/jets/resource/api_gateway/domain_name.rb +56 -0
- data/lib/jets/resource/api_gateway/method.rb +3 -4
- data/lib/jets/resource/api_gateway/resource.rb +4 -3
- data/lib/jets/resource/api_gateway/rest_api.rb +42 -14
- data/lib/jets/resource/api_gateway/rest_api/change_detection.rb +42 -0
- data/lib/jets/resource/api_gateway/rest_api/logical_id.rb +59 -0
- data/lib/jets/resource/api_gateway/rest_api/routes.rb +127 -0
- data/lib/jets/resource/child_stack/api_deployment.rb +5 -1
- data/lib/jets/resource/function.rb +3 -20
- data/lib/jets/resource/function/environment.rb +23 -0
- data/lib/jets/resource/iam/application_role.rb +1 -1
- data/lib/jets/resource/route53.rb +3 -0
- data/lib/jets/resource/route53/record_set.rb +70 -0
- data/lib/jets/router.rb +2 -0
- data/lib/jets/ruby_server.rb +6 -3
- data/lib/jets/stack.rb +1 -3
- data/lib/jets/stack/main/dsl.rb +1 -1
- data/lib/jets/stack/main/extensions/lambda.rb +4 -2
- data/lib/jets/turbine.rb +0 -3
- data/lib/jets/version.rb +1 -1
- data/vendor/jets-gems/lib/jets/gems.rb +1 -0
- data/vendor/jets-gems/lib/jets/gems/agree.rb +41 -0
- data/vendor/jets-gems/lib/jets/gems/check.rb +15 -2
- metadata +30 -2
data/lib/jets/camelizer.rb
CHANGED
data/lib/jets/cfn/builders.rb
CHANGED
@@ -16,6 +16,33 @@ class Jets::Cfn::Builders
|
|
16
16
|
add_resource(deployment)
|
17
17
|
add_parameters(deployment.parameters)
|
18
18
|
add_outputs(deployment.outputs)
|
19
|
+
|
20
|
+
add_base_path_mapping
|
21
|
+
end
|
22
|
+
|
23
|
+
# Because Jets generates a new timestamped logical id for the API Deployment
|
24
|
+
# resource it also creates a new root base path mapping and fails. Additionally,
|
25
|
+
# the base path mapping depends on the API Deploy for the stage name.
|
26
|
+
#
|
27
|
+
# We resolve this by using a custom resource that does an in-place update.
|
28
|
+
#
|
29
|
+
# Note, also tried to change the domain name of to something like demo-dev-[random].mydomain.com
|
30
|
+
# but that does not work because the domain name has to match the route53 record exactly.
|
31
|
+
#
|
32
|
+
def add_base_path_mapping
|
33
|
+
return unless Jets.custom_domain?
|
34
|
+
|
35
|
+
function = Jets::Resource::ApiGateway::BasePath::Function.new
|
36
|
+
add_resource(function)
|
37
|
+
add_outputs(function.outputs)
|
38
|
+
|
39
|
+
mapping = Jets::Resource::ApiGateway::BasePath::Mapping.new
|
40
|
+
add_resource(mapping)
|
41
|
+
add_outputs(mapping.outputs)
|
42
|
+
|
43
|
+
iam_role = Jets::Resource::ApiGateway::BasePath::Role.new
|
44
|
+
add_resource(iam_role)
|
45
|
+
add_outputs(iam_role.outputs)
|
19
46
|
end
|
20
47
|
|
21
48
|
# template_path is an interface method
|
@@ -13,6 +13,7 @@ class Jets::Cfn::Builders
|
|
13
13
|
return unless @options[:templates] || @options[:stack_type] != :minimal
|
14
14
|
|
15
15
|
add_gateway_rest_api
|
16
|
+
add_custom_domain
|
16
17
|
add_gateway_routes
|
17
18
|
end
|
18
19
|
|
@@ -33,7 +34,26 @@ class Jets::Cfn::Builders
|
|
33
34
|
add_outputs(rest_api.outputs)
|
34
35
|
|
35
36
|
deployment = Jets::Resource::ApiGateway::Deployment.new
|
36
|
-
|
37
|
+
outputs = deployment.outputs(true)
|
38
|
+
add_output("RestApiUrl", Value: outputs["RestApiUrl"])
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_custom_domain
|
42
|
+
return unless Jets.custom_domain?
|
43
|
+
add_domain_name
|
44
|
+
add_route53_dns
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_domain_name
|
48
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
49
|
+
add_resource(domain_name)
|
50
|
+
add_outputs(domain_name.outputs)
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_route53_dns
|
54
|
+
dns = Jets::Resource::Route53::RecordSet.new
|
55
|
+
add_resource(dns)
|
56
|
+
add_outputs(dns.outputs)
|
37
57
|
end
|
38
58
|
|
39
59
|
# Adds route related Resources and Outputs
|
@@ -51,7 +71,7 @@ class Jets::Cfn::Builders
|
|
51
71
|
homepage = path == ''
|
52
72
|
next if homepage # handled by RootResourceId output already
|
53
73
|
|
54
|
-
resource = Jets::Resource::ApiGateway::Resource.new(path)
|
74
|
+
resource = Jets::Resource::ApiGateway::Resource.new(path, internal: true)
|
55
75
|
add_resource(resource)
|
56
76
|
add_outputs(resource.outputs)
|
57
77
|
end
|
data/lib/jets/cfn/ship.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
class Jets::Cfn
|
2
2
|
class Ship
|
3
|
+
extend Memoist
|
3
4
|
include Jets::AwsServices
|
4
5
|
|
5
6
|
def initialize(options)
|
@@ -32,7 +33,9 @@ class Jets::Cfn
|
|
32
33
|
|
33
34
|
wait_for_stack
|
34
35
|
prewarm
|
36
|
+
clean_deploy_logs
|
35
37
|
show_api_endpoint
|
38
|
+
show_custom_domain
|
36
39
|
end
|
37
40
|
|
38
41
|
def save_stack
|
@@ -89,15 +92,34 @@ class Jets::Cfn
|
|
89
92
|
end
|
90
93
|
end
|
91
94
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
resp, status = stack_status
|
96
|
-
return if status.include?("ROLLBACK")
|
95
|
+
def clean_deploy_logs
|
96
|
+
Jets::Commands::Clean::Log.new.clean_deploys
|
97
|
+
end
|
97
98
|
|
99
|
+
def endpoint_unavailable?
|
100
|
+
return true unless @options[:stack_type] == :full # s3 bucket is available
|
101
|
+
return true if Jets::Router.routes.empty?
|
102
|
+
_, status = stack_status
|
103
|
+
return true if status.include?("ROLLBACK")
|
104
|
+
return true unless api_gateway
|
105
|
+
end
|
106
|
+
|
107
|
+
# Do not memoize this because on first stack run it will be nil
|
108
|
+
# It only gets called one more time so just let it get called.
|
109
|
+
def api_gateway
|
98
110
|
resp = cfn.describe_stack_resources(stack_name: @parent_stack_name)
|
99
111
|
resources = resp.stack_resources
|
100
|
-
|
112
|
+
resources.find { |resource| resource.logical_resource_id == "ApiGateway" }
|
113
|
+
end
|
114
|
+
memoize :api_gateway
|
115
|
+
|
116
|
+
def endpoint_available?
|
117
|
+
!endpoint_unavailable?
|
118
|
+
end
|
119
|
+
|
120
|
+
def show_api_endpoint
|
121
|
+
return unless endpoint_available?
|
122
|
+
|
101
123
|
stack_id = api_gateway["physical_resource_id"]
|
102
124
|
|
103
125
|
resp = cfn.describe_stacks(stack_name: stack_id)
|
@@ -107,6 +129,16 @@ class Jets::Cfn
|
|
107
129
|
puts "API Gateway Endpoint: #{endpoint}"
|
108
130
|
end
|
109
131
|
|
132
|
+
def show_custom_domain
|
133
|
+
return unless endpoint_available? && Jets.custom_domain?
|
134
|
+
|
135
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
136
|
+
# Looks funny but its right.
|
137
|
+
# domain_name is a method on the Jets::Resource::ApiGateway::Domain instance
|
138
|
+
url = "https://#{domain_name.domain_name}"
|
139
|
+
puts "Custom Domain: #{url}"
|
140
|
+
end
|
141
|
+
|
110
142
|
# All CloudFormation states listed here:
|
111
143
|
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
|
112
144
|
def stack_status
|
data/lib/jets/commands/call.rb
CHANGED
@@ -25,6 +25,24 @@ class Jets::Commands::Clean
|
|
25
25
|
say "Removed CloudWatch logs for #{prefix_guess}"
|
26
26
|
end
|
27
27
|
|
28
|
+
def clean_deploys
|
29
|
+
groups = deploy_log_groups.sort_by do |g|
|
30
|
+
g.log_group_name
|
31
|
+
end
|
32
|
+
# Keep the last 2 recent log groups so we can see the deleted logic
|
33
|
+
groups = groups[0..-3]
|
34
|
+
groups.each do |g|
|
35
|
+
logs.delete_log_group(log_group_name: g.log_group_name) unless @options[:noop]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def deploy_log_groups
|
40
|
+
log_groups.select do |g|
|
41
|
+
!keep_log_group?(g.log_group_name) &&
|
42
|
+
g.log_group_name.include?('jets-base-path')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
28
46
|
private
|
29
47
|
def prefix_guess
|
30
48
|
Jets::Naming.parent_stack_name
|
data/lib/jets/commands/runner.rb
CHANGED
@@ -47,4 +47,15 @@ Jets.application.configure do
|
|
47
47
|
# config.assets.cache_control = nil # IE: "public, max-age=3600" # override max_age for more fine-grain control.
|
48
48
|
# config.assets.base_url = nil # IE: https://cloudfront.com/my/base/path, defaults to the s3 bucket url
|
49
49
|
# IE: https://s3-us-west-2.amazonaws.com/demo-dev-s3bucket-1inlzkvujq8zb
|
50
|
+
|
51
|
+
# config.api.endpoint_type = 'PRIVATE' # Default is 'EDGE' https://amzn.to/2r0Iu2L
|
52
|
+
# config.api.authorization_type = "AWS_IAM" # default is 'NONE' https://amzn.to/2qZ7zLh
|
53
|
+
|
54
|
+
|
55
|
+
# config.domain.hosted_zone_name = "example.com"
|
56
|
+
# us-west-2 REGIONAL endpoint
|
57
|
+
# config.domain.cert_arn = "arn:aws:acm:us-west-2:112233445566:certificate/8d8919ce-a710-4050-976b-b33da991e123"
|
58
|
+
# us-east-1 EDGE endpoint
|
59
|
+
# config.domain.cert_arn = "arn:aws:acm:us-east-1:112233445566:certificate/d68472ba-04f8-45ba-b9db-14f839d57123"
|
60
|
+
# config.domain.endpoint_type = "EDGE"
|
50
61
|
end
|
data/lib/jets/commands/url.rb
CHANGED
@@ -16,14 +16,16 @@ module Jets::Commands
|
|
16
16
|
stack = cfn.describe_stacks(stack_name: stack_name).stacks.first
|
17
17
|
|
18
18
|
api_gateway_stack_arn = lookup(stack[:outputs], "ApiGateway")
|
19
|
-
if api_gateway_stack_arn
|
20
|
-
|
19
|
+
if api_gateway_stack_arn && endpoint_available?
|
20
|
+
api_gateway_endpoint = get_gateway_endpoint(api_gateway_stack_arn)
|
21
|
+
STDOUT.puts "API Gateway Endpoint: #{api_gateway_endpoint}"
|
22
|
+
show_custom_domain
|
21
23
|
else
|
22
24
|
puts "API Gateway not found. This jets app does have an API Gateway associated with it. Please double check your config/routes.rb if you were expecting to see a url for the app. Also check that #{stack_name.colorize(:green)} is a jets app."
|
23
25
|
end
|
24
26
|
end
|
25
27
|
|
26
|
-
def
|
28
|
+
def get_gateway_endpoint(api_gateway_stack_arn)
|
27
29
|
stack = cfn.describe_stacks(stack_name: api_gateway_stack_arn).stacks.first
|
28
30
|
rest_api = lookup(stack[:outputs], "RestApi")
|
29
31
|
region_id = lookup(stack[:outputs], "Region")
|
@@ -34,10 +36,33 @@ module Jets::Commands
|
|
34
36
|
"https://#{rest_api}.execute-api.#{region_id}.amazonaws.com/#{stage_name}"
|
35
37
|
end
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
def show_custom_domain
|
40
|
+
return unless endpoint_available? && Jets.custom_domain?
|
41
|
+
|
42
|
+
domain_name = Jets::Resource::ApiGateway::DomainName.new
|
43
|
+
# Looks funny but its right.
|
44
|
+
# domain_name is a method on the Jets::Resource::ApiGateway::Domain instance
|
45
|
+
url = "https://#{domain_name.domain_name}"
|
46
|
+
puts "Custom Domain: #{url}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def endpoint_unavailable?
|
50
|
+
return false if Jets::Router.routes.empty?
|
51
|
+
resp, status = stack_status
|
52
|
+
return false if status.include?("ROLLBACK")
|
53
|
+
end
|
54
|
+
|
55
|
+
def endpoint_available?
|
56
|
+
!endpoint_unavailable?
|
57
|
+
end
|
58
|
+
|
59
|
+
# All CloudFormation states listed here:
|
60
|
+
# http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html
|
61
|
+
def stack_status
|
62
|
+
resp = cfn.describe_stacks(stack_name: @parent_stack_name)
|
63
|
+
status = resp.stacks[0].stack_status
|
64
|
+
[resp, status]
|
41
65
|
end
|
66
|
+
|
42
67
|
end
|
43
68
|
end
|
data/lib/jets/controller/base.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "active_support/core_ext/hash"
|
2
|
-
require "active_support/core_ext/object"
|
3
1
|
require "json"
|
4
2
|
require "rack/utils" # Rack::Utils.parse_nested_query
|
5
3
|
|
@@ -37,9 +35,7 @@ class Jets::Controller
|
|
37
35
|
|
38
36
|
def dispatch!
|
39
37
|
t1 = Time.now
|
40
|
-
|
41
|
-
Jets.logger.info " Event: #{@event.inspect}"
|
42
|
-
Jets.logger.info " Parameters: #{params(raw: true).to_h.inspect}"
|
38
|
+
log_info_start
|
43
39
|
|
44
40
|
run_before_actions
|
45
41
|
send(@meth)
|
@@ -53,6 +49,26 @@ class Jets::Controller
|
|
53
49
|
triplet # status, headers, body
|
54
50
|
end
|
55
51
|
|
52
|
+
def log_info_start
|
53
|
+
display_event = @event.dup
|
54
|
+
display_event['body'] = '[BASE64_ENCODED]' if @event['isBase64Encoded']
|
55
|
+
# Interesting, JSON.dump makes logging look like JSON.pretty_generate in
|
56
|
+
# CloudWatch but not locally. This is what we want.
|
57
|
+
ip = request.ip
|
58
|
+
Jets.logger.info "Started #{@event['httpMethod']} \"#{@event['path']}\" for #{ip} at #{Time.now}"
|
59
|
+
Jets.logger.info "Processing #{self.class.name}##{@meth}"
|
60
|
+
Jets.logger.info " Event: #{json_dump(display_event)}"
|
61
|
+
Jets.logger.info " Parameters: #{JSON.dump(params(raw: true).to_h)}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handles binary data safely
|
65
|
+
def json_dump(data)
|
66
|
+
JSON.dump(data)
|
67
|
+
rescue Encoding::UndefinedConversionError
|
68
|
+
data['body'] = '[BINARY]'
|
69
|
+
JSON.dump(data)
|
70
|
+
end
|
71
|
+
|
56
72
|
def self.process(event, context={}, meth)
|
57
73
|
controller = new(event, context, meth)
|
58
74
|
# Using send because process! is private method in Jets::RackController so
|
@@ -63,10 +63,10 @@ class Jets::Controller::Middleware::Local
|
|
63
63
|
h[key] = v
|
64
64
|
h
|
65
65
|
end
|
66
|
-
# Content type is not prepended with HTTP_
|
66
|
+
# Content type is not prepended with HTTP_
|
67
67
|
headers["Content-Type"] = @env["CONTENT_TYPE"] if @env["CONTENT_TYPE"]
|
68
68
|
|
69
|
-
# Adjust the casing so it matches the Lambda AWS Proxy
|
69
|
+
# Adjust the casing so it matches the Lambda AWS Proxy structure
|
70
70
|
CASING_MAP.each do |nice_casing, bad_casing|
|
71
71
|
if headers.key?(nice_casing)
|
72
72
|
headers[bad_casing] = headers.delete(nice_casing)
|
@@ -83,9 +83,6 @@ class Jets::Controller::Middleware::Local
|
|
83
83
|
# To get the post body:
|
84
84
|
# rack.input: #<StringIO:0x007f8ccf8db9a0>
|
85
85
|
def get_body
|
86
|
-
# @env["rack.input"] is always provided by rack and we should make
|
87
|
-
# the test data always have rack.input to mimic rack defaulting to
|
88
|
-
# StringIO.new to help make testing easier
|
89
86
|
input = @env["rack.input"] || StringIO.new
|
90
87
|
body = input.read
|
91
88
|
input.rewind # IMPORTANT or else it screws up other middlewares that use the body
|
@@ -27,12 +27,12 @@ class Jets::Controller::Middleware::Local
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def event
|
30
|
-
ApiGateway.new(@route, @env).event
|
30
|
+
@env['adapter.event'] || ApiGateway.new(@route, @env).event
|
31
31
|
end
|
32
32
|
memoize :event
|
33
33
|
|
34
34
|
def context
|
35
|
-
{}
|
35
|
+
@env['adapter.context'] || {"fake" => "context in mimic_aws_call.rb"}
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
require "action_controller/metal/strong_parameters"
|
2
|
+
require "action_dispatch"
|
2
3
|
require "rack"
|
3
4
|
|
4
5
|
class Jets::Controller
|
5
6
|
module Params
|
7
|
+
extend Memoist
|
8
|
+
|
6
9
|
# Merge all the parameters together for convenience. Users still have
|
7
10
|
# access via events.
|
8
11
|
#
|
@@ -10,46 +13,75 @@ class Jets::Controller
|
|
10
13
|
# 1. path parameters have highest precdence
|
11
14
|
# 2. query string parameters
|
12
15
|
# 3. body parameters
|
13
|
-
def params(raw: false, path_parameters: true)
|
14
|
-
query_string_params = event["queryStringParameters"] || {}
|
16
|
+
def params(raw: false, path_parameters: true, body_parameters: true)
|
15
17
|
path_params = event["pathParameters"] || {}
|
16
|
-
|
17
|
-
|
18
|
+
|
19
|
+
params = {}
|
20
|
+
params = params.deep_merge(body_params) if body_parameters
|
21
|
+
params = params.deep_merge(query_parameters) # always
|
18
22
|
params = params.deep_merge(path_params) if path_parameters
|
19
23
|
|
20
24
|
if raw
|
21
25
|
params
|
22
26
|
else
|
27
|
+
params = ActionDispatch::Request::Utils.normalize_encode_params(params) # for file uploads
|
23
28
|
ActionController::Parameters.new(params)
|
24
29
|
end
|
25
30
|
end
|
26
31
|
|
27
|
-
|
32
|
+
def query_parameters
|
33
|
+
event["queryStringParameters"] || {}
|
34
|
+
end
|
35
|
+
|
28
36
|
def body_params
|
29
|
-
body = event["body"]
|
37
|
+
body = event['isBase64Encoded'] ? base64_decode(event["body"]) : event["body"]
|
30
38
|
return {} if body.nil?
|
31
39
|
|
32
|
-
# Try json parsing
|
33
40
|
parsed_json = parse_json(body)
|
34
41
|
return parsed_json if parsed_json
|
35
42
|
|
36
|
-
|
37
|
-
# For content-type application/x-www-form-urlencoded CGI.parse the body
|
38
43
|
headers = event["headers"] || {}
|
39
|
-
headers = headers.transform_keys { |key| key.downcase }
|
40
44
|
# API Gateway seems to use either: content-type or Content-Type
|
45
|
+
headers = headers.transform_keys { |key| key.downcase }
|
41
46
|
content_type = headers["content-type"]
|
47
|
+
|
42
48
|
if content_type.to_s.include?("application/x-www-form-urlencoded")
|
43
49
|
return ::Rack::Utils.parse_nested_query(body)
|
50
|
+
elsif content_type.to_s.include?("multipart/form-data")
|
51
|
+
return parse_multipart(body)
|
44
52
|
end
|
45
53
|
|
46
54
|
{} # fallback to empty Hash
|
47
55
|
end
|
56
|
+
memoize :body_params
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def parse_multipart(body)
|
61
|
+
boundary = ::Rack::Multipart::Parser.parse_boundary(headers["content-type"])
|
62
|
+
options = multipart_options(body, boundary)
|
63
|
+
env = ::Rack::MockRequest.env_for("/", options)
|
64
|
+
::Rack::Multipart.parse_multipart(env) # params Hash
|
65
|
+
end
|
66
|
+
|
67
|
+
def multipart_options(data, boundary = "AaB03x")
|
68
|
+
type = %(multipart/form-data; boundary=#{boundary})
|
69
|
+
length = data.bytesize
|
70
|
+
|
71
|
+
{ "CONTENT_TYPE" => type,
|
72
|
+
"CONTENT_LENGTH" => length.to_s,
|
73
|
+
:input => StringIO.new(data) }
|
74
|
+
end
|
48
75
|
|
49
76
|
def parse_json(text)
|
50
77
|
JSON.parse(text)
|
51
78
|
rescue JSON::ParserError
|
52
79
|
nil
|
53
80
|
end
|
81
|
+
|
82
|
+
def base64_decode(body)
|
83
|
+
return nil if body.nil?
|
84
|
+
Base64.decode64(body)
|
85
|
+
end
|
54
86
|
end
|
55
87
|
end
|