functions_framework 0.4.1 → 0.5.0

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