functions_framework 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d09fadc9facfc650793e4c6c30f8dab5f2e5909d0d068d18e0da0bd8a748115d
4
- data.tar.gz: 7b99ab480e94f1ed4eae784121030a07106451cdc1ddd0de5e5b1d7edabbe41f
3
+ metadata.gz: cdf75f322fe9e9e78b5e65bfcc9682097606deffb46137d85b6b1cfde68d3ab8
4
+ data.tar.gz: 54f8e668875607738141963c70b1960da6c06df69083a7ef4357f66889e1a328
5
5
  SHA512:
6
- metadata.gz: b4cc30e0acb40f3de6e9a412b240c2f910a539d9b9e76085bcb701d605f657b5239b4bbeabf9a7f5e78c220ff4931ae34c7f21d028d4e472874308c91d26d997
7
- data.tar.gz: d059a1fc586dfb12bbd06a757abf46227f0dcfc54c8653365198b596045a0c34d3fd4d00fcddc331a19d5bb028f5e52f5684aa022136b1258ce59a31ec195bd2
6
+ metadata.gz: 3d6fd59bc3730333e91a049d03089169720390f5f9300420449480924800d97da118a87cdae33224644c3550fe78dfee4a07b18e32e534d9bb7645340fbdf4c9
7
+ data.tar.gz: 897cef8cc366475123250d4c9a7cea0ccd07989c9ebdc0d88d358d996c5fa727aa9bcc5eb46be286d38e72a9d2512adfbaed743975d0300502516db9e3a5fde2
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ### v0.4.0 / 2020-06-29
4
+
5
+ * Dropped the legacy and largely unsupported `:event` function type. All event functions should be of type `:cloud_event`.
6
+ * Define the object context for function execution, and include an extensible context helper.
7
+ * Support for CloudEvents with specversion 0.3.
8
+ * CloudEvents now correct percent-encodes/decodes binary headers.
9
+ * CloudEvents now includes more robust RFC 2045 parsing of the Content-Type header.
10
+ * The CloudEventsError class now properly subclasses StandardError instead of RuntimeError.
11
+ * Removed redundant `_string` accessors from event classes since raw forms are already available via `[]`.
12
+ * A variety of corrections to event-related class documentation.
13
+
3
14
  ### v0.3.1 / 2020-06-27
4
15
 
5
16
  * Fixed crash when using "return" directly in a function block.
@@ -12,7 +23,7 @@
12
23
 
13
24
  ### v0.2.1 / 2020-06-25
14
25
 
15
- * The `--signature-type` check recognizes the legacy `event` type.
26
+ * The `--signature-type` check recognizes the legacy `event` type for `:cloud_event` functions.
16
27
 
17
28
  ### v0.2.0 / 2020-06-24
18
29
 
data/README.md CHANGED
@@ -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.3"
63
+ gem "functions_framework", "~> 0.4"
64
64
  ```
65
65
 
66
66
  Create a file called `app.rb` and include the following code. This defines a
@@ -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.3"
67
+ gem "functions_framework", "~> 0.4"
68
68
  ```
69
69
 
70
70
  Create a file called `app.rb` and include the following code. This defines a
@@ -30,7 +30,7 @@ that returns a simple message in the HTTP response body:
30
30
  ```ruby
31
31
  require "functions_framework"
32
32
 
33
- FunctionsFramework.http("hello") do |request|
33
+ FunctionsFramework.http "hello" do |request|
34
34
  # Return the response body.
35
35
  "Hello, world!\n"
36
36
  end
@@ -51,7 +51,7 @@ request information in the response:
51
51
  ```ruby
52
52
  require "functions_framework"
53
53
 
54
- FunctionsFramework.http("request_info") do |request|
54
+ FunctionsFramework.http "request_info_example" do |request|
55
55
  # Include some request info in the response body.
56
56
  "Received #{request.method} from #{request.url}!\n"
57
57
  end
@@ -66,7 +66,7 @@ hosting environment.
66
66
  ```ruby
67
67
  require "functions_framework"
68
68
 
69
- FunctionsFramework.http("logging_example") do |request|
69
+ FunctionsFramework.http "logging_example" do |request|
70
70
  # Log some request info.
71
71
  request.logger.info "I received #{request.method} from #{request.url}!"
72
72
  # A simple response body.
@@ -106,10 +106,19 @@ framework such as Ruby on Rails, you may want to consider a solution such as
106
106
  Google Cloud Run that is tailored to larger applications. However, a lightweight
107
107
  framework such as Sinatra is sometimes useful when writing HTTP functions.
108
108
 
109
- It is easy to connect an HTTP function to a Sinatra app. Write the Sinatra app
110
- using the "modular" Sinatra interface (i.e. subclass `Sinatra::Base`), and then
111
- simply run the Sinatra app as a Rack handler from the function. Here is a basic
112
- example:
109
+ It is easy to connect an HTTP function to a Sinatra app. First, declare the
110
+ dependency on Sinatra in your `Gemfile`:
111
+
112
+ ```ruby
113
+ # Gemfile
114
+ source "https://rubygems.org"
115
+ gem "functions_framework", "~> 0.4"
116
+ gem "sinatra", "~> 2.0"
117
+ ```
118
+
119
+ Write the Sinatra app using the "modular" Sinatra interface (i.e. subclass
120
+ `Sinatra::Base`), and then run the Sinatra app directly as a Rack handler from
121
+ the function. Here is a basic example:
113
122
 
114
123
  ```ruby
