Class: CloudEvents::HttpBinding

Inherits:
Object
  • Object
show all
Defined in:
lib/cloud_events/http_binding.rb

Overview

HTTP binding for CloudEvents.

This class implements HTTP binding, including unmarshalling of events from Rack environment data, and marshalling of events to Rack environment data. It supports binary (i.e. header-based) HTTP content, as well as structured (body-based) content that can delegate to formatters such as JSON.

Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format. See https://github.com/cloudevents/spec/blob/v0.3/http-transport-binding.md and https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md.

Constant Summary collapse

JSON_FORMAT =

The name of the JSON decoder/encoder

Returns:

  • (String)
"json"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHttpBinding

Create an empty HTTP binding.



40
41
42
43
44
45
46
# File 'lib/cloud_events/http_binding.rb', line 40

def initialize
  @event_decoders = []
  @event_encoders = {}
  @data_decoders = [DefaultDataFormat]
  @data_encoders = [DefaultDataFormat]
  @default_encoder_name = nil
end

Instance Attribute Details

#default_encoder_nameString?

The name of the encoder to use if none is specified

Returns:

  • (String, nil)


108
109
110
# File 'lib/cloud_events/http_binding.rb', line 108

def default_encoder_name
  @default_encoder_name
end

Class Method Details

.defaultHttpBinding

Returns a default HTTP binding, including support for JSON format.

Returns:



28
29
30
31
32
33
34
35
# File 'lib/cloud_events/http_binding.rb', line 28

def self.default
  @default ||= begin
    http_binding = new
    http_binding.register_formatter JsonFormat.new, JSON_FORMAT
    http_binding.default_encoder_name = JSON_FORMAT
    http_binding
  end
end

Instance Method Details

#decode_event(env, allow_opaque: false, **format_args) ⇒ CloudEvents::Event+

Decode an event from the given Rack environment hash. Following the CloudEvents spec, this chooses a handler based on the Content-Type of the request.

Note that this method will read the body (i.e. rack.input) stream. If you need to access the body after calling this method, you will need to rewind the stream. To determine whether the request is a CloudEvent without reading the body, use #probable_event?.

Parameters:

  • env (Hash)

    The Rack environment.

  • allow_opaque (boolean) (defaults to: false)

    If true, returns opaque event objects if the input is not in a recognized format. If false, raises UnsupportedFormatError in that case. Default is false.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

Raises:



149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/cloud_events/http_binding.rb', line 149

def decode_event env, allow_opaque: false, **format_args
  content_type_string = env["CONTENT_TYPE"]
  content_type = ContentType.new content_type_string if content_type_string
  content = read_with_charset env["rack.input"], content_type&.charset
  result = decode_binary_content(content, content_type, env) ||
           decode_structured_content(content, content_type, allow_opaque, **format_args)
  if result.nil?
    content_type_string = content_type_string ? content_type_string.inspect : "not present"
    raise NotCloudEventError, "Content-Type is #{content_type_string}, and CE-SpecVersion is not present"
  end
  result
end

#decode_rack_env(env, **format_args) ⇒ CloudEvents::Event, ...

Deprecated.

Will be removed in version 1.0. Use #decode_event instead.

Decode an event from the given Rack environment hash. Following the CloudEvents spec, this chooses a handler based on the Content-Type of the request.

Parameters:

  • env (Hash)

    The Rack environment.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (CloudEvents::Event)

    if the request includes a single structured or binary event.

  • (Array<CloudEvents::Event>)

    if the request includes a batch of structured events.

  • (nil)

    if the request does not appear to be a CloudEvent.

Raises:



224
225
226
227
228
229
230
231
# File 'lib/cloud_events/http_binding.rb', line 224

def decode_rack_env env, **format_args
  content_type_string = env["CONTENT_TYPE"]
  content_type = ContentType.new content_type_string if content_type_string
  content = read_with_charset env["rack.input"], content_type&.charset
  env["rack.input"].rewind rescue nil
  decode_binary_content(content, content_type, env, legacy_data_decode: true) ||
    decode_structured_content(content, content_type, false, **format_args)
end

#encode_batched_content(event_batch, format_name, **format_args) ⇒ Array(headers,String)

Deprecated.

Will be removed in version 1.0. Use #encode_event instead.

Encode a batch of events in structured content mode in the given format.

Parameters:

  • event_batch (Array<CloudEvents::Event>)

    The batch of events.

  • format_name (String)

    The format name.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))

Raises:

  • (::ArgumentError)


263
264
265
266
267
268
269
270
271
# File 'lib/cloud_events/http_binding.rb', line 263

def encode_batched_content event_batch, format_name, **format_args
  Array(@event_encoders[format_name]).reverse_each do |handler|
    result = handler.encode_event event_batch: event_batch, **format_args
    if result&.key?(:content) && result&.key?(:content_type)
      return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
    end
  end
  raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
end

#encode_binary_content(event, legacy_data_encode: true, **format_args) ⇒ Array(headers,String)

Deprecated.

Will be removed in version 1.0. Use #encode_event instead.

Encode an event in binary content mode.

Parameters:

  • event (CloudEvents::Event)

    The event.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))


282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/cloud_events/http_binding.rb', line 282

def encode_binary_content event, legacy_data_encode: true, **format_args
  headers = {}
  event.to_h.each do |key, value|
    unless ["data", "datacontenttype"].include? key
      headers["CE-#{key}"] = percent_encode value
    end
  end
  if legacy_data_encode
    body = event.data
    content_type = event.data_content_type&.to_s
    case body
    when ::String
      content_type ||= string_content_type body
    when nil
      content_type = nil
    else
      body = ::JSON.dump body
      content_type ||= "application/json; charset=#{body.encoding.name.downcase}"
    end
  else
    body, content_type = encode_data event.spec_version, event.data, event.data_content_type, **format_args
  end
  headers["Content-Type"] = content_type.to_s if content_type
  [headers, body]
