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.
- checksums.yaml +4 -4
- data/.yardopts +6 -2
- data/CHANGELOG.md +38 -0
- data/README.md +57 -137
- data/bin/functions-framework-ruby +19 -0
- data/docs/deploying-functions.md +182 -0
- data/docs/overview.md +142 -0
- data/docs/running-a-functions-server.md +122 -0
- data/docs/testing-functions.md +169 -0
- data/docs/writing-functions.md +261 -0
- data/lib/functions_framework.rb +19 -42
- data/lib/functions_framework/cli.rb +71 -13
- data/lib/functions_framework/cloud_events.rb +9 -109
- data/lib/functions_framework/cloud_events/errors.rb +42 -0
- data/lib/functions_framework/cloud_events/event.rb +51 -249
- data/lib/functions_framework/cloud_events/event/v1.rb +363 -0
- data/lib/functions_framework/cloud_events/http_binding.rb +270 -0
- data/lib/functions_framework/cloud_events/json_format.rb +122 -0
- data/lib/functions_framework/function.rb +7 -11
- data/lib/functions_framework/legacy_event_converter.rb +145 -0
- data/lib/functions_framework/registry.rb +3 -27
- data/lib/functions_framework/server.rb +63 -42
- data/lib/functions_framework/testing.rb +60 -20
- data/lib/functions_framework/version.rb +1 -1
- metadata +16 -6
- data/lib/functions_framework/cloud_events/binary_content.rb +0 -59
- data/lib/functions_framework/cloud_events/json_structure.rb +0 -88
@@ -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
|