functions_framework 0.5.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -33,7 +33,7 @@ module FunctionsFramework
33
33
  return nil unless input
34
34
  context = normalized_context input
35
35
  return nil unless context
36
- construct_cloud_event context, input["data"], content_type.charset
36
+ construct_cloud_event context, input["data"]
37
37
  end
38
38
 
39
39
  private
@@ -49,16 +49,19 @@ module FunctionsFramework
49
49
  end
50
50
 
51
51
  def normalized_context input
52
- raw_context = input["context"]
53
- id = raw_context&.[]("eventId") || input["eventId"]
54
- timestamp = raw_context&.[]("timestamp") || input["timestamp"]
55
- type = raw_context&.[]("eventType") || input["eventType"]
56
- service, resource = analyze_resource raw_context&.[]("resource") || input["resource"]
52
+ id = normalized_context_field input, "eventId"
53
+ timestamp = normalized_context_field input, "timestamp"
54
+ type = normalized_context_field input, "eventType"
55
+ service, resource = analyze_resource normalized_context_field input, "resource"
57
56
  service ||= service_from_type type
58
57
  return nil unless id && timestamp && type && service && resource
59
58
  { id: id, timestamp: timestamp, type: type, service: service, resource: resource }
60
59
  end
61
60
 
61
+ def normalized_context_field input, field
62
+ input["context"]&.[](field) || input[field]
63
+ end
64
+
62
65
  def analyze_resource raw_resource
63
66
  service = resource = nil
64
67
  case raw_resource
@@ -78,37 +81,47 @@ module FunctionsFramework
78
81
  nil
79
82
  end
80
83
 
81
- def construct_cloud_event context, data, charset
84
+ def construct_cloud_event context, data
82
85
  source, subject = convert_source context[:service], context[:resource]
83
86
  type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
84
87
  return nil unless type && source
85
- ce_data = convert_data context[:service], data
86
- content_type = "application/json; charset=#{charset}"
88
+ ce_data, data_subject = convert_data context[:service], data
89
+ content_type = "application/json"
87
90
  ::CloudEvents::Event.new id: context[:id],
88
91
  source: source,
89
92
  type: type,
90
93
  spec_version: "1.0",
91
94
  data_content_type: content_type,
92
95
  data: ce_data,
93
- subject: subject,
96
+ subject: subject || data_subject,
94
97
  time: context[:timestamp]
95
98
  end
96
99
 
97
100
  def convert_source service, resource
98
- if service == "storage.googleapis.com"
99
- match = %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}.match resource
100
- return [nil, nil] unless match
101
- ["//#{service}/#{match[1]}", match[2]]
102
- else
103
- ["//#{service}/#{resource}", nil]
104
- end
101
+ return ["//#{service}/#{resource}", nil] unless CE_SERVICE_TO_RESOURCE_RE.key? service
102
+
103
+ match = CE_SERVICE_TO_RESOURCE_RE[service].match resource
104
+ return [nil, nil] unless match
105
+ ["//#{service}/#{match[1]}", match[2]]
105
106
  end
106
107
 
107
108
  def convert_data service, data
108
- if service == "pubsub.googleapis.com"
109
- { "message" => data, "subscription" => nil }
109
+ case service
110
+ when "pubsub.googleapis.com"
111
+ [{ "message" => data }, nil]
112
+ when "firebaseauth.googleapis.com"
113
+ if data.key? "metadata"
114
+ FIREBASE_AUTH_METADATA_LEGACY_TO_CE.each do |old_key, new_key|
115
+ if data["metadata"].key? old_key
116
+ data["metadata"][new_key] = data["metadata"][old_key]
117
+ data["metadata"].delete old_key
118
+ end
119
+ end
120
+ end
121
+ subject = "users/#{data['uid']}" if data.key? "uid"
122
+ [data, subject]
110
123
  else
111
- data
124
+ [data, nil]
112
125
  end
113
126
  end
114
127
 
@@ -116,8 +129,9 @@ module FunctionsFramework
116
129
  %r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
117
130
  %r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
118
131
  %r{^providers/cloud\.storage/} => "storage.googleapis.com",