end

#encode_event(event, structured_format: false, **format_args) ⇒ Array(headers,String)

Encode an event or batch of events into HTTP headers and body.

You may provide an event, an array of events, or an opaque event. You may also specify what content mode and format to use.

The result is a two-element array where the first element is a headers list (as defined in the Rack specification) and the second is a string containing the HTTP body content. When using structured content mode, the headers list will contain only a Content-Type header and the body will contain the serialized event. When using binary mode, the header list will contain the serialized event attributes and the body will contain the serialized event data.

Parameters:

  • event (CloudEvents::Event, Array<CloudEvents::Event>, CloudEvents::Event::Opaque)

    The event, batch, or opaque event.

  • structured_format (boolean, String) (defaults to: false)

    If given, the data will be encoded in structured content mode. You can pass a string to select a format name, or pass true to use the default format. If set to false (the default), the data will be encoded in binary mode.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))


185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/cloud_events/http_binding.rb', line 185

def encode_event event, structured_format: false, **format_args
  if event.is_a? Event::Opaque
    [{ "Content-Type" => event.content_type.to_s }, event.content]
  elsif !structured_format
    if event.is_a? ::Array
      raise ::ArgumentError, "Encoding a batch requires structured_format"
    end
    encode_binary_content event, legacy_data_encode: false, **format_args
  else
    structured_format = default_encoder_name if structured_format == true
    raise ::ArgumentError, "Format name not specified, and no default is set" unless structured_format
    case event
    when ::Array
      encode_batched_content event, structured_format, **format_args
    when Event
      encode_structured_content event, structured_format, **format_args
    else
      raise ::ArgumentError, "Unknown event type: #{event.class}"
    end
  end
end

#encode_structured_content(event, format_name, **format_args) ⇒ Array(headers,String)

Deprecated.

Will be removed in version 1.0. Use #encode_event instead.

Encode a single event in structured content mode in the given format.

Parameters:

  • event (CloudEvents::Event)

    The event.

  • format_name (String)

    The format name.

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))

Raises:

  • (::ArgumentError)


243
244
245
246
247
248
249
250
251
# File 'lib/cloud_events/http_binding.rb', line 243

def encode_structured_content event, format_name, **format_args
  Array(@event_encoders[format_name]).reverse_each do |handler|
    result = handler.encode_event event: event, **format_args
    if result&.key?(:content) && result&.key?(:content_type)
      return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
    end
  end
  raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
end

#probable_event?(env) ⇒ boolean

Analyze a Rack environment hash and determine whether it is probably a CloudEvent. This is done by examining headers only, and does not read or parse the request body. The result is a best guess: false negatives or false positives are possible for edge cases, but the logic should generally detect canonically-formatted events.

Parameters:

  • env (Hash)

    The Rack environment.

Returns:

  • (boolean)

    Whether the request is likely a CloudEvent.



120
121
122
123
124
125
# File 'lib/cloud_events/http_binding.rb', line 120

def probable_event? env
  return true if env["HTTP_CE_SPECVERSION"]
  content_type = ContentType.new env["CONTENT_TYPE"].to_s
  content_type.media_type == "application" &&
    ["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
end

#register_formatter(formatter, name = nil) ⇒ self

Register a formatter for all operations it supports, based on which methods are implemented by the formatter object. See Format for a list of possible methods.

Parameters:

  • formatter (Object)

    The formatter

  • name (String) (defaults to: nil)

    The encoder name under which this formatter will register its encode operations. Optional. If not specified, any event encoder will not be registered.

Returns:

  • (self)


59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/cloud_events/http_binding.rb', line 59

def register_formatter formatter, name = nil
  name = name.to_s.strip.downcase if name
  decode_event = formatter.respond_to? :decode_event
  encode_event = name if formatter.respond_to? :encode_event
  decode_data = formatter.respond_to? :decode_data
  encode_data = formatter.respond_to? :encode_data
  register_formatter_methods formatter,
                             decode_event: decode_event,
                             encode_event: encode_event,
                             decode_data: decode_data,
                             encode_data: encode_data
  self
end

#register_formatter_methods(formatter, decode_event: false, encode_event: nil, decode_data: false, encode_data: false) ⇒ self

Registers the given formatter for the given operations. Some arguments are activated by passing true, whereas those that rely on a format name are activated by passing in a name string.

Parameters:

  • formatter (Object)

    The formatter

  • decode_event (boolean) (defaults to: false)

    If true, register the formatter's Format#decode_event method.

  • encode_event (String) (defaults to: nil)

    If set to a string, use the formatter's Format#encode_event method when that name is requested.

  • decode_data (boolean) (defaults to: false)

    If true, register the formatter's Format#decode_data method.

  • encode_data (boolean) (defaults to: false)

    If true, register the formatter's Format#encode_data method.

Returns:

  • (self)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/cloud_events/http_binding.rb', line 89

def register_formatter_methods formatter,
                               decode_event: false,
                               encode_event: nil,
                               decode_data: false,
                               encode_data: false
  @event_decoders << formatter if decode_event
  if encode_event
    formatters = @event_encoders[encode_event] ||= []
    formatters << formatter unless formatters.include? formatter
  end
  @data_decoders << formatter if decode_data
  @data_encoders << formatter if encode_data
  self
end