functions_framework 0.1.1 → 0.2.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.
@@ -0,0 +1,122 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "base64"
16
+ require "json"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ ##
21
+ # An implementation of JSON format and JSON batch format.
22
+ #
23
+ # See https://github.com/cloudevents/spec/blob/master/json-format.md
24
+ #
25
+ class JsonFormat
26
+ ##
27
+ # Decode an event from the given input JSON string.
28
+ #
29
+ # @param json [String] A JSON-formatted string
30
+ # @return [FunctionsFramework::CloudEvents::Event]
31
+ #
32
+ def decode json, **_other_kwargs
33
+ structure = ::JSON.parse json
34
+ decode_hash_structure structure
35
+ end
36
+
37
+ ##
38
+ # Encode an event to a JSON string.
39
+ #
40
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
41
+ # @param sort [boolean] Whether to sort keys of the JSON output.
42
+ # @return [String] The JSON representation.
43
+ #
44
+ def encode event, sort: false, **_other_kwargs
45
+ structure = encode_hash_structure event
46
+ structure = sort_keys structure if sort
47
+ ::JSON.dump structure
48
+ end
49
+
50
+ ##
51
+ # Decode a batch of events from the given input string.
52
+ #
53
+ # @param json [String] A JSON-formatted string
54
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
55
+ #
56
+ def decode_batch json, **_other_kwargs
57
+ structure_array = Array(::JSON.parse(json))
58
+ structure_array.map do |structure|
59
+ decode_hash_structure structure
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Encode a batch of event to a JSON string.
65
+ #
66
+ # @param events [Array<FunctionsFramework::CloudEvents::Event>] An array
67
+ # of input events.
68
+ # @param sort [boolean] Whether to sort keys of the JSON output.
69
+ # @return [String] The JSON representation.
70
+ #
71
+ def encode_batch events, sort: false, **_other_kwargs
72
+ structure_array = Array(events).map do |event|
73
+ structure = encode_hash_structure event
74
+ sort ? sort_keys(structure) : structure
75
+ end
76
+ ::JSON.dump structure_array
77
+ end
78
+
79
+ ##
80
+ # Decode a single event from a hash data structure with keys and types
81
+ # conforming to the JSON event format.
82
+ #
83
+ # @param structure [Hash] An input hash.
84
+ # @return [FunctionsFramework::CloudEvents::Event]
85
+ #
86
+ def decode_hash_structure structure
87
+ if structure.key? "data_base64"
88
+ structure = structure.dup
89
+ structure["data"] = ::Base64.decode64 structure.delete "data_base64"
90
+ end
91
+ Event.create spec_version: structure["specversion"], attributes: structure
92
+ end
93
+
94
+ ##
95
+ # Encode a single event to a hash data structure with keys and types
96
+ # conforming to the JSON event format.
97
+ #
98
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
99
+ # @return [String] The hash structure.
100
+ #
101
+ def encode_hash_structure event
102
+ structure = event.to_h
103
+ data = structure["data"]
104
+ if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
105
+ structure.delete "data"
106
+ structure["data_base64"] = ::Base64.encode64 data
107
+ end
108
+ structure
109
+ end
110
+
111
+ private
112
+
113
+ def sort_keys hash
114
+ result = {}
115
+ hash.keys.sort.each do |key|
116
+ result[key] = hash[key]
117
+ end
118
+ result
119
+ end
120
+ end
121
+ end
122
+ end
@@ -54,11 +54,9 @@ module FunctionsFramework
54
54
  #
55
55
  # * A `:http` type function takes a `Rack::Request` argument, and returns
56
56
  # a Rack response type. See {FunctionsFramework::Registry.add_http}.
57
- # * A `:event` or `:cloud_event` type function takes a
57
+ # * A `:cloud_event` type function takes a
58
58
  # {FunctionsFramework::CloudEvents::Event} argument, and does not
59
59
  # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
60
- # Note that for an `:event` type function, the passed event argument is
61
- # split into two arguments when passed to the underlying block.
62
60
  #
63
61
  # @param argument [Rack::Request,FunctionsFramework::CloudEvents::Event]
64
62
  # @return [Object]
