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.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/cloud_events/http_binding.rb', line 43

def initialize
  @event_decoders = Format::Multi.new do |result|
    result&.key?(:event) || result&.key?(:event_batch) ? result : nil
  end
  @event_encoders = {}
  @data_decoders = Format::Multi.new do |result|
    result&.key?(:data) && result.key?(:content_type) ? result : nil
  end
  @data_encoders = Format::Multi.new do |result|
    result&.key?(:content) && result.key?(:content_type) ? result : nil
  end
  text_format = TextFormat.new
  @data_decoders.formats.replace([text_format, DefaultDataFormat])
  @data_encoders.formats.replace([text_format, 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)


127
128
129
# File 'lib/cloud_events/http_binding.rb', line 127

def default_encoder_name
  @default_encoder_name
end

Class Method Details

.defaultHttpBinding

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

Returns:



31
32
33
34
35
36
37
38
# File 'lib/cloud_events/http_binding.rb', line 31

def self.default
  @default ||= begin
    http_binding = new
    http_binding.register_formatter(JsonFormat.new, encoder_name: 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:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/cloud_events/http_binding.rb', line 169

def decode_event(env, allow_opaque: false, **format_args)
  request_method = env["REQUEST_METHOD"]
  raise(NotCloudEventError, "Request method is #{request_method}") if ILLEGAL_METHODS.include?(request_method)
  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, false, **format_args) ||
           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:



246
247
248
249
250
251
252
253
# File 'lib/cloud_events/http_binding.rb', line 246

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, true, **format_args) ||
    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)


283
284
285
286
287
288
289
# File 'lib/cloud_events/http_binding.rb', line 283

def encode_batched_content(event_batch, format_name, **format_args)
  result = @event_encoders[format_name]&.encode_event(event_batch: event_batch,
                                                      data_encoder: @data_encoders,
                                                      **format_args)
  return [{ "Content-Type" => result[:content_type].to_s }, result[:content]] if result
  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))


300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/cloud_events/http_binding.rb', line 300

def encode_binary_content(event, legacy_data_encode: true, **format_args)
  headers = {}
  event.to_h.each do |key, value|
    unless ["data", "data_encoded", "datacontenttype"].include?(key)
      headers["CE-#{key}"] = percent_encode(value)
    end
  end
  body, content_type =
    if legacy_data_encode || event.spec_version.start_with?("0.")
      legacy_extract_event_data(event)
    else
      normal_extract_event_data(event, 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))


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/cloud_events/http_binding.rb', line 207

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)


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

def encode_structured_content(event, format_name, **format_args)
  result = @event_encoders[format_name]&.encode_event(event: event,
                                                      data_encoder: @data_encoders,
                                                      **format_args)
  return [{ "Content-Type" => result[:content_type].to_s }, result[:content]] if result
  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.



139
140
141
142
143
144
145
# File 'lib/cloud_events/http_binding.rb', line 139

def probable_event?(env)
  return false if ILLEGAL_METHODS.include?(env["REQUEST_METHOD"])
  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, deprecated_name = nil, encoder_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

  • encoder_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.

  • deprecated_name (String) (defaults to: nil)

    This positional argument is deprecated and will be removed in version 1.0. Use encoder_name instead.

Returns:

  • (self)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/cloud_events/http_binding.rb', line 74

def register_formatter(formatter, deprecated_name = nil, encoder_name: nil)
  encoder_name ||= deprecated_name
  encoder_name = encoder_name.to_s.strip.downcase if encoder_name
  decode_event = formatter.respond_to?(:decode_event)
  encode_event = encoder_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)


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/cloud_events/http_binding.rb', line 105

def register_formatter_methods(formatter,
                               decode_event: false,
                               encode_event: nil,
                               decode_data: false,
                               encode_data: false)
  @event_decoders.formats.unshift(formatter) if decode_event
  if encode_event
    encoders = @event_encoders[encode_event] ||= Format::Multi.new do |result|
      result&.key?(:content) && result.key?(:content_type) ? result : nil
    end
    encoders.formats.unshift(formatter)
  end
  @data_decoders.formats.unshift(formatter) if decode_data
  @data_encoders.formats.unshift(formatter) if encode_data
  self
end