cloud_events 0.3.1 → 0.7.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.
@@ -15,14 +15,24 @@ module CloudEvents
15
15
  #
16
16
  class HttpBinding
17
17
  ##
18
- # Returns a default binding, with JSON supported.
18
+ # The name of the JSON decoder/encoder
19
+ # @return [String]
20
+ #
21
+ JSON_FORMAT = "json"
22
+
23
+ # @private
24
+ ILLEGAL_METHODS = ["GET", "HEAD"].freeze
25
+
26
+ ##
27
+ # Returns a default HTTP binding, including support for JSON format.
28
+ #
29
+ # @return [HttpBinding]
19
30
  #
20
31
  def self.default
21
32
  @default ||= begin
22
33
  http_binding = new
23
- json_format = JsonFormat.new
24
- http_binding.register_structured_formatter "json", json_format
25
- http_binding.register_batched_formatter "json", json_format
34
+ http_binding.register_formatter JsonFormat.new, encoder_name: JSON_FORMAT
35
+ http_binding.default_encoder_name = JSON_FORMAT
26
36
  http_binding
27
37
  end
28
38
  end
@@ -31,229 +41,290 @@ module CloudEvents
31
41
  # Create an empty HTTP binding.
32
42
  #
33
43
  def initialize
34
- @structured_formatters = {}
35
- @batched_formatters = {}
44
+ @event_decoders = Format::Multi.new do |result|
45
+ result&.key?(:event) || result&.key?(:event_batch) ? result : nil
46
+ end
47
+ @event_encoders = {}
48
+ @data_decoders = Format::Multi.new do |result|
49
+ result&.key?(:data) && result&.key?(:content_type) ? result : nil
50
+ end
51
+ @data_encoders = Format::Multi.new do |result|
52
+ result&.key?(:content) && result&.key?(:content_type) ? result : nil
53
+ end
54
+ text_format = TextFormat.new
55
+ @data_decoders.formats.replace [text_format, DefaultDataFormat]
56
+ @data_encoders.formats.replace [text_format, DefaultDataFormat]
57
+
58
+ @default_encoder_name = nil
36
59
  end
37
60
 
38
61
  ##
39
- # Register a formatter for the given type.
40
- #
41
- # A formatter must respond to the methods `#encode` and `#decode`. See
42
- # {CloudEvents::JsonFormat} for an example.
62
+ # Register a formatter for all operations it supports, based on which
63
+ # methods are implemented by the formatter object. See
64
+ # {CloudEvents::Format} for a list of possible methods.
43
65
  #
44
- # @param type [String] The subtype format that should be handled by
45
- # this formatter.
46
- # @param formatter [Object] The formatter object.
66
+ # @param formatter [Object] The formatter
67
+ # @param encoder_name [String] The encoder name under which this formatter
68
+ # will register its encode operations. Optional. If not specified, any
69
+ # event encoder will _not_ be registered.
70
+ # @param deprecated_name [String] This positional argument is deprecated
71
+ # and will be removed in version 1.0. Use encoder_name instead.
47
72
  # @return [self]
48
73
  #
49
- def register_structured_formatter type, formatter
50
- formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
51
- formatters << formatter unless formatters.include? formatter
74
+ def register_formatter formatter, deprecated_name = nil, encoder_name: nil
75
+ encoder_name ||= deprecated_name
76
+ encoder_name = encoder_name.to_s.strip.downcase if encoder_name
77
+ decode_event = formatter.respond_to? :decode_event
78
+ encode_event = encoder_name if formatter.respond_to? :encode_event
79
+ decode_data = formatter.respond_to? :decode_data
80
+ encode_data = formatter.respond_to? :encode_data
81
+ register_formatter_methods formatter,
82
+ decode_event: decode_event,
83
+ encode_event: encode_event,
84
+ decode_data: decode_data,
85
+ encode_data: encode_data
52
86
  self
53
87
  end
54
88
 
55
89
  ##
56
- # Register a batch formatter for the given type.
90
+ # Registers the given formatter for the given operations. Some arguments
91
+ # are activated by passing `true`, whereas those that rely on a format name
92
+ # are activated by passing in a name string.
57
93
  #
