functions_framework 0.4.0 → 0.6.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.
@@ -1,223 +0,0 @@
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 represents a complete CloudEvent, including the event data
25
- # and context attributes. It supports the standard required and optional
26
- # attributes defined in CloudEvents V1.0, 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/v1.0/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
76
- interpreter = FieldInterpreter.new attributes || args
77
- @spec_version = interpreter.spec_version ["specversion", "spec_version"], accept: /^1(\.|$)/
78
- @id = interpreter.string ["id"], required: true
79
- @source = interpreter.uri ["source"], required: true
80
- @type = interpreter.string ["type"], required: true
81
- @data = interpreter.object ["data"], allow_nil: true
82
- @data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
83
- @data_schema = interpreter.uri ["dataschema", "data_schema"]
84
- @subject = interpreter.string ["subject"]
85
- @time = interpreter.rfc3339_date_time ["time"]
86
- @attributes = interpreter.finish_attributes
87
- end
88
-
89
- ##
90
- # Create and return a copy of this event with the given changes. See
91
- # the constructor for the parameters that can be passed. In general,
92
- # you can pass a new value for any attribute, or pass `nil` to remove
93
- # an optional attribute.
94
- #
95
- # @param changes [keywords] See {#initialize} for a list of arguments.
96
- # @return [FunctionFramework::CloudEvents::Event]
97
- #
98
- def with **changes
99
- attributes = @attributes.merge changes
100
- V1.new attributes: attributes
101
- end
102
-
103
- ##
104
- # Return the value of the given named attribute. Both standard and
105
- # extension attributes are supported.
106
- #
107
- # Attribute names must be given as defined in the standard CloudEvents
108
- # specification. For example `specversion` rather than `spec_version`.
109
- #
110
- # Results are given in their "raw" form, generally a string. This may
111
- # be different from the Ruby object returned from corresponding
112
- # attribute methods. For example:
113
- #
114
- # event["time"] # => String rfc3339 representation
115
- # event.time # => DateTime object
116
- #
117
- # @param key [String,Symbol] The attribute name.
118
- # @return [String,nil]
119
- #
120
- def [] key
121
- @attributes[key.to_s]
122
- end
123
-
124
- ##
125
- # Return a hash representation of this event.
126
- #
127
- # @return [Hash]
128
- #
129
- def to_h
130
- @attributes.dup
131
- end
132
-
133
- ##
134
- # The `id` field. Required.
135
- #
136
- # @return [String]
137
- #
138
- attr_reader :id
139
-
140
- ##
141
- # The `source` field as a `URI` object. Required.
142
- #
143
- # @return [URI]
144
- #
145
- attr_reader :source
146
-
147
- ##
148
- # The `type` field. Required.
149
- #
150
- # @return [String]
151
- #
152
- attr_reader :type
153
-
154
- ##
155
- # The `specversion` field. Required.
156
- #
157
- # @return [String]
158
- #
159
- attr_reader :spec_version
160
- alias specversion spec_version
161
-
162
- ##
163
- # The event-specific data, or `nil` if there is no data.
164
- #
165
- # Data may be one of the following types:
166
- # * Binary data, represented by a `String` using the `ASCII-8BIT`
167
- # encoding.
168
- # * A string in some other encoding such as `UTF-8` or `US-ASCII`.
169
- # * Any JSON data type, such as a Boolean, Integer, Array, Hash, or
170
- # `nil`.
171
- #
172
- # @return [Object]
173
- #
174
- attr_reader :data
175
-
176
- ##
177
- # The optional `datacontenttype` field as a
178
- # {FunctionsFramework::CloudEvents::ContentType} object, or `nil` if
179
- # the field is absent.
180
- #
181
- # @return [FunctionsFramework::CloudEvents::ContentType,nil]
182
- #
183
- attr_reader :data_content_type
184
- alias datacontenttype data_content_type
185
-
186
- ##
187
- # The optional `dataschema` field as a `URI` object, or `nil` if the
188
- # field is absent.
189
- #
190
- # @return [URI,nil]
191
- #
192
- attr_reader :data_schema
193
- alias dataschema data_schema
194
-
195
- ##
196
- # The optional `subject` field, or `nil` if the field is absent.
197
- #
198
- # @return [String,nil]
199
- #
200
- attr_reader :subject
201
-
202
- ##
203
- # The optional `time` field as a `DateTime` object, or `nil` if the
204
- # field is absent.
205
- #
206
- # @return [DateTime,nil]
207
- #
208
- attr_reader :time
209
-
210
- ## @private
211
- def == other
212
- other.is_a?(V1) && @attributes == other.instance_variable_get(:@attributes)
213
- end
214
- alias eql? ==
215
-
216
- ## @private
217
- def hash
218
- @hash ||= @attributes.hash
219
- end
220
- end
221
- end
222
- end
223
- end
@@ -1,310 +0,0 @@
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
- # Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format.
28
- # See https://github.com/cloudevents/spec/blob/v0.3/http-transport-binding.md
29
- # and https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md.
30
- #
31
- class HttpBinding
32
- ##
33
- # Returns a default binding, with JSON supported.
34
- #
35
- def self.default
36
- @default ||= begin
37
- http_binding = new
38
- json_format = JsonFormat.new
39
- http_binding.register_structured_formatter "json", json_format
40
- http_binding.register_batched_formatter "json", json_format
41
- http_binding
42
- end
43
- end
44
-
45
- ##
46
- # Create an empty HTTP binding.
47
- #
48
- def initialize
49
- @structured_formatters = {}
50
- @batched_formatters = {}
51
- end
52
-
53
- ##
54
- # Register a formatter for the given type.
55
- #
56
- # A formatter must respond to the methods `#encode` and `#decode`. See
57
- # {FunctionsFramework::CloudEvents::JsonFormat} for an example.
58
- #
59
- # @param type [String] The subtype format that should be handled by
60
- # this formatter.
61
- # @param formatter [Object] The formatter object.
62
- # @return [self]
63
- #
64
- def register_structured_formatter type, formatter
65
- formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
66
- formatters << formatter unless formatters.include? formatter
67
- self
68
- end
69
-
70
- ##
71
- # Register a batch formatter for the given type.
72
- #
73
- # A batch formatter must respond to the methods `#encode_batch` and
74
- # `#decode_batch`. See {FunctionsFramework::CloudEvents::JsonFormat} for
75
- # an example.
76
- #
77
- # @param type [String] The subtype format that should be handled by
78
- # this formatter.
79
- # @param formatter [Object] The formatter object.
80
- # @return [self]
81
- #
82
- def register_batched_formatter type, formatter
83
- formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
84
- formatters << formatter unless formatters.include? formatter
85
- self
86
- end
87
-
88
- ##
89
- # Decode an event from the given Rack environment hash. Following the
90
- # CloudEvents spec, this chooses a handler based on the Content-Type of
91
- # the request.
92
- #
93
- # @param env [Hash] The Rack environment.
94
- # @param format_args [keywords] Extra args to pass to the formatter.
95
- # @return [FunctionsFramework::CloudEvents::Event] if the request
96
- # includes a single structured or binary event.
97
- # @return [Array<FunctionsFramework::CloudEvents::Event>] if the request
98
- # includes a batch of structured events.
99
- # @return [nil] if the request was not recognized as a CloudEvent.
100
- #
101
- def decode_rack_env env, **format_args
102
- content_type_header = env["CONTENT_TYPE"]
103
- content_type = ContentType.new content_type_header if content_type_header
104
- input = env["rack.input"]
105
- if input && content_type&.media_type == "application"
106
- case content_type.subtype_base
107
- when "cloudevents"
108
- input.set_encoding content_type.charset if content_type.charset
109
- return decode_structured_content input.read, content_type.subtype_format, **format_args
110
- when "cloudevents-batch"
111
- input.set_encoding content_type.charset if content_type.charset
112
- return decode_batched_content input.read, content_type.subtype_format, **format_args
113
- end
114
- end
115
- decode_binary_content env, content_type
116
- end
117
-
118
- ##
119
- # Decode a single event from the given content data. This should be
120
- # passed the request body, if the Content-Type is of the form
121
- # `application/cloudevents+format`.
122
- #
123
- # @param input [String] The string content.
124
- # @param format [String] The format code (e.g. "json").
125
- # @param format_args [keywords] Extra args to pass to the formatter.
126
- # @return [FunctionsFramework::CloudEvents::Event]
127
- #
128
- def decode_structured_content input, format, **format_args
129
- handlers = @structured_formatters[format] || []
130
- handlers.reverse_each do |handler|
131
- event = handler.decode input, **format_args
132
- return event if event
133
- end
134
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
135
- end
136
-
137
- ##
138
- # Decode a batch of events from the given content data. This should be
139
- # passed the request body, if the Content-Type is of the form
140
- # `application/cloudevents-batch+format`.
141
- #
142
- # @param input [String] The string content.
143
- # @param format [String] The format code (e.g. "json").
144
- # @param format_args [keywords] Extra args to pass to the formatter.
145
- # @return [Array<FunctionsFramework::CloudEvents::Event>]
146
- #
147
- def decode_batched_content input, format, **format_args
148
- handlers = @batched_formatters[format] || []
149
- handlers.reverse_each do |handler|
150
- events = handler.decode_batch input, **format_args
151
- return events if events
152
- end
153
- raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
154
- end
155
-
156
- ##
157
- # Decode an event from the given Rack environment in binary content mode.
158
- #
159
- # @param env [Hash] Rack environment hash.
160
- # @param content_type [FunctionsFramework::CloudEvents::ContentType]
161
- # the content type from the Rack environment.
162
- # @return [FunctionsFramework::CloudEvents::Event] if a CloudEvent
163
- # could be decoded from the Rack environment.
164
- # @return [nil] if the Rack environment does not indicate a CloudEvent
165
- #
166
- def decode_binary_content env, content_type
167
- spec_version = env["HTTP_CE_SPECVERSION"]
168
- return nil if spec_version.nil?
169
- raise SpecVersionError, "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
170
- input = env["rack.input"]
171
- data = if input
172
- input.set_encoding content_type.charset if content_type&.charset
173
- input.read
174
- end
175
- attributes = { "spec_version" => spec_version, "data" => data }
176
- attributes["data_content_type"] = content_type if content_type
177
- omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
178
- env.each do |key, value|
179
- match = /^HTTP_CE_(\w+)$/.match key
180
- next unless match
181
- attr_name = match[1].downcase
182
- attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
183
- end
184
- Event.create spec_version: spec_version, attributes: attributes
185
- end
186
-
187
- ##
188
- # Encode a single event to content data in the given format.
189
- #
190
- # The result is a two-element array where the first element is a headers
191
- # list (as defined in the Rack specification) and the second is a string
192
- # containing the HTTP body content. The headers list will contain only
193
- # one header, a `Content-Type` whose value is of the form
194
- # `application/cloudevents+format`.
195
- #
196
- # @param event [FunctionsFramework::CloudEvents::Event] The event.
197
- # @param format [String] The format code (e.g. "json")
198
- # @param format_args [keywords] Extra args to pass to the formatter.
199
- # @return [Array(headers,String)]
200
- #
201
- def encode_structured_content event, format, **format_args
202
- handlers = @structured_formatters[format] || []
203
- handlers.reverse_each do |handler|
204
- content = handler.encode event, **format_args
205
- return [{ "Content-Type" => "application/cloudevents+#{format}" }, content] if content
206
- end
207
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
208
- end
209
-
210
- ##
211
- # Encode a batch of events to content data in the given format.
212
- #
213
- # The result is a two-element array where the first element is a headers
214
- # list (as defined in the Rack specification) and the second is a string
215
- # containing the HTTP body content. The headers list will contain only
216
- # one header, a `Content-Type` whose value is of the form
217
- # `application/cloudevents-batch+format`.
218
- #
219
- # @param events [Array<FunctionsFramework::CloudEvents::Event>] The batch
220
- # of events.
221
- # @param format [String] The format code (e.g. "json").
222
- # @param format_args [keywords] Extra args to pass to the formatter.
223
- # @return [Array(headers,String)]
224
- #
225
- def encode_batched_content events, format, **format_args
226
- handlers = @batched_formatters[format] || []
227
- handlers.reverse_each do |handler|
228
- content = handler.encode_batch events, **format_args
229
- return [{ "Content-Type" => "application/cloudevents-batch+#{format}" }, content] if content
230
- end
231
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
232
- end
233
-
234
- ##
235
- # Encode an event to content and headers, in binary content mode.
236
- #
237
- # The result is a two-element array where the first element is a headers
238
- # list (as defined in the Rack specification) and the second is a string
239
- # containing the HTTP body content.
240
- #
241
- # @param event [FunctionsFramework::CloudEvents::Event] The event.
242
- # @return [Array(headers,String)]
243
- #
244
- def encode_binary_content event
245
- headers = {}
246
- body = nil
247
- event.to_h.each do |key, value|
248
- if key == "data"
249
- body = value
250
- elsif key == "datacontenttype"
251
- headers["Content-Type"] = value
252
- else
253
- headers["CE-#{key}"] = percent_encode value
254
- end
255
- end
256
- if body.is_a? ::String
257
- headers["Content-Type"] ||= if body.encoding == ::Encoding.ASCII_8BIT
258
- "application/octet-stream"
259
- else
260
- "text/plain; charset=#{body.encoding.name.downcase}"
261
- end
262
- elsif body.nil?
263
- headers.delete "Content-Type"
264
- else
265
- body = ::JSON.dump body
266
- headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
267
- end
268
- [headers, body]
269
- end
270
-
271
- ##
272
- # Decode a percent-encoded string to a UTF-8 string.
273
- #
274
- # @param str [String] Incoming ascii string from an HTTP header, with one
275
- # cycle of percent-encoding.
276
- # @return [String] Resulting decoded string in UTF-8.
277
- #
278
- def percent_decode str
279
- decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
280
- decoded_str.force_encoding ::Encoding::UTF_8
281
- end
282
-
283
- ##
284
- # Transcode an arbitrarily-encoded string to UTF-8, then percent-encode
285
- # non-printing and non-ascii characters to result in an ASCII string
286
- # suitable for setting as an HTTP header value.
287
- #
288
- # @param str [String] Incoming arbitrary string that can be represented
289
- # in UTF-8.
290
- # @return [String] Resulting encoded string in ASCII.
291
- #
292
- def percent_encode str
293
- arr = []
294
- utf_str = str.to_s.encode ::Encoding::UTF_8
295
- utf_str.each_byte do |byte|
296
- if byte >= 33 && byte <= 126 && byte != 37
297
- arr << byte
298
- else
299
- hi = byte / 16
300
- hi = hi > 9 ? 55 + hi : 48 + hi
301
- lo = byte % 16
302
- lo = lo > 9 ? 55 + lo : 48 + lo
303
- arr << 37 << hi << lo
304
- end
305
- end
306
- arr.pack "C*"
307
- end
308
- end
309
- end
310
- end