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.

See https://github.com/cloudevents/spec/blob/master/http-protocol-binding.md

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeHttpBinding

Create an empty HTTP binding.



46
47
48
49
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 46

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

Class Method Details

.defaultObject

Returns a default binding, with JSON supported.



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

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:



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

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:



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

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] = 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:



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

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



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

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:



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

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


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

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}"] = 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:



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

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

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


80
81
82
83
84
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 80

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)


62
63
64
65
66
# File 'lib/functions_framework/cloud_events/http_binding.rb', line 62

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