functions_framework 0.6.0 → 0.10.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.
@@ -12,20 +12,16 @@
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
- @mutex = ::Monitor.new
24
+ @mutex = ::Mutex.new
29
25
  @functions = {}
30
26
  @start_tasks = []
31
27
  end
@@ -51,17 +47,12 @@ module FunctionsFramework
51
47
  end
52
48
 
53
49
  ##
54
- # Run all startup tasks.
50
+ # Return an array of startup tasks.
55
51
  #
56
- # @param server [FunctionsFramework::Server] The server that is starting.
57
- # @return [self]
52
+ # @return [Array<FunctionsFramework::Function>]
58
53
  #
59
- def run_startup_tasks server
60
- tasks = @mutex.synchronize { @start_tasks.dup }
61
- tasks.each do |task|
62
- task.call server.function, server.config
63
- end
64
- self
54
+ def startup_tasks
55
+ @mutex.synchronize { @start_tasks.dup }
65
56
  end
66
57
 
67
58
  ##
@@ -85,7 +76,7 @@ module FunctionsFramework
85
76
  name = name.to_s
86
77
  @mutex.synchronize do
87
78
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
88
- @functions[name] = Function.new name, :http, &block
79
+ @functions[name] = Function.http name, &block
89
80
  end
90
81
  self
91
82
  end
@@ -106,7 +97,7 @@ module FunctionsFramework
106
97
  name = name.to_s
107
98
  @mutex.synchronize do
108
99
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
109
- @functions[name] = Function.new name, :cloud_event, &block
100
+ @functions[name] = Function.cloud_event name, &block
110
101
  end
111
102
  self
112
103
  end
@@ -115,16 +106,15 @@ module FunctionsFramework
115
106
  # Add a startup task.
116
107
  #
117
108
  # Startup tasks are generally run just before a server starts. They are
118
- # passed two arguments: the {FunctionsFramework::Function} identifying the
119
- # function to execute, and the {FunctionsFramework::Server::Config}
120
- # specifying the (frozen) server configuration. Tasks have no return value.
109
+ # passed the {FunctionsFramework::Function} identifying the function to
110
+ # execute, and have no return value.
121
111
  #
122
112
  # @param block [Proc] The startup task
123
113
  # @return [self]
124
114
  #
125
115
  def add_startup_task &block
126
116
  @mutex.synchronize do
127
- @start_tasks << block
117
+ @start_tasks << Function.startup_task(&block)
128
118
  end
129
119
  self
130
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
@@ -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
  ##
@@ -341,9 +346,9 @@ module FunctionsFramework
341
346
  when ::Rack::Response
342
347
  response.finish
343
348
  when ::String
344
- string_response response, "text/plain", 200
349
+ string_response response, 200
345
350
  when ::Hash
346
- string_response ::JSON.dump(response), "application/json", 200
351
+ string_response ::JSON.dump(response), 200, content_type: "application/json"
347
352
  when ::CloudEvents::CloudEventsError
348
353
  cloud_events_error_response response
349
354
  when ::StandardError
@@ -354,10 +359,17 @@ module FunctionsFramework
354
359
  end
355
360
 
356
361
  def notfound_response
357
- string_response "Not found", "text/plain", 404
362
+ string_response "Not found", 404
358
363
  end
359
364
 
