rack-shelf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b938a108e6359907bc7bec04a69ddae9e81207a8c5c7aded0d189359ef9a29ec
4
+ data.tar.gz: 454922f5c0b22591dafccf73850b29aaef98909e46b4ad749298f7481d26e437
5
+ SHA512:
6
+ metadata.gz: 63d4c509ea364e4ae94923f939eb051f107abee03145714d0a3cfad11512d18321d7b465763df0c27926f17db2480f1291574edd54a9f53c0faedb15334e1f49
7
+ data.tar.gz: 8e5aa94d9a2f057e25547b1652d4aba7043fa4684b3d034aa66cde63ca324c991a9ca19425943b176eb5b7ef882eb95128dfba9fb6caa07afa0c4943196595ca
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,37 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-shelf (0.0.1)
5
+ rack
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ jaro_winkler (1.5.4)
12
+ parallel (1.19.1)
13
+ parser (2.7.0.4)
14
+ ast (~> 2.4.0)
15
+ rack (2.1.1)
16
+ rainbow (3.0.0)
17
+ rexml (3.2.4)
18
+ rubocop (0.80.1)
19
+ jaro_winkler (~> 1.5.1)
20
+ parallel (~> 1.10)
21
+ parser (>= 2.7.0.1)
22
+ rainbow (>= 2.2.2, < 4.0)
23
+ rexml
24
+ ruby-progressbar (~> 1.7)
25
+ unicode-display_width (>= 1.4.0, < 1.7)
26
+ ruby-progressbar (1.10.1)
27
+ unicode-display_width (1.6.1)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ rack-shelf!
34
+ rubocop (~> 0.80.1)
35
+
36
+ BUNDLED WITH
37
+ 2.0.2
@@ -0,0 +1,21 @@
1
+ # MIT LICENSE
2
+
3
+ Copyright (c) 2020 Michael Miller <icy.arctic.fox@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ Rack Shelf
2
+ ==========
3
+
4
+ Easily integrate a Rack application into AWS Lambda.
5
+
6
+ Provides adapters to convert AWS Lambda events to Rack environment requests
7
+ and Rack responses to AWS Lambda proxy integrations.
8
+
9
+ Shelf currently works with API Gateway.
10
+
11
+ Installation
12
+ ------------
13
+
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem 'rack-shelf'
18
+ ```
19
+
20
+ or gemspec:
21
+
22
+ ```ruby
23
+ s.add_dependency 'rack-shelf'
24
+ ```
25
+
26
+ Usage
27
+ -----
28
+
29
+ In your AWS Lambda handler file:
30
+
31
+ ```ruby
32
+ require 'rack/shelf'
33
+
34
+ class App
35
+ def call
36
+ [200, {}, 'Hello world!']
37
+ end
38
+ end
39
+
40
+ def handler(event:, context:)
41
+ Rack::Shelf.api_gateway(App.new, event, context)
42
+ end
43
+ ```
44
+
45
+ ### Detailed Explanation
46
+
47
+ The `#api_gateway` method indicates the Lambda is expected to be invoked by API Gateway.
48
+ The method accepts three arguments: the Rack application, the Lambda event, and the Lambda context.
49
+ Shelf will translate the Lambda information to a Rack environment instance and invoke `#call` on the application.
50
+ It then takes the return value of `#call` (the three-element array) and translates it to a hash.
51
+ That hash contains the expected keys and values for an AWS Lambda proxy integration.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shelf/api_gateway'
4
+ require_relative 'shelf/base64_response_adapter'
5
+ require_relative 'shelf/environment_builder'
6
+ require_relative 'shelf/response_adapter'
7
+ require_relative 'shelf/version'
8
+
9
+ module Rack
10
+ # Adapts AWS Lambda event sources to Rack environments.
11
+ module Shelf
12
+ extend self
13
+
14
+ # Runs a Rack application, translating the request and response.
15
+ # This method assumes the Lambda event came from API Gateway.
16
+ # @param app [#call] Rack application to call.
17
+ # @param event [Hash] Lambda event hash.
18
+ # @param context [Object] Lambda context object.
19
+ # @return [Hash] AWS Lambda response.
20
+ def api_gateway(app, event, context)
21
+ run(APIGateway, ResponseAdapter, app, event, context)
22
+ end
23
+
24
+ # Runs a Rack application, translating the request and response.
25
+ # This method assumes the Lambda event came from API Gateway.
26
+ # The response body is encoded as base-64.
27
+ # @param app [#call] Rack application to call.
28
+ # @param event [Hash] Lambda event hash.
29
+ # @param context [Object] Lambda context object.
30
+ # @return [Hash] AWS Lambda response.
31
+ def api_gateway_base64(app, event, context)
32
+ run(APIGateway, Base64ResponseAdapter, app, event, context)
33
+ end
34
+
35
+ private
36
+
37
+ # Runs the app and translates the request and response.
38
+ # @param request_adapter [#env] Translates the Lambda event.
39
+ # @param response_adapter [#convert, #error] Translates the Rack response.
40
+ # @param app [#call] Rack application to call.
41
+ # @param event [Hash] Lambda event hash.
42
+ # @param context [Object] Lambda context object.
43
+ # @return [Hash] AWS Lambda response.
44
+ def run(request_adapter, response_adapter, app, event, context)
45
+ env = request_adapter.env(event, context)
46
+ response = app.call(env)
47
+ response_adapter.convert(response)
48
+ rescue StandardError => e
49
+ response_adapter.error(e)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'environment_builder'
4
+
5
+ module Rack
6
+ module Shelf
7
+ # Converts AWS API Gateway events given to Lambda
8
+ # to Rack environment instances.
9
+ class APIGateway
10
+ # Produces a Rack compatible environment instance
11
+ # from a Lambda event and context.
12
+ # @param event [Hash] Event from the Lambda handler.
13
+ # @param context [Object] Context from the Lambda handler.
14
+ # @return [Hash] Rack environment.
15
+ def self.env(event, context)
16
+ new(event, context).build
17
+ end
18
+
19
+ # Creates an instance dedicated to building a Rack environment
20
+ # for a single event and context.
21
+ # @param event [Hash] Event from the Lambda handler.
22
+ # @param context [Object] Context from the Lambda handler.
23
+ def initialize(event, context)
24
+ @event = event
25
+ @context = context
26
+ @builder = EnvironmentBuilder.new
27
+ end
28
+
29
+ # Builds the rack environment.
30
+ # @return [Hash] Rack environment.
31
+ def build
32
+ build_common
33
+ build_headers
34
+ build_body
35
+
36
+ builder.build
37
+ end
38
+
39
+ private
40
+
41
+ # Rack environment builder instance.
42
+ # @return [EnvironmentBuilder]
43
+ attr_reader :builder
44
+
45
+ # Event from the Lambda handler.
46
+ # @return [Hash]
47
+ attr_reader :event
48
+
49
+ # Context from the Lambda handler.
50
+ # @return [Object]
51
+ attr_reader :context
52
+
53
+ # Retrieves the HTTP headers from the Lambda event.
54
+ # @return [Hash] HTTP headers.
55
+ def headers
56
+ event['headers'] || {}
57
+ end
58
+
59
+ # Retrieves the HTTP request method from the Lambda event.
60
+ # @return [String] HTTP request method.
61
+ def request_method
62
+ event.fetch('httpMethod')
63
+ end
64
+
65
+ # Retrieves the path information, or a default value.
66
+ # @return [String]
67
+ def path_info
68
+ event['path'] || '/'
69
+ end
70
+
71
+ # Retrieves the query parameters from the Lambda event.
72
+ # @return [Hash]
73
+ def query_params
74
+ event['queryStringParameters'] || {}
75
+ end
76
+
77
+ # Retrieves the server hostname, or a default value.
78
+ # @return [String]
79
+ def server_name
80
+ event['Host'] || 'localhost'
81
+ end
82
+
83
+ # Retrieves the server port, or a default value.
84
+ # @return [Integer]
85
+ def server_port
86
+ event['X-Forwarded-Port'] || 80
87
+ end
88
+
89
+ # Retrieves the URL scheme, or a default value.
90
+ # @return [String]
91
+ def url_scheme
92
+ headers.fetch('CloudFront-Forwarded-Proto') do
93
+ headers.fetch('X-Forwarded-Proto', 'http')
94
+ end
95
+ end
96
+
97
+ # Configures common Rack environment attributes.
98
+ # @return [void]
99
+ def build_common # rubocop:disable Metrics/AbcSize
100
+ builder.request_method(request_method)
101
+ builder.path_info(path_info)
102
+ builder.query_params(query_params)
103
+ builder.server_name(server_name)
104
+ builder.server_port(server_port)
105
+ builder.url_scheme(url_scheme)
106
+ end
107
+
108
+ # Configures the HTTP request headers.
109
+ # @return [void]
110
+ def build_headers
111
+ headers.each do |key, value|
112
+ builder.header(key, value)
113
+ end
114
+ end
115
+
116
+ # Configures the client request body portion.
117
+ # @return [void]
118
+ def build_body
119
+ return unless (body = event['body'])
120
+
121
+ if event['isBase64Encoded']
122
+ builder.base64_body(body)
123
+ else
124
+ builder.body(body)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require_relative 'response_adapter'
5
+
6
+ module Rack
7
+ module Shelf
8
+ # Transforms a standard Rack response array
9
+ # to a return value required by AWS Lambda.
10
+ # Encodes the response body as base-64.
11
+ # This is typically used for sending binary data (not text).
12
+ class Base64ResponseAdapter < ResponseAdapter
13
+ # Constructs the AWS Lambda response.
14
+ # @return [Hash]
15
+ def build
16
+ super.merge('isBase64Encoded' => true)
17
+ end
18
+
19
+ private
20
+
21
+ # Constructs the response body encoded in base-64.
22
+ # @return [String]
23
+ def body
24
+ Base64.encode64(super)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'rack'
5
+ require 'stringio'
6
+
7
+ module Rack
8
+ module Shelf
9
+ # Builds up a Rack env hash.
10
+ class EnvironmentBuilder
11
+ # Default values for the Rack environment.
12
+ DEFAULTS = {
13
+ 'REQUEST_METHOD' => 'GET',
14
+ 'SCRIPT_NAME' => '',
15
+ 'PATH_INFO' => '/',
16
+ 'QUERY_STRING' => '',
17
+ 'SERVER_NAME' => 'localhost',
18
+ 'SERVER_PORT' => '80',
19
+ 'rack.version' => Rack::VERSION,
20
+ 'rack.url_scheme' => 'http',
21
+ 'rack.input' => StringIO.new,
22
+ 'rack.errors' => $stderr,
23
+ 'rack.multithread' => false,
24
+ 'rack.multiprocess' => false,
25
+ 'rack.run_once' => false,
26
+ 'rack.hijack?' => false
27
+ }.freeze
28
+
29
+ # Creates the builder with reasonable defaults.
30
+ def initialize
31
+ @env = DEFAULTS.clone(freeze: false)
32
+ end
33
+
34
+ # Specifies the request method.
35
+ # @param method [String] Must be one of:
36
+ # +GET+, +POST+, +PUT+, +HEAD+, +DELETE+, +PATCH+, or +OPTIONS+.
37
+ # @return [void]
38
+ def request_method(method)
39
+ @env['REQUEST_METHOD'] = method
40
+ end
41
+
42
+ # Specifies the name (or mounting point) of the application.
43
+ # @param name [String] Script name.
44
+ # @return [void]
45
+ def script_name(name)
46
+ @env['SCRIPT_NAME'] = name
47
+ end
48
+
49
+ # Specifies the path info (path after the mounting point).
50
+ # @param path [String] Path info.
51
+ # @return [void]
52
+ def path_info(path)
53
+ @env['PATH_INFO'] = path
54
+ end
55
+
56
+ # Specifies the entire query string.
57
+ # @param string [String] Pre-encoded query string.
58
+ # @return [void]
59
+ def query_string(string)
60
+ @env['QUERY_STRING'] = string
61
+ end
62
+
63
+ # Specifies the query parameters as a hash.
64
+ # @param params [Hash] Query parameters.
65
+ # @return [void]
66
+ def query_params(params)
67
+ string = Rack::Utils.build_query(params)
68
+ query_string(string)
69
+ end
70
+
71
+ # Specifies the hostname of the server.
72
+ # @param name [String] Server name.
73
+ # @return [void]
74
+ def server_name(name)
75
+ @env['SERVER_NAME'] = name
76
+ end
77
+
78
+ # Specifies the port the server is listening on.
79
+ # @param port [#to_s] Port number.
80
+ # @return [void]
81
+ def server_port(port)
82
+ @env['SERVER_PORT'] = port.to_s
83
+ end
84
+
85
+ # Specifies the URL scheme for the request.
86
+ # @param scheme [String] Must be: +http+ or +https+.
87
+ # @return [void]
88
+ def url_scheme(scheme)
89
+ @env['rack.url_scheme'] = scheme
90
+ end
91
+
92
+ # Specifies the stream used for input (request body).
93
+ # @param io [IO] Input stream.
94
+ # @return [void]
95
+ def input_stream(io)
96
+ @env['rack.input'] = io
97
+ end
98
+
99
+ # Specifies the client request body.
100
+ # @param content [String] Request body.
101
+ # @return [void]
102
+ def body(content)
103
+ io = StringIO.new(content)
104
+ input_stream(io)
105
+ end
106
+
107
+ # Specifies the client request body encoded in base-64.
108
+ # @param content [String] Base-64 encoded request body.
109
+ # @return [void]
110
+ def base64_body(content)
111
+ decoded = Base64.decode64(content)
112
+ body(decoded)
113
+ end
114
+
115
+ # Specifies the stream used to display errors.
116
+ # @param io [IO] Error stream.
117
+ # @return [void]
118
+ def error_stream(io)
119
+ @env['rack.errors'] = io
120
+ end
121
+
122
+ # Defines an HTTP header in the request.
123
+ # @param header [String] HTTP header name.
124
+ # @param value [String] Value of the HTTP header.
125
+ # @return [void]
126
+ def header(header, value)
127
+ name = header.upcase.gsub('-', '_')
128
+ key = case name
129
+ when 'CONTENT_TYPE', 'CONTENT_LENGTH'
130
+ name
131
+ else
132
+ 'HTTP_' + name
133
+ end
134
+
135
+ @env[key] = value
136
+ end
137
+
138
+ # Defines a custom application value.
139
+ # @param prefix [String] Prefix of the key.
140
+ # @param name [String] Name of the application key.
141
+ # @param value [Object] Value of the key.
142
+ # @return [void]
143
+ # @raise [ArgumentError] The prefix can't be +rack+.
144
+ def application(prefix, name, value)
145
+ raise ArgumentError, "Prefix can't be 'rack'" if prefix == 'rack'
146
+
147
+ key = [prefix, name].join('.')
148
+ @env[key] = value
149
+ end
150
+
151
+ # Creates the Rack env hash.
152
+ # @return [Hash]
153
+ def build
154
+ @env.clone
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module Shelf
5
+ # Transforms a standard Rack response array
6
+ # to a return value required by AWS Lambda.
7
+ # @see https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
8
+ class ResponseAdapter
9
+ # Converts a Rack response to one supported by AWS Lambda.
10
+ # @param response [Array] Three-element array.
11
+ # This is the standard response from a Rack application.
12
+ # Must have the elements: status code, headers, and body.
13
+ # @return [Hash] AWS Lambda response.
14
+ # @see https://www.rubydoc.info/github/rack/rack/file/SPEC#label-The+Response
15
+ def self.convert(response)
16
+ new(*response).build
17
+ end
18
+
19
+ # Generates a Lambda response for an error.
20
+ # @param exception [Exception, #to_s] Caught exception.
21
+ # @param status_code [Integer] HTTP response code.
22
+ # @return [Hash] AWS Lambda response.
23
+ def self.error(exception, status_code = 500)
24
+ new(status_code, {}, exception.to_s).build
25
+ end
26
+
27
+ # Creates an adapter dedicated to processing one response.
28
+ # @param status_code [#to_i] HTTP status code.
29
+ # @param headers [#each] HTTP headers.
30
+ # @param body [#each] Response body.
31
+ def initialize(status_code, headers, body)
32
+ @status_code = status_code
33
+ @headers = headers
34
+ @body = body
35
+ end
36
+
37
+ # Constructs the AWS Lambda response.
38
+ # @return [Hash]
39
+ def build
40
+ {
41
+ 'status_code' => status_code,
42
+ 'headers' => headers,
43
+ 'body' => body,
44
+ 'isBase64Encoded' => false
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ # The integer HTTP status code.
51
+ # @return [Integer]
52
+ def status_code
53
+ @status_code.to_i
54
+ end
55
+
56
+ # Constructs the HTTP headers.
57
+ # @return [Hash]
58
+ def headers
59
+ # Typically, the headers are already a hash.
60
+ # But, the Rack Spec only requires the object to expose `#each`.
61
+ {}.tap do |hash|
62
+ @headers.each do |key, value|
63
+ hash[key] = value
64
+ end
65
+ end
66
+ end
67
+
68
+ # Constructs the response body.
69
+ # @return [String]
70
+ def body
71
+ StringIO.new.tap do |io|
72
+ @body.each do |part|
73
+ io.write(part)
74
+ end
75
+ @body.close if @body.respond_to?(:close)
76
+ end.string
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module Shelf
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-shelf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael Miller
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.80.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.80.1
41
+ description: Provides a translation for AWS Lambda events to Rack environments.
42
+ email:
43
+ - icy.arctic.fox@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - Gemfile.lock
50
+ - LICENSE.md
51
+ - README.md
52
+ - lib/rack/shelf.rb
53
+ - lib/rack/shelf/api_gateway.rb
54
+ - lib/rack/shelf/base64_response_adapter.rb
55
+ - lib/rack/shelf/environment_builder.rb
56
+ - lib/rack/shelf/response_adapter.rb
57
+ - lib/rack/shelf/version.rb
58
+ homepage: https://gitlab.com/arctic-fox/rack-shelf
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.0.3
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Adapts AWS Lambda event sources to Rack environments.
81
+ test_files: []