58
- # A batch formatter must respond to the methods `#encode_batch` and
59
- # `#decode_batch`. See {CloudEvents::JsonFormat} for an example.
60
- #
61
- # @param type [String] The subtype format that should be handled by
62
- # this formatter.
63
- # @param formatter [Object] The formatter object.
94
+ # @param formatter [Object] The formatter
95
+ # @param decode_event [boolean] If true, register the formatter's
96
+ # {CloudEvents::Format#decode_event} method.
97
+ # @param encode_event [String] If set to a string, use the formatter's
98
+ # {CloudEvents::Format#encode_event} method when that name is requested.
99
+ # @param decode_data [boolean] If true, register the formatter's
100
+ # {CloudEvents::Format#decode_data} method.
101
+ # @param encode_data [boolean] If true, register the formatter's
102
+ # {CloudEvents::Format#encode_data} method.
64
103
  # @return [self]
65
104
  #
66
- def register_batched_formatter type, formatter
67
- formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
68
- formatters << formatter unless formatters.include? formatter
105
+ def register_formatter_methods formatter,
106
+ decode_event: false,
107
+ encode_event: nil,
108
+ decode_data: false,
109
+ encode_data: false
110
+ @event_decoders.formats.unshift formatter if decode_event
111
+ if encode_event
112
+ encoders = @event_encoders[encode_event] ||= Format::Multi.new do |result|
113
+ result&.key?(:content) && result&.key?(:content_type) ? result : nil
114
+ end
115
+ encoders.formats.unshift formatter
116
+ end
117
+ @data_decoders.formats.unshift formatter if decode_data
118
+ @data_encoders.formats.unshift formatter if encode_data
69
119
  self
70
120
  end
71
121
 
