jets 1.0.18 → 1.1.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/Gemfile.lock +10 -10
  4. data/README/testing.md +5 -2
  5. data/lib/jets.rb +2 -2
  6. data/lib/jets/application.rb +69 -40
  7. data/lib/jets/booter.rb +17 -20
  8. data/lib/jets/builders/code_builder.rb +7 -8
  9. data/lib/jets/cfn/ship.rb +0 -6
  10. data/lib/jets/commands/build.rb +0 -5
  11. data/lib/jets/commands/deploy.rb +0 -4
  12. data/lib/jets/commands/main.rb +31 -4
  13. data/lib/jets/commands/templates/skeleton/{.env → .env.tt} +1 -0
  14. data/lib/jets/commands/templates/skeleton/config.ru +1 -0
  15. data/lib/jets/commands/upgrade/v1.rb +12 -0
  16. data/lib/jets/controller.rb +5 -0
  17. data/lib/jets/controller/base.rb +43 -21
  18. data/lib/jets/controller/cookies.rb +40 -0
  19. data/lib/jets/controller/cookies/jar.rb +269 -0
  20. data/lib/jets/controller/middleware.rb +4 -0
  21. data/lib/jets/controller/middleware/local.rb +119 -0
  22. data/lib/jets/{server/lambda_aws_proxy.rb → controller/middleware/local/api_gateway.rb} +11 -49
  23. data/lib/jets/controller/middleware/local/mimic_aws_call.rb +38 -0
  24. data/lib/jets/{server → controller/middleware/local}/route_matcher.rb +4 -4
  25. data/lib/jets/controller/middleware/main.rb +46 -0
  26. data/lib/jets/{server → controller/middleware}/webpacker_setup.rb +0 -1
  27. data/lib/jets/controller/params.rb +2 -1
  28. data/lib/jets/controller/rack.rb +5 -0
  29. data/lib/jets/controller/rack/adapter.rb +60 -0
  30. data/lib/jets/controller/rack/env.rb +96 -0
  31. data/lib/jets/controller/redirection.rb +1 -1
  32. data/lib/jets/controller/renderers.rb +1 -1
  33. data/lib/jets/controller/renderers/base_renderer.rb +0 -4
  34. data/lib/jets/controller/renderers/{aws_proxy_renderer.rb → rack_renderer.rb} +7 -19
  35. data/lib/jets/controller/renderers/template_renderer.rb +1 -1
  36. data/lib/jets/controller/request.rb +14 -44
  37. data/lib/jets/controller/response.rb +55 -7
  38. data/lib/jets/internal/app/controllers/jets/rack_controller.rb +13 -3
  39. data/lib/jets/mega.rb +7 -0
  40. data/lib/jets/{rack → mega}/hash_converter.rb +1 -1
  41. data/lib/jets/{rack → mega}/request.rb +17 -4
  42. data/lib/jets/middleware.rb +38 -0
  43. data/lib/jets/middleware/configurator.rb +84 -0
  44. data/lib/jets/middleware/default_stack.rb +44 -0
  45. data/lib/jets/middleware/layer.rb +34 -0
  46. data/lib/jets/middleware/stack.rb +77 -0
  47. data/lib/jets/resource/function.rb +1 -1
  48. data/lib/jets/ruby_server.rb +1 -1
  49. data/lib/jets/server.rb +48 -13
  50. data/lib/jets/version.rb +1 -1
  51. metadata +24 -17
  52. data/lib/jets/application/middleware.rb +0 -23
  53. data/lib/jets/default/application.rb +0 -23
  54. data/lib/jets/rack.rb +0 -7
  55. data/lib/jets/rack/server.rb +0 -47
  56. data/lib/jets/server/api_gateway.rb +0 -39
  57. data/lib/jets/server/timing_middleware.rb +0 -33
  58. data/lib/jets/timing.rb +0 -65
  59. data/lib/jets/timing/report.rb +0 -82
