lib/functions_framework/function.rb (87 lines of code) (raw):

# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module FunctionsFramework ## # Representation of a function. # # A function has a name, a type, and an implementation. # # ## Function implementations # # The implementation in general is an object that responds to the `call` # method. # # * For a function of type `:http`, the `call` method takes a single # `Rack::Request` argument and returns one of various HTTP response # types. See {FunctionsFramework::Registry.add_http}. # * For a function of type `:cloud_event`, the `call` method takes a single # [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event) # argument, and does not return a value. See # {FunctionsFramework::Registry.add_cloud_event}. # * For a function of type `:startup_task`, the `call` method takes a # single {FunctionsFramework::Function} argument, and does not return a # value. See {FunctionsFramework::Registry.add_startup_task}. # # The implementation can be specified in one of three ways: # # * A callable object can be passed in the `callable` keyword argument. The # object's `call` method will be invoked for every function execution. # Note that this means it may be called multiple times concurrently in # separate threads. # * A callable _class_ can be passed in the `callable` keyword argument. # This class should subclass {FunctionsFramework::Function::Callable} and # define the `call` method. A separate instance of this class will be # created for each function invocation. # * A block can be provided. It will be used to define the `call` method in # an anonymous subclass of {FunctionsFramework::Function::Callable}. # Thus, providing a block is really just syntactic sugar for providing a # class. (This means, for example, that the `return` keyword will work # as expected within the block because it is treated as a method.) # # When the implementation is provided as a callable class or block, it is # executed in the context of a {FunctionsFramework::Function::Callable} # object. This object provides a convenience accessor for the Logger, and # access to _globals_, which are data defined by the application startup # process and available to each function invocation. Typically, globals are # used for shared global resources such as service connections and clients. # class Function ## # Create a new HTTP function definition. # # @param name [String] The function name # @param callable [Class,#call] A callable object or class. # @param block [Proc] The function code as a block. # @return [FunctionsFramework::Function] # def self.http name, callable: nil, &block new name, :http, callable: callable, &block end ## # Create a new Typed function definition. # # @param name [String] The function name # @param callable [Class,#call] A callable object or class. # @param request_class [#decode_json] A class that can be read from JSON. # @param block [Proc] The function code as a block. # @return [FunctionsFramework::Function] # def self.typed name, request_class: nil, callable: nil, &block if request_class && !(request_class.respond_to? :decode_json) raise ::ArgumentError, "Type does not implement 'decode_json' class method" end new name, :typed, callable: callable, request_class: request_class, &block end ## # Create a new CloudEvents function definition. # # @param name [String] The function name # @param callable [Class,#call] A callable object or class. # @param block [Proc] The function code as a block. # @return [FunctionsFramework::Function] # def self.cloud_event name, callable: nil, &block new name, :cloud_event, callable: callable, &block end ## # Create a new startup task function definition. # # @param callable [Class,#call] A callable object or class. # @param block [Proc] The function code as a block. # @return [FunctionsFramework::Function] # def self.startup_task callable: nil, &block new nil, :startup_task, callable: callable, &block end ## # Create a new function definition. # # @param name [String] The function name # @param type [Symbol] The type of function. Valid types are `:http`, # `:cloud_event`, and `:startup_task`. # @param callable [Class,#call] A callable object or class. # @param block [Proc] The function code as a block. # def initialize name, type, callable: nil, request_class: nil, &block @name = name @type = type @request_class = request_class @callable = @callable_class = nil if callable.respond_to? :call @callable = callable elsif callable.is_a? ::Class @callable_class = callable elsif block_given? @callable_class = ::Class.new Callable do define_method :call, &block end else raise ::ArgumentError, "No callable given for function" end end ## # @return [String] The function name # attr_reader :name ## # @return [Symbol] The function type # attr_reader :type ## # @return [#decode_json] The class for the request parameter. Only used for typed functions. # attr_reader :request_class ## # Populate the given globals hash with this function's info. # # @param globals [Hash] Initial globals hash (optional). # @return [Hash] A new globals hash with this function's info included. # def populate_globals globals = nil result = { function_name: name, function_type: type } result.merge! globals if globals result end ## # Call the function given a set of arguments. Set the given logger and/or # globals in the context if the callable supports it. # # If the given arguments exceeds what the function will accept, the args # are silently truncated. However, if the function requires more arguments # than are provided, an ArgumentError is raised. # # @param args [Array] Argument to pass to the function. # @param logger [Logger] Logger for use by function executions. # @param globals [Hash] Globals for the function execution context # @return [Object] The function return value. # def call *args, globals: nil, logger: nil callable = @callable || @callable_class.new(globals: globals, logger: logger) params = callable.method(:call).parameters.map(&:first) unless params.include? :rest max_params = params.count(:req) + params.count(:opt) args = args.take max_params end callable.call(*args) end ## # A lazy evaluator for a global # @private # class LazyGlobal def initialize block @block = block @value = nil @mutex = ::Mutex.new end def value @mutex.synchronize do if @block @value = @block.call @block = nil end @value end end end ## # A base class for a callable object that provides calling context. # # An object of this class is `self` while a function block is running. # class Callable ## # Create a callable object with the given context. # # @param globals [Hash] A set of globals available to the call. # @param logger [Logger] A logger for use by the function call. # def initialize globals: nil, logger: nil @__globals = globals || {} @__logger = logger || FunctionsFramework.logger end ## # Get the given named global. # # For most function calls, the following globals will be defined: # # * **:function_name** (`String`) The name of the running function. # * **:function_type** (`Symbol`) The type of the running function, # either `:http` or `:cloud_event`. # # You can also set additional globals from a startup task. # # @param key [Symbol,String] The name of the global to get. # @return [Object] # def global key value = @__globals[key] value = value.value if LazyGlobal === value value end ## # Set a global. This can be called from startup tasks, but the globals # are frozen when the server starts, so this call will raise an exception # if called from a normal function. # # You can set a global to a final value, or you can provide a block that # lazily computes the global the first time it is requested. # # @overload set_global(key, value) # Set the given global to the given value. For example: # # set_global(:project_id, "my-project-id") # # @param key [Symbol,String] # @param value [Object] # @return [self] # # @overload set_global(key, &block) # Call the given block to compute the global's value only when the # value is actually requested. This block will be called at most once, # and its result reused for subsequent calls. For example: # # set_global(:connection_pool) do # ExpensiveConnectionPool.new # end # # @param key [Symbol,String] # @param block [Proc] A block that lazily computes a value # @yieldreturn [Object] The value # @return [self] # def set_global key, value = nil, &block @__globals[key] = block ? LazyGlobal.new(block) : value self end ## # A logger for use by this call. # # @return [Logger] # def logger @__logger end end end end