functions_framework 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "logger"
16
+
17
+ require "functions_framework/cloud_events"
18
+ require "functions_framework/function"
19
+ require "functions_framework/registry"
20
+ require "functions_framework/version"
21
+
22
+ ##
23
+ # The Functions Framework for Ruby.
24
+ #
25
+ # Functions Framework is an open source framework for writing lightweight,
26
+ # portable Ruby functions that run in a serverless environment. For general
27
+ # information about the Functions Framework, see
28
+ # https://github.com/GoogleCloudPlatform/functions-framework.
29
+ # To get started with the functions framework for Ruby, see
30
+ # https://github.com/GoogleCloudPlatform/functions-framework-ruby for basic
31
+ # examples.
32
+ #
33
+ # ## Inside the FunctionsFramework module
34
+ #
35
+ # The FunctionsFramework module includes the main entry points for the
36
+ # functions framework. Use the {FunctionsFramework.http},
37
+ # {FunctionsFramework.event}, or {FunctionsFramework.cloud_event} methods to
38
+ # define functions. To serve functions via a web service, invoke the
39
+ # `functions-framework` executable, or use the {FunctionsFramework.start} or
40
+ # {FunctionsFramework.run} methods.
41
+ #
42
+ # ## Internal modules
43
+ #
44
+ # Here is a roadmap to the internal modules in the Ruby functions framework.
45
+ #
46
+ # * {FunctionsFramework::CloudEvents} provides an implementation of the
47
+ # [CloudEvents](https://cloudevents.io) specification. In particular, if
48
+ # you define an event function, you will receive the event as a
49
+ # {FunctionsFramework::CloudEvents::Event} object.
50
+ # * {FunctionsFramework::CLI} is the implementation of the
51
+ # `functions-framework` executable. Most apps will not need to interact
52
+ # with this class directly.
53
+ # * {FunctionsFramework::Function} is the internal representation of a
54
+ # function, indicating the type of function (http or cloud event), the
55
+ # name of the function, and the block of code implementing it. Most apps
56
+ # do not need to interact with this class directly.
57
+ # * {FunctionsFramework::Registry} looks up functions by name. When you
58
+ # define a set of named functions, they are added to a registry, and when
59
+ # you start a server and specify the target function by name, it is looked
60
+ # up from the registry. Most apps do not need to interact with this class
61
+ # directly.
62
+ # * {FunctionsFramework::Server} is a web server that makes a function
63
+ # available via HTTP. It wraps the Puma web server and runs a specific
64
+ # {FunctionsFramework::Function}. Many apps can simply run the
65
+ # `functions-framework` executable to spin up a server. However, if you
66
+ # need closer control over your execution environment, you can use the
67
+ # {FunctionsFramework::Server} class to run a server. Note that, in most
68
+ # cases, it is easier to use the {FunctionsFramework.start} or
69
+ # {FunctionsFramework.run} wrapper methods rather than instantiate a
70
+ # {FunctionsFramework::Server} class directly.
71
+ # * {FunctionsFramework::Testing} provides helpers that are useful when
72
+ # writing unit tests for functions.
73
+ #
74
+ module FunctionsFramework
75
+ @global_registry = Registry.new
76
+ @logger = ::Logger.new ::STDERR
77
+ @logger.level = ::Logger::INFO
78
+
79
+ ##
80
+ # The default target function name. If you define a function without
81
+ # specifying a name, or run the framework without giving a target, this name
82
+ # is used.
83
+ #
84
+ # @return [String]
85
+ #
86
+ DEFAULT_TARGET = "function".freeze
87
+
88
+ ##
89
+ # The default source file path. The CLI loads functions from this file if no
90
+ # source file is given explicitly.
91
+ #
92
+ # @return [String]
93
+ #
94
+ DEFAULT_SOURCE = "./app.rb".freeze
95
+
96
+ class << self
97
+ ##
98
+ # The "global" registry that holds events defined by the
99
+ # {FunctionsFramework} class methods.
100
+ #
101
+ # @return [FunctionsFramework::Registry]
102
+ #
103
+ attr_accessor :global_registry
104
+
105
+ ##
106
+ # A "global" logger that is used by the framework's web server, and can
107
+ # also be used by functions.
108
+ #
109
+ # @return [Logger]
110
+ #
111
+ attr_accessor :logger
112
+
113
+ ##
114
+ # Define a function that response to HTTP requests.
115
+ #
116
+ # You must provide a name for the function, and a block that implemets the
117
+ # function. The block should take a single `Rack::Request` argument. It
118
+ # should return one of the following:
119
+ # * A standard 3-element Rack response array. See
120
+ # https://github.com/rack/rack/blob/master/SPEC
121
+ # * A `Rack::Response` object.
122
+ # * A simple String that will be sent as the response body.
123
+ # * A Hash object that will be encoded as JSON and sent as the response
124
+ # body.
125
+ #
126
+ # ## Example
127
+ #
128
+ # FunctionsFramework.http "my-function" do |request|
129
+ # "I received a request for #{request.url}"
130
+ # end
131
+ #
132
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
133
+ # @param block [Proc] The function code as a proc.
134
+ # @return [self]
135
+ #
136
+ def http name = DEFAULT_TARGET, &block
137
+ global_registry.add_http name, &block
138
+ self
139
+ end
140
+
141
+ ##
142
+ # Define a function that responds to CloudEvents.
143
+ #
144
+ # You must provide a name for the function, and a block that implemets the
145
+ # function. The block should take two arguments: the event _data_ and the
146
+ # event _context_. Any return value is ignored.
147
+ #
148
+ # The event data argument will be one of the following types:
149
+ # * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
150
+ # binary data. You may choose to perform additional interpretation of
151
+ # the binary data using information in the content type provided by the
152
+ # context argument.
153
+ # * Any data type that can be represented in JSON (i.e. `String`,
154
+ # `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
155
+ # came with a JSON payload. The content type may also be set in the
156
+ # context if the data is a String.
157
+ #
158
+ # The context argument will be of type {FunctionsFramework::CloudEvents::Event},
159
+ # and will contain CloudEvents context attributes such as `id` and `type`.
160
+ #
161
+ # See also {FunctionsFramework.cloud_event} which defines a function that
162
+ # takes a single argument of type {FunctionsFramework::CloudEvents::Event}.
163
+ #
164
+ # ## Example
165
+ #
166
+ # FunctionsFramework.event "my-function" do |data, context|
167
+ # FunctionsFramework.logger.info "Event data: #{data.inspect}"
168
+ # end
169
+ #
170
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
171
+ # @param block [Proc] The function code as a proc.
172
+ # @return [self]
173
+ #
174
+ def event name = DEFAULT_TARGET, &block
175
+ global_registry.add_event name, &block
176
+ self
177
+ end
178
+
179
+ ##
180
+ # Define a function that responds to CloudEvents.
181
+ #
182
+ # You must provide a name for the function, and a block that implemets the
183
+ # function. The block should take _one_ argument: the event object of type
184
+ # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
185
+ #
186
+ # See also {FunctionsFramework.event} which creates a function that takes
187
+ # data and context as separate arguments.
188
+ #
189
+ # ## Example
190
+ #
191
+ # FunctionsFramework.cloud_event "my-function" do |event|
192
+ # FunctionsFramework.logger.info "Event data: #{event.data.inspect}"
193
+ # end
194
+ #
195
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
196
+ # @param block [Proc] The function code as a proc.
197
+ # @return [self]
198
+ #
199
+ def cloud_event name = DEFAULT_TARGET, &block
200
+ global_registry.add_cloud_event name, &block
201
+ self
202
+ end
203
+
204
+ ##
205
+ # Start the functions framework server in the background. The server will
206
+ # look up the given target function name in the global registry.
207
+ #
208
+ # @param target [String] The name of the function to run
209
+ # @yield [FunctionsFramework::Server::Config] A config object that can be
210
+ # manipulated to configure the server.
211
+ # @return [FunctionsFramework::Server]
212
+ #
213
+ def start target, &block
214
+ require "functions_framework/server"
215
+ function = global_registry[target]
216
+ raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
217
+ server = Server.new function, &block
218
+ server.respond_to_signals
219
+ server.start
220
+ end
221
+
222
+ ##
223
+ # Run the functions framework server and block until it stops. The server
224
+ # will look up the given target function name in the global registry.
225
+ #
226
+ # @param target [String] The name of the function to run
227
+ # @yield [FunctionsFramework::Server::Config] A config object that can be
228
+ # manipulated to configure the server.
229
+ # @return [self]
230
+ #
231
+ def run target, &block
232
+ server = start target, &block
233
+ server.wait_until_stopped
234
+ self
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,113 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "optparse"
16
+
17
+ require "functions_framework"
18
+
19
+ module FunctionsFramework
20
+ ##
21
+ # Implementation of the functions-framework executable.
22
+ #
23
+ class CLI
24
+ ##
25
+ # Create a new CLI, setting arguments to their defaults.
26
+ #
27
+ def initialize
28
+ @target = ::ENV["FUNCTION_TARGET"] || ::FunctionsFramework::DEFAULT_TARGET
29
+ @source = ::ENV["FUNCTION_SOURCE"] || ::FunctionsFramework::DEFAULT_SOURCE
30
+ @env = nil
31
+ @port = nil
32
+ @bind = nil
33
+ @min_threads = nil
34
+ @max_threads = nil
35
+ @detailed_errors = nil
36
+ end
37
+
38
+ ##
39
+ # Parse the given command line arguments.
40
+ # Exits if argument parsing failed.
41
+ #
42
+ # @param argv [Array<String>]
43
+ # @return [self]
44
+ #
45
+ def parse_args argv # rubocop:disable Metrics/MethodLength
46
+ option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
47
+ op.on "-t", "--target TARGET",
48
+ "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
49
+ @target = val
50
+ end
51
+ op.on "-s", "--source SOURCE",
52
+ "Set the source file to load (defaults to #{DEFAULT_SOURCE})" do |val|
53
+ @source = val
54
+ end
55
+ op.on "-p", "--port PORT", "Set the port to listen to (defaults to 8080)" do |val|
56
+ @port = val.to_i
57
+ end
58
+ op.on "-b", "--bind BIND", "Set the address to bind to (defaults to 0.0.0.0)" do |val|
59
+ @bind = val
60
+ end
61
+ op.on "-e", "--environment ENV", "Set the Rack environment" do |val|
62
+ @env = val
63
+ end
64
+ op.on "--min-threads NUM", "Set the minimum threead pool size" do |val|
65
+ @min_threads = val
66
+ end
67
+ op.on "--max-threads NUM", "Set the maximum threead pool size" do |val|
68
+ @max_threads = val
69
+ end
70
+ op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
71
+ @detailed_errors = val
72
+ end
73
+ op.on "-v", "--verbose", "Increase log verbosity" do
74
+ ::FunctionsFramework.logger.level -= 1
75
+ end
76
+ op.on "-q", "--quiet", "Decrease log verbosity" do
77
+ ::FunctionsFramework.logger.level += 1
78
+ end
79
+ op.on "--help", "Display help" do
80
+ puts op
81
+ exit
82
+ end
83
+ end
84
+ option_parser.parse! argv
85
+ unless argv.empty?
86
+ warn "Unrecognized arguments: #{argv}"
87
+ puts op
88
+ exit 1
89
+ end
90
+ self
91
+ end
92
+
93
+ ##
94
+ # Run the configured server, and block until it stops.
95
+ # @return [self]
96
+ #
97
+ def run
98
+ FunctionsFramework.logger.info \
99
+ "FunctionsFramework: Loading functions from #{@source.inspect}..."
100
+ load @source
101
+ server = ::FunctionsFramework.start @target do |config|
102
+ config.rack_env = @env
103
+ config.port = @port
104
+ config.bind_addr = @bind
105
+ config.show_error_details = @detailed_errors
106
+ config.min_threads = @min_threads
107
+ config.max_threads = @max_threads
108
+ end
109
+ server.wait_until_stopped
110
+ self
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,143 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "functions_framework/cloud_events/binary_content"
16
+ require "functions_framework/cloud_events/content_type"
17
+ require "functions_framework/cloud_events/event"
18
+
19
+ module FunctionsFramework
20
+ ##
21
+ # CloudEvents implementation.
22
+ #
23
+ # This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
24
+ # [1.0 specification](https://github.com/cloudevents/spec/blob/master/spec.md).
25
+ # It provides for unmarshaling of events from Rack environment data from
26
+ # binary (i.e. header-based) format, as well as structured (body-based) and
27
+ # batch formats. A standard JSON structure parser is included. It is also
28
+ # possible to register handlers for other formats.
29
+ #
30
+ # TODO: Unmarshaling of events is implemented, but marshaling is not.
31
+ #
32
+ module CloudEvents
33
+ @structured_formats = {}
34
+ @batched_formats = {}
35
+
36
+ class << self
37
+ ##
38
+ # Register a handler for the given structured format.
39
+ # The handler object must respond to the method
40
+ # `#decode_structured_content`. See
41
+ # {FunctionsFramework::CloudEvents::JsonStructure} for an example.
42
+ #
43
+ # @param format [String] The subtype format that should be handled by
44
+ # this handler
45
+ # @param handler [#decode_structured_content] The handler object
46
+ # @return [self]
47
+ #
48
+ def register_structured_format format, handler
49
+ handlers = @structured_formats[format.to_s.strip.downcase] ||= []
50
+ handlers << handler unless handlers.include? handler
51
+ self
52
+ end
53
+
54
+ ##
55
+ # Register a handler for the given batched format.
56
+ # The handler object must respond to the method
57
+ # `#decode_batched_content`. See
58
+ # {FunctionsFramework::CloudEvents::JsonStructure} for an example.
59
+ #
60
+ # @param format [String] The subtype format that should be handled by
61
+ # this handler
62
+ # @param handler [#decode_batched_content] The handler object
63
+ # @return [self]
64
+ #
65
+ def register_batched_format format, handler
66
+ handlers = @batched_formats[format.to_s.strip.downcase] ||= []
67
+ handlers << handler unless handlers.include? handler
68
+ self
69
+ end
70
+
71
+ ##
72
+ # Decode an event from the given Rack environment hash. Following the
73
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
74
+ # the request.
75
+ #
76
+ # @param env [Hash] The Rack environment
77
+ # @return [FunctionsFramework::CloudEvents::Event] if the request
78
+ # includes a single structured or binary event
79
+ # @return [Array<FunctionsFramework::CloudEvents::Event>] if the request
80
+ # includes a batch of structured events
81
+ #
82
+ def decode_rack_env env
83
+ content_type_header = env["CONTENT_TYPE"]
84
+ raise "Missing content-type header" unless content_type_header
85
+ content_type = ContentType.new content_type_header
86
+ if content_type.media_type == "application"
87
+ case content_type.subtype_prefix
88
+ when "cloudevents"
89
+ return decode_structured_content env["rack.input"], content_type
90
+ when "cloudevents-batch"
91
+ return decode_batched_content env["rack.input"], content_type
92
+ end
93
+ end
94
+ BinaryContent.decode_rack_env env, content_type
95
+ end
96
+
97
+ ##
98
+ # Decode a single event from the given content data. This should be
99
+ # passed the request body, if the Content-Type is of the form
100
+ # `application/cloudevents+format`.
101
+ #
102
+ # @param input [IO] An IO-like object providing the content
103
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType] the
104
+ # content type
105
+ # @return [FunctionsFramework::CloudEvents::Event]
106
+ #
107
+ def decode_structured_content input, content_type
108
+ handlers = @structured_formats[content_type.subtype_format] || []
109
+ handlers.reverse_each do |handler|
110
+ event = handler.decode_structured_content input, content_type
111
+ return event if event
112
+ end
113
+ raise "Unknown cloudevents format: #{content_type.subtype_format.inspect}"
114
+ end
115
+
116
+ ##
117
+ # Decode a batch of events from the given content data. This should be
118
+ # passed the request body, if the Content-Type is of the form
119
+ # `application/cloudevents-batch+format`.
120
+ #
121
+ # @param input [IO] An IO-like object providing the content
122
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType] the
123
+ # content type
124
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
125
+ #
126
+ def decode_batched_content input, content_type
127
+ handlers = @batched_formats[content_type.subtype_format] || []
128
+ handlers.reverse_each do |handler|
129
+ events = handler.decode_batched_content input, content_type
130
+ return events if events
131
+ end
132
+ raise "Unknown cloudevents batch format: #{content_type.subtype_format.inspect}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ require "functions_framework/cloud_events/json_structure"
139
+
140
+ FunctionsFramework::CloudEvents.register_structured_format \
141
+ "json", FunctionsFramework::CloudEvents::JsonStructure
142
+ FunctionsFramework::CloudEvents.register_batched_format \
143
+ "json", FunctionsFramework::CloudEvents::JsonStructure