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.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHttpBinding

Create an empty HTTP binding.



33
34
35
36
# File 'lib/cloud_events/http_binding.rb', line 33

def initialize
  @structured_formatters = {}
  @batched_formatters = {}
end

Class Method Details

.defaultObject

Returns a default binding, with JSON supported.



20
21
22
23
24
25
26
27
28
# File 'lib/cloud_events/http_binding.rb', line 20

def self.default
  @default ||= begin
    http_binding = new
    json_format = JsonFormat.new
    http_binding.register_structured_formatter "json", json_format
    http_binding.register_batched_formatter "json", json_format
    http_binding
  end
end

Instance Method Details

#decode_batched_content(input, format, **format_args) ⇒ Array<CloudEvents::Event>

Decode a batch of events from the given content data. This should be passed the request body, if the Content-Type is of the form application/cloudevents-batch+format.

Parameters:

  • input (String)

    The string content.

  • format (String)

    The format code (e.g. "json").

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

Raises:



131
132
133
134
135
136
137
138
# File 'lib/cloud_events/http_binding.rb', line 131

def decode_batched_content input, format, **format_args
  handlers = @batched_formatters[format] || []
  handlers.reverse_each do |handler|
    events = handler.decode_batch input, **format_args
    return events if events
  end
  raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
end

#decode_binary_content(env, content_type) ⇒ CloudEvents::Event?

Decode an event from the given Rack environment in binary content mode.

Parameters:

  • env (Hash)

    Rack environment hash.

  • content_type (CloudEvents::ContentType)

    the content type from the Rack environment.

Returns:

  • (CloudEvents::Event)

    if a CloudEvent could be decoded from the Rack environment.

  • (nil)

    if the Rack environment does not indicate a CloudEvent

Raises:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/cloud_events/http_binding.rb', line 150

def decode_binary_content env, content_type
  spec_version = env["HTTP_CE_SPECVERSION"]
  return nil if spec_version.nil?
  raise SpecVersionError, "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
  input = env["rack.input"]
  data = if input
           input.set_encoding content_type.charset if content_type&.charset
           input.read
         end
  attributes = { "spec_version" => spec_version, "data" => data }
  attributes["data_content_type"] = content_type if content_type
  omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
  env.each do |key, value|
    match = /^HTTP_CE_(\w+)$/.match key
    next unless match
    attr_name = match[1].downcase
    attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
  end
  Event.create spec_version: spec_version, attributes: attributes
end

#decode_rack_env(env, **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.

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 was not recognized as a CloudEvent.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/cloud_events/http_binding.rb', line 85

def decode_rack_env env, **format_args
  content_type_header = env["CONTENT_TYPE"]
  content_type = ContentType.new content_type_header if content_type_header
  input = env["rack.input"]
  if input && content_type&.media_type == "application"
    case content_type.subtype_base
    when "cloudevents"
      input.set_encoding content_type.charset if content_type.charset
      return decode_structured_content input.read, content_type.subtype_format, **format_args
    when "cloudevents-batch"
      input.set_encoding content_type.charset if content_type.charset
      return decode_batched_content input.read, content_type.subtype_format, **format_args
    end
  end
  decode_binary_content env, content_type
end

#decode_structured_content(input, format, **format_args) ⇒ CloudEvents::Event

Decode a single event from the given content data. This should be passed the request body, if the Content-Type is of the form application/cloudevents+format.

Parameters:

  • input (String)

    The string content.

  • format (String)

    The format code (e.g. "json").

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

Raises:



112
113
114
115
116
117
118
119
# File 'lib/cloud_events/http_binding.rb', line 112

def decode_structured_content input, format, **format_args
  handlers = @structured_formatters[format] || []
  handlers.reverse_each do |handler|
    event = handler.decode input, **format_args
    return event if event
  end
  raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
end

#encode_batched_content(events, format, **format_args) ⇒ Array(headers,String)

Encode a batch of events to content data in the given format.

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. The headers list will contain only one header, a Content-Type whose value is of the form application/cloudevents-batch+format.

Parameters:

  • events (Array<CloudEvents::Event>)

    The batch of events.

  • format (String)

    The format code (e.g. "json").

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))

Raises:



208
209
210
211
212
213
214
215
# File 'lib/cloud_events/http_binding.rb', line 208

