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.
@@ -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} or
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 two arguments: the event _data_ and the
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 name of the function to run
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
- function = global_registry[target]
216
- raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
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 name of the function to run
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
- ::FunctionsFramework.logger.level -= 1
88
+ @logging_level -= 1
75
89
  end
76
90
  op.on "-q", "--quiet", "Decrease log verbosity" do
77
- ::FunctionsFramework.logger.level += 1
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
- FunctionsFramework.logger.info \
99
- "FunctionsFramework: Loading functions from #{@source.inspect}..."
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
- server = ::FunctionsFramework.start @target do |config|
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
- server.wait_until_stopped
110
- self
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
- # [1.0 specification](https://github.com/cloudevents/spec/blob/master/spec.md).
25
- # It provides for unmarshaling of events from Rack environment data from
26
- # binary (i.e. header-based) format, as well as structured (body-based) and
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
- @structured_formats = {}
34
- @batched_formats = {}
31
+ # @private
32
+ SUPPORTED_SPEC_VERSIONS = ["0.3", "1.0"].freeze
35
33
 
36
34
  class << self
37
35
  ##
38
- # Register a handler for the given structured format.
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
- # @param format [String] The subtype format that should be handled by
61
- # this handler
62
- # @param handler [#decode_batched_content] The handler object
63
- # @return [self]
38
+ # @return [Array<String>]
64
39
  #
65
- def register_batched_format format, handler
66
- handlers = @batched_formats[format.to_s.strip.downcase] ||= []
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
- # 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