functions_framework 0.1.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,310 @@
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
@@ -0,0 +1,173 @@
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 "base64"
16
+ require "json"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ ##
21
+ # An implementation of JSON format and JSON batch format.
22
+ #
23
+ # Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format.
24
+ # See https://github.com/cloudevents/spec/blob/v0.3/json-format.md and
25
+ # https://github.com/cloudevents/spec/blob/v1.0/json-format.md.
26
+ #
27
+ class JsonFormat
28
+ ##
29
+ # Decode an event from the given input JSON string.
30
+ #
31
+ # @param json [String] A JSON-formatted string
32
+ # @return [FunctionsFramework::CloudEvents::Event]
33
+ #
34
+ def decode json, **_other_kwargs
35
+ structure = ::JSON.parse json
36
+ decode_hash_structure structure
37
+ end
38
+
39
+ ##
40
+ # Encode an event to a JSON string.
41
+ #
42
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
43
+ # @param sort [boolean] Whether to sort keys of the JSON output.
44
+ # @return [String] The JSON representation.
45
+ #
46
+ def encode event, sort: false, **_other_kwargs
47
+ structure = encode_hash_structure event
48
+ structure = sort_keys structure if sort
49
+ ::JSON.dump structure
50
+ end
51
+
52
+ ##
53
+ # Decode a batch of events from the given input string.
54
+ #
55
+ # @param json [String] A JSON-formatted string
56
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
57
+ #
58
+ def decode_batch json, **_other_kwargs
59
+ structure_array = Array(::JSON.parse(json))
60
+ structure_array.map do |structure|
61
+ decode_hash_structure structure
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Encode a batch of event to a JSON string.
67
+ #
68
+ # @param events [Array<FunctionsFramework::CloudEvents::Event>] An array
69
+ # of input events.
70
+ # @param sort [boolean] Whether to sort keys of the JSON output.
71
+ # @return [String] The JSON representation.
72
+ #
73
+ def encode_batch events, sort: false, **_other_kwargs
74
+ structure_array = Array(events).map do |event|
75
+ structure = encode_hash_structure event
76
+ sort ? sort_keys(structure) : structure
77
+ end
78
+ ::JSON.dump structure_array
79
+ end
80
+
81
+ ##
82
+ # Decode a single event from a hash data structure with keys and types
83
+ # conforming to the JSON envelope.
84
+ #
85
+ # @param structure [Hash] An input hash.
86
+ # @return [FunctionsFramework::CloudEvents::Event]
87
+ #
88
+ def decode_hash_structure structure
89
+ spec_version = structure["specversion"].to_s
90
+ case spec_version
91
+ when "0.3"
92
+ decode_hash_structure_v0 structure
93
+ when /^1(\.|$)/
94
+ decode_hash_structure_v1 structure
95
+ else
96
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Encode a single event to a hash data structure with keys and types
102
+ # conforming to the JSON envelope.
103
+ #
104
+ # @param event [FunctionsFramework::CloudEvents::Event] An input event.
105
+ # @return [String] The hash structure.
106
+ #
107
+ def encode_hash_structure event
108
+ case event
109
+ when Event::V0
110
+ encode_hash_structure_v0 event
111
+ when Event::V1
112
+ encode_hash_structure_v1 event
113
+ else
114
+ raise SpecVersionError, "Unrecognized specversion: #{event.spec_version}"
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def sort_keys hash
121
+ result = {}
122
+ hash.keys.sort.each do |key|
123
+ result[key] = hash[key]
124
+ end
125
+ result
126
+ end
127
+
128
+ def decode_hash_structure_v0 structure
129
+ data = structure["data"]
130
+ content_type = structure["datacontenttype"]
131
+ if data.is_a?(::String) && content_type.is_a?(::String)
132
+ content_type = ContentType.new content_type
133
+ if content_type.subtype == "json" || content_type.subtype_format == "json"
134
+ structure = structure.dup
135
+ structure["data"] = ::JSON.parse data rescue data
136
+ structure["datacontenttype"] = content_type
137
+ end
138
+ end
139
+ Event::V0.new attributes: structure
140
+ end
141
+
142
+ def decode_hash_structure_v1 structure
143
+ if structure.key? "data_base64"
144
+ structure = structure.dup
145
+ structure["data"] = ::Base64.decode64 structure.delete "data_base64"
146
+ end
147
+ Event::V1.new attributes: structure
148
+ end
149
+
150
+ def encode_hash_structure_v0 event
151
+ structure = event.to_h
152
+ data = event.data
153
+ content_type = event.data_content_type
154
+ if data.is_a?(::String) && !content_type.nil?
155
+ if content_type.subtype == "json" || content_type.subtype_format == "json"
156
+ structure["data"] = ::JSON.parse data rescue data
157
+ end
158
+ end
159
+ structure
160
+ end
161
+
162
+ def encode_hash_structure_v1 event
163
+ structure = event.to_h
164
+ data = structure["data"]
165
+ if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
166
+ structure.delete "data"
167
+ structure["data_base64"] = ::Base64.encode64 data
168
+ end
169
+ structure
170
+ end
171
+ end
172
+ end
173
+ end