functions_framework 0.7.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6d562086a839aaf78661b9417d3b931df10c74c98cb07b1710d40d6294b0490
4
- data.tar.gz: fd070c121ce3e0c1bab7f1a738178a37c72c5d25c7726d3f3540e687d01bbe25
3
+ metadata.gz: feaa072a9d05a5a0308e4721d338aea1d5bdcb65d257f74210073518d3f21735
4
+ data.tar.gz: ef5d554a4ebf4dcb5e5d00b88f17ac36bd151d3b061038a35a26ed520dcd02b7
5
5
  SHA512:
6
- metadata.gz: 478554bfc8b8bafb2efd5bb880b8e053e2442a878bcd1363a4c6ddeab77a811b49530b84f467009b01f2299b70105f17a8a9840d9b35ebbec94359ca14d7114c
7
- data.tar.gz: 6873da52a257f542cb782fde59ba7da7f96042f4b69eaf470ea23d81cdb8479e8ff2791a6fbd0fd36ec166039f9718b7ce1b14632c690b44bea2833e280835b5
6
+ metadata.gz: 699655e4efcd96b4fa46ccf2017f38d76848b2827115909b599bd722d2eb0d447da1fb3bc0323f825bf0b69024f1a947eaf969e95b205d90e6393babe2b8c15e
7
+ data.tar.gz: 297c2eda593a1b995c233b7ff89d92e11c7921322bceeb14eac71eb351bf04d0956e6c0de18e91137884ace3fc6f0a4422c2e3b79d7598fdc0ab567f93bfe638
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ### v1.0.0 / 2021-07-07
4
+
5
+ * Bumped the version to 1.0.
6
+ * Removed the "preview" notices for Google Cloud Functions since the Ruby runtime is now GA.
7
+
8
+ ### v0.11.0 / 2021-06-28
9
+
10
+ * UPDATED: Update CloudEvents dependency to 0.5 to get fixes for JSON formatting cases
11
+ * FIXED: Updated Pub/Sub and Firebase event conversion logic to better align to Eventarc
12
+
13
+ ### v0.10.0 / 2021-06-01
14
+
15
+ * ADDED: Support raw pubsub events sent by the pubsub emulator
16
+ * FIXED: Set proper response content-type charset when a function returns a string (plain text) or hash (JSON)
17
+ * FIXED: Properly handle conversion of non-ascii characters in legacy event strings
18
+
19
+ ### v0.9.0 / 2021-03-18
20
+
21
+ * BREAKING CHANGE: Servers are configured as single-threaded in production by default, matching the current behavior of Google Cloud Functions.
22
+ * FIXED: Fixed conversion of Firebase events to CloudEvents to conform to the specs used by Cloud Functions and Cloud Run.
23
+ * FIXED: Fixed an error when reading a global set to a Minitest::Mock. This will make it easier to write tests that use mocks for global resources.
24
+
25
+ ### v0.8.0 / 2021-03-02
26
+
27
+ * ADDED: Support for lazily-initialized globals
28
+
3
29
  ### v0.7.1 / 2021-01-26
4
30
 
5
31
  * DOCS: Fixed several errors in the writing-functions doc samples
data/README.md CHANGED
@@ -4,7 +4,7 @@ An open source framework for writing lightweight, portable Ruby functions that
4
4
  run in a serverless environment. Functions written to this Framework will run
5
5
  in many different environments, including:
6
6
 
