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.
@@ -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 =