functions_framework 0.1.0 → 0.3.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.
@@ -0,0 +1,363 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "date"
16
+ require "uri"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ module Event
21
+ ##
22
+ # A CloudEvents V1 data type.
23
+ #
24
+ # This object a complete CloudEvent, including the event data and its
25
+ # context attributes. It supports the standard required and optional
26
+ # attributes defined in CloudEvents V1, and arbitrary extension
27
+ # attributes. All attribute values can be obtained (in their string form)
28
+ # via the {Event::V1#[]} method. Additionally, standard attributes have
29
+ # their own accessor methods that may return typed objects (such as
30
+ # `DateTime` for the `time` attribute).
31
+ #
32
+ # This object is immutable. The data and attribute values can be
33
+ # retrieved but not modified. To obtain an event with modifications, use
34
+ # the {#with} method to create a copy with the desired changes.
35
+ #
36
+ # See https://github.com/cloudevents/spec/blob/master/spec.md for
37
+ # descriptions of the standard attributes.
38
+ #
39
+ class V1
40
+ include Event
41
+
42
+ ##
43
+ # Create a new cloud event object with the given data and attributes.
44
+ #
45
+ # Event attributes may be presented as keyword arguments, or as a Hash
46
+ # passed in via the `attributes` argument (but not both).
47
+ #
48
+ # The following standard attributes are supported and exposed as
49
+ # attribute methods on the object.
50
+ #
51
+ # * **:spec_version** (or **:specversion**) [`String`] - _required_ -
52
+ # The CloudEvents spec version (i.e. the `specversion` field.)
53
+ # * **:id** [`String`] - _required_ - The event `id` field.
54
+ # * **:source** [`String`, `URI`] - _required_ - The event `source`
55
+ # field.
56
+ # * **:type** [`String`] - _required_ - The event `type` field.
57
+ # * **:data** [`Object`] - _optional_ - The data associated with the
58
+ # event (i.e. the `data` field.)
59
+ # * **:data_content_type** (or **:datacontenttype**) [`String`,
60
+ # {ContentType}] - _optional_ - The content-type for the data, if
61
+ # the data is a string (i.e. the event `datacontenttype` field.)
62
+ # * **:data_schema** (or **:dataschema**) [`String`, `URI`] -
63
+ # _optional_ - The event `dataschema` field.
64
+ # * **:subject** [`String`] - _optional_ - The event `subject` field.
65
+ # * **:time** [`String`, `DateTime`, `Time`] - _optional_ - The
66
+ # event `time` field.
67
+ #
68
+ # Any additional attributes are assumed to be extension attributes.
69
+ # They are not available as separate methods, but can be accessed via
70
+ # the {Event::V1#[]} operator.
71
+ #
72
+ # @param attributes [Hash] The data and attributes, as a hash.
73
+ # @param args [keywords] The data and attributes, as keyword arguments.
74
+ #
75
+ def initialize attributes: nil, **args # rubocop:disable Metrics/AbcSize
76
+ args = keys_to_strings(attributes || args)
77
+ @attributes = {}
78
+ @spec_version, _unused = interpret_string args, ["specversion", "spec_version"], required: true
79
+ raise SpecVersionError, "Unrecognized specversion: #{@spec_version}" unless /^1(\.|$)/ =~ @spec_version
80
+ @id, _unused = interpret_string args, ["id"], required: true
81
+ @source, @source_string = interpret_uri args, ["source"], required: true
82
+ @type, _unused = interpret_string args, ["type"], required: true
83
+ @data, _unused = interpret_value args, ["data"], allow_nil: true
84
+ @data_content_type, @data_content_type_string =
85
+ interpret_content_type args, ["datacontenttype", "data_content_type"]
86
+ @data_schema, @data_schema_string = interpret_uri args, ["dataschema", "data_schema"]
87
+ @subject, _unused = interpret_string args, ["subject"]
88
+ @time, @time_string = interpret_date_time args, ["time"]
89
+ @attributes.merge! args
90
+ end
91
+
92
+ ##
93
+ # Create and return a copy of this event with the given changes. See
94
+ # the constructor for the parameters that can be passed. In general,
95
+ # you can pass a new value for any attribute, or pass `nil` to remove
96
+ # an optional attribute.
97
+ #
98
+ # @param changes [keywords] See {#initialize} for a list of arguments.
99
+ # @return [FunctionFramework::CloudEvents::Event]
100
+ #
101
+ def with **changes
102
+ attributes = @attributes.merge keys_to_strings changes
103
+ V1.new attributes: attributes
104
+ end
105
+
106
+ ##
107
+ # Return the value of the given named attribute. Both standard and
108
+ # extension attributes are supported.
109
+ #
110
+ # Attribute names must be given as defined in the standard CloudEvents
111
+ # specification. For example `specversion` rather than `spec_version`.
112
+ #
113
+ # Results are given in their "raw" form, generally a string. This may
114
+ # be different from what is returned from corresponding attribute
115
+ # methods. For example:
116
+ #
117
+ # event["time"] # => String rfc3339 representation
118
+ # event.time # => DateTime object
119
+ # event.time_string # => String rfc3339 representation
120
+ #
121
+ # @param key [String,Symbol] The attribute name.
122
+ # @return [String,nil]
123
+ #
124
+ def [] key
125
+ @attributes[key.to_s]
126
+ end
127
+
128
+ ##
129
+ # Return a hash representation of this event.
130
+ #
131
+ # @return [Hash]
132
+ #
133
+ def to_h
134
+ @attributes.dup
135
+ end
136
+
137
+ ##
138
+ # The `id` field. Required.
139
+ #
140
+ # @return [String]
141
+ #
142
+ attr_reader :id
143
+
144
+ ##
145
+ # The `source` field as a `URI` object. Required.
146
+ #
147
+ # @return [URI]
148
+ #
149
+ attr_reader :source
150
+
151
+ ##
152
+ # The string representation of the `source` field. Required.
153
+ #
154
+ # @return [String]
155
+ #
156
+ attr_reader :source_string
157
+
158
+ ##
159
+ # The `type` field. Required.
160
+ #
161
+ # @return [String]
162
+ #
163
+ attr_reader :type
164
+
165
+ ##
166
+ # The `specversion` field. Required.
167
+ #
168
+ # @return [String]
169
+ #
170
+ attr_reader :spec_version
171
+ alias specversion spec_version
172
+
173
+ ##
174
+ # The event-specific data, or `nil` if there is no data.
175
+ #
176
+ # Data may be one of the following types:
177
+ # * Binary data, represented by a `String` using the `ASCII-8BIT`
178
+ # encoding.
179
+ # * A string in some other encoding such as `UTF-8` or `US-ASCII`.
180
+ # * Any JSON data type, such as String, boolean, Integer, Array, or
181
+ # Hash
182
+ #
183
+ # @return [Object]
184
+ #
185
+ attr_reader :data
186
+
187
+ ##
188
+ # The optional `datacontenttype` field as a
189
+ # {FunctionsFramework::CloudEvents::ContentType} object, or `nil` if
190
+ # the field is absent.
191
+ #
192
+ # @return [FunctionsFramework::CloudEvents::ContentType,nil]
193
+ #
194
+ attr_reader :data_content_type
195
+ alias datacontenttype data_content_type
196
+
197
+ ##
198
+ # The string representation of the optional `datacontenttype` field, or
199
+ # `nil` if the field is absent.
200
+ #
201
+ # @return [String,nil]
202
+ #
203
+ attr_reader :data_content_type_string
204
+ alias datacontenttype_string data_content_type_string
205
+
206
+ ##
207
+ # The optional `dataschema` field as a `URI` object, or `nil` if the
208
+ # field is absent.
209
+ #
210
+ # @return [URI,nil]
211
+ #
212
+ attr_reader :data_schema
213
+ alias dataschema data_schema
214
+
215
+ ##
216
+ # The string representation of the optional `dataschema` field, or
217
+ # `nil` if the field is absent.
218
+ #
219
+ # @return [String,nil]
220
+ #
221
+ attr_reader :data_schema_string
222
+ alias dataschema_string data_schema_string
223
+
224
+ ##
225
+ # The optional `subject` field, or `nil` if the field is absent.
226
+ #
227
+ # @return [String,nil]
228
+ #
229
+ attr_reader :subject
230
+
231
+ ##
232
+ # The optional `time` field as a `DateTime` object, or `nil` if the
233
+ # field is absent.
234
+ #
235
+ # @return [DateTime,nil]
236
+ #
237
+ attr_reader :time
238
+
239
+ ##
240
+ # The rfc3339 string representation of the optional `time` field, or
241
+ # `nil` if the field is absent.
242
+ #
243
+ # @return [String,nil]
244
+ #
245
+ attr_reader :time_string
246
+
247
+ ## @private
248
+ def == other
249
+ other.is_a?(V1) && @attributes == other.instance_variable_get(:@attributes)
250
+ end
251
+ alias eql? ==
252
+
253
+ ## @private
254
+ def hash
255
+ @hash ||= @attributes.hash
256
+ end
257
+
258
+ private
259
+
260
+ def keys_to_strings hash
261
+ result = {}
262
+ hash.each do |key, val|
263
+ result[key.to_s] = val
264
+ end
265
+ result
266
+ end
267
+
268
+ def interpret_string args, keys, required: false
269
+ interpret_value args, keys, required: required do |value|
270
+ case value
271
+ when ::String
272
+ raise AttributeError, "The #{keys.last} field cannot be empty" if value.empty?
273
+ [value, value]
274
+ else
275
+ raise AttributeError, "Illegal type for #{keys.last}:" \
276
+ " String expected but #{value.class} found"
277
+ end
278
+ end
279
+ end
280
+
281
+ def interpret_uri args, keys, required: false
282
+ interpret_value args, keys, required: required do |value|
283
+ case value
284
+ when ::String
285
+ raise AttributeError, "The #{keys.last} field cannot be empty" if value.empty?
286
+ begin
287
+ [::URI.parse(value), value]
288
+ rescue ::URI::InvalidURIError => e
289
+ raise AttributeError, "Illegal format for #{keys.last}: #{e.message}"
290
+ end
291
+ when ::URI::Generic
292
+ [value, value.to_s]
293
+ else
294
+ raise AttributeError, "Illegal type for #{keys.last}:" \
295
+ " String or URI expected but #{value.class} found"
296
+ end
297
+ end
298
+ end
299
+
300
+ def interpret_date_time args, keys, required: false
301
+ interpret_value args, keys, required: required do |value|
302
+ case value
303
+ when ::String
304
+ begin
305
+ [::DateTime.rfc3339(value), value]
306
+ rescue ::Date::Error => e
307
+ raise AttributeError, "Illegal format for #{keys.last}: #{e.message}"
308
+ end
309
+ when ::DateTime
310
+ [value, value.rfc3339]
311
+ when ::Time
312
+ value = value.to_datetime
313
+ [value, value.rfc3339]
314
+ else
315
+ raise AttributeError, "Illegal type for #{keys.last}:" \
316
+ " String, Time, or DateTime expected but #{value.class} found"
317
+ end
318
+ end
319
+ end
320
+
321
+ def interpret_content_type args, keys, required: false
322
+ interpret_value args, keys, required: required do |value|
323
+ case value
324
+ when ::String
325
+ raise AttributeError, "The #{keys.last} field cannot be empty" if value.empty?
326
+ [ContentType.new(value), value]
327
+ when ContentType
328
+ [value, value.to_s]
329
+ else
330
+ raise AttributeError, "Illegal type for #{keys.last}:" \
331
+ " String, or ContentType expected but #{value.class} found"
332
+ end
333
+ end
334
+ end
335
+
336
+ def interpret_value args, keys, required: false, allow_nil: false
337
+ value = nil
338
+ found = false
339
+ keys.each do |key|
340
+ key_present = args.key? key
341
+ val = args.delete key
342
+ if allow_nil && key_present || !allow_nil && !val.nil?
343
+ value = val
344
+ found = true
345
+ end
346
+ end
347
+ if found
348
+ if block_given?
349
+ converted, raw = yield value
350
+ else
351
+ converted = raw = value
352
+ end
353
+ @attributes[keys.first] = raw
354
+ [converted, raw]
355
+ else
356
+ raise AttributeError, "The #{keys.last} field is required" if required
357
+ [nil, nil]
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,270 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "functions_framework/cloud_events/json_format"
16
+
17
+ module FunctionsFramework
18
+ module CloudEvents
19
+ ##
20
+ # HTTP binding for CloudEvents.
21
+ #
22
+ # This class implements HTTP binding, including unmarshalling of events from
23
+ # Rack environment data, and marshalling of events to Rack environment data.
24
+ # It supports binary (i.e. header-based) HTTP content, as well as structured
25
+ # (body-based) content that can delegate to formatters such as JSON.
26
+ #
27
+ # See https://github.com/cloudevents/spec/blob/master/http-protocol-binding.md
28
+ #
29
+ class HttpBinding
30
+ ##
31
+ # Returns a default binding, with JSON supported.
32
+ #
33
+ def self.default
34
+ @default ||= begin
35
+ http_binding = new
36
+ json_format = JsonFormat.new
37
+ http_binding.register_structured_formatter "json", json_format
38
+ http_binding.register_batched_formatter "json", json_format
39
+ http_binding
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Create an empty HTTP binding.
45
+ #
46
+ def initialize
47
+ @structured_formatters = {}
48
+ @batched_formatters = {}
49
+ end
50
+
51
+ ##
52
+ # Register a formatter for the given type.
53
+ #
54
+ # A formatter must respond to the methods `#encode` and `#decode`. See
55
+ # {FunctionsFramework::CloudEvents::JsonFormat} for an example.
56
+ #
57
+ # @param type [String] The subtype format that should be handled by
58
+ # this formatter.
59
+ # @param formatter [Object] The formatter object.
60
+ # @return [self]
61
+ #
62
+ def register_structured_formatter type, formatter
63
+ formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
64
+ formatters << formatter unless formatters.include? formatter
65
+ self
66
+ end
67
+
68
+ ##
69
+ # Register a batch formatter for the given type.
70
+ #
71
+ # A batch formatter must respond to the methods `#encode_batch` and
72
+ # `#decode_batch`. See {FunctionsFramework::CloudEvents::JsonFormat} for
73
+ # an example.
74
+ #
75
+ # @param type [String] The subtype format that should be handled by
76
+ # this formatter.
77
+ # @param formatter [Object] The formatter object.
78
+ # @return [self]
79
+ #
80
+ def register_batched_formatter type, formatter
81
+ formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
82
+ formatters << formatter unless formatters.include? formatter
83
+ self
84
+ end
85
+
86
+ ##
87
+ # Decode an event from the given Rack environment hash. Following the
88
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
89
+ # the request.
90
+ #
91
+ # @param env [Hash] The Rack environment.
92
+ # @param format_args [keywords] Extra args to pass to the formatter.
93
+ # @return [FunctionsFramework::CloudEvents::Event] if the request
94
+ # includes a single structured or binary event.
95
+ # @return [Array<FunctionsFramework::CloudEvents::Event>] if the request
96
+ # includes a batch of structured events.
97
+ # @return [nil] if the request was not recognized as a CloudEvent.
98
+ #
99
+ def decode_rack_env env, **format_args
100
+ content_type_header = env["CONTENT_TYPE"]
101
+ content_type = ContentType.new content_type_header if content_type_header
102
+ input = env["rack.input"]
103
+ if input && content_type&.media_type == "application"
104
+ case content_type.subtype_prefix
105
+ when "cloudevents"
106
+ input.set_encoding content_type.charset if content_type.charset
107
+ return decode_structured_content input.read, content_type.subtype_format, **format_args
108
+ when "cloudevents-batch"
109
+ input.set_encoding content_type.charset if content_type.charset
110
+ return decode_batched_content input.read, content_type.subtype_format, **format_args
111
+ end
112
+ end
113
+ decode_binary_content env, content_type
114
+ end
115
+
116
+ ##
117
+ # Decode a single event from the given content data. This should be
118
+ # passed the request body, if the Content-Type is of the form
119
+ # `application/cloudevents+format`.
120
+ #
121
+ # @param input [String] The string content.
122
+ # @param format [String] The format code (e.g. "json").
123
+ # @param format_args [keywords] Extra args to pass to the formatter.
124
+ # @return [FunctionsFramework::CloudEvents::Event]
125
+ #
126
+ def decode_structured_content input, format, **format_args
127
+ handlers = @structured_formatters[format] || []
128
+ handlers.reverse_each do |handler|
129
+ event = handler.decode input, **format_args
130
+ return event if event
131
+ end
132
+ raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
133
+ end
134
+
135
+ ##
136
+ # Decode a batch of events from the given content data. This should be
137
+ # passed the request body, if the Content-Type is of the form
138
+ # `application/cloudevents-batch+format`.
139
+ #
140
+ # @param input [String] The string content.
141
+ # @param format [String] The format code (e.g. "json").
142
+ # @param format_args [keywords] Extra args to pass to the formatter.
143
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
144
+ #
145
+ def decode_batched_content input, format, **format_args
146
+ handlers = @batched_formatters[format] || []
147
+ handlers.reverse_each do |handler|
148
+ events = handler.decode_batch input, **format_args
149
+ return events if events
150
+ end
151
+ raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
152
+ end
153
+
154
+ ##
155
+ # Decode an event from the given Rack environment in binary content mode.
156
+ #
157
+ # @param env [Hash] Rack environment hash.
158
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType]
159
+ # the content type from the Rack environment.
160
+ # @return [FunctionsFramework::CloudEvents::Event] if a CloudEvent
161
+ # could be decoded from the Rack environment.
162
+ # @return [nil] if the Rack environment does not indicate a CloudEvent
163
+ #
164
+ def decode_binary_content env, content_type
165
+ spec_version = env["HTTP_CE_SPECVERSION"]
166
+ return nil if spec_version.nil?
167
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
168
+ input = env["rack.input"]
169
+ data = if input
170
+ input.set_encoding content_type.charset if content_type&.charset
171
+ input.read
172
+ end
173
+ attributes = { "spec_version" => spec_version, "data" => data }
174
+ attributes["data_content_type"] = content_type if content_type
175
+ omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
176
+ env.each do |key, value|
177
+ match = /^HTTP_CE_(\w+)$/.match key
178
+ next unless match
179
+ attr_name = match[1].downcase
180
+ attributes[attr_name] = value unless omit_names.include? attr_name
181
+ end
182
+ Event.create spec_version: spec_version, attributes: attributes
183
+ end
184
+
185
+ ##
186
+ # Encode a single event to content data in the given format.
187
+ #
188
+ # The result is a two-element array where the first element is a headers
189
+ # list (as defined in the Rack specification) and the second is a string
190
+ # containing the HTTP body content. The headers list will contain only
191
+ # one header, a `Content-Type` whose value is of the form
192
+ # `application/cloudevents+format`.
193
+ #
194
+ # @param event [FunctionsFramework::CloudEvents::Event] The event.
195
+ # @param format [String] The format code (e.g. "json")
196
+ # @param format_args [keywords] Extra args to pass to the formatter.
197
+ # @return [Array(headers,String)]
198
+ #
199
+ def encode_structured_content event, format, **format_args
200
+ handlers = @structured_formatters[format] || []
201
+ handlers.reverse_each do |handler|
202
+ content = handler.encode event, **format_args
203
+ return [{ "Content-Type" => "application/cloudevents+#{format}" }, content] if content
204
+ end
205
+ raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
206
+ end
207
+
208
+ ##
209
+ # Encode a batch of events to content data in the given format.
210
+ #
211
+ # The result is a two-element array where the first element is a headers
212
+ # list (as defined in the Rack specification) and the second is a string
213
+ # containing the HTTP body content. The headers list will contain only
214
+ # one header, a `Content-Type` whose value is of the form
215
+ # `application/cloudevents-batch+format`.
216
+ #
217
+ # @param events [Array<FunctionsFramework::CloudEvents::Event>] The batch
218
+ # of events.
219
+ # @param format [String] The format code (e.g. "json").
220
+ # @param format_args [keywords] Extra args to pass to the formatter.
221
+ # @return [Array(headers,String)]
222
+ #
223
+ def encode_batched_content events, format, **format_args
224
+ handlers = @batched_formatters[format] || []
225
+ handlers.reverse_each do |handler|
226
+ content = handler.encode_batch events, **format_args
227
+ return [{ "Content-Type" => "application/cloudevents-batch+#{format}" }, content] if content
228
+ end
229
+ raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
230
+ end
231
+
232
+ ##
233
+ # Encode an event to content and headers, in binary content mode.
234
+ #
235
+ # The result is a two-element array where the first element is a headers
236
+ # list (as defined in the Rack specification) and the second is a string
237
+ # containing the HTTP body content.
238
+ #
239
+ # @param event [FunctionsFramework::CloudEvents::Event] The event.
240
+ # @return [Array(headers,String)]
241
+ #
242
+ def encode_binary_content event
243
+ headers = {}
244
+ body = nil
245
+ event.to_h.each do |key, value|
246
+ if key == "data"
247
+ body = value
248
+ elsif key == "datacontenttype"
249
+ headers["Content-Type"] = value
250
+ else
251
+ headers["CE-#{key}"] = value
252
+ end
253
+ end
254
+ if body.is_a? ::String
255
+ headers["Content-Type"] ||= if body.encoding == ::Encoding.ASCII_8BIT
256
+ "application/octet-stream"
257
+ else
258
+ "text/plain; charset=#{body.encoding.name.downcase}"
259
+ end
260
+ elsif body.nil?
261
+ headers.delete "Content-Type"
262
+ else
263
+ body = ::JSON.dump body
264
+ headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
265
+ end
266
+ [headers, body]
267
+ end
268
+ end
269
+ end
270
+ end