@@ -0,0 +1,136 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "json"
16
+
17
+ module FunctionsFramework
18
+ ##
19
+ # Converter from legacy GCF event formats to CloudEvents.
20
+ #
21
+ class LegacyEventConverter
22
+ ##
23
+ # Decode an event from the given Rack environment hash.
24
+ #
25
+ # @param env [Hash] The Rack environment
26
+ # @return [FunctionsFramework::CloudEvents::Event] if the request could
27
+ # be converted
28
+ # @return [nil] if the event format was not recognized.
29
+ #
30
+ def decode_rack_env env
31
+ content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
32
+ return nil unless content_type.media_type == "application" && content_type.subtype_prefix == "json"
33
+ input = read_input_json env["rack.input"], content_type.charset
34
+ return nil unless input
35
+ raw_context = input["context"] || input
36
+ context = normalized_context raw_context
37
+ return nil unless context
38
+ construct_cloud_event context, input["data"]
39
+ end
40
+
41
+ private
42
+
43
+ def read_input_json input, charset
44
+ input = input.read if input.respond_to? :read
45
+ input = input.encode charset if charset
46
+ content = ::JSON.parse input
47
+ content = nil unless content.is_a? ::Hash
48
+ content
49
+ rescue ::JSON::ParserError
50
+ nil
51
+ end
52
+
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
62
+ { id: id, timestamp: timestamp, type: type, service: service, resource: resource }
63
+ end
64
+
65
+ def analyze_resource raw_resource, type
66
+ case raw_resource
67
+ when ::Hash
68
+ [raw_resource["service"], raw_resource["name"]]
69
+ when ::String
70
+ [service_from_type(type), raw_resource]
71
+ else
72
+ [nil, nil]
73
+ end
74
+ end
75
+
76
+ def service_from_type type
77
+ LEGACY_TYPE_TO_SERVICE.each do |pattern, service|
78
+ return service if pattern =~ type
79
+ end
80
+ nil
81
+ end
82
+
83
+ def construct_cloud_event context, data
84
+ source, subject = convert_source context[:service], context[:resource]
85
+ type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
86
+ return nil unless type && source
87
+ CloudEvents::Event.new id: context[:id],
88
+ source: source,
89
+ type: type,
90
+ spec_version: "1.0",
91
+ data_content_type: "application/json",
92
+ data: data,
93
+ subject: subject,
94
+ time: context[:timestamp]
95
+ end
96
+
97
+ def convert_source service, resource
98
+ if service == "storage.googleapis.com"
99
+ match = %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}.match resource
100
+ return [nil, nil] unless match
101
+ ["//#{service}/#{match[1]}", match[2]]
102
+ else
103
+ ["//#{service}/#{resource}", nil]
104
+ end
105
+ end
106
+
107
+ LEGACY_TYPE_TO_SERVICE = {
108
+ %r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
109
+ %r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
110
+ %r{^providers/cloud\.storage/} => "storage.googleapis.com",
111
+ %r{^providers/firebase\.auth/} => "firebase.googleapis.com",
112
+ %r{^providers/google\.firebase} => "firebase.googleapis.com"
113
+ }.freeze
114
+
115
+ LEGACY_TYPE_TO_CE_TYPE = {
116
+ "google.pubsub.topic.publish" => "google.cloud.pubsub.topic.v1.messagePublished",
117
+ "providers/cloud.pubsub/eventTypes/topic.publish" => "google.cloud.pubsub.topic.v1.messagePublished",
118
+ "google.storage.object.finalize" => "google.cloud.storage.object.v1.finalized",
119
+ "google.storage.object.delete" => "google.cloud.storage.object.v1.deleted",
120
+ "google.storage.object.archive" => "google.cloud.storage.object.v1.archived",
121
+ "google.storage.object.metadataUpdate" => "google.cloud.storage.object.v1.metadataUpdated",
122
+ "providers/cloud.firestore/eventTypes/document.write" => "google.cloud.firestore.document.v1.written",
123
+ "providers/cloud.firestore/eventTypes/document.create" => "google.cloud.firestore.document.v1.created",
124
+ "providers/cloud.firestore/eventTypes/document.update" => "google.cloud.firestore.document.v1.updated",
125
+ "providers/cloud.firestore/eventTypes/document.delete" => "google.cloud.firestore.document.v1.deleted",
126
+ "providers/firebase.auth/eventTypes/user.create" => "google.firebase.auth.user.v1.created",
127
+ "providers/firebase.auth/eventTypes/user.delete" => "google.firebase.auth.user.v1.deleted",
128
+ "providers/google.firebase.analytics/eventTypes/event.log" => "google.firebase.analytics.log.v1.written",
129
+ "providers/google.firebase.database/eventTypes/ref.create" => "google.firebase.database.document.v1.created",
130
+ "providers/google.firebase.database/eventTypes/ref.write" => "google.firebase.database.document.v1.written",
131
+ "providers/google.firebase.database/eventTypes/ref.update" => "google.firebase.database.document.v1.updated",
132
+ "providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
133
+ "providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
134
+ }.freeze
135
+ end
136
+ end
@@ -76,31 +76,10 @@ module FunctionsFramework
76
76
  end
