functions_framework 0.2.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +2 -2
- data/docs/deploying-functions.md +21 -2
- data/docs/overview.md +2 -2
- data/docs/running-a-functions-server.md +1 -1
- data/docs/writing-functions.md +37 -22
- data/lib/functions_framework.rb +0 -11
- data/lib/functions_framework/cli.rb +1 -1
- data/lib/functions_framework/cloud_events.rb +4 -2
- data/lib/functions_framework/cloud_events/content_type.rb +114 -31
- data/lib/functions_framework/cloud_events/errors.rb +1 -1
- data/lib/functions_framework/cloud_events/event.rb +11 -6
- data/lib/functions_framework/cloud_events/event/field_interpreter.rb +150 -0
- data/lib/functions_framework/cloud_events/event/v0.rb +236 -0
- data/lib/functions_framework/cloud_events/event/v1.rb +21 -161
- data/lib/functions_framework/cloud_events/http_binding.rb +44 -4
- data/lib/functions_framework/cloud_events/json_format.rb +64 -13
- data/lib/functions_framework/function.rb +80 -24
- data/lib/functions_framework/legacy_event_converter.rb +30 -21
- data/lib/functions_framework/registry.rb +0 -15
- data/lib/functions_framework/server.rb +13 -7
- data/lib/functions_framework/testing.rb +40 -14
- data/lib/functions_framework/version.rb +1 -1
- metadata +9 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 817129c1773079fed039152996a18e91a054241940b8d6e3437cb66e10046304
|
4
|
+
data.tar.gz: ff3f61597726b60241e8b528ef520641fa3f08ddb7adcdfb32f9413fcb125f45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7396117d6e94bec87c70c697c7364b9256c7575f61c7259eb25df65694e9627b6c33a93e0caaea5d9d7955707e731eaddc27b90d4595f0029d181fc6cc18bfdf
|
7
|
+
data.tar.gz: 8eb929ace2a49ae764f5de0fef6dd7e0d668c80864368ad2379d0e52a89a44c7a3514e92dc02802d940374808a271a89f3076f1c8f3d0a532204118615e0c1af
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,37 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
### v0.4.1 / 2020-07-08
|
4
|
+
|
5
|
+
* Fixed unsupported signal error on Windows.
|
6
|
+
* Fixed several edge case errors in legacy event conversion.
|
7
|
+
* Generated Content-Type headers now properly quote param values if needed.
|
8
|
+
* Minor documentation updates.
|
9
|
+
|
10
|
+
### v0.4.0 / 2020-06-29
|
11
|
+
|
12
|
+
* Dropped the legacy and largely unsupported `:event` function type. All event functions should be of type `:cloud_event`.
|
13
|
+
* Define the object context for function execution, and include an extensible context helper.
|
14
|
+
* Support for CloudEvents with specversion 0.3.
|
15
|
+
* CloudEvents now correct percent-encodes/decodes binary headers.
|
16
|
+
* CloudEvents now includes more robust RFC 2045 parsing of the Content-Type header.
|
17
|
+
* The CloudEventsError class now properly subclasses StandardError instead of RuntimeError.
|
18
|
+
* Removed redundant `_string` accessors from event classes since raw forms are already available via `[]`.
|
19
|
+
* A variety of corrections to event-related class documentation.
|
20
|
+
|
21
|
+
### v0.3.1 / 2020-06-27
|
22
|
+
|
23
|
+
* Fixed crash when using "return" directly in a function block.
|
24
|
+
* Added a more flexible request generation helper in the testing module.
|
25
|
+
* Fixed several typos in the documentation.
|
26
|
+
|
27
|
+
### v0.3.0 / 2020-06-26
|
28
|
+
|
29
|
+
* Updated the CloudEvent data format for converted pubsub events to conform to Cloud Run's conversion.
|
30
|
+
|
31
|
+
### v0.2.1 / 2020-06-25
|
32
|
+
|
33
|
+
* The `--signature-type` check recognizes the legacy `event` type for `:cloud_event` functions.
|
34
|
+
|
3
35
|
### v0.2.0 / 2020-06-24
|
4
36
|
|
5
37
|
Significant changes:
|
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.
|
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
|
@@ -88,7 +88,7 @@ bundle exec functions-framework-ruby --target hello
|
|
88
88
|
In a separate shell, you can send requests to this function using curl:
|
89
89
|
|
90
90
|
```sh
|
91
|
-
curl
|
91
|
+
curl http://localhost:8080
|
92
92
|
# Output: Hello, world!
|
93
93
|
```
|
94
94
|
|
data/docs/deploying-functions.md
CHANGED
@@ -30,12 +30,21 @@ Google Cloud Functions is Google's scalable pay-as-you-go Functions-as-a-Service
|
|
30
30
|
Functions Framework is designed especially for functions that can be hosted on
|
31
31
|
Cloud Functions.
|
32
32
|
|
33
|
+
You can run Ruby functions on Google Cloud Functions by selecting the `ruby26`
|
34
|
+
runtime. This runtime uses a recent release of Ruby 2.6. Support for other
|
35
|
+
versions of Ruby may be added in the future.
|
36
|
+
|
37
|
+
> **Note:** Ruby support on Cloud Functions is currently in limited preview.
|
38
|
+
> It is not yet suitable for production workloads, and support is best-effort
|
39
|
+
> only. Access is currently limited to selected early-access users.
|
40
|
+
|
33
41
|
### Deploying and updating your function
|
34
42
|
|
35
43
|
Before you can deploy to Cloud Functions, make sure your bundle, and in
|
36
44
|
particular your `Gemfile.lock` file, is up to date. The easiest way to do this
|
37
45
|
is to `bundle install` or `bundle update` and run your local tests prior to
|
38
|
-
deploying.
|
46
|
+
deploying. Cloud Functions will not accept your function unless an up-to-date
|
47
|
+
`Gemfile.lock` is present.
|
39
48
|
|
40
49
|
Choose a name for your function. This function name is how it will appear in the
|
41
50
|
cloud console, and will also be part of the function's URL. (It's different from
|
@@ -96,6 +105,12 @@ to adapt it if you have an Anthos installation.
|
|
96
105
|
|
97
106
|
### Building an image for your function
|
98
107
|
|
108
|
+
Before you can deploy to Cloud Run, make sure your bundle, and in
|
109
|
+
particular your `Gemfile.lock` file, is up to date. The easiest way to do this
|
110
|
+
is to `bundle install` or `bundle update` and run your local tests prior to
|
111
|
+
deploying. The configuration used in the Dockerfile below will not accept your
|
112
|
+
function unless an up-to-date `Gemfile.lock` is present.
|
113
|
+
|
99
114
|
First, build a Docker image containing your function. Following is a simple
|
100
115
|
Dockerfile that you can use as a starting point. Feel free to adjust it to the
|
101
116
|
needs of your project:
|
@@ -126,6 +141,10 @@ You must use your project ID, but you can choose an app name and build ID. The
|
|
126
141
|
command may ask you for permission to enable the Cloud Build API for the project
|
127
142
|
if it isn't already enabled.
|
128
143
|
|
144
|
+
Because you provide your own Docker image when deploying to Cloud Run, you can
|
145
|
+
use any version of Ruby supported by the Functions Framework, from 2.4 through
|
146
|
+
2.7.
|
147
|
+
|
129
148
|
### Deploying an image to Cloud Run
|
130
149
|
|
131
150
|
To deploy to Cloud Run, specify the same image URL that you built above. For
|
@@ -152,7 +171,7 @@ deployed function.
|
|
152
171
|
|
153
172
|
Note that our Dockerfile's entrypoint did not pass any source file or target
|
154
173
|
name to the Functions Framework. If these are not specified, the Framework will
|
155
|
-
use the source
|
174
|
+
use the source `./app.rb` and the target `function` by default. To use different
|
156
175
|
values, you need to set the appropriate environment variables when deploying, as
|
157
176
|
illustrated above with the `FUNCTION_SOURCE` and `FUNCTION_TARGET` variables.
|
158
177
|
|
data/docs/overview.md
CHANGED
@@ -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.
|
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
|
@@ -92,7 +92,7 @@ bundle exec functions-framework-ruby --target hello
|
|
92
92
|
In a separate shell, you can send requests to this function using curl:
|
93
93
|
|
94
94
|
```sh
|
95
|
-
curl
|
95
|
+
curl http://localhost:8080
|
96
96
|
# Output: Hello, world!
|
97
97
|
```
|
98
98
|
|
@@ -101,7 +101,7 @@ Command-line flag | Environment variable | Description
|
|
101
101
|
`--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080`.
|
102
102
|
`--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function`.
|
103
103
|
`--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `app.rb` (in the current working directory).
|
104
|
-
`--signature-type` | `FUNCTION_SIGNATURE_TYPE` | Verifies that the function has the expected signature. Allowed values: `http` or `cloudevent`.
|
104
|
+
`--signature-type` | `FUNCTION_SIGNATURE_TYPE` | Verifies that the function has the expected signature. Allowed values: `http`, `event`, or `cloudevent`.
|
105
105
|
`--environment` | `RACK_ENV` | Sets the Rack environment.
|
106
106
|
`--bind` | `FUNCTION_BIND_ADDR` | Binds to the given address. Default: `0.0.0.0`.
|
107
107
|
`--min-threads` | `FUNCTION_MIN_THREADS` | Sets the minimum thread pool size, overriding Puma's default.
|
data/docs/writing-functions.md
CHANGED
@@ -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
|
33
|
+
FunctionsFramework.http "hello" do |request|
|
34
34
|
# Return the response body.
|
35
35
|
"Hello, world!\n"
|
36
36
|
end
|
@@ -43,7 +43,7 @@ now cover these in a bit more detail.
|
|
43
43
|
|
44
44
|
An HTTP function is passed a request, which is an object of type
|
45
45
|
[Rack::Request](https://rubydoc.info/gems/rack/Rack/Request). This object
|
46
|
-
provides methods
|
46
|
+
provides methods for obtaining request information such as the method,
|
47
47
|
path, query parameters, body content, and headers. You can also obtain the raw
|
48
48
|
Rack environment using the `env` method. The following example includes some
|
49
49
|
request information in the response:
|
@@ -51,7 +51,7 @@ request information in the response:
|
|
51
51
|
```ruby
|
52
52
|
require "functions_framework"
|
53
53
|
|
54
|
-
FunctionsFramework.http
|
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
|
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.
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
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
|
152
|
-
[CloudEvents
|
153
|
-
object
|
154
|
-
|
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 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
|
192
|
+
FunctionsFramework.http "error_reporter" do |request|
|
179
193
|
begin
|
180
194
|
raise "whoops!"
|
181
195
|
rescue RuntimeError => e
|
@@ -188,9 +202,10 @@ end
|
|
188
202
|
|
189
203
|
A Functions Framework based "project" or "application" is a typical Ruby
|
190
204
|
application. It should include a `Gemfile` that specifies the gem dependencies
|
191
|
-
(including the `functions_framework` gem itself), and
|
192
|
-
|
193
|
-
|
205
|
+
(including the `functions_framework` gem itself), and any other dependencies
|
206
|
+
needed by the function. It must include at least one Ruby source file that
|
207
|
+
defines functions, and can also include additional Ruby files defining classes
|
208
|
+
and methods that assist in the function implementation.
|
194
209
|
|
195
210
|
The "entrypoint" to the project, also called the "source", is a Ruby file. It
|
196
211
|
can define any number of functions (with distinct names), although it is often
|
@@ -221,7 +236,7 @@ A simple project might look like this:
|
|
221
236
|
```ruby
|
222
237
|
# Gemfile
|
223
238
|
source "https://rubygems.org"
|
224
|
-
gem "functions_framework", "~> 0.
|
239
|
+
gem "functions_framework", "~> 0.4"
|
225
240
|
```
|
226
241
|
|
227
242
|
```ruby
|
@@ -229,7 +244,7 @@ gem "functions_framework", "~> 0.2"
|
|
229
244
|
require "functions_framework"
|
230
245
|
require_relative "lib/hello"
|
231
246
|
|
232
|
-
FunctionsFramework.http
|
247
|
+
FunctionsFramework.http "hello" do |request|
|
233
248
|
Hello.new(request).build_response
|
234
249
|
end
|
235
250
|
```
|
@@ -237,7 +252,7 @@ end
|
|
237
252
|
```ruby
|
238
253
|
# lib/hello.rb
|
239
254
|
class Hello
|
240
|
-
def initialize
|
255
|
+
def initialize request
|
241
256
|
@request = request
|
242
257
|
end
|
243
258
|
|
data/lib/functions_framework.rb
CHANGED
@@ -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
|
#
|
@@ -137,7 +137,7 @@ module FunctionsFramework
|
|
137
137
|
raise "Undefined function: #{@target.inspect}" if function.nil?
|
138
138
|
unless @signature_type.nil? ||
|
139
139
|
@signature_type == "http" && function.type == :http ||
|
140
|
-
|
140
|
+
["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
|
141
141
|
raise "Function #{@target.inspect} does not match type #{@signature_type}"
|
142
142
|
end
|
143
143
|
::FunctionsFramework.start function do |config|
|
@@ -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
|
-
#
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@
|
39
|
-
@
|
40
|
-
|
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
|
-
@params.map { |k, v| "; #{k}=#{v}" }.join
|
46
|
+
@params.map { |k, v| "; #{k}=#{maybe_quote 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 :
|
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,96 @@ 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
|
127
|
-
|
128
|
-
|
129
|
-
|
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"
|
147
|
+
end
|
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
|
130
186
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
211
|
+
index += 1
|
212
|
+
consume_comments str[index..-1].strip
|
213
|
+
end
|
214
|
+
|
215
|
+
def maybe_quote str
|
216
|
+
return str if /^[\w!#\$%&'\*\+\.\^`\{\|\}-]+$/ =~ str
|
217
|
+
str = str.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
|
218
|
+
"\"#{str}\""
|
136
219
|
end
|
137
220
|
end
|
138
221
|
end
|