functions_framework 0.1.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,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2020 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "functions_framework/cli"
18
+
19
+ ::FunctionsFramework::CLI.new.parse_args(::ARGV).run
@@ -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