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.
@@ -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 [FunctionsFramework::CloudEvents::Event] if the request could
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
- source: source,
90
- type: type,
91
- spec_version: "1.0",
92
- data_content_type: content_type,
93
- data: ce_data,
94
- subject: subject,
95
- time: context[:timestamp]
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
- super()
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.new name, :http, &block
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
- # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
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.new name, :cloud_event, &block
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. Yields a
31
- # {FunctionsFramework::Server::Config} object that you can use to set
32
- # server configuration parameters. This block is the only opportunity to
33
- # set configuration; once the server is initialized, configuration is
34
- # frozen.
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
- calling_context = @function.new_call logger: logger
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
- @cloud_events = CloudEvents::HttpBinding.default
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
- calling_context = @function.new_call logger: logger
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
- function = ::FunctionsFramework.global_registry[name]
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 { function.new_call.call request }
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 [FunctionsFramework::CloudEvets::Event] The event to send
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
- function = ::FunctionsFramework.global_registry[name]
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.new_call.call event
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,FunctionsFramework::CloudEvents::ContentType]
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 [FunctionsFramework::CloudEvents::Event]
247
+ # @return [::CloudEvents::Event]
183
248
  #
184
249
  def make_cloud_event data,
185
- id: nil, source: nil, type: nil, spec_version: nil,
186
- data_content_type: nil, data_schema: nil, subject: nil, time: nil
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: id, source: source, type: type, spec_version: spec_version,
192
- data_content_type: data_content_type, data_schema: data_schema,
193
- subject: subject, time: time, data: data
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
- @mutex.synchronize do
206
- if @testing_registries.key? path
207
- ::FunctionsFramework.global_registry = @testing_registries[path]
208
- else
209
- new_registry = ::FunctionsFramework::Registry.new
210
- ::FunctionsFramework.global_registry = new_registry
211
- ::Kernel.load path
212
- @testing_registries[path] = new_registry
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
- end
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 =