functions_framework 0.4.1 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +11 -11
- data/bin/functions-framework +4 -1
- data/bin/functions-framework-ruby +1 -1
- data/docs/deploying-functions.md +22 -13
- data/docs/overview.md +6 -6
- data/docs/testing-functions.md +59 -11
- data/docs/writing-functions.md +202 -13
- data/lib/functions_framework.rb +41 -10
- data/lib/functions_framework/cli.rb +97 -22
- data/lib/functions_framework/function.rb +142 -47
- data/lib/functions_framework/legacy_event_converter.rb +10 -11
- data/lib/functions_framework/registry.rb +36 -12
- data/lib/functions_framework/server.rb +27 -22
- data/lib/functions_framework/testing.rb +123 -24
- data/lib/functions_framework/version.rb +1 -1
- metadata +24 -16
- data/lib/functions_framework/cloud_events.rb +0 -45
- data/lib/functions_framework/cloud_events/content_type.rb +0 -222
- data/lib/functions_framework/cloud_events/errors.rb +0 -42
- data/lib/functions_framework/cloud_events/event.rb +0 -84
- data/lib/functions_framework/cloud_events/event/field_interpreter.rb +0 -150
- data/lib/functions_framework/cloud_events/event/v0.rb +0 -236
- data/lib/functions_framework/cloud_events/event/v1.rb +0 -223
- data/lib/functions_framework/cloud_events/http_binding.rb +0 -310
- data/lib/functions_framework/cloud_events/json_format.rb +0 -173
@@ -23,12 +23,11 @@ module FunctionsFramework
|
|
23
23
|
# Decode an event from the given Rack environment hash.
|
24
24
|
#
|
25
25
|
# @param env [Hash] The Rack environment
|
26
|
-
# @return [
|
27
|
-
# be converted
|
26
|
+
# @return [::CloudEvents::Event] if the request could be converted
|
28
27
|
# @return [nil] if the event format was not recognized.
|
29
28
|
#
|
30
29
|
def decode_rack_env env
|
31
|
-
content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
30
|
+
content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
|
32
31
|
return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
|
33
32
|
input = read_input_json env["rack.input"], content_type.charset
|
34
33
|
return nil unless input
|
@@ -85,14 +84,14 @@ module FunctionsFramework
|
|
85
84
|
return nil unless type && source
|
86
85
|
ce_data = convert_data context[:service], data
|
87
86
|
content_type = "application/json; charset=#{charset}"
|
88
|
-
CloudEvents::Event.new id: context[:id],
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
87
|
+
::CloudEvents::Event.new id: context[:id],
|
88
|
+
source: source,
|
89
|
+
type: type,
|
90
|
+
spec_version: "1.0",
|
91
|
+
data_content_type: content_type,
|
92
|
+
data: ce_data,
|
93
|
+
subject: subject,
|
94
|
+
time: context[:timestamp]
|
96
95
|
end
|
97
96
|
|
98
97
|
def convert_source service, resource
|
@@ -12,21 +12,18 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
require "monitor"
|
16
|
-
|
17
15
|
module FunctionsFramework
|
18
16
|
##
|
19
17
|
# Registry providing lookup of functions by name.
|
20
18
|
#
|
21
19
|
class Registry
|
22
|
-
include ::MonitorMixin
|
23
|
-
|
24
20
|
##
|
25
21
|
# Create a new empty registry.
|
26
22
|
#
|
27
23
|
def initialize
|
28
|
-
|
24
|
+
@mutex = ::Mutex.new
|
29
25
|
@functions = {}
|
26
|
+
@start_tasks = []
|
30
27
|
end
|
31
28
|
|
32
29
|
##
|
@@ -37,7 +34,7 @@ module FunctionsFramework
|
|
37
34
|
# @return [nil] if the function is not found
|
38
35
|
#
|
39
36
|
def [] name
|
40
|
-
@functions[name.to_s]
|
37
|
+
@mutex.synchronize { @functions[name.to_s] }
|
41
38
|
end
|
42
39
|
|
43
40
|
##
|
@@ -46,7 +43,16 @@ module FunctionsFramework
|
|
46
43
|
# @return [Array<String>]
|
47
44
|
#
|
48
45
|
def names
|
49
|
-
@functions.keys.sort
|
46
|
+
@mutex.synchronize { @functions.keys.sort }
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Return an array of startup tasks.
|
51
|
+
#
|
52
|
+
# @return [Array<FunctionsFramework::Function>]
|
53
|
+
#
|
54
|
+
def startup_tasks
|
55
|
+
@mutex.synchronize { @start_tasks.dup }
|
50
56
|
end
|
51
57
|
|
52
58
|
##
|
@@ -68,9 +74,9 @@ module FunctionsFramework
|
|
68
74
|
#
|
69
75
|
def add_http name, &block
|
70
76
|
name = name.to_s
|
71
|
-
synchronize do
|
77
|
+
@mutex.synchronize do
|
72
78
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
73
|
-
@functions[name] = Function.
|
79
|
+
@functions[name] = Function.http name, &block
|
74
80
|
end
|
75
81
|
self
|
76
82
|
end
|
@@ -80,7 +86,8 @@ module FunctionsFramework
|
|
80
86
|
#
|
81
87
|
# You must provide a name for the function, and a block that implemets the
|
82
88
|
# function. The block should take _one_ argument: the event object of type
|
83
|
-
#
|
89
|
+
# [`CloudEvents::Event`](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event).
|
90
|
+
# Any return value is ignored.
|
84
91
|
#
|
85
92
|
# @param name [String] The function name
|
86
93
|
# @param block [Proc] The function code as a proc
|
@@ -88,9 +95,26 @@ module FunctionsFramework
|
|
88
95
|
#
|
89
96
|
def add_cloud_event name, &block
|
90
97
|
name = name.to_s
|
91
|
-
synchronize do
|
98
|
+
@mutex.synchronize do
|
92
99
|
raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
|
93
|
-
@functions[name] = Function.
|
100
|
+
@functions[name] = Function.cloud_event name, &block
|
101
|
+
end
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Add a startup task.
|
107
|
+
#
|
108
|
+
# Startup tasks are generally run just before a server starts. They are
|
109
|
+
# passed the {FunctionsFramework::Function} identifying the function to
|
110
|
+
# execute, and have no return value.
|
111
|
+
#
|
112
|
+
# @param block [Proc] The startup task
|
113
|
+
# @return [self]
|
114
|
+
#
|
115
|
+
def add_startup_task &block
|
116
|
+
@mutex.synchronize do
|
117
|
+
@start_tasks << Function.startup_task(&block)
|
94
118
|
end
|
95
119
|
self
|
96
120
|
end
|
@@ -27,17 +27,22 @@ module FunctionsFramework
|
|
27
27
|
include ::MonitorMixin
|
28
28
|
|
29
29
|
##
|
30
|
-
# Create a new web server given a function
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
30
|
+
# Create a new web server given a function definition, a set of application
|
31
|
+
# globals, and server configuration.
|
32
|
+
#
|
33
|
+
# To configure the server, pass a block that takes a
|
34
|
+
# {FunctionsFramework::Server::Config} object as the parameter. This block
|
35
|
+
# is the only opportunity to modify the configuration; once the server is
|
36
|
+
# initialized, configuration is frozen.
|
35
37
|
#
|
36
38
|
# @param function [FunctionsFramework::Function] The function to execute.
|
39
|
+
# @param globals [Hash] Globals to pass to invocations. This hash should
|
40
|
+
# normally be frozen so separate function invocations cannot interfere
|
41
|
+
# with one another's globals.
|
37
42
|
# @yield [FunctionsFramework::Server::Config] A config object that can be
|
38
43
|
# manipulated to configure this server.
|
39
44
|
#
|
40
|
-
def initialize function
|
45
|
+
def initialize function, globals
|
41
46
|
super()
|
42
47
|
@config = Config.new
|
43
48
|
yield @config if block_given?
|
@@ -46,9 +51,9 @@ module FunctionsFramework
|
|
46
51
|
@app =
|
47
52
|
case function.type
|
48
53
|
when :http
|
49
|
-
HttpApp.new function, @config
|
54
|
+
HttpApp.new function, globals, @config
|
50
55
|
when :cloud_event
|
51
|
-
EventApp.new function, @config
|
56
|
+
EventApp.new function, globals, @config
|
52
57
|
else
|
53
58
|
raise "Unrecognized function type: #{function.type}"
|
54
59
|
end
|
@@ -82,9 +87,9 @@ module FunctionsFramework
|
|
82
87
|
@server.max_threads = @config.max_threads
|
83
88
|
@server.leak_stack_on_error = @config.show_error_details?
|
84
89
|
@server.binder.add_tcp_listener @config.bind_addr, @config.port
|
85
|
-
@server.run true
|
86
90
|
@config.logger.info "FunctionsFramework: Serving function #{@function.name.inspect}" \
|
87
91
|
" on port #{@config.port}..."
|
92
|
+
@server.run true
|
88
93
|
end
|
89
94
|
end
|
90
95
|
self
|
@@ -344,7 +349,7 @@ module FunctionsFramework
|
|
344
349
|
string_response response, "text/plain", 200
|
345
350
|
when ::Hash
|
346
351
|
string_response ::JSON.dump(response), "application/json", 200
|
347
|
-
when CloudEvents::CloudEventsError
|
352
|
+
when ::CloudEvents::CloudEventsError
|
348
353
|
cloud_events_error_response response
|
349
354
|
when ::StandardError
|
350
355
|
error_response "#{response.class}: #{response.message}\n#{response.backtrace}\n"
|
@@ -379,9 +384,10 @@ module FunctionsFramework
|
|
379
384
|
|
380
385
|
## @private
|
381
386
|
class HttpApp < AppBase
|
382
|
-
def initialize function, config
|
387
|
+
def initialize function, globals, config
|
383
388
|
super config
|
384
389
|
@function = function
|
390
|
+
@globals = globals
|
385
391
|
end
|
386
392
|
|
387
393
|
def call env
|
@@ -391,8 +397,7 @@ module FunctionsFramework
|
|
391
397
|
logger = env["rack.logger"] ||= @config.logger
|
392
398
|
request = ::Rack::Request.new env
|
393
399
|
logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
|
394
|
-
|
395
|
-
calling_context.call request
|
400
|
+
@function.call request, globals: @globals, logger: logger
|
396
401
|
rescue ::StandardError => e
|
397
402
|
e
|
398
403
|
end
|
@@ -402,10 +407,11 @@ module FunctionsFramework
|
|
402
407
|
|
403
408
|
## @private
|
404
409
|
class EventApp < AppBase
|
405
|
-
def initialize function, config
|
410
|
+
def initialize function, globals, config
|
406
411
|
super config
|
407
412
|
@function = function
|
408
|
-
@
|
413
|
+
@globals = globals
|
414
|
+
@cloud_events = ::CloudEvents::HttpBinding.default
|
409
415
|
@legacy_events = LegacyEventConverter.new
|
410
416
|
end
|
411
417
|
|
@@ -415,11 +421,11 @@ module FunctionsFramework
|
|
415
421
|
event = decode_event env
|
416
422
|
response =
|
417
423
|
case event
|
418
|
-
when CloudEvents::Event
|
424
|
+
when ::CloudEvents::Event
|
419
425
|
handle_cloud_event event, logger
|
420
426
|
when ::Array
|
421
|
-
CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
|
422
|
-
when CloudEvents::CloudEventsError
|
427
|
+
::CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
|
428
|
+
when ::CloudEvents::CloudEventsError
|
423
429
|
event
|
424
430
|
else
|
425
431
|
raise "Unexpected event type: #{event.class}"
|
@@ -432,15 +438,14 @@ module FunctionsFramework
|
|
432
438
|
def decode_event env
|
433
439
|
@cloud_events.decode_rack_env(env) ||
|
434
440
|
@legacy_events.decode_rack_env(env) ||
|
435
|
-
raise(CloudEvents::HttpContentError, "Unrecognized event format")
|
436
|
-
rescue CloudEvents::CloudEventsError => e
|
441
|
+
raise(::CloudEvents::HttpContentError, "Unrecognized event format")
|
442
|
+
rescue ::CloudEvents::CloudEventsError => e
|
437
443
|
e
|
438
444
|
end
|
439
445
|
|
440
446
|
def handle_cloud_event event, logger
|
441
447
|
logger.info "FunctionsFramework: Handling CloudEvent"
|
442
|
-
|
443
|
-
calling_context.call event
|
448
|
+
@function.call event, globals: @globals, logger: logger
|
444
449
|
"ok"
|
445
450
|
rescue ::StandardError => e
|
446
451
|
e
|
@@ -75,19 +75,71 @@ module FunctionsFramework
|
|
75
75
|
Testing.load_for_testing path, &block
|
76
76
|
end
|
77
77
|
|
78
|
+
##
|
79
|
+
# Run startup tasks for the given function name and return the initialized
|
80
|
+
# globals hash.
|
81
|
+
#
|
82
|
+
# Normally, this will be run automatically prior to the first call to the
|
83
|
+
# function using {call_http} or {call_event}, if it has not already been
|
84
|
+
# run. However, you can call it explicitly to test its behavior. It cannot
|
85
|
+
# be called more than once for any given function.
|
86
|
+
#
|
87
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
88
|
+
# override that by providing your own logger. In particular, to disable
|
89
|
+
# logging, you can pass `Logger.new(nil)`.
|
90
|
+
#
|
91
|
+
# @param name [String] The name of the function to start up.
|
92
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
93
|
+
# Framework's global logger. Optional.
|
94
|
+
# @param lenient [Boolean] If false (the default), raise an error if the
|
95
|
+
# given function has already had its startup tasks run. If true,
|
96
|
+
# duplicate requests to run startup tasks are ignored.
|
97
|
+
# @return [Hash] The initialized globals.
|
98
|
+
#
|
99
|
+
def run_startup_tasks name, logger: nil, lenient: false
|
100
|
+
function = Testing.current_registry[name]
|
101
|
+
raise "Unknown function name #{name}" unless function
|
102
|
+
globals = Testing.current_globals name
|
103
|
+
if globals
|
104
|
+
raise "Function #{name} has already been started up" unless lenient
|
105
|
+
else
|
106
|
+
globals = function.populate_globals
|
107
|
+
Testing.current_registry.startup_tasks.each do |task|
|
108
|
+
task.call function, globals: globals, logger: logger
|
109
|
+
end
|
110
|
+
Testing.current_globals name, globals
|
111
|
+
end
|
112
|
+
globals.freeze
|
113
|
+
end
|
114
|
+
|
78
115
|
##
|
79
116
|
# Call the given HTTP function for testing. The underlying function must
|
80
|
-
# be of type `:http`.
|
117
|
+
# be of type `:http`. Returns the Rack response.
|
118
|
+
#
|
119
|
+
# By default, the startup tasks will be run for the given function if they
|
120
|
+
# have not already been run. You can, however, disable running startup
|
121
|
+
# tasks by providing an explicit globals hash.
|
122
|
+
#
|
123
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
124
|
+
# override that by providing your own logger. In particular, to disable
|
125
|
+
# logging, you can pass `Logger.new(nil)`.
|
81
126
|
#
|
82
127
|
# @param name [String] The name of the function to call
|
83
128
|
# @param request [Rack::Request] The Rack request to send
|
129
|
+
# @param globals [Hash] Do not run startup tasks, and instead provide the
|
130
|
+
# globals directly. Optional.
|
131
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
132
|
+
# Framework's global logger. Optional.
|
84
133
|
# @return [Rack::Response]
|
85
134
|
#
|
86
|
-
def call_http name, request
|
87
|
-
|
135
|
+
def call_http name, request, globals: nil, logger: nil
|
136
|
+
globals ||= run_startup_tasks name, logger: logger, lenient: true
|
137
|
+
function = Testing.current_registry[name]
|
88
138
|
case function&.type
|
89
139
|
when :http
|
90
|
-
Testing.interpret_response
|
140
|
+
Testing.interpret_response do
|
141
|
+
function.call request, globals: globals, logger: logger
|
142
|
+
end
|
91
143
|
when nil
|
92
144
|
raise "Unknown function name #{name}"
|
93
145
|
else
|
@@ -99,15 +151,28 @@ module FunctionsFramework
|
|
99
151
|
# Call the given event function for testing. The underlying function must
|
100
152
|
# be of type :cloud_event`.
|
101
153
|
#
|
154
|
+
# By default, the startup tasks will be run for the given function if they
|
155
|
+
# have not already been run. You can, however, disable running startup
|
156
|
+
# tasks by providing an explicit globals hash.
|
157
|
+
#
|
158
|
+
# By default, the {FunctionsFramework.logger} will be used, but you can
|
159
|
+
# override that by providing your own logger. In particular, to disable
|
160
|
+
# logging, you can pass `Logger.new(nil)`.
|
161
|
+
#
|
102
162
|
# @param name [String] The name of the function to call
|
103
|
-
# @param event [
|
163
|
+
# @param event [::CloudEvents::Event] The event to send
|
164
|
+
# @param globals [Hash] Do not run startup tasks, and instead provide the
|
165
|
+
# globals directly. Optional.
|
166
|
+
# @param logger [Logger] Use the given logger instead of the Functions
|
167
|
+
# Framework's global logger. Optional.
|
104
168
|
# @return [nil]
|
105
169
|
#
|
106
|
-
def call_event name, event
|
107
|
-
|
170
|
+
def call_event name, event, globals: nil, logger: nil
|
171
|
+
globals ||= run_startup_tasks name, logger: logger, lenient: true
|
172
|
+
function = Testing.current_registry[name]
|
108
173
|
case function&.type
|
109
174
|
when :cloud_event
|
110
|
-
function.
|
175
|
+
function.call event, globals: globals, logger: logger
|
111
176
|
nil
|
112
177
|
when nil
|
113
178
|
raise "Unknown function name #{name}"
|
@@ -174,49 +239,83 @@ module FunctionsFramework
|
|
174
239
|
# @param source [String,URI] Event source (optional)
|
175
240
|
# @param type [String] Event type (optional)
|
176
241
|
# @param spec_version [String] Spec version (optional)
|
177
|
-
# @param data_content_type [String
|
242
|
+
# @param data_content_type [String,::CloudEvents::ContentType]
|
178
243
|
# Content type for the data (optional)
|
179
244
|
# @param data_schema [String,URI] Data schema (optional)
|
180
245
|
# @param subject [String] Subject (optional)
|
181
246
|
# @param time [String,DateTime] Event timestamp (optional)
|
182
|
-
# @return [
|
247
|
+
# @return [::CloudEvents::Event]
|
183
248
|
#
|
184
249
|
def make_cloud_event data,
|
185
|
-
id: nil,
|
186
|
-
|
250
|
+
id: nil,
|
251
|
+
source: nil,
|
252
|
+
type: nil,
|
253
|
+
spec_version: nil,
|
254
|
+
data_content_type: nil,
|
255
|
+
data_schema: nil,
|
256
|
+
subject: nil,
|
257
|
+
time: nil
|
187
258
|
id ||= "random-id-#{rand 100_000_000}"
|
188
259
|
source ||= "functions-framework-testing"
|
189
260
|
type ||= "com.example.test"
|
190
261
|
spec_version ||= "1.0"
|
191
|
-
CloudEvents::Event.new id:
|
192
|
-
|
193
|
-
|
262
|
+
::CloudEvents::Event.new id: id,
|
263
|
+
source: source,
|
264
|
+
type: type,
|
265
|
+
spec_version: spec_version,
|
266
|
+
data_content_type: data_content_type,
|
267
|
+
data_schema: data_schema,
|
268
|
+
subject: subject,
|
269
|
+
time: time,
|
270
|
+
data: data
|
194
271
|
end
|
195
272
|
|
196
273
|
extend self
|
197
274
|
|
198
275
|
@testing_registries = {}
|
276
|
+
@main_globals = {}
|
199
277
|
@mutex = ::Mutex.new
|
200
278
|
|
201
279
|
class << self
|
202
280
|
## @private
|
203
281
|
def load_for_testing path
|
204
282
|
old_registry = ::FunctionsFramework.global_registry
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
283
|
+
::Thread.current[:functions_framework_testing_registry] =
|
284
|
+
@mutex.synchronize do
|
285
|
+
if @testing_registries.key? path
|
286
|
+
::FunctionsFramework.global_registry = @testing_registries[path]
|
287
|
+
else
|
288
|
+
new_registry = ::FunctionsFramework::Registry.new
|
289
|
+
::FunctionsFramework.global_registry = new_registry
|
290
|
+
::Kernel.load path
|
291
|
+
@testing_registries[path] = new_registry
|
292
|
+
end
|
213
293
|
end
|
214
|
-
|
294
|
+
::Thread.current[:functions_framework_testing_globals] = {}
|
215
295
|
yield
|
216
296
|
ensure
|
297
|
+
::Thread.current[:functions_framework_testing_registry] = nil
|
298
|
+
::Thread.current[:functions_framework_testing_globals] = nil
|
217
299
|
::FunctionsFramework.global_registry = old_registry
|
218
300
|
end
|
219
301
|
|
302
|
+
## @private
|
303
|
+
def current_registry
|
304
|
+
::Thread.current[:functions_framework_testing_registry] ||
|
305
|
+
::FunctionsFramework.global_registry
|
306
|
+
end
|
307
|
+
|
308
|
+
## @private
|
309
|
+
def current_globals name, globals = nil
|
310
|
+
name = name.to_s
|
311
|
+
globals_by_name = ::Thread.current[:functions_framework_testing_globals] || @main_globals
|
312
|
+
if globals
|
313
|
+
globals_by_name[name] = globals
|
314
|
+
else
|
315
|
+
globals_by_name[name]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
220
319
|
## @private
|
221
320
|
def interpret_response
|
222
321
|
response =
|