77
77
 
78
78
  ##
79
- # Add a CloudEvent function to the registry.
80
- #
81
- # You must provide a name for the function, and a block that implemets the
82
- # function. The block should take two arguments: the event _data_ and the
83
- # event _context_. Any return value is ignored.
84
- #
85
- # The event data argument will be one of the following types:
86
- # * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
87
- # binary data. You may choose to perform additional interpretation of
88
- # the binary data using information in the content type provided by the
89
- # context argument.
90
- # * Any data type that can be represented in JSON (i.e. `String`,
91
- # `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
92
- # came with a JSON payload. The content type may also be set in the
93
- # context if the data is a String.
94
- #
95
- # The context argument will be of type {FunctionsFramework::CloudEvents::Event},
96
- # and will contain CloudEvents context attributes such as `id` and `type`.
79
+ # This is an obsolete interface that defines an event function taking two
80
+ # arguments (data and context) rather than one.
97
81
  #
98
- # See also {#add_cloud_event} which creates a function that takes a single
99
- # argument of type {FunctionsFramework::CloudEvents::Event}.
100
- #
101
- # @param name [String] The function name
102
- # @param block [Proc] The function code as a proc
103
- # @return [self]
82
+ # @deprecated Use {Registry#add_cloud_event} instead.
104
83
  #
105
84
  def add_event name, &block
106
85
  name = name.to_s
@@ -118,9 +97,6 @@ module FunctionsFramework
118
97
  # function. The block should take _one_ argument: the event object of type
119
98
  # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
120
99
  #
121
- # See also {#add_event} which creates a function that takes data and
122
- # context as separate arguments.
123
- #
124
100
  # @param name [String] The function name
125
101
  # @param block [Proc] The function code as a proc
126
102
  # @return [self]
@@ -212,7 +212,7 @@ module FunctionsFramework
212
212
  # @param bind_addr [String,nil]
213
213
  #
214
214
  def bind_addr= bind_addr
215
- @bind_addr = bind_addr || ::ENV["BIND_ADDR"] || "0.0.0.0"
215
+ @bind_addr = bind_addr || ::ENV["FUNCTION_BIND_ADDR"] || "0.0.0.0"
216
216
  end
217
217
 
218
218
  ##
@@ -228,7 +228,7 @@ module FunctionsFramework
228
228
  # @param min_threads [Integer,nil]
229
229
  #
230
230
  def min_threads= min_threads
231
- @min_threads = (min_threads || ::ENV["MIN_THREADS"])&.to_i
231
+ @min_threads = (min_threads || ::ENV["FUNCTION_MIN_THREADS"])&.to_i
232
232
  end
233
233
 
234
234
  ##
@@ -236,7 +236,7 @@ module FunctionsFramework
236
236
  # @param max_threads [Integer,nil]
237
237
  #
238
238
  def max_threads= max_threads
239
- @max_threads = (max_threads || ::ENV["MAX_THREADS"])&.to_i
239
+ @max_threads = (max_threads || ::ENV["FUNCTION_MAX_THREADS"])&.to_i
240
240
  end
241
241
 
242
242
  ##
@@ -244,8 +244,12 @@ module FunctionsFramework
244
244
  # @param show_error_details [Boolean,nil]
245
245
  #
246
246
  def show_error_details= show_error_details
247
- val = show_error_details.nil? ? ::ENV["DETAILED_ERRORS"] : show_error_details
248
- @show_error_details = val ? true : false
247
+ @show_error_details =
248
+ if show_error_details.nil?
249
+ !::ENV["FUNCTION_DETAILED_ERRORS"].to_s.empty?
250
+ else
251
+ show_error_details ? true : false
252
+ end
249
253
  end
250
254
 
251
255
  ##
@@ -315,15 +319,15 @@ module FunctionsFramework
315
319
 
316
320
  ## @private
317
321
  class AppBase
318
- BLACKLISTED_PATHS = ["/favicon.ico", "/robots.txt"].freeze
322
+ EXCLUDED_PATHS = ["/favicon.ico", "/robots.txt"].freeze
319
323
 
320
324
  def initialize config
321
325
  @config = config
322
326
  end
323
327
 
324
- def blacklisted_path? env
328
+ def excluded_path? env
325
329
  path = env[::Rack::SCRIPT_NAME].to_s + env[::Rack::PATH_INFO].to_s