360
- def string_response string, content_type, status
365
+ def string_response string, status, content_type: nil
366
+ string.force_encoding ::Encoding::ASCII_8BIT unless string.valid_encoding?
367
+ if string.encoding == ::Encoding::ASCII_8BIT
368
+ content_type ||= "application/octet-stream"
369
+ else
370
+ content_type ||= "text/plain"
371
+ content_type = "#{content_type}; charset=#{string.encoding.name.downcase}"
372
+ end
361
373
  headers = {
362
374
  "Content-Type" => content_type,
363
375
  "Content-Length" => string.bytesize
@@ -367,21 +379,22 @@ module FunctionsFramework
367
379
 
368
380
  def cloud_events_error_response error
369
381
  @config.logger.warn error
370
- string_response "#{error.class}: #{error.message}", "text/plain", 400
382
+ string_response "#{error.class}: #{error.message}", 400
371
383
  end
372
384
 
373
385
  def error_response message
374
386
  @config.logger.error message
375
387
  message = "Unexpected internal error" unless @config.show_error_details?
376
- string_response message, "text/plain", 500
388
+ string_response message, 500
377
389
  end
378
390
  end
379
391
 
380
392
  ## @private
381
393
  class HttpApp < AppBase
382
- def initialize function, config
394
+ def initialize function, globals, config
383
395
  super config
384
396
  @function = function
397
+ @globals = globals
385
398
  end
386
399
 
387
400
  def call env
@@ -391,8 +404,7 @@ module FunctionsFramework
391
404
  logger = env["rack.logger"] ||= @config.logger
392
405
  request = ::Rack::Request.new env
393
406
  logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
394
- calling_context = @function.new_call logger: logger
395
- calling_context.call request
407
+ @function.call request, globals: @globals, logger: logger
396
408
  rescue ::StandardError => e
397
409
  e
398
410
  end
@@ -402,9 +414,10 @@ module FunctionsFramework
402
414
 
403
415
  ## @private
404
416
  class EventApp < AppBase
405
- def initialize function, config
417
+ def initialize function, globals, config
406
418
  super config
407
419
  @function = function
420
+ @globals = globals
408
421
  @cloud_events = ::CloudEvents::HttpBinding.default
409
422
  @legacy_events = LegacyEventConverter.new
410
423
  end
@@ -439,8 +452,7 @@ module FunctionsFramework
439
452
 
440
453
  def handle_cloud_event event, logger
441
454
  logger.info "FunctionsFramework: Handling CloudEvent"
442
- calling_context = @function.new_call logger: logger
443
- calling_context.call event
455
+ @function.call event, globals: @globals, logger: logger
444
456
  "ok"
445
457
  rescue ::StandardError => e
446
458
  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 =
@@ -243,20 +330,27 @@ module FunctionsFramework
243
330
  when ::Array
244
331
  ::Rack::Response.new response[2], response[0], response[1]
245
332
  when ::String
246
- string_response response, "text/plain", 200
333
+ string_response response, 200
247
334
  when ::Hash
248
335
  json = ::JSON.dump response
249
- string_response json, "application/json", 200
336
+ string_response json, 200, content_type: "application/json"
250
337
  when ::StandardError
251
338
  message = "#{response.class}: #{response.message}\n#{response.backtrace}\n"
252
- string_response message, "text/plain", 500
339
+ string_response message, 500
253
340
  else
254
341
  raise "Unexpected response type: #{response.inspect}"
255
342
  end
256
343
  end
257
344
 
258
345
  ## @private
259
- def string_response string, content_type, status
346
+ def string_response string, status, content_type: nil
347
+ string.force_encoding ::Encoding::ASCII_8BIT unless string.valid_encoding?
348
+ if string.encoding == ::Encoding::ASCII_8BIT
349
+ content_type ||= "application/octet-stream"
350
+ else
351
+ content_type ||= "text/plain"
352
+ content_type = "#{content_type}; charset=#{string.encoding.name.downcase}"
353
+ end
260
354
  headers = {
261
355
  "Content-Type" => content_type,
262
356
  "Content-Length" => string.bytesize
@@ -279,9 +373,10 @@ module FunctionsFramework
279
373
  ::Rack::RACK_ERRORS => ::StringIO.new
280
374
  }
281
375
  headers.each do |header|
282
- if header.is_a? String
376
+ case header
377
+ when String
283
378
  name, value = header.split ":"
284
- elsif header.is_a? Array
379
+ when ::Array
285
380
  name, value = header
286
381
  end
287
382
  next unless name && value
@@ -17,5 +17,5 @@ module FunctionsFramework
17
17
  # Version of the Ruby Functions Framework
18
18
  # @return [String]
19
19
  #
20
- VERSION = "0.6.0".freeze
20
+ VERSION = "0.10.0".freeze
21
21
  end
metadata CHANGED
@@ -1,43 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: functions_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-17 00:00:00.000000000 Z
11
+ date: 2021-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cloud_events
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ - - "<"
18
21
  - !ruby/object:Gem::Version
19
- version: '0.1'
22
+ version: 2.a
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0.4'
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: '0.1'
32
+ version: 2.a
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: puma
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - "~>"
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 4.3.0
40
+ - - "<"
32
41
  - !ruby/object:Gem::Version
33
- version: '4.3'
42
+ version: 6.a
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
37
46
  requirements:
38
- - - "~>"
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 4.3.0
50
+ - - "<"
39
51
  - !ruby/object:Gem::Version
40
- version: '4.3'
52
+ version: 6.a
41
53
  - !ruby/object:Gem::Dependency
42
54
  name: rack
43
55
  requirement: !ruby/object:Gem::Requirement
@@ -54,8 +66,8 @@ dependencies:
54
66
  version: '2.1'
55
67
  description: The Functions Framework is an open source framework for writing lightweight,
56
68
  portable Ruby functions that run in a serverless environment. Functions written
57
- to this Framework will run Google Cloud Google Cloud Functions, Google Cloud Run,
58
- or any other Knative-based environment.
69
+ to this Framework will run on Google Cloud Functions, Google Cloud Run, or any other
70
+ Knative-based environment.
59
71
  email:
60
72
  - dazuma@google.com
61
73
  executables:
@@ -87,10 +99,10 @@ homepage: https://github.com/GoogleCloudPlatform/functions-framework-ruby
87
99
  licenses:
88
100
  - Apache-2.0
89
101
  metadata:
90
- changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.6.0/file.CHANGELOG.html
102
+ changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.10.0/file.CHANGELOG.html
91
103
  source_code_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby
92
104
  bug_tracker_uri: https://github.com/GoogleCloudPlatform/functions-framework-ruby/issues
93
- documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.6.0
105
+ documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v0.10.0
94
106
  post_install_message:
95
107
  rdoc_options: []
96
108
  require_paths:
@@ -99,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
111
  requirements:
100
112
  - - ">="
101
113
  - !ruby/object:Gem::Version
102
- version: 2.4.0
114
+ version: 2.5.0
103
115
  required_rubygems_version: !ruby/object:Gem::Requirement
104
116
  requirements:
105
117
  - - ">="
106
118
  - !ruby/object:Gem::Version
107
119
  version: '0'
108
120
  requirements: []
109
- rubygems_version: 3.0.3
121
+ rubygems_version: 3.1.6
110
122
  signing_key:
111
123
  specification_version: 4
112
124
  summary: Functions Framework for Ruby