functions_framework 0.1.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 +4 -4
- data/.yardopts +6 -2
- data/CHANGELOG.md +44 -0
- data/README.md +56 -136
- data/bin/functions-framework-ruby +19 -0
- data/docs/deploying-functions.md +182 -0
- data/docs/overview.md +142 -0
- data/docs/running-a-functions-server.md +122 -0
- data/docs/testing-functions.md +169 -0
- data/docs/writing-functions.md +275 -0
- data/lib/functions_framework.rb +16 -50
- data/lib/functions_framework/cli.rb +71 -13
- data/lib/functions_framework/cloud_events.rb +12 -110
- data/lib/functions_framework/cloud_events/content_type.rb +107 -30
- data/lib/functions_framework/cloud_events/errors.rb +42 -0
- data/lib/functions_framework/cloud_events/event.rb +56 -249
- 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 +223 -0
- data/lib/functions_framework/cloud_events/http_binding.rb +310 -0
- data/lib/functions_framework/cloud_events/json_format.rb +173 -0
- data/lib/functions_framework/function.rb +80 -26
- data/lib/functions_framework/legacy_event_converter.rb +145 -0
- data/lib/functions_framework/registry.rb +0 -39
- data/lib/functions_framework/server.rb +61 -51
- data/lib/functions_framework/testing.rb +64 -24
- data/lib/functions_framework/version.rb +1 -1
- metadata +16 -4
- data/lib/functions_framework/cloud_events/binary_content.rb +0 -59
- data/lib/functions_framework/cloud_events/json_structure.rb +0 -88
data/lib/functions_framework.rb
CHANGED
@@ -16,6 +16,7 @@ require "logger"
|
|
16
16
|
|
17
17
|
require "functions_framework/cloud_events"
|
18
18
|
require "functions_framework/function"
|
19
|
+
require "functions_framework/legacy_event_converter"
|
19
20
|
require "functions_framework/registry"
|
20
21
|
require "functions_framework/version"
|
21
22
|
|
@@ -36,8 +37,8 @@ require "functions_framework/version"
|
|
36
37
|
# functions framework. Use the {FunctionsFramework.http},
|
37
38
|
# {FunctionsFramework.event}, or {FunctionsFramework.cloud_event} methods to
|
38
39
|
# define functions. To serve functions via a web service, invoke the
|
39
|
-
# `functions-framework` executable, or use the {FunctionsFramework.start}
|
40
|
-
# {FunctionsFramework.run} methods.
|
40
|
+
# `functions-framework-ruby` executable, or use the {FunctionsFramework.start}
|
41
|
+
# or {FunctionsFramework.run} methods.
|
41
42
|
#
|
42
43
|
# ## Internal modules
|
43
44
|
#
|
@@ -48,7 +49,7 @@ require "functions_framework/version"
|
|
48
49
|
# you define an event function, you will receive the event as a
|
49
50
|
# {FunctionsFramework::CloudEvents::Event} object.
|
50
51
|
# * {FunctionsFramework::CLI} is the implementation of the
|
51
|
-
# `functions-framework` executable. Most apps will not need to interact
|
52
|
+
# `functions-framework-ruby` executable. Most apps will not need to interact
|
52
53
|
# with this class directly.
|
53
54
|
# * {FunctionsFramework::Function} is the internal representation of a
|
54
55
|
# function, indicating the type of function (http or cloud event), the
|
@@ -62,7 +63,7 @@ require "functions_framework/version"
|
|
62
63
|
# * {FunctionsFramework::Server} is a web server that makes a function
|
63
64
|
# available via HTTP. It wraps the Puma web server and runs a specific
|
64
65
|
# {FunctionsFramework::Function}. Many apps can simply run the
|
65
|
-
# `functions-framework` executable to spin up a server. However, if you
|
66
|
+
# `functions-framework-ruby` executable to spin up a server. However, if you
|
66
67
|
# need closer control over your execution environment, you can use the
|
67
68
|
# {FunctionsFramework::Server} class to run a server. Note that, in most
|
68
69
|
# cases, it is easier to use the {FunctionsFramework.start} or
|
@@ -142,50 +143,9 @@ module FunctionsFramework
|
|
142
143
|
# Define a function that responds to CloudEvents.
|
143
144
|
#
|
144
145
|
# You must provide a name for the function, and a block that implemets the
|
145
|
-
# function. The block should take
|
146
|
-
# event _context_. Any return value is ignored.
|
147
|
-
#
|
148
|
-
# The event data argument will be one of the following types:
|
149
|
-
# * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
|
150
|
-
# binary data. You may choose to perform additional interpretation of
|
151
|
-
# the binary data using information in the content type provided by the
|
152
|
-
# context argument.
|
153
|
-
# * Any data type that can be represented in JSON (i.e. `String`,
|
154
|
-
# `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
|
155
|
-
# came with a JSON payload. The content type may also be set in the
|
156
|
-
# context if the data is a String.
|
157
|
-
#
|
158
|
-
# The context argument will be of type {FunctionsFramework::CloudEvents::Event},
|
159
|
-
# and will contain CloudEvents context attributes such as `id` and `type`.
|
160
|
-
#
|
161
|
-
# See also {FunctionsFramework.cloud_event} which defines a function that
|
162
|
-
# takes a single argument of type {FunctionsFramework::CloudEvents::Event}.
|
163
|
-
#
|
164
|
-
# ## Example
|
165
|
-
#
|
166
|
-
# FunctionsFramework.event "my-function" do |data, context|
|
167
|
-
# FunctionsFramework.logger.info "Event data: #{data.inspect}"
|
168
|
-
# end
|
169
|
-
#
|
170
|
-
# @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
|
171
|
-
# @param block [Proc] The function code as a proc.
|
172
|
-
# @return [self]
|
173
|
-
#
|
174
|
-
def event name = DEFAULT_TARGET, &block
|
175
|
-
global_registry.add_event name, &block
|
176
|
-
self
|
177
|
-
end
|
178
|
-
|
179
|
-
##
|
180
|
-
# Define a function that responds to CloudEvents.
|
181
|
-
#
|
182
|
-
# You must provide a name for the function, and a block that implemets the
|
183
|
-
# function. The block should take _one_ argument: the event object of type
|
146
|
+
# function. The block should take one argument: the event object of type
|
184
147
|
# {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
|
185
148
|
#
|
186
|
-
# See also {FunctionsFramework.event} which creates a function that takes
|
187
|
-
# data and context as separate arguments.
|
188
|
-
#
|
189
149
|
# ## Example
|
190
150
|
#
|
191
151
|
# FunctionsFramework.cloud_event "my-function" do |event|
|
@@ -205,15 +165,20 @@ module FunctionsFramework
|
|
205
165
|
# Start the functions framework server in the background. The server will
|
206
166
|
# look up the given target function name in the global registry.
|
207
167
|
#
|
208
|
-
# @param target [String] The
|
168
|
+
# @param target [FunctionsFramework::Function,String] The function to run,
|
169
|
+
# or the name of the function to look up in the global registry.
|
209
170
|
# @yield [FunctionsFramework::Server::Config] A config object that can be
|
210
171
|
# manipulated to configure the server.
|
211
172
|
# @return [FunctionsFramework::Server]
|
212
173
|
#
|
213
174
|
def start target, &block
|
214
175
|
require "functions_framework/server"
|
215
|
-
|
216
|
-
|
176
|
+
if target.is_a? ::FunctionsFramework::Function
|
177
|
+
function = target
|
178
|
+
else
|
179
|
+
function = global_registry[target]
|
180
|
+
raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
|
181
|
+
end
|
217
182
|
server = Server.new function, &block
|
218
183
|
server.respond_to_signals
|
219
184
|
server.start
|
@@ -223,7 +188,8 @@ module FunctionsFramework
|
|
223
188
|
# Run the functions framework server and block until it stops. The server
|
224
189
|
# will look up the given target function name in the global registry.
|
225
190
|
#
|
226
|
-
# @param target [String] The
|
191
|
+
# @param target [FunctionsFramework::Function,String] The function to run,
|
192
|
+
# or the name of the function to look up in the global registry.
|
227
193
|
# @yield [FunctionsFramework::Server::Config] A config object that can be
|
228
194
|
# manipulated to configure the server.
|
229
195
|
# @return [self]
|
@@ -12,15 +12,22 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
require "logger"
|
15
16
|
require "optparse"
|
16
17
|
|
17
18
|
require "functions_framework"
|
18
19
|
|
19
20
|
module FunctionsFramework
|
20
21
|
##
|
21
|
-
# Implementation of the functions-framework executable.
|
22
|
+
# Implementation of the functions-framework-ruby executable.
|
22
23
|
#
|
23
24
|
class CLI
|
25
|
+
##
|
26
|
+
# The default logging level, if not given in the environment variable.
|
27
|
+
# @return [Integer]
|
28
|
+
#
|
29
|
+
DEFAULT_LOGGING_LEVEL = ::Logger::Severity::INFO
|
30
|
+
|
24
31
|
##
|
25
32
|
# Create a new CLI, setting arguments to their defaults.
|
26
33
|
#
|
@@ -33,6 +40,8 @@ module FunctionsFramework
|
|
33
40
|
@min_threads = nil
|
34
41
|
@max_threads = nil
|
35
42
|
@detailed_errors = nil
|
43
|
+
@signature_type = ::ENV["FUNCTION_SIGNATURE_TYPE"]
|
44
|
+
@logging_level = init_logging_level
|
36
45
|
end
|
37
46
|
|
38
47
|
##
|
@@ -52,6 +61,11 @@ module FunctionsFramework
|
|
52
61
|
"Set the source file to load (defaults to #{DEFAULT_SOURCE})" do |val|
|
53
62
|
@source = val
|
54
63
|
end
|
64
|
+
op.on "--signature-type TYPE",
|
65
|
+
"Asserts that the function has the given signature type." \
|
66
|
+
" Supported values are 'http' and 'cloudevent'." do |val|
|
67
|
+
@signature_type = val
|
68
|
+
end
|
55
69
|
op.on "-p", "--port PORT", "Set the port to listen to (defaults to 8080)" do |val|
|
56
70
|
@port = val.to_i
|
57
71
|
end
|
@@ -71,10 +85,10 @@ module FunctionsFramework
|
|
71
85
|
@detailed_errors = val
|
72
86
|
end
|
73
87
|
op.on "-v", "--verbose", "Increase log verbosity" do
|
74
|
-
|
88
|
+
@logging_level -= 1
|
75
89
|
end
|
76
90
|
op.on "-q", "--quiet", "Decrease log verbosity" do
|
77
|
-
|
91
|
+
@logging_level += 1
|
78
92
|
end
|
79
93
|
op.on "--help", "Display help" do
|
80
94
|
puts op
|
@@ -82,23 +96,51 @@ module FunctionsFramework
|
|
82
96
|
end
|
83
97
|
end
|
84
98
|
option_parser.parse! argv
|
85
|
-
unless argv.empty?
|
86
|
-
warn "Unrecognized arguments: #{argv}"
|
87
|
-
puts op
|
88
|
-
exit 1
|
89
|
-
end
|
99
|
+
error "Unrecognized arguments: #{argv}\n#{op}" unless argv.empty?
|
90
100
|
self
|
91
101
|
end
|
92
102
|
|
93
103
|
##
|
94
104
|
# Run the configured server, and block until it stops.
|
105
|
+
# If a validation error occurs, print a message and exit.
|
106
|
+
#
|
95
107
|
# @return [self]
|
96
108
|
#
|
97
109
|
def run
|
98
|
-
|
99
|
-
|
110
|
+
begin
|
111
|
+
server = start_server
|
112
|
+
rescue ::StandardError => e
|
113
|
+
error e.message
|
114
|
+
end
|
115
|
+
server.wait_until_stopped
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
##
|
120
|
+
# Start the configured server and return the running server object.
|
121
|
+
# If a validation error occurs, raise an exception.
|
122
|
+
# This is used for testing the CLI.
|
123
|
+
#
|
124
|
+
# @return [FunctionsFramework::Server]
|
125
|
+
#
|
126
|
+
# @private
|
127
|
+
#
|
128
|
+
def start_server
|
129
|
+
::FunctionsFramework.logger.level = @logging_level
|
130
|
+
::FunctionsFramework.logger.info "FunctionsFramework v#{VERSION} server starting."
|
131
|
+
::ENV["FUNCTION_TARGET"] = @target
|
132
|
+
::ENV["FUNCTION_SOURCE"] = @source
|
133
|
+
::ENV["FUNCTION_SIGNATURE_TYPE"] = @signature_type
|
134
|
+
::FunctionsFramework.logger.info "FunctionsFramework: Loading functions from #{@source.inspect}..."
|
100
135
|
load @source
|
101
|
-
|
136
|
+
function = ::FunctionsFramework.global_registry[@target]
|
137
|
+
raise "Undefined function: #{@target.inspect}" if function.nil?
|
138
|
+
unless @signature_type.nil? ||
|
139
|
+
@signature_type == "http" && function.type == :http ||
|
140
|
+
["cloudevent", "event"].include?(@signature_type) && function.type == :cloud_event
|
141
|
+
raise "Function #{@target.inspect} does not match type #{@signature_type}"
|
142
|
+
end
|
143
|
+
::FunctionsFramework.start function do |config|
|
102
144
|
config.rack_env = @env
|
103
145
|
config.port = @port
|
104
146
|
config.bind_addr = @bind
|
@@ -106,8 +148,24 @@ module FunctionsFramework
|
|
106
148
|
config.min_threads = @min_threads
|
107
149
|
config.max_threads = @max_threads
|
108
150
|
end
|
109
|
-
|
110
|
-
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def init_logging_level
|
156
|
+
level_name = ::ENV["FUNCTION_LOGGING_LEVEL"].to_s.upcase.to_sym
|
157
|
+
::Logger::Severity.const_get level_name
|
158
|
+
rescue ::NameError
|
159
|
+
DEFAULT_LOGGING_LEVEL
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Print the given error message and exit.
|
164
|
+
# @param message [String]
|
165
|
+
#
|
166
|
+
def error message
|
167
|
+
warn message
|
168
|
+
exit 1
|
111
169
|
end
|
112
170
|
end
|
113
171
|
end
|
@@ -12,132 +12,34 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
require "functions_framework/cloud_events/binary_content"
|
16
15
|
require "functions_framework/cloud_events/content_type"
|
16
|
+
require "functions_framework/cloud_events/errors"
|
17
17
|
require "functions_framework/cloud_events/event"
|
18
|
+
require "functions_framework/cloud_events/http_binding"
|
19
|
+
require "functions_framework/cloud_events/json_format"
|
18
20
|
|
19
21
|
module FunctionsFramework
|
20
22
|
##
|
21
23
|
# CloudEvents implementation.
|
22
24
|
#
|
23
25
|
# This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# batch formats. A standard JSON structure parser is included. It is also
|
28
|
-
# possible to register handlers for other formats.
|
29
|
-
#
|
30
|
-
# TODO: Unmarshaling of events is implemented, but marshaling is not.
|
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).
|
31
29
|
#
|
32
30
|
module CloudEvents
|
33
|
-
@
|
34
|
-
|
31
|
+
# @private
|
32
|
+
SUPPORTED_SPEC_VERSIONS = ["0.3", "1.0"].freeze
|
35
33
|
|
36
34
|
class << self
|
37
35
|
##
|
38
|
-
#
|
39
|
-
# The handler object must respond to the method
|
40
|
-
# `#decode_structured_content`. See
|
41
|
-
# {FunctionsFramework::CloudEvents::JsonStructure} for an example.
|
42
|
-
#
|
43
|
-
# @param format [String] The subtype format that should be handled by
|
44
|
-
# this handler
|
45
|
-
# @param handler [#decode_structured_content] The handler object
|
46
|
-
# @return [self]
|
47
|
-
#
|
48
|
-
def register_structured_format format, handler
|
49
|
-
handlers = @structured_formats[format.to_s.strip.downcase] ||= []
|
50
|
-
handlers << handler unless handlers.include? handler
|
51
|
-
self
|
52
|
-
end
|
53
|
-
|
54
|
-
##
|
55
|
-
# Register a handler for the given batched format.
|
56
|
-
# The handler object must respond to the method
|
57
|
-
# `#decode_batched_content`. See
|
58
|
-
# {FunctionsFramework::CloudEvents::JsonStructure} for an example.
|
36
|
+
# The spec versions supported by this implementation.
|
59
37
|
#
|
60
|
-
# @
|
61
|
-
# this handler
|
62
|
-
# @param handler [#decode_batched_content] The handler object
|
63
|
-
# @return [self]
|
38
|
+
# @return [Array<String>]
|
64
39
|
#
|
65
|
-
def
|
66
|
-
|
67
|
-
handlers << handler unless handlers.include? handler
|
68
|
-
self
|
69
|
-
end
|
70
|
-
|
71
|
-
##
|
72
|
-
# Decode an event from the given Rack environment hash. Following the
|
73
|
-
# CloudEvents spec, this chooses a handler based on the Content-Type of
|
74
|
-
# the request.
|
75
|
-
#
|
76
|
-
# @param env [Hash] The Rack environment
|
77
|
-
# @return [FunctionsFramework::CloudEvents::Event] if the request
|
78
|
-
# includes a single structured or binary event
|
79
|
-
# @return [Array<FunctionsFramework::CloudEvents::Event>] if the request
|
80
|
-
# includes a batch of structured events
|
81
|
-
#
|
82
|
-
def decode_rack_env env
|
83
|
-
content_type_header = env["CONTENT_TYPE"]
|
84
|
-
raise "Missing content-type header" unless content_type_header
|
85
|
-
content_type = ContentType.new content_type_header
|
86
|
-
if content_type.media_type == "application"
|
87
|
-
case content_type.subtype_prefix
|
88
|
-
when "cloudevents"
|
89
|
-
return decode_structured_content env["rack.input"], content_type
|
90
|
-
when "cloudevents-batch"
|
91
|
-
return decode_batched_content env["rack.input"], content_type
|
92
|
-
end
|
93
|
-
end
|
94
|
-
BinaryContent.decode_rack_env env, content_type
|
95
|
-
end
|
96
|
-
|
97
|
-
##
|
98
|
-
# Decode a single event from the given content data. This should be
|
99
|
-
# passed the request body, if the Content-Type is of the form
|
100
|
-
# `application/cloudevents+format`.
|
101
|
-
#
|
102
|
-
# @param input [IO] An IO-like object providing the content
|
103
|
-
# @param content_type [FunctionsFramework::CloudEvents::ContentType] the
|
104
|
-
# content type
|
105
|
-
# @return [FunctionsFramework::CloudEvents::Event]
|
106
|
-
#
|
107
|
-
def decode_structured_content input, content_type
|
108
|
-
handlers = @structured_formats[content_type.subtype_format] || []
|
109
|
-
handlers.reverse_each do |handler|
|
110
|
-
event = handler.decode_structured_content input, content_type
|
111
|
-
return event if event
|
112
|
-
end
|
113
|
-
raise "Unknown cloudevents format: #{content_type.subtype_format.inspect}"
|
114
|
-
end
|
115
|
-
|
116
|
-
##
|
117
|
-
# Decode a batch of events from the given content data. This should be
|
118
|
-
# passed the request body, if the Content-Type is of the form
|
119
|
-
# `application/cloudevents-batch+format`.
|
120
|
-
#
|
121
|
-
# @param input [IO] An IO-like object providing the content
|
122
|
-
# @param content_type [FunctionsFramework::CloudEvents::ContentType] the
|
123
|
-
# content type
|
124
|
-
# @return [Array<FunctionsFramework::CloudEvents::Event>]
|
125
|
-
#
|
126
|
-
def decode_batched_content input, content_type
|
127
|
-
handlers = @batched_formats[content_type.subtype_format] || []
|
128
|
-
handlers.reverse_each do |handler|
|
129
|
-
events = handler.decode_batched_content input, content_type
|
130
|
-
return events if events
|
131
|
-
end
|
132
|
-
raise "Unknown cloudevents batch format: #{content_type.subtype_format.inspect}"
|
40
|
+
def supported_spec_versions
|
41
|
+
SUPPORTED_SPEC_VERSIONS
|
133
42
|
end
|
134
43
|
end
|
135
44
|
end
|
136
45
|
end
|
137
|
-
|
138
|
-
require "functions_framework/cloud_events/json_structure"
|
139
|
-
|
140
|
-
FunctionsFramework::CloudEvents.register_structured_format \
|
141
|
-
"json", FunctionsFramework::CloudEvents::JsonStructure
|
142
|
-
FunctionsFramework::CloudEvents.register_batched_format \
|
143
|
-
"json", FunctionsFramework::CloudEvents::JsonStructure
|
@@ -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
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 :
|
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
|
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"
|
130
147
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
211
|
+
index += 1
|
212
|
+
consume_comments str[index..-1].strip
|
136
213
|
end
|
137
214
|
end
|
138
215
|
end
|