functions_framework 0.6.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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