7
- * [Google Cloud Functions](https://cloud.google.com/functions) *(public preview)*
7
+ * [Google Cloud Functions](https://cloud.google.com/functions)
8
8
  * [Google Cloud Run](https://cloud.google.com/run)
9
9
  * Any other [Knative](https://github.com/knative)-based environment
10
10
  * Your local development machine
@@ -60,7 +60,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
60
60
  ```ruby
61
61
  # Gemfile
62
62
  source "https://rubygems.org"
63
- gem "functions_framework", "~> 0.7"
63
+ gem "functions_framework", "~> 1.0"
64
64
  ```
65
65
 
66
66
  Create a file called `app.rb` and include the following code. This defines a
data/docs/overview.md CHANGED
@@ -8,7 +8,7 @@ The Functions Framework is an open source framework for writing lightweight,
8
8
  portable Ruby functions that run in a serverless environment. Functions written
9
9
  to this Framework will run in many different environments, including:
10
10
 
11
- * [Google Cloud Functions](https://cloud.google.com/functions) *(public preview)*
11
+ * [Google Cloud Functions](https://cloud.google.com/functions)
12
12
  * [Google Cloud Run](https://cloud.google.com/run)
13
13
  * Any other [Knative](https://github.com/knative)-based environment
14
14
  * Your local development machine
@@ -64,7 +64,7 @@ Create a `Gemfile` listing the Functions Framework as a dependency:
64
64
  ```ruby
65
65
  # Gemfile
66
66
  source "https://rubygems.org"
67
- gem "functions_framework", "~> 0.7"
67
+ gem "functions_framework", "~> 1.0"
68
68
  ```
69
69
 
70
70
  Create a file called `app.rb` and include the following code. This defines a
@@ -111,7 +111,7 @@ dependency on Sinatra in your `Gemfile`:
111
111
 
112
112
  ```ruby
113
113
  source "https://rubygems.org"
114
- gem "functions_framework", "~> 0.7"
114
+ gem "functions_framework", "~> 1.0"
115
115
  gem "sinatra", "~> 2.0"
116
116
  ```
117
117
 
@@ -152,7 +152,7 @@ information about it:
152
152
  require "functions_framework"
153
153
 
154
154
  FunctionsFramework.cloud_event "hello" do |event|
155
- FunctionsFramework.logger.info "I received an event of type #{event.type}!"
155
+ logger.info "I received an event of type #{event.type}!"
156
156
  end
157
157
  ```
158
158
 
@@ -248,10 +248,10 @@ FunctionsFramework.http "hello" do |request|
248
248
  end
249
249
  ```
250
250
 
251
- Startup tasks are run once per Ruby instance, before the framework starts
252
- receiving requests and executing functions. You can define multiple startup
253
- tasks, and they will run in order, and are guaranteed to complete before any
254
- function is executed.
251
+ Startup tasks are run once per Ruby instance during cold start -- that is,
252
+ after the Ruby VM boots up but before the framework starts receiving requests
253
+ and executing functions. You can define multiple startup tasks, and they will
254
+ run in order, and are guaranteed to complete before any function is executed.
255
255
 
256
256
  The block is optionally passed the {FunctionsFramework::Function} representing
257
257
  the function that will be run. You code can, for example, perform different
@@ -286,6 +286,11 @@ end
286
286
  # ...
287
287
  ```
288
288
 
289
+ Because startup tasks run during cold start, they could have an impact on your
290
+ function's startup latency. To mitigate this issue, it is possible to run parts
291
+ of your initialization lazily, as described below in the section below on
292
+ [lazy initialization](#Lazy_initialization).
293
+
289
294
  ### The execution context and global data
290
295
 
291
296
  When your function block executes, the _object context_ (i.e. `self`) is set to
@@ -331,8 +336,9 @@ resources, as described below.
331
336
  Using the global data mechanism is generally preferred over actual Ruby global
332
337
  variables, because the Functions Framework can help you avoid concurrent edits.
333
338
  Additionally, the framework will isolate the sets of global data associated
334
- with different sets of functions, which lets you test functions in isolation
335
- without the tests interfering with one another by writing to global variables.
339
+ with different sets of functions, which lets you run functions in isolation
340
+ during unit tests. If you are testing multiple functions, they will not
341
+ interfere with each other as they might if they used global variables.
336
342
 
337
343
  ### Sharing resources
338
344
 
@@ -345,10 +351,10 @@ re-establishing it for every function invocation.
345
351
 
346
352
  The best practice for sharing a resource across function invocations is to
347
353
  initialize it in a {FunctionsFramework.on_startup} block, and reference it from
348
- global shared data. (As discussed above, prefer to initialize shared resources
349
- in a startup task rather than at the top level of a Ruby file, and prefer using
350
- the Functions Framework's global data mechanism rather than Ruby's global
351
- variables.)
354
+ global shared data. (As discussed above, the best practice is to initialize
355
+ shared resources in a startup task rather than at the top level of a Ruby file,
356
+ and to use the Functions Framework's global data mechanism rather than Ruby's
357
+ global variables.)
352
358
 
353
359
  Here is a simple example:
354
360
 
@@ -383,6 +389,48 @@ may perform CPU throttling, and therefore there may not be an opportunity for
383
389
  cleanup tasks to run. (For example, you could register a `Kernel.at_exit` task,
384
390
  but the Ruby VM may still terminate without calling it.)
385
391
 
392
+ ### Lazy initialization
393
+
394
+ Because startup tasks run during cold start, they could have an impact on your
395
+ function's startup latency. You can mitigate this by initializing some globals
396
+ _lazily_. When setting a global, instead of computing and setting the value
397
+ directly (e.g. constructing a shared API client object directly), you can
398
+ provide a block that describes how to construct it on demand.
399
+
400
+ Here is an example using the storage client we saw above.
401
+
402
+ ```ruby
403
+ require "functions_framework"
404
+
405
+ # This startup block describes _how_ to initialize a shared client, but
406
+ # does not construct it immediately.
407
+ FunctionsFramework.on_startup do
408
+ require "google/cloud/storage"
409
+ set_global :storage_client do
410
+ Google::Cloud::Storage.new
411
+ end
412
+ end
413
+
414
+ # The first time this function is invoked, it will call the above block
415
+ # to construct the storage client. Subsequent invocations will not need
416
+ # to construct it again, but will reuse the same shared object.
417
+ FunctionsFramework.http "storage_example" do |request|
418
+ bucket = global(:storage_client).bucket "my-bucket"
419
+ file = bucket.file "path/to/my-file.txt"
420
+ file.download.to_s
421
+ end
422
+ ```
423
+
424
+ The block will not be called until a function actually attempts to access the
425
+ global. From that point, subsequent accesses of the global will return that
426
+ same shared value; the block will be called at most once. This is true even if
427
+ multiple functions are run concurrently in different threads.
428
+
429
+ Lazy initialization is particularly useful if you define several different
430
+ functions that may use different sets of shared resources. Instead of
431
+ initializing all resources eagerly up front, you could initialize them lazily
432
+ and run only the code needed by the function that is actually invoked.
433
+
386
434
  ## Structuring a project
387
435
 
388
436
  A Functions Framework based "project" or "application" is a typical Ruby
@@ -422,7 +470,7 @@ Following is a typical layout for a Functions Framework based project.
422
470
  ```ruby
423
471
  # Gemfile
424
472
  source "https://rubygems.org"
425
- gem "functions_framework", "~> 0.7"
473
+ gem "functions_framework", "~> 1.0"
426
474
  ```
427
475
 
428
476
  ```ruby
@@ -74,7 +74,7 @@ module FunctionsFramework
74
74
  # @param argv [Array<String>]
75
75
  # @return [self]
76
76
  #
77
- def parse_args argv # rubocop:disable Metrics/MethodLength
77
+ def parse_args argv # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
78
78
  @option_parser = ::OptionParser.new do |op| # rubocop:disable Metrics/BlockLength
79
79
  op.on "-t", "--target TARGET",
80
80
  "Set the name of the function to execute (defaults to #{DEFAULT_TARGET})" do |val|
@@ -164,6 +164,28 @@ module FunctionsFramework
164
164
  callable.call(*args)
165
165
  end
166
166
 
167
+ ##
168
+ # A lazy evaluator for a global
169
+ # @private
170
+ #
171
+ class LazyGlobal
172
+ def initialize block
173
+ @block = block
174
+ @value = nil
175
+ @mutex = ::Mutex.new
176
+ end
177
+
178
+ def value
179
+ @mutex.synchronize do
180
+ if @block
181
+ @value = @block.call
182
+ @block = nil
183
+ end
184
+ @value
185
+ end
186
+ end
187
+ end
188
+
167
189
  ##
168
190
  # A base class for a callable object that provides calling context.
169
191
  #
@@ -196,7 +218,9 @@ module FunctionsFramework
196
218
  # @return [Object]
197
219
  #
198
220
  def global key
199
- @__globals[key]
221
+ value = @__globals[key]
222
+ value = value.value if LazyGlobal === value
223
+ value
200
224
  end
201
225
 
202
226
  ##
@@ -204,11 +228,35 @@ module FunctionsFramework
204
228
  # are frozen when the server starts, so this call will raise an exception
205
229
  # if called from a normal function.
206
230
  #
207
- # @param key [Symbol,String]
208
- # @param value [Object]
231
+ # You can set a global to a final value, or you can provide a block that
232
+ # lazily computes the global the first time it is requested.
233
+ #
234
+ # @overload set_global(key, value)
235
+ # Set the given global to the given value. For example:
236
+ #
237
+ # set_global(:project_id, "my-project-id")
238
+ #
239
+ # @param key [Symbol,String]
240
+ # @param value [Object]
241
+ # @return [self]
242
+ #
243
+ # @overload set_global(key, &block)
244
+ # Call the given block to compute the global's value only when the
245
+ # value is actually requested. This block will be called at most once,
246
+ # and its result reused for subsequent calls. For example:
247
+ #
248
+ # set_global(:connection_pool) do
249
+ # ExpensiveConnectionPool.new
250
+ # end
251
+ #
252
+ # @param key [Symbol,String]
253
+ # @param block [Proc] A block that lazily computes a value
254
+ # @yieldreturn [Object] The value
255
+ # @return [self]
209
256
  #
210
- def set_global key, value
211
- @__globals[key] = value
257
+ def set_global key, value = nil, &block
258
+ @__globals[key] = block ? LazyGlobal.new(block) : value
259
+ self
212
260
  end
213
261
 
214
262
  ##
@@ -27,20 +27,21 @@ module FunctionsFramework
27
27
  # @return [nil] if the event format was not recognized.
28
28
  #
29
29
  def decode_rack_env env
30
- content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"]
30
+ content_type = ::CloudEvents::ContentType.new env["CONTENT_TYPE"], default_charset: "utf-8"
31
31
  return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
32
32
  input = read_input_json env["rack.input"], content_type.charset
33
33
  return nil unless input
34
+ input = convert_raw_pubsub_event input, env if raw_pubsub_payload? input
34
35
  context = normalized_context input
35
36
  return nil unless context
36
- construct_cloud_event context, input["data"], content_type.charset
37
+ construct_cloud_event context, input["data"]
37
38
  end
38
39
 
39
40
  private
40
41
 
41
42
  def read_input_json input, charset
42
43
  input = input.read if input.respond_to? :read
43
- input = input.encode charset if charset
44
+ input.force_encoding charset if charset
44
45
  content = ::JSON.parse input
45
46
  content = nil unless content.is_a? ::Hash
46
47
  content
@@ -48,15 +49,50 @@ module FunctionsFramework
48
49
  nil
49
50
  end
50
51
 
52
+ def raw_pubsub_payload? input
53
+ return false if input.include?("context") || !input.include?("subscription")
54
+ message = input["message"]
55
+ message.is_a?(::Hash) && message.include?("data") && message.include?("messageId")
56
+ end
57
+
58
+ def convert_raw_pubsub_event input, env
59
+ message = input["message"]
60
+ path = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
61
+ path_match = %r{projects/[^/?]+/topics/[^/?]+}.match path
62
+ topic = path_match ? path_match[0] : "UNKNOWN_PUBSUB_TOPIC"
63
+ timestamp = message["publishTime"] || ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
64
+ {
65
+ "context" => {
66
+ "eventId" => message["messageId"],
67
+ "timestamp" => timestamp,
68
+ "eventType" => "google.pubsub.topic.publish",
69
+ "resource" => {
70
+ "service" => "pubsub.googleapis.com",
71
+ "type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
72
+ "name" => topic
73
+ }
74
+ },
75
+ "data" => {
76
+ "@type" => "type.googleapis.com/google.pubsub.v1.PubsubMessage",
77
+ "data" => message["data"],
78
+ "attributes" => message["attributes"]
79
+ }
80
+ }
81
+ end
82
+
51
83
  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"]
84
+ id = normalized_context_field input, "eventId"
85
+ timestamp = normalized_context_field input, "timestamp"
86
+ type = normalized_context_field input, "eventType"
87
+ domain = normalized_context_field input, "domain"
88
+ service, resource = analyze_resource normalized_context_field input, "resource"
57
89
  service ||= service_from_type type
58
90
  return nil unless id && timestamp && type && service && resource
59
- { id: id, timestamp: timestamp, type: type, service: service, resource: resource }
91
+ { id: id, timestamp: timestamp, type: type, service: service, resource: resource, domain: domain }
92
+ end
93
+
94
+ def normalized_context_field input, field
95
+ input["context"]&.[](field) || input[field]
60
96
  end
61
97
 
62
98
  def analyze_resource raw_resource
@@ -78,37 +114,66 @@ module FunctionsFramework
78
114
  nil
79
115
  end
80
116
 
81
- def construct_cloud_event context, data, charset
82
- source, subject = convert_source context[:service], context[:resource]
117
+ def construct_cloud_event context, data
118
+ source, subject = convert_source context[:service], context[:resource], context[:domain]
83
119
  type = LEGACY_TYPE_TO_CE_TYPE[context[:type]]
84
120
  return nil unless type && source
85
- ce_data = convert_data context[:service], data
86
- content_type = "application/json; charset=#{charset}"
121
+ ce_data, data_subject = convert_data context, data
122
+ content_type = "application/json"
87
123
  ::CloudEvents::Event.new id: context[:id],
88
124
  source: source,
89
125
  type: type,
90
126
  spec_version: "1.0",
91
127
  data_content_type: content_type,
92
128
  data: ce_data,
93
- subject: subject,
129
+ subject: subject || data_subject,
94
130
  time: context[:timestamp]
95
131
  end
96
132
 
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]]
133
+ def convert_source service, resource, domain
134
+ return ["//#{service}/#{resource}", nil] unless CE_SERVICE_TO_RESOURCE_RE.key? service
135
+
136
+ match = CE_SERVICE_TO_RESOURCE_RE[service].match resource
137
+ return [nil, nil] unless match
138
+ resource_fragment = match[1]
139
+ subject = match[2]
140
+
141
+ if service == "firebasedatabase.googleapis.com"
142
+ location =
143
+ case domain
144
+ when "firebaseio.com"
145
+ "us-central1"
146
+ when /^([\w-]+)\./
147
+ Regexp.last_match[1]
148
+ else
149
+ return [nil, nil]
150
+ end
151
+ ["//#{service}/projects/_/locations/#{location}/#{resource_fragment}", subject]
102
152
  else
103
- ["//#{service}/#{resource}", nil]
153
+ ["//#{service}/#{resource_fragment}", subject]
104
154
  end
105
155
  end
106
156
 
107
- def convert_data service, data
108
- if service == "pubsub.googleapis.com"
109
- { "message" => data, "subscription" => nil }
157
+ def convert_data context, data
158
+ service = context[:service]
159
+ case service
160
+ when "pubsub.googleapis.com"
161
+ data["messageId"] = context[:id]
162
+ data["publishTime"] = context[:timestamp]
163
+ [{ "message" => data }, nil]
164
+ when "firebaseauth.googleapis.com"
165
+ if data.key? "metadata"
166
+ FIREBASE_AUTH_METADATA_LEGACY_TO_CE.each do |old_key, new_key|
167
+ if data["metadata"].key? old_key
168
+ data["metadata"][new_key] = data["metadata"][old_key]
169
+ data["metadata"].delete old_key
170
+ end
171
+ end
172
+ end
173
+ subject = "users/#{data['uid']}" if data.key? "uid"
174
+ [data, subject]
110
175
  else
111
- data
176
+ [data, nil]
112
177
  end
113
178
  end
114
179
 
@@ -116,8 +181,9 @@ module FunctionsFramework
116
181
  %r{^providers/cloud\.firestore/} => "firestore.googleapis.com",
117
182
  %r{^providers/cloud\.pubsub/} => "pubsub.googleapis.com",
118
183
  %r{^providers/cloud\.storage/} => "storage.googleapis.com",
119
- %r{^providers/firebase\.auth/} => "firebase.googleapis.com",
120
- %r{^providers/google\.firebase} => "firebase.googleapis.com"
184
+ %r{^providers/firebase\.auth/} => "firebaseauth.googleapis.com",
185
+ %r{^providers/google\.firebase\.analytics/} => "firebase.googleapis.com",
186
+ %r{^providers/google\.firebase\.database/} => "firebasedatabase.googleapis.com"
121
187
  }.freeze
122
188
 
123
189
  LEGACY_TYPE_TO_CE_TYPE = {
@@ -140,5 +206,18 @@ module FunctionsFramework
140
206
  "providers/google.firebase.database/eventTypes/ref.delete" => "google.firebase.database.document.v1.deleted",
141
207
  "providers/cloud.storage/eventTypes/object.change" => "google.cloud.storage.object.v1.finalized"
142
208
  }.freeze
209
+
210
+ CE_SERVICE_TO_RESOURCE_RE = {
211
+ "firebase.googleapis.com" => %r{^(projects/[^/]+)/(events/[^/]+)$},
212
+ "firebasedatabase.googleapis.com" => %r{^projects/_/(instances/[^/]+)/(refs/.+)$},
213
+ "firestore.googleapis.com" => %r{^(projects/[^/]+/databases/\(default\))/(documents/.+)$},
214
+ "storage.googleapis.com" => %r{^(projects/[^/]+/buckets/[^/]+)/([^#]+)(?:#.*)?$}
215
+ }.freeze
216
+
217
+ # Map Firebase Auth legacy event metadata field names to their equivalent CloudEvent field names.
218
+ FIREBASE_AUTH_METADATA_LEGACY_TO_CE = {
219
+ "createdAt" => "createTime",
220
+ "lastSignedInAt" => "lastSignInTime"
221
+ }.freeze
143
222
  end
144
223
  end
@@ -158,7 +158,7 @@ module FunctionsFramework
158
158
  ::Signal.trap "SIGHUP" do
159
159
  Server.signal_enqueue "SIGHUP", @config.logger, @server
160
160
  end
161
- rescue ::ArgumentError # rubocop:disable Lint/HandleExceptions
161
+ rescue ::ArgumentError
162
162
  # Not available on all systems
163
163
  end
164
164
  @signals_installed = true
@@ -306,7 +306,7 @@ module FunctionsFramework
306
306
  # @return [Integer]
307
307
  #
308
308
  def max_threads
309
- @max_threads || (@rack_env == "development" ? 1 : 16)
309
+ @max_threads || 1
310
310
  end
311
311
 
312
312
  ##
@@ -346,9 +346,9 @@ module FunctionsFramework
346
346
  when ::Rack::Response
347
347
  response.finish
348
348
  when ::String
349
- string_response response, "text/plain", 200
349
+ string_response response, 200
350
350
  when ::Hash
351
- string_response ::JSON.dump(response), "application/json", 200
351
+ string_response ::JSON.dump(response), 200, content_type: "application/json"
352
352
  when ::CloudEvents::CloudEventsError
353
353
  cloud_events_error_response response
354
354
  when ::StandardError
@@ -359,10 +359,17 @@ module FunctionsFramework
359
359
  end
360
360
 
361
361
  def notfound_response
362
- string_response "Not found", "text/plain", 404
362
+ string_response "Not found", 404
363
363
  end
364
364
 
365
- 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
366
373
  headers = {
367
374
  "Content-Type" => content_type,
368
375
  "Content-Length" => string.bytesize
@@ -372,13 +379,13 @@ module FunctionsFramework
372
379
 
373
380
  def cloud_events_error_response error
374
381
  @config.logger.warn error
375
- string_response "#{error.class}: #{error.message}", "text/plain", 400
382
+ string_response "#{error.class}: #{error.message}", 400
376
383
  end
377
384
 
378
385
  def error_response message
379
386
  @config.logger.error message
380
387
  message = "Unexpected internal error" unless @config.show_error_details?
381
- string_response message, "text/plain", 500
388
+ string_response message, 500
382
389
  end
383
390
  end
384
391
 
@@ -424,7 +431,7 @@ module FunctionsFramework
424
431
  when ::CloudEvents::Event
425
432
  handle_cloud_event event, logger
426
433
  when ::Array
427
- ::CloudEvents::HttpContentError.new "Batched CloudEvents are not supported"
434
+ ::CloudEvents::CloudEventsError.new "Batched CloudEvents are not supported"
428
435
  when ::CloudEvents::CloudEventsError
429
436
  event
430
437
  else
@@ -436,9 +443,13 @@ module FunctionsFramework
436
443
  private
437
444
 
438
445
  def decode_event env
439
- @cloud_events.decode_rack_env(env) ||
446
+ begin
447
+ @cloud_events.decode_event env
448
+ rescue ::CloudEvents::NotCloudEventError
449
+ env["rack.input"].rewind rescue nil
440
450
  @legacy_events.decode_rack_env(env) ||
441
- raise(::CloudEvents::HttpContentError, "Unrecognized event format")
451
+ raise(::CloudEvents::CloudEventsError, "Unrecognized event format")
452
+ end
442
453
  rescue ::CloudEvents::CloudEventsError => e
443
454
  e
444
455
  end
@@ -330,20 +330,27 @@ module FunctionsFramework
330
330
  when ::Array
331
331
  ::Rack::Response.new response[2], response[0], response[1]
332
332
  when ::String
333
- string_response response, "text/plain", 200
333
+ string_response response, 200
334
334
  when ::Hash
335
335
  json = ::JSON.dump response
336
- string_response json, "application/json", 200
336
+ string_response json, 200, content_type: "application/json"
337
337
  when ::StandardError
338
338
  message = "#{response.class}: #{response.message}\n#{response.backtrace}\n"
339
- string_response message, "text/plain", 500
339
+ string_response message, 500
340
340
  else
341
341
  raise "Unexpected response type: #{response.inspect}"
342
342
  end
343
343
  end
344
344
 
345
345
  ## @private
346
- 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
347
354
  headers = {
348
355
  "Content-Type" => content_type,
349
356
  "Content-Length" => string.bytesize
@@ -366,9 +373,10 @@ module FunctionsFramework
366
373
  ::Rack::RACK_ERRORS => ::StringIO.new
367
374
  }
368
375
  headers.each do |header|
369
- if header.is_a? String
376
+ case header
377
+ when String
370
378
  name, value = header.split ":"
371
- elsif header.is_a? Array
379
+ when ::Array
372
380
  name, value = header
373
381
  end
374
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.7.1".freeze
20
+ VERSION = "1.0.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.7.1
4
+ version: 1.0.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: 2021-01-26 00:00:00.000000000 Z
11
+ date: 2021-07-07 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.5.1
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.5.1
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
@@ -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.7.1/file.CHANGELOG.html
102
+ changelog_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v1.0.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.7.1
105
+ documentation_uri: https://googlecloudplatform.github.io/functions-framework-ruby/v1.0.0
94
106
  post_install_message:
95
107
  rdoc_options: []
96
108
  require_paths:
@@ -106,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
118
  - !ruby/object:Gem::Version
107
119
  version: '0'
108
120
  requirements: []
109
- rubygems_version: 3.1.4
121
+ rubygems_version: 3.1.6
110
122
  signing_key:
111
123
  specification_version: 4
112
124
  summary: Functions Framework for Ruby