functions_framework 0.4.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 =
|