326
- BLACKLISTED_PATHS.include? path
330
+ EXCLUDED_PATHS.include? path
327
331
  end
328
332
 
329
333
  def interpret_response response
@@ -335,15 +339,13 @@ module FunctionsFramework
335
339
  when ::String
336
340
  string_response response, "text/plain", 200
337
341
  when ::Hash
338
- json = ::JSON.dump response
339
- string_response json, "application/json", 200
342
+ string_response ::JSON.dump(response), "application/json", 200
343
+ when CloudEvents::CloudEventsError
344
+ cloud_events_error_response response
340
345
  when ::StandardError
341
- error = error_message response
342
- string_response error, "text/plain", 500
346
+ error_response "#{response.class}: #{response.message}\n#{response.backtrace}\n"
343
347
  else
344
- e = ::StandardError.new "Unexpected response type: #{response.class}"
345
- error = error_message e
346
- string_response error, "text/plain", 500
348
+ error_response "Unexpected response type: #{response.class}"
347
349
  end
348
350
  end
349
351
 
@@ -359,20 +361,15 @@ module FunctionsFramework
359
361
  [status, headers, [string]]
360
362
  end
361
363
 
362
- def error_message error
363
- if @config.show_error_details?
364
- "#{error.class}: #{error.message}\n#{error.backtrace}\n"
365
- else
366
- "Unexpected internal error"
367
- end
364
+ def cloud_events_error_response error
365
+ @config.logger.warn error
366
+ string_response "#{error.class}: #{error.message}", "text/plain", 400
368
367
  end
369
368
 
370
- def usage_message error
371
- if @config.show_error_details?
372
- "Failed to decode CloudEvent: #{error.inspect}"
373
- else
374
- "Failed to decode CloudEvent"
375
- end
369
+ def error_response message
370
+ @config.logger.error message
371
+ message = "Unexpected internal error" unless @config.show_error_details?
372
+ string_response message, "text/plain", 500
376
373
  end
377
374
  end
378
375
 
@@ -384,7 +381,7 @@ module FunctionsFramework
384
381
  end
385
382
 
386
383
  def call env
387
- return notfound_response if blacklisted_path? env
384
+ return notfound_response if excluded_path? env
388
385
  response =
389
386
  begin
390
387
  logger = env["rack.logger"] = @config.logger
@@ -392,7 +389,6 @@ module FunctionsFramework
392
389
  logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
393
390
  @function.call request
394
391
  rescue ::StandardError => e
395
- logger.warn e
396
392
  e
397
393
  end
398
394
  interpret_response response
@@ -404,33 +400,45 @@ module FunctionsFramework
404
400
  def initialize function, config
405
401
  super config
406
402
  @function = function
403
+ @cloud_events = CloudEvents::HttpBinding.default
404
+ @legacy_events = LegacyEventConverter.new
407
405
  end
408
406
 
409
407
  def call env
410
- return notfound_response if blacklisted_path? env
408
+ return notfound_response if excluded_path? env
411
409
  logger = env["rack.logger"] = @config.logger
412
- event =
413
- begin
414
- CloudEvents.decode_rack_env env
415
- rescue ::StandardError => e
416
- e
417
- end
410
+ event = decode_event env
418
411
  response =
419
- if event.is_a? CloudEvents::Event
420
- logger.info "FunctionsFramework: Handling CloudEvent"
421
- begin
422
- @function.call event
423
- "ok"
424
- rescue ::StandardError => e
425
- logger.warn e
426
- e
427
- end
412
+ case event
413
+ when CloudEvents::Event
414
+ handle_cloud_event event, logger
415
+ when ::Array
416
+ CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
417
+ when CloudEvents::CloudEventsError
418
+ event
428
419
  else
429
- logger.warn e.inspect
430
- string_response usage_message(e), "text/plain", 400
420
+ raise "Unexpected event type: #{event.class}"
431
421
  end
432
422
  interpret_response response
433
423
  end
424
+
425
+ private
426
+
427
+ def decode_event env
428
+ @cloud_events.decode_rack_env(env) ||
429
+ @legacy_events.decode_rack_env(env) ||
430
+ raise(CloudEvents::HttpContentError, "Unrecognized event format")
431
+ rescue CloudEvents::CloudEventsError => e
432
+ e
433
+ end
434
+
435
+ def handle_cloud_event event, logger
436
+ logger.info "FunctionsFramework: Handling CloudEvent"
437
+ @function.call event
438
+ "ok"
439
+ rescue ::StandardError => e
440
+ e
441
+ end
434
442
  end
435
443
  end
436
444
  end