functions_framework 0.4.0 → 0.6.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.
@@ -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
@@ -23,8 +23,9 @@ module FunctionsFramework
23
23
  # `Rack::Request` argument and returns one of various HTTP response types.
24
24
  # See {FunctionsFramework::Registry.add_http}. For a function of type
25
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}.
26
+ # [CloudEvent](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event)
27
+ # argument, and does not return a value.
28
+ # See {FunctionsFramework::Registry.add_cloud_event}.
28
29
  #
29
30
  # If a callable object is provided directly, its `call` method is invoked for
30
31
  # every function execution. Note that this means it may be called multiple
@@ -23,19 +23,17 @@ module FunctionsFramework
23
23
  # Decode an event from the given Rack environment hash.
24
24
  #
25
25
  # @param env [Hash] The Rack environment
26
- # @return [FunctionsFramework::CloudEvents::Event] if the request could
27
- # be converted
26
+ # @return [::CloudEvents::Event] if the request could be converted
28
27
  # @return [nil] if the event format was not recognized.
29
28
  #
30
29
  def decode_rack_env env
31
- content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
30
+ content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
32
31
  return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
33
32
  input = read_input_json env["rack.input"], content_type.charset
34
33
  return nil unless input
35
- raw_context = input["context"] || input
36
- context = normalized_context raw_context
34
+ context = normalized_context input
37
35
  return nil unless context
38
- construct_cloud_event context, input["data"]
36
+ construct_cloud_event context, input["data"], content_type.charset
39
37
  end
40
38
 
41
39
  private
@@ -50,27 +48,27 @@ module FunctionsFramework
50
48
  nil
51
49
  end
52
50
 
53
- def normalized_context raw_context
54
- id = raw_context["eventId"]
55
- return nil unless id
56
- timestamp = raw_context["timestamp"]
57
- return nil unless timestamp
58
- type = raw_context["eventType"]
59
- return nil unless type
60
- service, resource = analyze_resource raw_context["resource"], type
61
- return nil unless service && resource
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"]
57
+ service ||= service_from_type type
58
+ return nil unless id && timestamp && type && service && resource
62
59
  { id: id, timestamp: timestamp, type: type, service: service, resource: resource }
63
60
  end
64
61
 
65
- def analyze_resource raw_resource, type
62
+ def analyze_resource raw_resource
63
+ service = resource = nil
66
64
  case raw_resource
67
65
  when ::Hash
68
- [raw_resource["service"], raw_resource["name"]]
66
+ service = raw_resource["service"]
67
+ resource = raw_resource["name"]
69
68
  when ::String
70
- [service_from_type(type), raw_resource]
71
- else
72
- [nil, nil]
69
+ resource = raw_resource
73
70
  end
71
+ [service, resource]
74
72
  end
75
73
 
76
74
  def service_from_type type
@@ -80,19 +78,20 @@ module FunctionsFramework
80
78
  nil
81
79
  end
82
80
 
83
- def construct_cloud_event context, data
81
+ def construct_cloud_event context, data, charset
84
82
  source, subject = convert_source context[:service], context[:resource]
85
83
  type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
86
84
  return nil unless type && source
87
85
  ce_data = convert_data context[:service], data
88
- CloudEvents::Event.new id: context[:id],
89
- source: source,
90
- type: type,
91
- spec_version: "1.0",
92
- data_content_type: "application/json",
93
- data: ce_data,
94
- subject: subject,
95
- time: context[:timestamp]
86
+ content_type = "application/json; charset=#{charset}"
87
+ ::CloudEvents::Event.new id: context[:id],
88
+ source: source,
89
+ type: type,
90
+ spec_version: "1.0",
91
+ data_content_type: content_type,
92
+ data: ce_data,
93
+ subject: subject,
94
+ time: context[:timestamp]
96
95
  end
97
96
 
98
97
  def convert_source service, resource
@@ -25,8 +25,9 @@ module FunctionsFramework
25
25
  # Create a new empty registry.
26
26
  #
27
27
  def initialize
28
- super()
28
+ @mutex = ::Monitor.new
29
29
  @functions = {}
30
+ @start_tasks = []
30
31
  end
31
32
 
32
33
  ##
@@ -37,7 +38,7 @@ module FunctionsFramework
37
38
  # @return [nil] if the function is not found
38
39
  #
39
40
  def [] name
40
- @functions[name.to_s]
41
+ @mutex.synchronize { @functions[name.to_s] }
41
42
  end
42
43
 
43
44
  ##
@@ -46,7 +47,21 @@ module FunctionsFramework
46
47
  # @return [Array<String>]
47
48
  #
48
49
  def names
49
- @functions.keys.sort
50
+ @mutex.synchronize { @functions.keys.sort }
51
+ end
52
+
53
+ ##
54
+ # Run all startup tasks.
55
+ #
56
+ # @param server [FunctionsFramework::Server] The server that is starting.
57
+ # @return [self]
58
+ #
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
50
65
  end
