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.
@@ -14,7 +14,8 @@
14
14
 
15
15
  require "logger"
16
16
 
17
- require "functions_framework/cloud_events"
17
+ require "cloud_events"
18
+
18
19
  require "functions_framework/function"
19
20
  require "functions_framework/legacy_event_converter"
20
21
  require "functions_framework/registry"
@@ -44,10 +45,6 @@ require "functions_framework/version"
44
45
  #
45
46
  # Here is a roadmap to the internal modules in the Ruby functions framework.
46
47
  #
47
- # * {FunctionsFramework::CloudEvents} provides an implementation of the
48
- # [CloudEvents](https://cloudevents.io) specification. In particular, if
49
- # you define an event function, you will receive the event as a
50
- # {FunctionsFramework::CloudEvents::Event} object.
51
48
  # * {FunctionsFramework::CLI} is the implementation of the
52
49
  # `functions-framework-ruby` executable. Most apps will not need to interact
53
50
  # with this class directly.
@@ -74,7 +71,7 @@ require "functions_framework/version"
74
71
  #
75
72
  module FunctionsFramework
76
73
  @global_registry = Registry.new
77
- @logger = ::Logger.new ::STDERR
74
+ @logger = ::Logger.new $stderr
78
75
  @logger.level = ::Logger::INFO
79
76
 
80
77
  ##
@@ -94,6 +91,12 @@ module FunctionsFramework
94
91
  #
95
92
  DEFAULT_SOURCE = "./app.rb".freeze
96
93
 
94
+ ##
95
+ # The CloudEvents implementation was extracted to become the official
96
+ # CloudEvents SDK. This alias is left here for backward compatibility.
97
+ #
98
+ CloudEvents = ::CloudEvents
99
+
97
100
  class << self
98
101
  ##
99
102
  # The "global" registry that holds events defined by the
@@ -144,7 +147,8 @@ module FunctionsFramework
144
147
  #
145
148
  # You must provide a name for the function, and a block that implemets the
146
149
  # function. The block should take one argument: the event object of type
147
- # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
150
+ # [`CloudEvents::Event`](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event).
151
+ # Any return value is ignored.
148
152
  #
149
153
  # ## Example
150
154
  #
@@ -162,8 +166,30 @@ module FunctionsFramework
162
166
  end
163
167
 
164
168
  ##
165
- # Start the functions framework server in the background. The server will
166
- # look up the given target function name in the global registry.
169
+ # Define a server startup task. This is useful for initializing shared
170
+ # resources that should be accessible across all function invocations in
171
+ # this Ruby VM.
172
+ #
173
+ # Startup tasks are run just before a server starts. All startup tasks are
174
+ # guaranteed to complete before any function executes. However, they are
175
+ # run only when preparing to run functions. They are not run, for example,
176
+ # if an app is loaded to verify its integrity during deployment.
177
+ #
178
+ # Startup tasks are passed the {FunctionsFramework::Function} identifying
179
+ # the function to execute, and have no return value.
180
+ #
181
+ # @param block [Proc] The startup task
182
+ # @return [self]
183
+ #
184
+ def on_startup &block
185
+ global_registry.add_startup_task(&block)
186
+ self
187
+ end
188
+
189
+ ##
190
+ # Run startup tasks, then start the functions framework server in the
191
+ # background. The startup tasks and target function will be looked up in
192
+ # the global registry.
167
193
  #
168
194
  # @param target [FunctionsFramework::Function,String] The function to run,
169
195
  # or the name of the function to look up in the global registry.
@@ -179,7 +205,12 @@ module FunctionsFramework
179
205
  function = global_registry[target]
180
206
  raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
181
207
  end
182
- server = Server.new function, &block
208
+ globals = function.populate_globals
209
+ server = Server.new function, globals, &block
210
+ global_registry.startup_tasks.each do |task|
211
+ task.call function, globals: globals, logger: server.config.logger
212
+ end
213
+ globals.freeze
183
214
  server.respond_to_signals
184
215
  server.start
185
216
  end
@@ -42,8 +42,31 @@ module FunctionsFramework
42
42
  @detailed_errors = nil