122
+ ##
123
+ # The name of the encoder to use if none is specified
124
+ #
125
+ # @return [String,nil]
126
+ #
127
+ attr_accessor :default_encoder_name
128
+
129
+ ##
130
+ # Analyze a Rack environment hash and determine whether it is _probably_
131
+ # a CloudEvent. This is done by examining headers only, and does not read
132
+ # or parse the request body. The result is a best guess: false negatives or
133
+ # false positives are possible for edge cases, but the logic should
134
+ # generally detect canonically-formatted events.
135
+ #
136
+ # @param env [Hash] The Rack environment.
137
+ # @return [boolean] Whether the request is likely a CloudEvent.
138
+ #
139
+ def probable_event? env
140
+ return false if ILLEGAL_METHODS.include? env["REQUEST_METHOD"]
141
+ return true if env["HTTP_CE_SPECVERSION"]
142
+ content_type = ContentType.new env["CONTENT_TYPE"].to_s
143
+ content_type.media_type == "application" &&
144
+ ["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
145
+ end
146
+
72
147
  ##
73
148
  # Decode an event from the given Rack environment hash. Following the
74
149
  # CloudEvents spec, this chooses a handler based on the Content-Type of
75
150
  # the request.
76
151
  #
152
+ # Note that this method will read the body (i.e. `rack.input`) stream.
153
+ # If you need to access the body after calling this method, you will need
154
+ # to rewind the stream. To determine whether the request is a CloudEvent
155
+ # without reading the body, use {#probable_event?}.
156
+ #
77
157
  # @param env [Hash] The Rack environment.
158
+ # @param allow_opaque [boolean] If true, returns opaque event objects if
159
+ # the input is not in a recognized format. If false, raises
160
+ # {CloudEvents::UnsupportedFormatError} in that case. Default is false.
78
161
  # @param format_args [keywords] Extra args to pass to the formatter.
79
162
  # @return [CloudEvents::Event] if the request includes a single structured
80
163
  # or binary event.
81
164
  # @return [Array<CloudEvents::Event>] if the request includes a batch of
82
165
  # structured events.
83
- # @return [nil] if the request was not recognized as a CloudEvent.
166
+ # @raise [CloudEvents::CloudEventsError] if an event could not be decoded
167
+ # from the request.
84
168
  #
85
- def decode_rack_env env, **format_args
86
- content_type_header = env["CONTENT_TYPE"]
87
- content_type = ContentType.new content_type_header if content_type_header
88
- input = env["rack.input"]
89
- if input && content_type&.media_type == "application"
90
- case content_type.subtype_base
91
- when "cloudevents"
92
- content = read_with_charset input, content_type.charset
93
- return decode_structured_content content, content_type.subtype_format, **format_args
94
- when "cloudevents-batch"
95
- content = read_with_charset input, content_type.charset
96
- return decode_batched_content content, content_type.subtype_format, **format_args
97
- end
169
+ def decode_event env, allow_opaque: false, **format_args
170
+ request_method = env["REQUEST_METHOD"]
171
+ raise NotCloudEventError, "Request method is #{request_method}" if ILLEGAL_METHODS.include? request_method
172
+ content_type_string = env["CONTENT_TYPE"]
173
+ content_type = ContentType.new content_type_string if content_type_string
174
+ content = read_with_charset env["rack.input"], content_type&.charset
175
+ result = decode_binary_content(content, content_type, env, false, **format_args) ||
176
+ decode_structured_content(content, content_type, allow_opaque, **format_args)
177
+ if result.nil?
178
+ content_type_string = content_type_string ? content_type_string.inspect : "not present"
179
+ raise NotCloudEventError, "Content-Type is #{content_type_string}, and CE-SpecVersion is not present"
98
180
  end
99
- decode_binary_content env, content_type
181
+ result
100
182
  end
101
183
 
102
184
  ##
103
- # Decode a single event from the given content data. This should be
104
- # passed the request body, if the Content-Type is of the form
105
- # `application/cloudevents+format`.
185
+ # Encode an event or batch of events into HTTP headers and body.
106
186
  #
107
- # @param input [String] The string content.
108
- # @param format [String] The format code (e.g. "json").
109
- # @param format_args [keywords] Extra args to pass to the formatter.
110
- # @return [CloudEvents::Event]
187
+ # You may provide an event, an array of events, or an opaque event. You may
188
+ # also specify what content mode and format to use.
111
189
  #
112
- def decode_structured_content input, format, **format_args
113
- handlers = @structured_formatters[format] || []
114
- handlers.reverse_each do |handler|
115
- event = handler.decode input, **format_args
116
- return event if event
117
- end
118
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
119
- end
120
-
121
- ##
122
- # Decode a batch of events from the given content data. This should be
123
- # passed the request body, if the Content-Type is of the form
124
- # `application/cloudevents-batch+format`.
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. When using structured content mode, the
193
+ # headers list will contain only a `Content-Type` header and the body will
194
+ # contain the serialized event. When using binary mode, the header list
195
+ # will contain the serialized event attributes and the body will contain
196
+ # the serialized event data.
125
197
  #
126
- # @param input [String] The string content.
127
- # @param format [String] The format code (e.g. "json").
198
+ # @param event [CloudEvents::Event,Array<CloudEvents::Event>,CloudEvents::Event::Opaque]
199
+ # The event, batch, or opaque event.
200
+ # @param structured_format [boolean,String] If given, the data will be
201
+ # encoded in structured content mode. You can pass a string to select
202
+ # a format name, or pass `true` to use the default format. If set to
203
+ # `false` (the default), the data will be encoded in binary mode.
128
204
  # @param format_args [keywords] Extra args to pass to the formatter.
129
- # @return [Array<CloudEvents::Event>]
205
+ # @return [Array(headers,String)]
130
206
  #
131
- def decode_batched_content input, format, **format_args
132
- handlers = @batched_formatters[format] || []
133
- handlers.reverse_each do |handler|
134
- events = handler.decode_batch input, **format_args
135
- return events if events
207
+ def encode_event event, structured_format: false, **format_args
208
+ if event.is_a? Event::Opaque
209
+ [{ "Content-Type" => event.content_type.to_s }, event.content]
210
+ elsif !structured_format
211
+ if event.is_a? ::Array
212
+ raise ::ArgumentError, "Encoding a batch requires structured_format"
213
+ end
214
+ encode_binary_content event, legacy_data_encode: false, **format_args
215
+ else
216
+ structured_format = default_encoder_name if structured_format == true
217
+ raise ::ArgumentError, "Format name not specified, and no default is set" unless structured_format
218
+ case event
219
+ when ::Array
220
+ encode_batched_content event, structured_format, **format_args
221
+ when Event
222
+ encode_structured_content event, structured_format, **format_args
223
+ else
224
+ raise ::ArgumentError, "Unknown event type: #{event.class}"
225
+ end
136
226
  end
137
- raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
138
227
  end
139
228
 
140
229
  ##
141
- # Decode an event from the given Rack environment in binary content mode.
230
+ # Decode an event from the given Rack environment hash. Following the
231
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
232
+ # the request.
142
233
  #
143
- # @param env [Hash] Rack environment hash.
144
- # @param content_type [CloudEvents::ContentType] the content type from the
145
- # Rack environment.
146
- # @return [CloudEvents::Event] if a CloudEvent could be decoded from the
147
- # Rack environment.
148
- # @return [nil] if the Rack environment does not indicate a CloudEvent
234
+ # @deprecated Will be removed in version 1.0. Use {#decode_event} instead.
149
235
  #
150
- def decode_binary_content env, content_type
151
- spec_version = env["HTTP_CE_SPECVERSION"]
152
- return nil if spec_version.nil?
153
- raise SpecVersionError, "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
154
- input = env["rack.input"]
155
- data = read_with_charset input, content_type&.charset if input
156
- attributes = { "spec_version" => spec_version, "data" => data }
157
- attributes["data_content_type"] = content_type if content_type
158
- omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
159
- env.each do |key, value|
160
- match = /^HTTP_CE_(\w+)$/.match key
161
- next unless match
162
- attr_name = match[1].downcase
163
- attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
164
- end
165
- Event.create spec_version: spec_version, attributes: attributes
236
+ # @param env [Hash] The Rack environment.
237
+ # @param format_args [keywords] Extra args to pass to the formatter.
238
+ # @return [CloudEvents::Event] if the request includes a single structured
239
+ # or binary event.
240
+ # @return [Array<CloudEvents::Event>] if the request includes a batch of
241
+ # structured events.
242
+ # @return [nil] if the request does not appear to be a CloudEvent.
243
+ # @raise [CloudEvents::CloudEventsError] if the request appears to be a
244
+ # CloudEvent but decoding failed.
245
+ #
246
+ def decode_rack_env env, **format_args
247
+ content_type_string = env["CONTENT_TYPE"]
248
+ content_type = ContentType.new content_type_string if content_type_string
249
+ content = read_with_charset env["rack.input"], content_type&.charset
250
+ env["rack.input"].rewind rescue nil
251
+ decode_binary_content(content, content_type, env, true, **format_args) ||
252
+ decode_structured_content(content, content_type, false, **format_args)
166
253
  end
167
254
 
168
255
  ##
169
- # Encode a single event to content data in the given format.
256
+ # Encode a single event in structured content mode in the given format.
170
257
  #
171
- # The result is a two-element array where the first element is a headers
172
- # list (as defined in the Rack specification) and the second is a string
173
- # containing the HTTP body content. The headers list will contain only
174
- # one header, a `Content-Type` whose value is of the form
175
- # `application/cloudevents+format`.
258
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
176
259
  #
177
260
  # @param event [CloudEvents::Event] The event.
178
- # @param format [String] The format code (e.g. "json")
261
+ # @param format_name [String] The format name.
179
262
  # @param format_args [keywords] Extra args to pass to the formatter.
180
263
  # @return [Array(headers,String)]
181
264
  #
182
- def encode_structured_content event, format, **format_args
183
- handlers = @structured_formatters[format] || []
184
- handlers.reverse_each do |handler|
185
- content = handler.encode event, **format_args
186
- return [{ "Content-Type" => "application/cloudevents+#{format}" }, content] if content
187
- end
188
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
265
+ def encode_structured_content event, format_name, **format_args
266
+ result = @event_encoders[format_name]&.encode_event event: event,
267
+ data_encoder: @data_encoders,
268
+ **format_args
269
+ return [{ "Content-Type" => result[:content_type].to_s }, result[:content]] if result
270
+ raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
189
271
  end
190
272
 
191
273
  ##
192
- # Encode a batch of events to content data in the given format.
274
+ # Encode a batch of events in structured content mode in the given format.
193
275
  #
194
- # The result is a two-element array where the first element is a headers
195
- # list (as defined in the Rack specification) and the second is a string
196
- # containing the HTTP body content. The headers list will contain only
197
- # one header, a `Content-Type` whose value is of the form
198
- # `application/cloudevents-batch+format`.
276
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
199
277
  #
200
- # @param events [Array<CloudEvents::Event>] The batch of events.
201
- # @param format [String] The format code (e.g. "json").
278
+ # @param event_batch [Array<CloudEvents::Event>] The batch of events.
279
+ # @param format_name [String] The format name.
202
280
  # @param format_args [keywords] Extra args to pass to the formatter.
203
281
  # @return [Array(headers,String)]
204
282
  #
205
- def encode_batched_content events, format, **format_args
206
- handlers = @batched_formatters[format] || []
207
- handlers.reverse_each do |handler|
208
- content = handler.encode_batch events, **format_args
209
- return [{ "Content-Type" => "application/cloudevents-batch+#{format}" }, content] if content
210
- end
211
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
283
+ def encode_batched_content event_batch, format_name, **format_args
284
+ result = @event_encoders[format_name]&.encode_event event_batch: event_batch,
285
+ data_encoder: @data_encoders,
286
+ **format_args
287
+ return [{ "Content-Type" => result[:content_type].to_s }, result[:content]] if result
288
+ raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
212
289
  end
213
290
 
214
291
  ##
215
- # Encode an event to content and headers, in binary content mode.
292
+ # Encode an event in binary content mode.
216
293
  #
217
- # The result is a two-element array where the first element is a headers
218
- # list (as defined in the Rack specification) and the second is a string
219
- # containing the HTTP body content.
294
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
220
295
  #
221
296
  # @param event [CloudEvents::Event] The event.
297
+ # @param format_args [keywords] Extra args to pass to the formatter.
222
298
  # @return [Array(headers,String)]
223
299
  #
224
- def encode_binary_content event
300
+ def encode_binary_content event, legacy_data_encode: true, **format_args
225
301
  headers = {}
226
- body = nil
227
302
  event.to_h.each do |key, value|
228
- case key
229
- when "data"
230
- body = value
231
- when "datacontenttype"
232
- headers["Content-Type"] = value
233
- else
303
+ unless ["data", "data_encoded", "datacontenttype"].include? key
234
304
  headers["CE-#{key}"] = percent_encode value
235
305
  end
236
306
  end
237
- case body
238
- when ::String
239
- headers["Content-Type"] ||= string_content_type body
240
- when nil
241
- headers.delete "Content-Type"
242
- else
243
- body = ::JSON.dump body
244
- headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
245
- end
307
+ body, content_type =
308
+ if legacy_data_encode || event.spec_version.start_with?("0.")
309
+ legacy_extract_event_data event
310
+ else
311
+ normal_extract_event_data event, format_args
312
+ end
313
+ headers["Content-Type"] = content_type.to_s if content_type
246
314
  [headers, body]
247
315
  end
248
316
 
249
317
  ##
250
318
  # Decode a percent-encoded string to a UTF-8 string.
251
319
  #
320
+ # @private
321
+ #
252
322
  # @param str [String] Incoming ascii string from an HTTP header, with one
253
323
  # cycle of percent-encoding.
254
324
  # @return [String] Resulting decoded string in UTF-8.
255
325
  #
256
326
  def percent_decode str
327
+ str = str.gsub(/"((?:[^"\\]|\\.)*)"/) { ::Regexp.last_match(1).gsub(/\\(.)/, '\1') }
257
328
  decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