@@ -0,0 +1,4 @@
1
+ module Jets::Controller::Middleware
2
+ autoload :Local, "jets/controller/middleware/local"
3
+ autoload :Main, "jets/controller/middleware/main"
4
+ end
@@ -0,0 +1,119 @@
1
+ require 'kramdown'
2
+
3
+ # Handles mimicking of API Gateway to Lambda function call locally
4
+ module Jets::Controller::Middleware
5
+ class Local
6
+ extend Memoist
7
+
8
+ autoload :ApiGateway, 'jets/controller/middleware/local/api_gateway'
9
+ autoload :MimicAwsCall, 'jets/controller/middleware/local/mimic_aws_call'
10
+ autoload :RouteMatcher, 'jets/controller/middleware/local/route_matcher'
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ route = RouteMatcher.new(env).find_route
18
+ unless route
19
+ return [404, {'Content-Type' => 'text/html'}, not_found(env)]
20
+ end
21
+
22
+ mimic = MimicAwsCall.new(route, env)
23
+ # Make @controller and @meth instance available so we dont not have to pass it around.
24
+ @controller, @meth, @event = mimic.controller, mimic.meth, mimic.event
25
+
26
+ if route.to == 'jets/rack#process' # megamode
27
+ # Bypass the Jets middlewares since it could interfere with the Rack
28
+ # application's middleware stack.
29
+ #
30
+ # Rails sends back a transfer-encoding=chunked. Curling Rails directly works,
31
+ # but passing the Rails response back through this middleware results in errors.
32
+ # Disable chunking responses by deleting the transfer-encoding response header.
33
+ # Would like to understand why this happens this better, if someone can explain please let me know.
34
+ status, headers, body = @controller.dispatch! # jets/rack_controller
35
+ headers.delete "transfer-encoding"
36
+ [status, headers, body]
37
+ elsif polymorphic_function?
38
+ # Will never hit when calling polymorphic function on AWS Lambda.
39
+ # This can only really get called with the local server.
40
+ run_polymophic_function
41
+ else # Normal Jets request
42
+ mimick_aws_lambda!(env, mimic.vars) unless on_aws?(env)
43
+ @app.call(env)
44
+ end
45
+ end
46
+
47
+ def polymorphic_function?
48
+ polymorphic_function.task.lang != :ruby
49
+ end
50
+
51
+ def polymorphic_function
52
+ # Abusing PolyFun to run polymorphic code, should call LambdaExecutor directly
53
+ # after reworking LambdaExecutor so it has a better interface.
54
+ Jets::PolyFun.new(@controller.class, @meth)
55
+ end
56
+ memoize :polymorphic_function
57
+
58
+ def run_polymophic_function
59
+ resp = polymorphic_function.run(@event, @meth) # polymorphic code
60
+ status = resp['statusCode']
61
+ headers = resp['headers']
62
+ body = StringIO.new(resp['body'])
63
+ [status, headers, body] # triplet
64
+ end
65
+
66
+ # Modifies env the in the same way real call from AWS lambda would modify env
67
+ def mimick_aws_lambda!(env, vars)
68
+ env.merge!(vars)
69
+ env
70
+ end
71
+
72
+ def on_aws?(env)
73
+ return false if ENV['TEST'] # usually with test we're passing in full API Gateway fixtures with the HTTP_X_AMZN_TRACE_ID
74
+ !!env['HTTP_X_AMZN_TRACE_ID']
75
+ end
76
+
77
+ def routes_error_message(env)
78
+ message = "<h2>404 Error: Route #{env['PATH_INFO'].sub('/','')} not found</h2>"
79
+ if Jets.env != "production"
80
+ message << "<p>Here are the routes defined in your application:</p>"
81
+ message << "#{routes_table}"
82
+ end
83
+ message
84
+ end
85
+
86
+ def not_found(env)
87
+ message = routes_error_message(env)
88
+ body = <<~HTML
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <meta charset="utf-8">
93
+ <title>Route not found</title>
94
+ </head>
95
+ <body>
96
+ #{message}
97
+ </body>
98
+ </html>
99
+ HTML
100
+ StringIO.new(body)
101
+ end
102
+
103
+ # Show pretty route table for user to help with debugging in non-production mode
104
+ def routes_table
105
+ routes = Jets::Router.routes
106
+
107
+ return "Your routes table is empty." if routes.empty?
108
+
109
+ text = "Verb | Path | Controller#action\n"
110
+ text << "--- | --- | ---\n"
111
+ routes.each do |route|
112
+ text << "#{route.method} | #{route.path} | #{route.to}\n"
113
+ end
114
+ html = Kramdown::Document.new(text).to_html
115
+ puts html
116
+ html
117
+ end
118
+ end
119
+ end
@@ -1,38 +1,13 @@
1
- require 'cgi'
2
- require 'stringio'
1
+ # Takes a Rack env and converts to ApiGateway event
2
+ class Jets::Controller::Middleware::Local
3
+ class ApiGateway
4
+ extend Memoist
3
5
 
4
- class Jets::Server
5
- # This doesnt really need to be middleware
6
- class LambdaAwsProxy
7
6
  def initialize(route, env)