43
43
  @signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
44
44
  @logging_level = init_logging_level
45
+ @what_to_do = nil
46
+ @error_message = nil
47
+ @exit_code = 0
45
48
  end
46
49
 
50
+ ##
51
+ # Determine if an error has occurred
52
+ #
53
+ # @return [boolean]
54
+ #
55
+ def error?
56
+ !@error_message.nil?
57
+ end
58
+
59
+ ##
60
+ # @return [Integer] The current exit status.
61
+ #
62
+ attr_reader :exit_code
63
+
64
+ ##
65
+ # @return [String] The current error message.
66
+ # @return [nil] if no error has occurred.
67
+ #
68
+ attr_reader :error_message
69
+
47
70
  ##
48
71
  # Parse the given command line arguments.
49
72
  # Exits if argument parsing failed.
@@ -52,7 +75,7 @@ module FunctionsFramework
52
75
  # @return [self]
53
76
  #
54
77
  def parse_args argv # rubocop:disable Metrics/MethodLength
55
- option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
78
+ @option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
56
79
  op.on "-t", "--target TARGET",
57
80
  "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
58
81
  @target = val
@@ -84,55 +107,92 @@ module FunctionsFramework
84
107
  op.on "--[no-]detailed-errors", "Set whether to show error details" do |val|
85
108
  @detailed_errors = val
86
109
  end
110
+ op.on "--verify", "Verify the app only, but do not run the server." do
111
+ @what_to_do ||= :verify
112
+ end
87
113
  op.on "-v", "--verbose", "Increase log verbosity" do
88
114
  @logging_level -= 1
89
115
  end
90
116
  op.on "-q", "--quiet", "Decrease log verbosity" do
91
117
  @logging_level += 1
92
118
  end
119
+ op.on "--version", "Display the framework version" do
120
+ @what_to_do ||= :version
121
+ end
93
122
  op.on "--help", "Display help" do
94
- puts op
95
- exit
123
+ @what_to_do ||= :help
96
124
  end
97
125
  end
98
- option_parser.parse! argv
99
- error "Unrecognized arguments: #{argv}\n#{op}" unless argv.empty?
126
+ begin
127
+ @option_parser.parse! argv
128
+ error! "Unrecognized arguments: #{argv}\n#{@option_parser}", 2 unless argv.empty?
129
+ rescue ::OptionParser::ParseError => e
130
+ error! "#{e.message}\n#{@option_parser}", 2
131
+ end
100
132
  self
101
133
  end
102
134
 
103
135
  ##
104
- # Run the configured server, and block until it stops.
105
- # If a validation error occurs, print a message and exit.
136
+ # Perform the requested function.
137
+ #
138
+ # * If the `--version` flag was given, display the version.
139
+ # * If the `--help` flag was given, display online help.
140
+ # * If the `--verify` flag was given, load and verify the function,
141
+ # displaying any errors, then exit without starting a server.
142
+ # * Otherwise, start the configured server and block until it stops.
106
143
  #
107
144
  # @return [self]
108
145
  #
109
146
  def run
110
- begin
111
- server = start_server
112
- rescue ::StandardError => e
113
- error e.message
147
+ return self if error?
148
+ case @what_to_do
149
+ when :version
150
+ puts ::FunctionsFramework::VERSION
151
+ when :help
152
+ puts @option_parser
153
+ when :verify
154
+ begin
155
+ load_function
156
+ puts "OK"
157
+ rescue ::StandardError => e
158
+ error! e.message
159
+ end
160
+ else
161
+ begin
162
+ start_server.wait_until_stopped
163
+ rescue ::StandardError => e
164
+ error! e.message
165
+ end
114
166
  end
115
- server.wait_until_stopped
116
167
  self
117
168
  end
118
169
 
119
170
  ##
120
- # Start the configured server and return the running server object.
171
+ # Finish the CLI, displaying any error status and exiting with the current
172
+ # exit code. Never returns.
173
+ #
174
+ def complete
175
+ warn @error_message if @error_message
176
+ exit @exit_code
177
+ end
178
+
179
+ ##
180
+ # Load the source and get and verify the requested function.
121
181
  # If a validation error occurs, raise an exception.
