functions_framework 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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