8
- @route = route
9
- @env = env
10
- # puts "Rack env:".colorize(:yellow)
11
- # @env.each do |k,v|
12
- # puts "#{k}: #{v}"
13
- # end
7
+ @route, @env = route, env
14
8
  end
15
9
 
16
- def response
17
- event = build_event
18
- context = {}
19
-
20
- controller_class = find_controller_class
21
- controller_action = find_controller_action
22
-
23
- fun = Jets::PolyFun.new(controller_class, controller_action)
24
- resp = fun.run(event, context) # check the logs for polymorphic function errors
25
-
26
- # Map lambda proxy response format to rack format
27
- status = resp["statusCode"]
28
- headers = resp["headers"] || {}
29
- headers = {'Content-Type' => 'text/html'}.merge(headers)
30
- body = resp["body"]
31
-
32
- [status, headers, [body]]
33
- end
34
-
35
- def build_event
10
+ def event
36
11
  resource = @route.path(:api_gateway) # posts/{id}/edit
37
12
  path = @env['PATH_INFO'].sub('/','') # remove beginning slash
38
13
  {
@@ -48,6 +23,7 @@ class Jets::Server
48
23
  "isBase64Encoded" => false,
49
24
  }
50
25
  end
26
+ memoize :event
51
27
 
52
28
  # Annoying. The headers part of the AWS Lambda proxy structure
53
29
  # does not consisently use the same casing scheme for the header keys.
@@ -97,12 +73,6 @@ class Jets::Server
97
73
  end
98
74
  end
99
75
 
100
- # Way to fake X-Amzn-Trace-Id which on_aws? helper checks.
101
- # This is how we distinguish a request from API gateway vs local.
102
- if ENV['JETS_ON_AWS']
103
- headers["X-Amzn-Trace-Id"] = "Root=fake-trace-id"
104
- end
105
-
106
76
  headers
107
77
  end
108
78
 
@@ -114,21 +84,13 @@ class Jets::Server
114
84
  # rack.input: #<StringIO:0x007f8ccf8db9a0>
115
85
  def get_body
116
86
  # @env["rack.input"] is always provided by rack and we should make
117
- # the test data always have rack.input to mimic rack, but but handling
118
- # it this way because it's simpler.
87
+ # the test data always have rack.input to mimic rack defaulting to
88
+ # StringIO.new to help make testing easier
119
89
  input = @env["rack.input"] || StringIO.new
120
90
  body = input.read
121
- # return nil for blank string, because thats what Lambda AWS_PROXY does
91
+ input.rewind # IMPORTANT or else it screws up other middlewares that use the body
92
+ # return nil for blank string, because Lambda AWS_PROXY does this
122
93
  body unless body.empty?
123
94
  end
124
-
125
- def find_controller_class
126
- # posts#edit => PostsController
127
- @route.controller_name.constantize
128
- end
129
-
130
- def find_controller_action
131
- @route.action_name
132
- end
133
95
  end
134
96
  end
@@ -0,0 +1,38 @@
1
+ class Jets::Controller::Middleware::Local
2
+ class MimicAwsCall
3
+ extend Memoist
4
+
5
+ def initialize(route, env)
6
+ @route, @env = route, env
7
+ end
8
+
9
+ def vars
10
+ {
11
+ 'jets.controller' => controller,
12
+ 'lambda.context' => context,
13
+ 'lambda.event' => event,
14
+ 'lambda.meth' => meth,
15
+ }
16
+ end
17
+
18
+ # Actual controller instance
19
+ def controller
20
+ controller_class = @route.controller_name.constantize
21
+ meth = @route.action_name
22
+ controller_class.new(event, context, meth)
23
+ end
24
+
25
+ def meth
26
+ @route.action_name
27
+ end
28
+
29
+ def event
30
+ ApiGateway.new(@route, @env).event
31
+ end
32
+ memoize :event
33
+
34
+ def context
35
+ {}
36
+ end
37
+ end
38
+ end
@@ -1,4 +1,4 @@
1
- class Jets::Server
1
+ class Jets::Controller::Middleware::Local
2
2
  class RouteMatcher
3
3
  def initialize(env)
4
4
  @env = env
@@ -12,14 +12,14 @@ class Jets::Server
12
12
  # Within these 2 groups we consider the routes with the longest path first
13
13
  # since posts/:id and posts/:id/edit can both match.
14
14
  routes = router.ordered_routes