122
- # This is used for testing the CLI.
123
182
  #
124
- # @return [FunctionsFramework::Server]
183
+ # @return [FunctionsFramework::Function]
125
184
  #
126
185
  # @private
127
186
  #
128
- def start_server
187
+ def load_function
129
188
  ::FunctionsFramework.logger.level = @logging_level
130
- ::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION} server starting."
189
+ ::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION}"
131
190
  ::ENV["FUNCTION_TARGET"] = @target
132
191
  ::ENV["FUNCTION_SOURCE"] = @source
133
192
  ::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
134
193
  ::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
135
194
  load @source
195
+ ::FunctionsFramework.logger.info "FunctionsFramework: Looking for function name #{@target.inspect}..."
136
196
  function = ::FunctionsFramework.global_registry[@target]
137
197
  raise "Undefined function: #{@target.inspect}" if function.nil?
138
198
  unless @signature_type.nil? ||
@@ -140,6 +200,20 @@ module FunctionsFramework
140
200
  ["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
141
201
  raise "Function #{@target.inspect} does not match type #{@signature_type}"
142
202
  end
203
+ function
204
+ end
205
+
206
+ ##
207
+ # Start the configured server and return the running server object.
208
+ # If a validation error occurs, raise an exception.
209
+ #
210
+ # @return [FunctionsFramework::Server]
211
+ #
212
+ # @private
213
+ #
214
+ def start_server
215
+ function = load_function
216
+ ::FunctionsFramework.logger.info "FunctionsFramework: Starting server..."
143
217
  ::FunctionsFramework.start function do |config|
144
218
  config.rack_env = @env
145
219
  config.port = @port
@@ -160,12 +234,13 @@ module FunctionsFramework
160
234
  end
161
235
 
162
236
  ##
163
- # Print the given error message and exit.
164
- # @param message [String]
237
+ # Set the error status.
238
+ # @param message [String] Error message.
239
+ # @param code [Integer] Exit code, defaults to 1.
165
240
  #
166
- def error message
167
- warn message
168
- exit 1
241
+ def error! message, code = 1
242
+ @error_message = message
243
+ @exit_code = code
169
244
  end
170
245
  end
171
246
  end
@@ -18,43 +18,91 @@ module FunctionsFramework
18
18
  #
19
19
  # A function has a name, a type, and an implementation.
20
20
  #
21
+ # ## Function implementations
22
+ #
21
23
  # The implementation in general is an object that responds to the `call`
22
- # method. For a function of type `:http`, the `call` method takes a single
23
- # `Rack::Request` argument and returns one of various HTTP response types.
24
- # See {FunctionsFramework::Registry.add_http}. For a function of type
25
- # `:cloud_event`, the `call` method takes a single
26
- # {FunctionsFramework::CloudEvents::Event CloudEvent} argument, and does not
27
- # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
24
+ # method.
25
+ #
26
+ # * For a function of type `:http`, the `call` method takes a single
27
+ # `Rack::Request` argument and returns one of various HTTP response
28
+ # types. See {FunctionsFramework::Registry.add_http}.
29
+ # * For a function of type `:cloud_event`, the `call` method takes a single
30
+ # [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
31
+ # argument, and does not return a value. See
32
+ # {FunctionsFramework::Registry.add_cloud_event}.
33
+ # * For a function of type `:startup_task`, the `call` method takes a
34
+ # single {FunctionsFramework::Function} argument, and does not return a
35
+ # value. See {FunctionsFramework::Registry.add_startup_task}.
28
36
  #
29
- # If a callable object is provided directly, its `call` method is invoked for
30
- # every function execution. Note that this means it may be called multiple
31
- # times concurrently in separate threads.
37
+ # The implementation can be specified in one of three ways:
32
38
  #
33
- # Alternately, the implementation may be provided as a class that should be
34
- # instantiated to produce a callable object. If a class is provided, it should
35
- # either subclass {FunctionsFramework::Function::CallBase} or respond to the
36
- # same constructor interface, i.e. accepting arbitrary keyword arguments. A
37
- # separate callable object will be instantiated from this class for every
38
- # function invocation, so each instance will be used for only one invocation.
39
+ # * A callable object can be passed in the `callable` keyword argument. The
40
+ # object's `call` method will be invoked for every function execution.
41
+ # Note that this means it may be called multiple times concurrently in
42
+ # separate threads.
43
+ # * A callable _class_ can be passed in the `callable` keyword argument.
44
+ # This class should subclass {FunctionsFramework::Function::Callable} and
45
+ # define the `call` method. A separate instance of this class will be
46
+ # created for each function invocation.
47
+ # * A block can be provided. It will be used to define the `call` method in
48
+ # an anonymous subclass of {FunctionsFramework::Function::Callable}.
49
+ # Thus, providing a block is really just syntactic sugar for providing a
50
+ # class. (This means, for example, that the `return` keyword will work
51
+ # as expected within the block because it is treated as a method.)
39
52
  #
40
- # Finally, an implementation can be provided as a block. If a block is
41
- # provided, it will be recast as a `call` method in an anonymous subclass of
42
- # {FunctionsFramework::Function::CallBase}. Thus, providing a block is really
43
- # just syntactic sugar for providing a class. (This means, for example, that
44
- # the `return` keyword will work within the block because it is treated as a
45
- # method.)
53
+ # When the implementation is provided as a callable class or block, it is
54
+ # executed in the context of a {FunctionsFramework::Function::Callable}
55
+ # object. This object provides a convenience accessor for the Logger, and
56
+ # access to _globals_, which are data defined by the application startup
57
+ # process and available to each function invocation. Typically, globals are
58
+ # used for shared global resources such as service connections and clients.
46
59
  #
47
60
  class Function
61
+ ##
62
+ # Create a new HTTP function definition.
63
+ #
64
+ # @param name [String] The function name
65
+ # @param callable [Class,#call] A callable object or class.
66
+ # @param block [Proc] The function code as a block.
67
+ # @return [FunctionsFramework::Function]
68
+ #
69
+ def self.http name, callable: nil, &block
70
+ new name, :http, callable: callable, &block
71
+ end
72
+
73
+ ##
74
+ # Create a new CloudEvents function definition.
75
+ #
76
+ # @param name [String] The function name
77
+ # @param callable [Class,#call] A callable object or class.
78
+ # @param block [Proc] The function code as a block.
79
+ # @return [FunctionsFramework::Function]
80
+ #
81
+ def self.cloud_event name, callable: nil, &block
82
+ new name, :cloud_event, callable: callable, &block
83
+ end
84
+
85
+ ##
86
+ # Create a new startup task function definition.
87
+ #
88
+ # @param callable [Class,#call] A callable object or class.
89
+ # @param block [Proc] The function code as a block.
90
+ # @return [FunctionsFramework::Function]
91
+ #
92
+ def self.startup_task callable: nil, &block
93
+ new nil, :startup_task, callable: callable, &block
94
+ end
95
+
48
96
  ##
49
97
  # Create a new function definition.
50
98
  #
51
99
  # @param name [String] The function name
52
- # @param type [Symbol] The type of function. Valid types are `:http` and
53
- # `:cloud_event`.
100
+ # @param type [Symbol] The type of function. Valid types are `:http`,
101
+ # `:cloud_event`, and `:startup_task`.
54
102
  # @param callable [Class,#call] A callable object or class.
55
103
  # @param block [Proc] The function code as a block.
56
104
  #
57
- def initialize name, type, callable = nil, &block
105
+ def initialize name, type, callable: nil, &block
58
106
  @name = name
59
107
  @type = type
60
108
  @callable = @callable_class = nil
@@ -63,7 +111,7 @@ module FunctionsFramework
63
111
  elsif callable.is_a? ::Class
64
112
  @callable_class = callable
65
113
  elsif block_given?
66
- @callable_class = ::Class.new CallBase do
114
+ @callable_class = ::Class.new Callable do
67
115
  define_method :call, &block
68
116
  end
69
117
  else
@@ -82,18 +130,38 @@ module FunctionsFramework
82
130
  attr_reader :type
83
131
 
84
132
  ##
85
- # Get a callable for performing a function invocation. This will either
86
- # return the singleton callable object, or instantiate a new callable from
87
- # the configured class.
88
- #
89
- # @param logger [::Logger] The logger for use by function executions. This
90
- # may or may not be used by the callable.
91
- # @return [#call]
92
- #
93
- def new_call logger: nil
94
- return @callable unless @callable.nil?
95
- logger ||= FunctionsFramework.logger
96
- @callable_class.new logger: logger, function_name: name, function_type: type
133
+ # Populate the given globals hash with this function's info.
134
+ #
135
+ # @param globals [Hash] Initial globals hash (optional).
136
+ # @return [Hash] A new globals hash with this function's info included.
137
+ #
138
+ def populate_globals globals = nil
139
+ result = { function_name: name, function_type: type }
140
+ result.merge! globals if globals
141
+ result
142
+ end
143
+
144
+ ##
145
+ # Call the function given a set of arguments. Set the given logger and/or
146
+ # globals in the context if the callable supports it.
147
+ #
148
+ # If the given arguments exceeds what the function will accept, the args
149
+ # are silently truncated. However, if the function requires more arguments
150
+ # than are provided, an ArgumentError is raised.
151
+ #
152
+ # @param args [Array] Argument to pass to the function.
153
+ # @param logger [Logger] Logger for use by function executions.
154
+ # @param globals [Hash] Globals for the function execution context
155
+ # @return [Object] The function return value.
156
+ #
157
+ def call *args, globals: nil, logger: nil
158
+ callable = @callable || @callable_class.new(globals: globals, logger: logger)
159
+ params = callable.method(:call).parameters.map(&:first)
160
+ unless params.include? :rest
161
+ max_params = params.count(:req) + params.count(:opt)
162
+ args = args.take max_params
163
+ end
164
+ callable.call(*args)
97
165
  end
98
166
 
99
167
  ##
@@ -101,29 +169,56 @@ module FunctionsFramework
101
169
  #
102
170
  # An object of this class is `self` while a function block is running.
103
171
  #
104
- class CallBase
172
+ class Callable
105
173
  ##
106
174
  # Create a callable object with the given context.
107
175
  #
108
- # @param context [keywords] A set of context arguments. See {#context} for
109
- # a list of keys that will generally be passed in. However,
110
- # implementations should be prepared to accept any abritrary keys.
176
+ # @param globals [Hash] A set of globals available to the call.
177
+ # @param logger [Logger] A logger for use by the function call.
111
178
  #
112
- def initialize **context
113
- @context = context
179
+ def initialize globals: nil, logger: nil
180
+ @__globals = globals || {}
181
+ @__logger = logger || FunctionsFramework.logger
114
182
  end
115
183
 
116
184
  ##
117
- # A keyed hash of context information. Common context keys include:
185
+ # Get the given named global.
186
+ #
187
+ # For most function calls, the following globals will be defined:
118
188
  #
119
- # * **:logger** (`Logger`) A logger for use by this function call.
120
189
  # * **:function_name** (`String`) The name of the running function.
121
190
  # * **:function_type** (`Symbol`) The type of the running function,
122
191
  # either `:http` or `:cloud_event`.
123
192
  #
124
- # @return [Hash]
193
+ # You can also set additional globals from a startup task.
194
+ #
195
+ # @param key [Symbol,String] The name of the global to get.
196
+ # @return [Object]
125
197
  #
126
- attr_reader :context
198
+ def global key
199
+ @__globals[key]
200
+ end
201
+
202
+ ##
203
+ # Set a global. This can be called from startup tasks, but the globals
204
+ # are frozen when the server starts, so this call will raise an exception
205
+ # if called from a normal function.
206
+ #
207
+ # @param key [Symbol,String]
208
+ # @param value [Object]
209
+ #
210
+ def set_global key, value
211
+ @__globals[key] = value
212
+ end
213
+
214
+ ##
215
+ # A logger for use by this call.
216
+ #
217
+ # @return [Logger]
218
+ #
219
+ def logger
220
+ @__logger
221
+ end
127
222
  end
128
223
  end
129
224
  end