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
47
48
49
50
51
52
53
54
55
56
# File 'lib/cloud_events/http_binding.rb', line 40

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)


124
125
126
# File 'lib/cloud_events/http_binding.rb', line 124

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, 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:



165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/cloud_events/http_binding.rb', line 165

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, 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:



240
241
242
243
244
245
246
247
# File 'lib/cloud_events/http_binding.rb', line 240

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)


277
278
279
280
281
282
283
# File 'lib/cloud_events/http_binding.rb', line 277

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))


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/cloud_events/http_binding.rb', line 294

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))


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/cloud_events/http_binding.rb', line 201

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)


259
260
261
262
263
264
265
# File 'lib/cloud_events/http_binding.rb', line 259

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.



136
137
138
139
140
141
# File 'lib/cloud_events/http_binding.rb', line 136

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, 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)


71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/cloud_events/http_binding.rb', line 71

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)


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

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