cloud_events 0.4.0 → 0.5.1

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