rack-shelf 0.0.1

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.
@@ -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: []