258
329
  decoded_str.force_encoding ::Encoding::UTF_8
259
330
  end
@@ -263,6 +334,8 @@ module CloudEvents
263
334
  # non-printing and non-ascii characters to result in an ASCII string
264
335
  # suitable for setting as an HTTP header value.
265
336
  #
337
+ # @private
338
+ #
266
339
  # @param str [String] Incoming arbitrary string that can be represented
267
340
  # in UTF-8.
268
341
  # @return [String] Resulting encoded string in ASCII.
@@ -271,7 +344,7 @@ module CloudEvents
271
344
  arr = []
272
345
  utf_str = str.to_s.encode ::Encoding::UTF_8
273
346
  utf_str.each_byte do |byte|
274
- if byte >= 33 && byte <= 126 && byte != 37
347
+ if byte >= 33 && byte <= 126 && byte != 34 && byte != 37
275
348
  arr << byte
276
349
  else
277
350
  hi = byte / 16
@@ -286,23 +359,135 @@ module CloudEvents
286
359
 
287
360
  private
288
361
 
362
+ def add_named_formatter collection, formatter, name
363
+ return unless name
364
+ formatters = collection[name] ||= []
365
+ formatters.unshift formatter unless formatters.include? formatter
366
+ end
367
+
368
+ ##
369
+ # Decode a single event from the given request body and content type in
370
+ # structured mode.
371
+ #
372
+ def decode_structured_content content, content_type, allow_opaque, **format_args
373
+ result = @event_decoders.decode_event content: content,
374
+ content_type: content_type,
375
+ data_decoder: @data_decoders,
376
+ **format_args
377
+ return result[:event] || result[:event_batch] if result
378
+ if content_type&.media_type == "application" &&
379
+ ["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
380
+ return Event::Opaque.new content, content_type if allow_opaque
381
+ raise UnsupportedFormatError, "Unknown cloudevents content type: #{content_type}"
382
+ end
383
+ nil
384
+ end
385
+
386
+ ##
387
+ # Decode an event from the given Rack environment in binary content mode.
388
+ #
389
+ # TODO: legacy_data_decode is deprecated and can be removed when
390
+ # decode_rack_env is removed.
391
+ #
392
+ def decode_binary_content content, content_type, env, legacy_data_decode, **format_args
393
+ spec_version = env["HTTP_CE_SPECVERSION"]
394
+ return nil unless spec_version
395
+ unless spec_version =~ /^0\.3|1(\.|$)/
396
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
397
+ end
398
+ attributes = { "spec_version" => spec_version }
399
+ if legacy_data_decode || spec_version.start_with?("0.")
400
+ legacy_populate_data_attributes attributes, content, content_type
401
+ else
402
+ normal_populate_data_attributes attributes, content, content_type, spec_version, format_args
403
+ end
404
+ populate_attributes_from_env attributes, env
405
+ Event.create spec_version: spec_version, set_attributes: attributes
406
+ end
407
+
408
+ def legacy_populate_data_attributes attributes, content, content_type
409
+ attributes["data"] = content
410
+ attributes["data_content_type"] = content_type if content_type
411
+ end
412
+
413
+ def normal_populate_data_attributes attributes, content, content_type, spec_version, format_args
414
+ attributes["data_encoded"] = content
415
+ result = @data_decoders.decode_data spec_version: spec_version,
416
+ content: content,
417
+ content_type: content_type,
418
+ **format_args
419
+ if result
420
+ attributes["data"] = result[:data]
421
+ content_type = result[:content_type]
422
+ end
423
+ attributes["data_content_type"] = content_type if content_type
424
+ end
425
+
426
+ def populate_attributes_from_env attributes, env
427
+ omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
428
+ env.each do |key, value|
429
+ match = /^HTTP_CE_(\w+)$/.match key
430
+ next unless match
431
+ attr_name = match[1].downcase
432
+ attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
433
+ end
434
+ end
435
+
436
+ def legacy_extract_event_data event
437
+ body = event.data
438
+ content_type = event.data_content_type&.to_s
439
+ case body
440
+ when ::String
441
+ [body, content_type || string_content_type(body)]
442
+ when nil
443
+ [nil, nil]
444
+ else
445
+ [::JSON.dump(body), content_type || "application/json; charset=#{body.encoding.name.downcase}"]
446
+ end
447
+ end
448
+
449
+ def normal_extract_event_data event, format_args
450
+ body = event.data_encoded
451
+ if body
452
+ [body, event.data_content_type]
453
+ elsif event.data?
454
+ result = @data_encoders.encode_data spec_version: event.spec_version,
455
+ data: event.data,
456
+ content_type: event.data_content_type,
457
+ **format_args
458
+ raise UnsupportedFormatError, "Could not encode unknown content-type: #{content_type}" unless result
459
+ [result[:content], result[:content_type]]
460
+ else
461
+ ["", nil]
462
+ end
463
+ end
464
+
289
465
  def read_with_charset io, charset
466
+ return nil if io.nil?
290
467
  str = io.read
291
468
  if charset
292
469
  begin
293
470
  str.force_encoding charset
294
471
  rescue ::ArgumentError
295
- # Do nothing for now if the charset is unrecognized
472
+ # Use binary for now if the charset is unrecognized
473
+ str.force_encoding ::Encoding::ASCII_8BIT
296
474
  end
297
475
  end
298
476
  str
299
477
  end
300
478
 
301
- def string_content_type str
302
- if str.encoding == ::Encoding.ASCII_8BIT
303
- "application/octet-stream"
304
- else
305
- "text/plain; charset=#{str.encoding.name.downcase}"
479
+ # @private
480
+ module DefaultDataFormat
481
+ # @private
482
+ def self.decode_data content: nil, content_type: nil, **_extra_kwargs
483
+ return nil unless content_type.nil?
484
+ { data: content, content_type: nil }
485
+ end
486
+
487
+ # @private
488
+ def self.encode_data data: nil, content_type: nil, **_extra_kwargs
489
+ return nil unless content_type.nil?
490
+ { content: data.to_s, content_type: nil }
306
491
  end
307
492
  end
308
493
  end