Class: FunctionsFramework::CloudEvents::HttpBinding

Inherits:
Object
  • Object
show all
Defined in:
lib/functions_framework/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.



48
49
50
51
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 48

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

Class Method Details

.defaultObject

Returns a default binding, with JSON supported.



35
36
37
38
39
40
41
42
43
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 35

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



147
148
149
150
151
152
153
154
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 147

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) ⇒ FunctionsFramework::CloudEvents::Event?

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

Parameters:

Returns:

Raises:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 166

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) ⇒ FunctionsFramework::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:



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

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) ⇒ FunctionsFramework::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:



128
129
130
131
132
133
134
135
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 128

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



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

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


244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 244

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:

Returns:

  • (Array(headers,String))

Raises:



201
202
203
204
205
206
207
208
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 201

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.



278
279
280
281
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 278

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.



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

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)


82
83
84
85
86
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 82

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)


64
65
66
67
68
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 64

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