51
66
 
52
67
  ##
@@ -68,7 +83,7 @@ module FunctionsFramework
68
83
  #
69
84
  def add_http name, &block
70
85
  name = name.to_s
71
- synchronize do
86
+ @mutex.synchronize do
72
87
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
73
88
  @functions[name] = Function.new name, :http, &block
74
89
  end
@@ -80,7 +95,8 @@ module FunctionsFramework
80
95
  #
81
96
  # You must provide a name for the function, and a block that implemets the
82
97
  # function. The block should take _one_ argument: the event object of type
83
- # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
98
+ # [`CloudEvents::Event`](https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event).
99
+ # Any return value is ignored.
84
100
  #
85
101
  # @param name [String] The function name
86
102
  # @param block [Proc] The function code as a proc
@@ -88,11 +104,29 @@ module FunctionsFramework
88
104
  #
89
105
  def add_cloud_event name, &block
90
106
  name = name.to_s
91
- synchronize do
107
+ @mutex.synchronize do
92
108
  raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
93
109
  @functions[name] = Function.new name, :cloud_event, &block
94
110
  end
95
111
  self
96
112
  end
113
+
114
+ ##
115
+ # Add a startup task.
116
+ #
117
+ # 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.
121
+ #
122
+ # @param block [Proc] The startup task
123
+ # @return [self]
124
+ #
125
+ def add_startup_task &block
126
+ @mutex.synchronize do
127
+ @start_tasks << block
128
+ end
129
+ self
130
+ end
97
131
  end
98
132
  end
@@ -82,9 +82,9 @@ module FunctionsFramework
82
82
  @server.max_threads = @config.max_threads
83
83
  @server.leak_stack_on_error = @config.show_error_details?
84
84
  @server.binder.add_tcp_listener @config.bind_addr, @config.port
85
- @server.run true
86
85
  @config.logger.info "FunctionsFramework: Serving function #{@function.name.inspect}" \
87
86
  " on port #{@config.port}..."
87
+ @server.run true
88
88
  end
89
89
  end
90
90
  self
@@ -149,8 +149,12 @@ module FunctionsFramework
149
149
  ::Signal.trap "SIGINT" do
150
150
  Server.signal_enqueue "SIGINT", @config.logger, @server
151
151
  end
152
- ::Signal.trap "SIGHUP" do
153
- Server.signal_enqueue "SIGHUP", @config.logger, @server
152
+ begin
153
+ ::Signal.trap "SIGHUP" do
154
+ Server.signal_enqueue "SIGHUP", @config.logger, @server
155
+ end
156
+ rescue ::ArgumentError # rubocop:disable Lint/HandleExceptions
157
+ # Not available on all systems
154
158
  end
155
159
  @signals_installed = true
156
160
  end
@@ -340,7 +344,7 @@ module FunctionsFramework
340
344
  string_response response, "text/plain", 200
341
345
  when ::Hash
342
346
  string_response ::JSON.dump(response), "application/json", 200
343
- when CloudEvents::CloudEventsError
347
+ when ::CloudEvents::CloudEventsError
344
348
  cloud_events_error_response response
345
349
  when ::StandardError
346
350
  error_response "#{response.class}: #{response.message}\n#{response.backtrace}\n"
@@ -401,7 +405,7 @@ module FunctionsFramework
401
405
  def initialize function, config
402
406
  super config
403
407
  @function = function
404
- @cloud_events = CloudEvents::HttpBinding.default
408
+ @cloud_events = ::CloudEvents::HttpBinding.default
405
409
  @legacy_events = LegacyEventConverter.new
406
410
  end
407
411
 
@@ -411,11 +415,11 @@ module FunctionsFramework
411
415
  event = decode_event env
412
416
  response =
413
417
  case event
414
- when CloudEvents::Event
418
+ when ::CloudEvents::Event
415
419
  handle_cloud_event event, logger
416
420
  when ::Array
417
- CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
418
- when CloudEvents::CloudEventsError
421
+ ::CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
422
+ when ::CloudEvents::CloudEventsError
419
423
  event
420
424
  else
421
425
  raise "Unexpected event type: #{event.class}"
@@ -428,8 +432,8 @@ module FunctionsFramework
428
432
  def decode_event env
429
433
  @cloud_events.decode_rack_env(env) ||
430
434
  @legacy_events.decode_rack_env(env) ||
431
- raise(CloudEvents::HttpContentError, "Unrecognized event format")
432
- rescue CloudEvents::CloudEventsError => e
435
+ raise(::CloudEvents::HttpContentError, "Unrecognized event format")
436
+ rescue ::CloudEvents::CloudEventsError => e
433
437
  e
434
438
  end
435
439