119
- %r{^providers/firebase\.auth/} => "firebase.googleapis.com",
120
- %r{^providers/google\.firebase} => "firebase.googleapis.com"
132
+ %r{^providers/firebase\.auth/} => "firebaseauth.googleapis.com",
133
+ %r{^providers/google\.firebase\.analytics/} => "firebase.googleapis.com",
134
+ %r{^providers/google\.firebase\.database/} => "firebasedatabase.googleapis.com"
121
135
  }.freeze
122
136
 
123
137
  LEGACY_TYPE_TO_CE_TYPE = {
@@ -140,5 +154,18 @@ module FunctionsFramework
140
154
  "providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
141
155
  "providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
142
156
  }.freeze
157
+
158
+ CE_SERVICE_TO_RESOURCE_RE = {
159
+ "firebase.googleapis.com" => %r{^(projects/[^/]+)/(events/[^/]+)$},
160
+ "firebasedatabase.googleapis.com" => %r{^(projects/_/instances/[^/]+)/(refs/.+)$},
161
+ "firestore.googleapis.com" => %r{^(projects/[^/]+/databases/\(default\))/(documents/.+)$},
162
+ "storage.googleapis.com" => %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}
163
+ }.freeze
164
+
165
+ # Map Firebase Auth legacy event metadata field names to their equivalent CloudEvent field names.
166
+ FIREBASE_AUTH_METADATA_LEGACY_TO_CE = {
167
+ "createdAt" => "createTime",
168
+ "lastSignedInAt" => "lastSignInTime"
169
+ }.freeze
143
170
  end
144
171
  end
@@ -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
@@ -89,9 +95,26 @@ module FunctionsFramework
89
95
  #
90
96
  def add_cloud_event name, &block
91
97
  name = name.to_s
92
- synchronize do
98
+ @mutex.synchronize do
93
99
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
94
- @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)
95
118
  end
96
119
  self
97
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
@@ -153,7 +158,7 @@ module FunctionsFramework
153
158
  ::Signal.trap "SIGHUP" do
154
159
  Server.signal_enqueue "SIGHUP", @config.logger, @server
155
160
  end
156
- rescue ::ArgumentError # rubocop:disable Lint/HandleExceptions
161
+ rescue ::ArgumentError
157
162
  # Not available on all systems
158
163
  end
159
164
  @signals_installed = true
@@ -301,7 +306,7 @@ module FunctionsFramework
301
306
  # @return [Integer]
302
307
  #
303
308
  def max_threads
304
- @max_threads || (@rack_env == "development" ? 1 : 16)
309
+ @max_threads || 1
305
310
  end
306
311
 
307
312
  ##
@@ -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,9 +407,10 @@ 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
413
+ @globals = globals
408
414
  @cloud_events = ::CloudEvents::HttpBinding.default
409
415
  @legacy_events = LegacyEventConverter.new
410
416
  end
@@ -439,8 +445,7 @@ module FunctionsFramework
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
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}"
@@ -208,27 +273,49 @@ module FunctionsFramework
208
273
  extend self
209
274
 
210
275
  @testing_registries = {}
276
+ @main_globals = {}
211
277
  @mutex = ::Mutex.new
212
278
 
213
279
  class << self
214
280
  ## @private
215
281
  def load_for_testing path
216
282
  old_registry = ::FunctionsFramework.global_registry
217
- @mutex.synchronize do
218
- if @testing_registries.key? path
219
- ::FunctionsFramework.global_registry = @testing_registries[path]
220
- else
221
- new_registry = ::FunctionsFramework::Registry.new
222
- ::FunctionsFramework.global_registry = new_registry
223
- ::Kernel.load path
224
- @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
225
293
  end
226
- end
294
+ ::Thread.current[:functions_framework_testing_globals] = {}
227
295
  yield
228
296
  ensure
297
+ ::Thread.current[:functions_framework_testing_registry] = nil
298
+ ::Thread.current[:functions_framework_testing_globals] = nil
229
299
  ::FunctionsFramework.global_registry = old_registry
230
300
  end
231
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
+
232
319
  ## @private
233
320
  def interpret_response
234
321
  response =
@@ -279,9 +366,10 @@ module FunctionsFramework
279
366
  ::Rack::RACK_ERRORS => ::StringIO.new
280
367
  }
281
368
  headers.each do |header|
282
- if header.is_a? String
369
+ case header
370
+ when String
283
371
  name, value = header.split ":"
284
- elsif header.is_a? Array
372
+ when ::Array
285
373
  name, value = header
286
374
  end
287
375
  next unless name && value