115
124
  require "functions_framework"
@@ -143,20 +152,25 @@ information about it:
143
152
  ```ruby
144
153
  require "functions_framework"
145
154
 
146
- FunctionsFramework.cloud_event("hello") do |event|
155
+ FunctionsFramework.cloud_event "hello" do |event|
147
156
  FunctionsFramework.logger.info "I received an event of type #{event.type}!"
148
157
  end
149
158
  ```
150
159
 
151
- The event parameter is a
152
- [CloudEvents V1 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V1)
153
- object. You can find detailed information about the fields of a CloudEvent from
154
- the [CloudEvents spec](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
160
+ The event parameter will be either a
161
+ [CloudEvents V0.3 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V0)
162
+ object ([see spec](https://github.com/cloudevents/spec/blob/v0.3/spec.md)) or a
163
+ [CloudEvents V1.0 Event](https://rubydoc.info/gems/functions_framework/FunctionsFramework/CloudEvents/Event/V1)
164
+ object ([see spec](https://github.com/cloudevents/spec/blob/v1.0/spec.md)).
155
165
 
156
166
  Some Google Cloud services send events in a legacy event format that was defined
157
167
  prior to CloudEvents. The Functions Framework will convert these legacy events
158
- to an equivalent CloudEvents type, so your function will always receive a
159
- CloudEvent object when it is sent an event from Google Cloud.
168
+ to an equivalent CloudEvents V1 type, so your function will always receive a
169
+ CloudEvent object when it is sent an event from Google Cloud. The precise
170
+ mapping between legacy events and CloudEvents is not specified in detail here,
171
+ but in general, the _data_ from the legacy event will be mapped to the `data`
172
+ field in the CloudEvent, and the _context_ from the legacy event will be mapped
173
+ to equivalent CloudEvent attributes.
160
174
 
161
175
  ## Error handling
162
176
 
@@ -175,7 +189,7 @@ HTTP response yourself. For example:
175
189
  ```ruby
176
190
  require "functions_framework"
177
191
 
178
- FunctionsFramework.http("error_reporter") do |request|
192
+ FunctionsFramework.http "error_reporter" do |request|
179
193
  begin
180
194
  raise "whoops!"
181
195
  rescue RuntimeError => e
@@ -222,7 +236,7 @@ A simple project might look like this:
222
236
  ```ruby
223
237
  # Gemfile
224
238
  source "https://rubygems.org"
225
- gem "functions_framework", "~> 0.3"
239
+ gem "functions_framework", "~> 0.4"
226
240
  ```
227
241
 
228
242
  ```ruby
@@ -230,7 +244,7 @@ gem "functions_framework", "~> 0.3"
230
244
  require "functions_framework"
231
245
  require_relative "lib/hello"
232
246
 
233
- FunctionsFramework.http("hello") do |request|
247
+ FunctionsFramework.http "hello" do |request|
234
248
  Hello.new(request).build_response
235
249
  end
236
250
  ```
@@ -238,7 +252,7 @@ end
238
252
  ```ruby
239
253
  # lib/hello.rb
240
254
  class Hello
241
- def initialize(request)
255
+ def initialize request
242
256
  @request = request
243
257
  end
244
258
 
@@ -139,17 +139,6 @@ module FunctionsFramework
139
139
  self
140
140
  end
141
141
 
142
- ##
143
- # This is an obsolete interface that defines an event function taking two
144
- # arguments (data and context) rather than one.
145
- #
146
- # @deprecated Use {FunctionsFramework.cloud_event} instead.
147
- #
148
- def event name = DEFAULT_TARGET, &block
149
- global_registry.add_event name, &block
150
- self
151
- end
152
-
153
142
  ##
154
143
  # Define a function that responds to CloudEvents.
155
144
  #
@@ -23,11 +23,13 @@ module FunctionsFramework
23
23
  # CloudEvents implementation.
24
24
  #
25
25
  # This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
26
- # [1.0 specification](https://github.com/cloudevents/spec/blob/master/spec.md).
26
+ # specification. It supports both
27
+ # [CloudEvents 0.3](https://github.com/cloudevents/spec/blob/v0.3/spec.md) and
28
+ # [CloudEvents 1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
27
29
  #
28
30
  module CloudEvents
29
31
  # @private
30
- SUPPORTED_SPEC_VERSIONS = ["1.0"].freeze
32
+ SUPPORTED_SPEC_VERSIONS = ["0.3", "1.0"].freeze
31
33
 
32
34
  class << self
33
35
  ##
@@ -23,29 +23,31 @@ module FunctionsFramework
23
23
  # Case-insensitive fields, such as media_type and subtype, are normalized
24
24
  # to lower case.
25
25
  #
26
+ # If parsing fails, this class will try to get as much information as it
27
+ # can, and fill the rest with defaults as recommended in RFC 2045 sec 5.2.
28
+ # In case of a parsing error, the {#error_message} field will be set.
29
+ #
26
30
  class ContentType
27
31
  ##
28
- # Parse the given header value
32
+ # Parse the given header value.
29
33
  #
30
34
  # @param string [String] Content-Type header value in RFC 2045 format
31
35
  #
32
36
  def initialize string
33
37
  @string = string
34
- # TODO: This handles simple cases but is not RFC-822 compliant.
35
- sections = string.to_s.split ";"
36
- media_type, subtype = sections.shift.split "/"
37
- subtype_prefix, subtype_format = subtype.split "+"
38
- @media_type = media_type.strip.downcase
39
- @subtype = subtype.strip.downcase
40
- @subtype_prefix = subtype_prefix.strip.downcase
41
- @subtype_format = subtype_format&.strip&.downcase
42
- @params = initialize_params sections
38
+ @media_type = "text"
39
+ @subtype_base = @subtype = "plain"
40
+ @subtype_format = nil
41
+ @params = []
42
+ @charset = "us-ascii"
43
+ @error_message = nil
44
+ parse consume_comments string.strip
43
45
  @canonical_string = "#{@media_type}/#{@subtype}" +
44
46
  @params.map { |k, v| "; #{k}=#{v}" }.join
45
47
  end
46
48
 
47
49
  ##
48
- # The original header content string
50
+ # The original header content string.
49
51
  # @return [String]
50
52
  #
51
53
  attr_reader :string
@@ -66,7 +68,7 @@ module FunctionsFramework
66
68
 
67
69
  ##
68
70
  # The entire content subtype (which could include an extension delimited
69
- # by a plus sign)
71
+ # by a plus sign).
70
72
  # @return [String]
71
73
  #
72
74
  attr_reader :subtype
@@ -75,7 +77,7 @@ module FunctionsFramework
75
77
  # The portion of the content subtype before any plus sign.
76
78
  # @return [String]
77
79
  #
78
- attr_reader :subtype_prefix
80
+ attr_reader :subtype_base
79
81
 
80
82
  ##
81
83
  # The portion of the content subtype after any plus sign, or nil if there
@@ -91,6 +93,18 @@ module FunctionsFramework
91
93
  #
92
94
  attr_reader :params
93
95
 
96
+ ##
97
+ # The charset, defaulting to "us-ascii" if none is explicitly set.
98
+ # @return [String]
99
+ #
100
+ attr_reader :charset
101
+
102
+ ##
103
+ # The error message when parsing, or `nil` if there was no error message.
104
+ # @return [String,nil]
105
+ #
106
+ attr_reader :error_message
107
+
94
108
  ##
95
109
  # An array of values for the given parameter name
96
110
  # @param key [String]
@@ -101,15 +115,6 @@ module FunctionsFramework
101
115
  @params.inject([]) { |a, (k, v)| key == k ? a << v : a }
102
116
  end
103
117
 
104
- ##
105
- # The first value of the "charset" parameter, or nil if there is no
106
- # charset.
107
- # @return [String,nil]
108
- #
109
- def charset
110
- param_values("charset").first
111
- end
112
-
113
118
  ## @private
114
119
  def == other
115
120
  other.is_a?(ContentType) && canonical_string == other.canonical_string
@@ -121,18 +126,90 @@ module FunctionsFramework
121
126
  canonical_string.hash
122
127
  end
123
128
 
129
+ ## @private
130
+ class ParseError < ::StandardError
131
+ end
132
+
124
133
  private
125
134
 
126
- def initialize_params sections
127
- params = sections.map do |s|
128
- k, v = s.split "="
129
- [k.strip.downcase, v.strip]
135
+ def parse str
136
+ @media_type, str = consume_token str, downcase: true, error_message: "Failed to parse media type"
137
+ str = consume_special str, "/"
138
+ @subtype, str = consume_token str, downcase: true, error_message: "Failed to parse subtype"
139
+ @subtype_base, @subtype_format = @subtype.split "+", 2
140
+ until str.empty?
141
+ str = consume_special str, ";"
142
+ name, str = consume_token str, downcase: true, error_message: "Faled to parse attribute name"
143
+ str = consume_special str, "=", error_message: "Failed to find value for attribute #{name}"
144
+ val, str = consume_token_or_quoted str, error_message: "Failed to parse value for attribute #{name}"
145
+ @params << [name, val]
146
+ @charset = val if name == "charset"
130
147
  end
131
- params.sort! do |(k1, v1), (k2, v2)|
132
- a = k1 <=> k2
133
- a.zero? ? v1 <=> v2 : a
148
+ rescue ParseError => e
149
+ @error_message = e.message
150
+ end
151
+
152
+ def consume_token str, downcase: false, error_message: nil
153
+ match = /^([\w!#\$%&'\*\+\.\^`\{\|\}-]+)(.*)$/.match str
154
+ raise ParseError, error_message || "Expected token" unless match
155
+ token = match[1]
156
+ token.downcase! if downcase
157
+ str = consume_comments match[2].strip
158
+ [token, str]
159
+ end
160
+
161
+ def consume_special str, expected, error_message: nil
162
+ raise ParseError, error_message || "Expected #{expected.inspect}" unless str.start_with? expected
163
+ consume_comments str[1..-1].strip
164
+ end
165
+
166
+ def consume_token_or_quoted str, error_message: nil
167
+ return consume_token str unless str.start_with? '"'
168
+ arr = []
169
+ index = 1
170
+ loop do
171
+ char = str[index]
172
+ case char
173
+ when nil
174
+ raise ParseError, error_message || "Quoted-string never finished"
175
+ when "\""
176
+ break
177
+ when "\\"
178
+ char = str[index + 1]
179
+ raise ParseError, error_message || "Quoted-string never finished" unless char
180
+ arr << char
181
+ index += 2
182
+ else
183
+ arr << char
184
+ index += 1
185
+ end
186
+ end
187
+ index += 1
188
+ str = consume_comments str[index..-1].strip
189
+ [arr.join, str]
190
+ end
191
+
192
+ def consume_comments str
193
+ return str unless str.start_with? "("
194
+ index = 1
195
+ loop do
196
+ char = str[index]
197
+ case char
198
+ when nil
199
+ raise ParseError, "Comment never finished"
200
+ when ")"
201
+ break
202
+ when "\\"
203
+ index += 2
204
+ when "("
205
+ str = consume_comments str[index..-1]
206
+ index = 0
207
+ else
208
+ index += 1
209
+ end
134
210
  end
135
- params
211
+ index += 1
212
+ consume_comments str[index..-1].strip
136
213
  end
137
214
  end
138
215
  end
@@ -17,7 +17,7 @@ module FunctionsFramework
17
17
  ##
18
18
  # Base class for all CloudEvents errors.
19
19
  #
20
- class CloudEventsError < ::RuntimeError
20
+ class CloudEventsError < ::StandardError
21
21
  end
22
22
 
23
23
  ##
@@ -15,15 +15,15 @@
15
15
  require "date"
16
16
  require "uri"
17
17
 
18
+ require "functions_framework/cloud_events/event/field_interpreter"
19
+ require "functions_framework/cloud_events/event/v0"
18
20
  require "functions_framework/cloud_events/event/v1"
19
21
 
20
22
  module FunctionsFramework
21
23
  module CloudEvents
22
24
  ##
23
- # CloudEvent object.
24
- #
25
- # An Event object represents a complete event, including both its data and
26
- # its context attributes. The following are true of all event objects:
25
+ # An Event object represents a complete CloudEvent, including both data and
26
+ # context attributes. The following are true of all event objects:
27
27
  #
28
28
  # * Event classes are defined within this module. For example, events
29
29
  # conforming to the CloudEvents 1.0 specification are of type
@@ -49,8 +49,11 @@ module FunctionsFramework
49
49
  # {CloudEvents::JsonFormat} to decode an event from JSON, or use
50
50
  # {CloudEvents::HttpBinding} to decode an event from an HTTP request.
51
51
  #
52
- # See https://github.com/cloudevents/spec/blob/master/spec.md for more
53
- # information about CloudEvents.
52
+ # See https://github.com/cloudevents/spec for more information about
53
+ # CloudEvents. The documentation for the individual event classes
54
+ # {FunctionsFramework::CloudEvents::Event::V0} and
55
+ # {FunctionsFramework::CloudEvents::Event::V1} also include links to their
56
+ # respective specifications.
54
57
  #
55
58
  module Event
56
59
  class << self
@@ -66,6 +69,8 @@ module FunctionsFramework
66
69
  #
67
70
  def create spec_version:, **kwargs
68
71
  case spec_version
72
+ when "0.3"
73
+ V0.new spec_version: spec_version, **kwargs
69
74
  when /^1(\.|$)/
70
75
  V1.new spec_version: spec_version, **kwargs
71
76
  else