def encode_batched_content events, format, **format_args
  handlers = @batched_formatters[format] || []
  handlers.reverse_each do |handler|
    content = handler.encode_batch events, **format_args
    return [{ "Content-Type" => "application/cloudevents-batch+#{format}" }, content] if content
  end
  raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
end

#encode_binary_content(event) ⇒ Array(headers,String)

Encode an event to content and headers, in binary content mode.

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.

Parameters:

Returns:

  • (Array(headers,String))


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/cloud_events/http_binding.rb', line 227

def encode_binary_content event
  headers = {}
  body = nil
  event.to_h.each do |key, value|
    if key == "data"
      body = value
    elsif key == "datacontenttype"
      headers["Content-Type"] = value
    else
      headers["CE-#{key}"] = percent_encode value
    end
  end
  if body.is_a? ::String
    headers["Content-Type"] ||= if body.encoding == ::Encoding.ASCII_8BIT
                                  "application/octet-stream"
                                else
                                  "text/plain; charset=#{body.encoding.name.downcase}"
                                end
  elsif body.nil?
    headers.delete "Content-Type"
  else
    body = ::JSON.dump body
    headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
  end
  [headers, body]
end

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

Encode a single event to content data in the given format.

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. The headers list will contain only one header, a Content-Type whose value is of the form application/cloudevents+format.

Parameters:

  • event (CloudEvents::Event)

    The event.

  • format (String)

    The format code (e.g. "json")

  • format_args (keywords)

    Extra args to pass to the formatter.

Returns:

  • (Array(headers,String))

Raises:



185
186
187
188
189
190
191
192
# File 'lib/cloud_events/http_binding.rb', line 185

def encode_structured_content event, format, **format_args
  handlers = @structured_formatters[format] || []
  handlers.reverse_each do |handler|
    content = handler.encode event, **format_args
    return [{ "Content-Type" => "application/cloudevents+#{format}" }, content] if content
  end
  raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
end

#percent_decode(str) ⇒ String

Decode a percent-encoded string to a UTF-8 string.

Parameters:

  • str (String)

    Incoming ascii string from an HTTP header, with one cycle of percent-encoding.

Returns:

  • (String)

    Resulting decoded string in UTF-8.



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

def percent_decode str
  decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
  decoded_str.force_encoding ::Encoding::UTF_8
end

#percent_encode(str) ⇒ String

Transcode an arbitrarily-encoded string to UTF-8, then percent-encode non-printing and non-ascii characters to result in an ASCII string suitable for setting as an HTTP header value.

Parameters:

  • str (String)

    Incoming arbitrary string that can be represented in UTF-8.

Returns:

  • (String)

    Resulting encoded string in ASCII.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/cloud_events/http_binding.rb', line 275

def percent_encode str
  arr = []
  utf_str = str.to_s.encode ::Encoding::UTF_8
  utf_str.each_byte do |byte|
    if byte >= 33 && byte <= 126 && byte != 37
      arr << byte
    else
      hi = byte / 16
      hi = hi > 9 ? 55 + hi : 48 + hi
      lo = byte % 16
      lo = lo > 9 ? 55 + lo : 48 + lo
      arr << 37 << hi << lo
    end
  end
  arr.pack "C*"
end

#register_batched_formatter(type, formatter) ⇒ self

Register a batch formatter for the given type.

A batch formatter must respond to the methods #encode_batch and #decode_batch. See JsonFormat for an example.

Parameters:

  • type (String)

    The subtype format that should be handled by this formatter.

  • formatter (Object)

    The formatter object.

Returns:

  • (self)


66
67
68
69
70
# File 'lib/cloud_events/http_binding.rb', line 66

def register_batched_formatter type, formatter
  formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
  formatters << formatter unless formatters.include? formatter
  self
end

#register_structured_formatter(type, formatter) ⇒ self

Register a formatter for the given type.

A formatter must respond to the methods #encode and #decode. See JsonFormat for an example.

Parameters:

  • type (String)

    The subtype format that should be handled by this formatter.

  • formatter (Object)

    The formatter object.

Returns:

  • (self)


49
50
51
52
53
# File 'lib/cloud_events/http_binding.rb', line 49

def register_structured_formatter type, formatter
  formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
  formatters << formatter unless formatters.include? formatter
  self
end