15
- route = routes.find do |route|
16
- route_found?(route)
15
+ route = routes.find do |r|
16
+ route_found?(r)
17
17
  end
18
18
  route
19
19
  end
20
20
 
21
21
  def route_found?(route)
22
- request_method = @env["REQUEST_METHOD"]
22
+ request_method = @env["REQUEST_METHOD"] || "GET"
23
23
  actual_path = @env["PATH_INFO"].sub(/^\//,'') # remove beginning slash
24
24
 
25
25
  # Immediately stop checking when the request method: GET, POST, ANY, etc
@@ -0,0 +1,46 @@
1
+ # All roads lead here
2
+ #
3
+ # 1. AWS Lambda: PostsController - Rack::Adapter - Jets.application.call
4
+ # 2. Local server: config.ru - run Jet.application - Jets.application.call
5
+ #
6
+ # Then eventually:
7
+ #
8
+ # Jets.application.call - Middleware stack - Jets::Controller::Middleware::Main
9
+ #
10
+ module Jets::Controller::Middleware
11
+ class Main
12
+ def initialize(env)
13
+ @env = env
14
+ @controller = env['jets.controller']
15
+ @event = env['lambda.event']
16
+ @context = env['lambda.context']
17
+ @meth = env['lambda.meth']
18
+ end
19
+
20
+ def call
21
+ dup.call!
22
+ end
23
+
24
+ def call!
25
+ setup
26
+ @controller.dispatch! # Returns triplet
27
+ end
28
+
29
+ # Common setup logical at this point of middleware processing right before
30
+ # calling any controller actions.
31
+ def setup
32
+ # We already recreated a mimicke rack env earlier as part of the very first
33
+ # middleware layer. However, by the time the rack env reaches the main middleware
34
+ # it could had been updated by other middlewares. We update the env here again.
35
+ @controller.request.set_env!(@env)
36
+ # This allows sesison helpers to work. Sessions are managed by
37
+ # the Rack::Session::Cookie middleware by default.
38
+ @controller.session = @env['rack.session'] || {}
39
+ end
40
+
41
+ def self.call(env)
42
+ instance = new(env)
43
+ instance.call
44
+ end
45
+ end
46
+ end
@@ -4,4 +4,3 @@ require "webpacker"
4
4
  require "webpacker/dev_server_proxy"
5
5
 
6
6
  Webpacker.bootstrap # whenever jets server is runs should Webpacker.bootstrap
7
-
@@ -1,4 +1,5 @@
1
1
  require "action_controller/metal/strong_parameters"
2
+ require "rack"
2
3
 
3
4
  class Jets::Controller
4
5
  module Params
@@ -39,7 +40,7 @@ class Jets::Controller
39
40
  # API Gateway seems to use either: content-type or Content-Type
40
41
  content_type = headers["content-type"]
41
42
  if content_type.to_s.include?("application/x-www-form-urlencoded")
42
- return Rack::Utils.parse_nested_query(body)
43
+ return ::Rack::Utils.parse_nested_query(body)
43
44
  end
44
45
 
45
46
  {} # fallback to empty Hash
@@ -0,0 +1,5 @@
1
+ module Jets::Controller::Rack
2
+ autoload :Adapter, "jets/controller/rack/adapter"
3
+ autoload :Env, "jets/controller/rack/env"
4
+ autoload :Middleware, "jets/controller/rack/middleware"
5
+ end
@@ -0,0 +1,60 @@
1
+ module Jets::Controller::Rack
2
+ class Adapter
3
+ extend Memoist
4
+
5
+ # Returns back API Gateway response hash structure
6
+ def self.process(event, context, meth)
7
+ adapter = new(event, context, meth)
8
+ adapter.process
9
+ end
10
+
11
+ def initialize(event, context, meth)
12
+ @event, @context, @meth = event, context, meth
13
+ end
14
+
15
+ # 1. Convert API Gateway event event to Rack env
16
+ # 2. Process using full Rack middleware stack
17
+ # 3. Convert back to API gateway response structure payload
18
+ def process
19
+ status, headers, body = Jets.application.call(env)
20
+ convert_to_api_gateway(status, headers, body)
21
+ end
22
+
23
+ def env
24
+ Env.new(@event, @context).convert # convert to Rack env
25
+ end
26
+ memoize :env
27
+
28
+ # Transform the structure to AWS_PROXY compatiable structure
29
+ # http://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
30
+ def convert_to_api_gateway(status, headers, body)
31
+ base64 = headers["x-jets-base64"] == 'true'
32
+ body = body.respond_to?(:read) ? body.read : body
33
+ {
34
+ "statusCode" => status,
35
+ "headers" => headers,
36
+ "body" => body,
37
+ "isBase64Encoded" => base64,
38
+ }
39
+ end
40
+
41
+ # Called from Jets::Controller::Base.process. Example:
42
+ #
43
+ # adapter.rack_vars(
44
+ # 'jets.controller' => self,
45
+ # 'lambda.context' => context,
46
+ # 'lambda.event' => event,
47
+ # 'lambda.meth' => meth,
48
+ # )
49
+ #
50
+ # Passes a these special variables so we have access to them in the middleware.
51
+ # The controller instance is called in the Main middleware.
52
+ # The lambda.* info is used by the Rack::Local middleware to create a mimicked
53
+ # controller for the local server.
54
+ #
55
+ def rack_vars(vars)
56
+ env.merge!(vars)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ require 'rack'
2
+
3
+ # Takes an ApiGateway event and converts it to an Rack env that can be used for
4
+ # rack.call(env).
5
+ module Jets::Controller::Rack
6
+ class Env
7
+ def initialize(event, context)
8
+ @event, @context = event, context
9
+ end
10
+
11
+ def convert
12
+ options = {}
13
+ options = add_top_level(options)
14
+ options = add_http_headers(options)
15
+ path = @event['path'] || '/' # always set by API Gateway but might not be when testing shim, so setting it to make testing easier
16
+ Rack::MockRequest.env_for(path, options)
17
+ end
18
+
19
+ private
20
+ def add_top_level(options)
21
+ map = {
22
+ 'CONTENT_TYPE' => content_type,
23
+ 'QUERY_STRING' => query_string,
24
+ 'REMOTE_ADDR' => headers['X-Forwarded-For'],
25
+ 'REMOTE_HOST' => headers['Host'],
26
+ 'REQUEST_METHOD' => @event['httpMethod'],
27
+ 'REQUEST_PATH' => @event['path'],
28
+ 'REQUEST_URI' => request_uri,
29
+ 'SCRIPT_NAME' => "",
30
+ 'SERVER_NAME' => headers['Host'],
31
+ 'SERVER_PORT' => headers['X-Forwarded-Port'],
32
+ 'SERVER_PROTOCOL' => "HTTP/1.1", # unsure if this should be set
33
+ 'SERVER_SOFTWARE' => "WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)",
34
+ }
35
+
36
+ map['CONTENT_LENGTH'] = content_length if content_length
37
+ # Even if not set, Rack always assigns an StringIO to "rack.input"
38
+ map[:input] = StringIO.new(body) if body
39
+
40
+ # TODO: handle decoding base64 encoded body from API Gateaway
41
+ # Will need to make sure that pass the base64 info via a request header
42
+
43
+ options.merge(map)
44
+ end
45
+
46
+ def content_type
47
+ headers['Content-Type'] || Jets::Controller::DEFAULT_CONTENT_TYPE
48
+ end
49
+
50
+ def content_length
51
+ bytesize = body.bytesize.to_s if body
52
+ headers['Content-Length'] || bytesize
53
+ end
54
+
55
+ def body
56
+ @event['body']
57
+ end
58
+
59
+ def add_http_headers(options)
60
+ headers.each do |k,v|
61
+ # content-type => HTTP_CONTENT_TYPE
62
+ key = k.gsub('-','_').upcase
63
+ key = "HTTP_#{key}"
64
+ options[key] = v
65
+ end
66
+ options
67
+ end
68
+
69
+ def request_uri
70
+ # IE: "http://localhost:8888/posts/tung/edit?foo=bar"
71
+ proto = headers['X-Forwarded-Proto']
72
+ host = headers['Host']
73
+ port = headers['X-Forwarded-Port']
74
+
75
+ # Add port if needed
76
+ if proto == 'https' && port != '443' or
77
+ proto == 'http' && port != '80'
78
+ host = "#{host}:#{port}"
79
+ end
80
+
81
+ path = @event['path']
82
+ qs = "?#{query_string}" unless query_string.empty?
83
+ "#{proto}://#{host}#{path}#{qs}"
84
+ end
85
+
86
+ def query_string
87
+ qs_params = @event["queryStringParameters"] || {} # always set with API Gateway but when testing node shim might not be
88
+ hash = Jets::Mega::HashConverter.encode(qs_params)
89
+ hash.to_query
90
+ end
91
+
92
+ def headers
93
+ @event['headers'] || {}
94
+ end
95
+ end
96
+ end