cloud_events 0.1.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,9 @@
3
3
  require "date"
4
4
  require "uri"
5
5
 
6
+ require "cloud_events/event/field_interpreter"
7
+ require "cloud_events/event/utils"
8
+
6
9
  module CloudEvents
7
10
  module Event
8
11
  ##
@@ -16,9 +19,10 @@ module CloudEvents
16
19
  # their own accessor methods that may return typed objects (such as
17
20
  # `DateTime` for the `time` attribute).
18
21
  #
19
- # This object is immutable. The data and attribute values can be
20
- # retrieved but not modified. To obtain an event with modifications, use
21
- # the {#with} method to create a copy with the desired changes.
22
+ # This object is immutable, and Ractor-shareable on Ruby 3. The data and
23
+ # attribute values can be retrieved but not modified. To obtain an event
24
+ # with modifications, use the {#with} method to create a copy with the
25
+ # desired changes.
22
26
  #
23
27
  # See https://github.com/cloudevents/spec/blob/v1.0/spec.md for
24
28
  # descriptions of the standard attributes.
@@ -42,7 +46,7 @@ module CloudEvents
42
46
  # field.
43
47
  # * **:type** [`String`] - _required_ - The event `type` field.
44
48
  # * **:data** [`Object`] - _optional_ - The data associated with the
45
- # event (i.e. the `data` field.)
49
+ # event (i.e. the `data` field).
46
50
  # * **:data_content_type** (or **:datacontenttype**) [`String`,
47
51
  # {ContentType}] - _optional_ - The content-type for the data, if
48
52
  # the data is a string (i.e. the event `datacontenttype` field.)
@@ -56,6 +60,11 @@ module CloudEvents
56
60
  # They are not available as separate methods, but can be accessed via
57
61
  # the {Event::V1#[]} operator.
58
62
  #
63
+ # Note that attribute objects passed in may get deep-frozen if they are
64
+ # used in the final event object. This is particularly important for the
65
+ # `:data` field, for example if you pass a structured hash. If this is an
66
+ # issue, make a deep copy of objects before passing to this constructor.
67
+ #
59
68
  # @param attributes [Hash] The data and attributes, as a hash.
60
69
  # @param args [keywords] The data and attributes, as keyword arguments.
61
70
  #
@@ -65,12 +74,13 @@ module CloudEvents
65
74
  @id = interpreter.string ["id"], required: true
66
75
  @source = interpreter.uri ["source"], required: true
67
76
  @type = interpreter.string ["type"], required: true
68
- @data = interpreter.object ["data"], allow_nil: true
77
+ @data = interpreter.data_object ["data"]
69
78
  @data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
70
79
  @data_schema = interpreter.uri ["dataschema", "data_schema"]
71
80
  @subject = interpreter.string ["subject"]
72
81
  @time = interpreter.rfc3339_date_time ["time"]
73
82
  @attributes = interpreter.finish_attributes
83
+ freeze
74
84
  end
75
85
 
76
86
  ##
@@ -101,6 +111,8 @@ module CloudEvents
101
111
  # event["time"] # => String rfc3339 representation
102
112
  # event.time # => DateTime object
103
113
  #
114
+ # Results are also always frozen and cannot be modified in place.
115
+ #
104
116
  # @param key [String,Symbol] The attribute name.
105
117
  # @return [String,nil]
106
118
  #
@@ -109,12 +121,13 @@ module CloudEvents
109
121
  end
110
122
 
111
123
  ##
112
- # Return a hash representation of this event.
124
+ # Return a hash representation of this event. The returned hash is an
125
+ # unfrozen deep copy. Modifications do not affect the original event.
113
126
  #
114
127
  # @return [Hash]
115
128
  #
116
129
  def to_h
117
- @attributes.dup
130
+ Utils.deep_dup @attributes
118
131
  end
119
132
 
120
133
  ##
@@ -201,7 +214,7 @@ module CloudEvents
201
214
 
202
215
  ## @private
203
216
  def hash
204
- @hash ||= @attributes.hash
217
+ @attributes.hash
205
218
  end
206
219
  end
207
220
  end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+
6
+ module CloudEvents
7
+ ##
8
+ # This module documents the method signatures that may be implemented by
9
+ # formatters.
10
+ #
11
+ # A formatter is an object that implenets "structured" event encoding and
12
+ # decoding strategies for a particular format (such as JSON). In general,
13
+ # this includes four operations:
14
+ #
15
+ # * Decoding an entire event or batch of events from a input source.
16
+ # This is implemented by the {Format#decode_event} method.
17
+ # * Encoding an entire event or batch of events to an output sink.
18
+ # This is implemented by the {Format#encode_event} method.
19
+ # * Decoding an event payload (i.e. the `data` attribute) Ruby object from a
20
+ # serialized representation.
21
+ # This is implemented by the {Format#decode_data} method.
22
+ # * Encoding an event payload (i.e. the `data` attribute) Ruby object to a
23
+ # serialized representation.
24
+ # This is implemented by the {Format#encode_data} method.
25
+ #
26
+ # Each method takes a set of keyword arguments, and returns either a `Hash`
27
+ # or `nil`. A Hash indicates that the formatter understands the request and
28
+ # is returning its response. A return value of `nil` means the formatter does
29
+ # not understand the request and is declining to perform the operation. In
30
+ # such a case, it is possible that a different formatter should handle it.
31
+ #
32
+ # Both the keyword arguments recognized and the returned hash members may
33
+ # vary from formatter to formatter; similarly, the keyword arguments provided
34
+ # and the resturned hash members recognized may also vary for different
35
+ # callers. This interface will define a set of common argument and result key
36
+ # names, but both callers and formatters must gracefully handle the case of
37
+ # missing or extra information. For example, if a formatter expects a certain
38
+ # argument but does not receive it, it can assume the caller does not have
39
+ # the required information, and it may respond by returning `nil` to decline
40
+ # the request. Similarly, if a caller expects a response key but does not
41
+ # receive it, it can assume the formatter does not provide it, and it may
42
+ # respond by trying a different formatter.
43
+ #
44
+ # Additionally, any particular formatter need not implement all methods. For
45
+ # example, an event formatter would generally implement {Format#decode_event}
46
+ # and {Format#encode_event}, but might not implement {Format#decode_data} or
47
+ # {Format#encode_data}.
48
+ #
49
+ # Finally, this module itself is present primarily for documentation, and
50
+ # need not be directly included by formatter implementations.
51
+ #
52
+ module Format
53
+ ##
54
+ # Decode an event or batch from the given serialized input. This is
55
+ # typically called by a protocol binding to deserialize event data from an
56
+ # input stream.
57
+ #
58
+ # Common arguments include:
59
+ #
60
+ # * `:content` (String) Serialized content to decode. For example, it could
61
+ # be from an HTTP request body.
62
+ # * `:content_type` ({CloudEvents::ContentType}) The content type. For
63
+ # example, it could be from the `Content-Type` header of an HTTP request.
64
+ #
65
+ # The formatter must first determine whether it is able to interpret the
66
+ # given input. Typically, this is done by inspecting the `content_type`.
67
+ # If the formatter determines that it is unable to interpret the input, it
68
+ # should return `nil`. Otherwise, if the formatter determines it can decode
69
+ # the input, it should return a `Hash`. Common hash keys include:
70
+ #
71
+ # * `:event` ({CloudEvents::Event}) A single event decoded from the input.
72
+ # * `:event_batch` (Array of {CloudEvents::Event}) A batch of events
73
+ # decoded from the input.
74
+ #
75
+ # The formatter may also raise a {CloudEvents::CloudEventsError} subclass
76
+ # if it understood the request but determines that the input source is
77
+ # malformed.
78
+ #
79
+ # @param _kwargs [keywords] Arguments
80
+ # @return [Hash] if accepting the request and returning a result
81
+ # @return [nil] if declining the request.
82
+ #
83
+ def decode_event **_kwargs
84
+ nil
85
+ end
86
+
87
+ ##
88
+ # Encode an event or batch to a string. This is typically called by a
89
+ # protocol binding to serialize event data to an output stream.
90
+ #
91
+ # Common arguments include:
92
+ #
93
+ # * `:event` ({CloudEvents::Event}) A single event to encode.
94
+ # * `:event_batch` (Array of {CloudEvents::Event}) A batch of events to
95
+ # encode.
96
+ #
97
+ # The formatter must first determine whether it is able to interpret the
98
+ # given input. Typically, most formatters should be able to handle any
99
+ # event or event batch, but a specialized formatter that can handle only
100
+ # certain kinds of events may return `nil` to decline unwanted inputs.
101
+ # Otherwise, if the formatter determines it can encode the input, it should
102
+ # return a `Hash`. common hash keys include:
103
+ #
104
+ # * `:content` (String) The serialized form of the event. This might, for
105
+ # example, be written to an HTTP request body. Care should be taken to
106
+ # set the string's encoding properly. In particular, to output binary
107
+ # data, the encoding should probably be set to `ASCII_8BIT`.
108
+ # * `:content_type` ({CloudEvents::ContentType}) The content type for the
109
+ # output. This might, for example, be written to the `Content-Type`
110
+ # header of an HTTP request.
111
+ #
112
+ # The formatter may also raise a {CloudEvents::CloudEventsError} subclass
113
+ # if it understood the request but determines that the input source is
114
+ # malformed.
115
+ #
116
+ # @param _kwargs [keywords] Arguments
117
+ # @return [Hash] if accepting the request and returning a result
118
+ # @return [nil] if declining the request.
119
+ #
120
+ def encode_event **_kwargs
121
+ nil
122
+ end
123
+
124
+ ##
125
+ # Decode an event data object from string format. This is typically called
126
+ # by a protocol binding to deserialize the payload (i.e. `data` attribute)
127
+ # of an event as part of "binary content mode" decoding.
128
+ #
129
+ # Common arguments include:
130
+ #
131
+ # * `:spec_version` (String) The `specversion` of the event.
132
+ # * `:content` (String) Serialized payload to decode. For example, it could
133
+ # be from an HTTP request body.
134
+ # * `:content_type` ({CloudEvents::ContentType}) The content type. For
135
+ # example, it could be from the `Content-Type` header of an HTTP request.
136
+ #
137
+ # The formatter must first determine whether it is able to interpret the
138
+ # given input. Typically, this is done by inspecting the `content_type`.
139
+ # If the formatter determines that it is unable to interpret the input, it
140
+ # should return `nil`. Otherwise, if the formatter determines it can decode
141
+ # the input, it should return a `Hash`. Common hash keys include:
142
+ #
143
+ # * `:data` (Object) The payload object to be set as the `data` attribute
144
+ # in a {CloudEvents::Event} object.
145
+ # * `:content_type` ({CloudEvents::ContentType}) The content type to be set
146
+ # as the `datacontenttype` attribute in a {CloudEvents::Event} object.
147
+ # In many cases, this may simply be copied from the `:content_type`
148
+ # argument, but a formatter could modify it to provide corrections or
149
+ # additional information.
150
+ #
151
+ # The formatter may also raise a {CloudEvents::CloudEventsError} subclass
152
+ # if it understood the request but determines that the input source is
153
+ # malformed.
154
+ #
155
+ # @param _kwargs [keywords] Arguments
156
+ # @return [Hash] if accepting the request and returning a result
157
+ # @return [nil] if declining the request.
158
+ #
159
+ def decode_data **_kwargs
160
+ nil
161
+ end
162
+
163
+ ##
164
+ # Encode an event data object to string format. This is typically called by
165
+ # a protocol binding to serialize the payload (i.e. `data` attribute and
166
+ # corresponding `datacontenttype` attribute) of an event as part of "binary
167
+ # content mode" encoding.
168
+ #
169
+ # Common arguments include:
170
+ #
171
+ # * `:spec_version` (String) The `specversion` of the event.
172
+ # * `:data` (Object) The payload object from an event's `data` attribute.
173
+ # * `:content_type` ({CloudEvents::ContentType}) The content type from an
174
+ # event's `datacontenttype` attribute.
175
+ #
176
+ # The formatter must first determine whether it is able to interpret the
177
+ # given input. Typically, this is done by inspecting the `content_type`.
178
+ # If the formatter determines that it is unable to interpret the input, it
179
+ # should return `nil`. Otherwise, if the formatter determines it can decode
180
+ # the input, it should return a `Hash`. Common hash keys include:
181
+ #
182
+ # * `:content` (String) The serialized form of the data. This might, for
183
+ # example, be written to an HTTP request body. Care should be taken to
184
+ # set the string's encoding properly. In particular, to output binary
185
+ # data, the encoding should probably be set to `ASCII_8BIT`.
186
+ # * `:content_type` ({CloudEvents::ContentType}) The content type for the
187
+ # output. This might, for example, be written to the `Content-Type`
188
+ # header of an HTTP request.
189
+ #
190
+ # The formatter may also raise a {CloudEvents::CloudEventsError} subclass
191
+ # if it understood the request but determines that the input source is
192
+ # malformed.
193
+ #
194
+ # @param _kwargs [keywords] Arguments
195
+ # @return [Hash] if accepting the request and returning a result
196
+ # @return [nil] if declining the request.
197
+ #
198
+ def encode_data **_kwargs
199
+ nil
200
+ end
201
+ end
202
+ end
@@ -15,14 +15,21 @@ module CloudEvents
15
15
  #
16
16
  class HttpBinding
17
17
  ##
18
- # Returns a default binding, with JSON supported.
18
+ # The name of the JSON decoder/encoder
19
+ # @return [String]
20
+ #
21
+ JSON_FORMAT = "json"
22
+
23
+ ##
24
+ # Returns a default HTTP binding, including support for JSON format.
25
+ #
26
+ # @return [HttpBinding]
19
27
  #
20
28
  def self.default
21
29
  @default ||= begin
22
30
  http_binding = new
23
- json_format = JsonFormat.new
24
- http_binding.register_structured_formatter "json", json_format
25
- http_binding.register_batched_formatter "json", json_format
31
+ http_binding.register_formatter JsonFormat.new, JSON_FORMAT
32
+ http_binding.default_encoder_name = JSON_FORMAT
26
33
  http_binding
27
34
  end
28
35
  end
@@ -31,234 +38,284 @@ module CloudEvents
31
38
  # Create an empty HTTP binding.
32
39
  #
33
40
  def initialize
34
- @structured_formatters = {}
35
- @batched_formatters = {}
41
+ @event_decoders = []
42
+ @event_encoders = {}
43
+ @data_decoders = [DefaultDataFormat]
44
+ @data_encoders = [DefaultDataFormat]
45
+ @default_encoder_name = nil
36
46
  end
37
47
 
38
48
  ##
39
- # Register a formatter for the given type.
49
+ # Register a formatter for all operations it supports, based on which
50
+ # methods are implemented by the formatter object. See
51
+ # {CloudEvents::Format} for a list of possible methods.
40
52
  #
41
- # A formatter must respond to the methods `#encode` and `#decode`. See
42
- # {CloudEvents::JsonFormat} for an example.
43
- #
44
- # @param type [String] The subtype format that should be handled by
45
- # this formatter.
46
- # @param formatter [Object] The formatter object.
53
+ # @param formatter [Object] The formatter
54
+ # @param name [String] The encoder name under which this formatter will
55
+ # register its encode operations. Optional. If not specified, any event
56
+ # encoder will _not_ be registered.
47
57
  # @return [self]
48
58
  #
49
- def register_structured_formatter type, formatter
50
- formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
51
- formatters << formatter unless formatters.include? formatter
59
+ def register_formatter formatter, name = nil
60
+ name = name.to_s.strip.downcase if name
61
+ decode_event = formatter.respond_to? :decode_event
62
+ encode_event = name if formatter.respond_to? :encode_event
63
+ decode_data = formatter.respond_to? :decode_data
64
+ encode_data = formatter.respond_to? :encode_data
65
+ register_formatter_methods formatter,
66
+ decode_event: decode_event,
67
+ encode_event: encode_event,
68
+ decode_data: decode_data,
69
+ encode_data: encode_data
52
70
  self
53
71
  end
54
72
 
55
73
  ##
56
- # Register a batch formatter for the given type.
74
+ # Registers the given formatter for the given operations. Some arguments
75
+ # are activated by passing `true`, whereas those that rely on a format name
76
+ # are activated by passing in a name string.
57
77
  #
58
- # A batch formatter must respond to the methods `#encode_batch` and
59
- # `#decode_batch`. See {CloudEvents::JsonFormat} for an example.
60
- #
61
- # @param type [String] The subtype format that should be handled by
62
- # this formatter.
63
- # @param formatter [Object] The formatter object.
78
+ # @param formatter [Object] The formatter
79
+ # @param decode_event [boolean] If true, register the formatter's
80
+ # {CloudEvents::Format#decode_event} method.
81
+ # @param encode_event [String] If set to a string, use the formatter's
82
+ # {CloudEvents::Format#encode_event} method when that name is requested.
83
+ # @param decode_data [boolean] If true, register the formatter's
84
+ # {CloudEvents::Format#decode_data} method.
85
+ # @param encode_data [boolean] If true, register the formatter's
86
+ # {CloudEvents::Format#encode_data} method.
64
87
  # @return [self]
65
88
  #
66
- def register_batched_formatter type, formatter
67
- formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
68
- formatters << formatter unless formatters.include? formatter
89
+ def register_formatter_methods formatter,
90
+ decode_event: false,
91
+ encode_event: nil,
92
+ decode_data: false,
93
+ encode_data: false
94
+ @event_decoders << formatter if decode_event
95
+ if encode_event
96
+ formatters = @event_encoders[encode_event] ||= []
97
+ formatters << formatter unless formatters.include? formatter
98
+ end
99
+ @data_decoders << formatter if decode_data
100
+ @data_encoders << formatter if encode_data
69
101
  self
70
102
  end
71
103
 
104
+ ##
105
+ # The name of the encoder to use if none is specified
106
+ # @return [String,nil]
107
+ #
108
+ attr_accessor :default_encoder_name
109
+
110
+ ##
111
+ # Analyze a Rack environment hash and determine whether it is _probably_
112
+ # a CloudEvent. This is done by examining headers only, and does not read
113
+ # or parse the request body. The result is a best guess: false negatives or
114
+ # false positives are possible for edge cases, but the logic should
115
+ # generally detect canonically-formatted events.
116
+ #
117
+ # @param env [Hash] The Rack environment.
118
+ # @return [boolean] Whether the request is likely a CloudEvent.
119
+ #
120
+ def probable_event? env
121
+ return true if env["HTTP_CE_SPECVERSION"]
122
+ content_type = ContentType.new env["CONTENT_TYPE"].to_s
123
+ content_type.media_type == "application" &&
124
+ ["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
125
+ end
126
+
72
127
  ##
73
128
  # Decode an event from the given Rack environment hash. Following the
74
129
  # CloudEvents spec, this chooses a handler based on the Content-Type of
75
130
  # the request.
76
131
  #
132
+ # Note that this method will read the body (i.e. `rack.input`) stream.
133
+ # If you need to access the body after calling this method, you will need
134
+ # to rewind the stream. To determine whether the request is a CloudEvent
135
+ # without reading the body, use {#probable_event?}.
136
+ #
77
137
  # @param env [Hash] The Rack environment.
138
+ # @param allow_opaque [boolean] If true, returns opaque event objects if
139
+ # the input is not in a recognized format. If false, raises
140
+ # {CloudEvents::UnsupportedFormatError} in that case. Default is false.
78
141
  # @param format_args [keywords] Extra args to pass to the formatter.
79
142
  # @return [CloudEvents::Event] if the request includes a single structured
80
143
  # or binary event.
81
144
  # @return [Array<CloudEvents::Event>] if the request includes a batch of
82
145
  # structured events.
83
- # @return [nil] if the request was not recognized as a CloudEvent.
146
+ # @raise [CloudEvents::CloudEventsError] if an event could not be decoded
147
+ # from the request.
84
148
  #
85
- def decode_rack_env env, **format_args
86
- content_type_header = env["CONTENT_TYPE"]
87
- content_type = ContentType.new content_type_header if content_type_header
88
- input = env["rack.input"]
89
- if input && content_type&.media_type == "application"
90
- case content_type.subtype_base
91
- when "cloudevents"
92
- input.set_encoding content_type.charset if content_type.charset
93
- return decode_structured_content input.read, content_type.subtype_format, **format_args
94
- when "cloudevents-batch"
95
- input.set_encoding content_type.charset if content_type.charset
96
- return decode_batched_content input.read, content_type.subtype_format, **format_args
97
- end
149
+ def decode_event env, allow_opaque: false, **format_args
150
+ content_type_string = env["CONTENT_TYPE"]
151
+ content_type = ContentType.new content_type_string if content_type_string
152
+ content = read_with_charset env["rack.input"], content_type&.charset
153
+ result = decode_binary_content(content, content_type, env) ||
154
+ decode_structured_content(content, content_type, allow_opaque, **format_args)
155
+ if result.nil?
156
+ content_type_string = content_type_string ? content_type_string.inspect : "not present"
157
+ raise NotCloudEventError, "Content-Type is #{content_type_string}, and CE-SpecVersion is not present"
98
158
  end
99
- decode_binary_content env, content_type
159
+ result
100
160
  end
101
161
 
102
162
  ##
103
- # Decode a single event from the given content data. This should be
104
- # passed the request body, if the Content-Type is of the form
105
- # `application/cloudevents+format`.
163
+ # Encode an event or batch of events into HTTP headers and body.
106
164
  #
107
- # @param input [String] The string content.
108
- # @param format [String] The format code (e.g. "json").
109
- # @param format_args [keywords] Extra args to pass to the formatter.
110
- # @return [CloudEvents::Event]
165
+ # You may provide an event, an array of events, or an opaque event. You may
166
+ # also specify what content mode and format to use.
111
167
  #
112
- def decode_structured_content input, format, **format_args
113
- handlers = @structured_formatters[format] || []
114
- handlers.reverse_each do |handler|
115
- event = handler.decode input, **format_args
116
- return event if event
117
- end
118
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
119
- end
120
-
121
- ##
122
- # Decode a batch of events from the given content data. This should be
123
- # passed the request body, if the Content-Type is of the form
124
- # `application/cloudevents-batch+format`.
168
+ # The result is a two-element array where the first element is a headers
169
+ # list (as defined in the Rack specification) and the second is a string
170
+ # containing the HTTP body content. When using structured content mode, the
171
+ # headers list will contain only a `Content-Type` header and the body will
172
+ # contain the serialized event. When using binary mode, the header list
173
+ # will contain the serialized event attributes and the body will contain
174
+ # the serialized event data.
125
175
  #
126
- # @param input [String] The string content.
127
- # @param format [String] The format code (e.g. "json").
176
+ # @param event [CloudEvents::Event,Array<CloudEvents::Event>,CloudEvents::Event::Opaque]
177
+ # The event, batch, or opaque event.
178
+ # @param structured_format [boolean,String] If given, the data will be
179
+ # encoded in structured content mode. You can pass a string to select
180
+ # a format name, or pass `true` to use the default format. If set to
181
+ # `false` (the default), the data will be encoded in binary mode.
128
182
  # @param format_args [keywords] Extra args to pass to the formatter.
129
- # @return [Array<CloudEvents::Event>]
183
+ # @return [Array(headers,String)]
130
184
  #
131
- def decode_batched_content input, format, **format_args
132
- handlers = @batched_formatters[format] || []
133
- handlers.reverse_each do |handler|
134
- events = handler.decode_batch input, **format_args
135
- return events if events
185
+ def encode_event event, structured_format: false, **format_args
186
+ if event.is_a? Event::Opaque
187
+ [{ "Content-Type" => event.content_type.to_s }, event.content]
188
+ elsif !structured_format
189
+ if event.is_a? ::Array
190
+ raise ::ArgumentError, "Encoding a batch requires structured_format"
191
+ end
192
+ encode_binary_content event, legacy_data_encode: false, **format_args
193
+ else
194
+ structured_format = default_encoder_name if structured_format == true
195
+ raise ::ArgumentError, "Format name not specified, and no default is set" unless structured_format
196
+ case event
197
+ when ::Array
198
+ encode_batched_content event, structured_format, **format_args
199
+ when Event
200
+ encode_structured_content event, structured_format, **format_args
201
+ else
202
+ raise ::ArgumentError, "Unknown event type: #{event.class}"
203
+ end
136
204
  end
137
- raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
138
205
  end
139
206
 
140
207
  ##
141
- # Decode an event from the given Rack environment in binary content mode.
208
+ # Decode an event from the given Rack environment hash. Following the
209
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
210
+ # the request.
142
211
  #
143
- # @param env [Hash] Rack environment hash.
144
- # @param content_type [CloudEvents::ContentType] the content type from the
145
- # Rack environment.
146
- # @return [CloudEvents::Event] if a CloudEvent could be decoded from the
147
- # Rack environment.
148
- # @return [nil] if the Rack environment does not indicate a CloudEvent
212
+ # @deprecated Will be removed in version 1.0. Use {#decode_event} instead.
149
213
  #
150
- def decode_binary_content env, content_type
151
- spec_version = env["HTTP_CE_SPECVERSION"]
152
- return nil if spec_version.nil?
153
- raise SpecVersionError, "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
154
- input = env["rack.input"]
155
- data = if input
156
- input.set_encoding content_type.charset if content_type&.charset
157
- input.read
158
- end
159
- attributes = { "spec_version" => spec_version, "data" => data }
160
- attributes["data_content_type"] = content_type if content_type
161
- omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
162
- env.each do |key, value|
163
- match = /^HTTP_CE_(\w+)$/.match key
164
- next unless match
165
- attr_name = match[1].downcase
166
- attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
167
- end
168
- Event.create spec_version: spec_version, attributes: attributes
214
+ # @param env [Hash] The Rack environment.
215
+ # @param format_args [keywords] Extra args to pass to the formatter.
216
+ # @return [CloudEvents::Event] if the request includes a single structured
217
+ # or binary event.
218
+ # @return [Array<CloudEvents::Event>] if the request includes a batch of
219
+ # structured events.
220
+ # @return [nil] if the request does not appear to be a CloudEvent.
221
+ # @raise [CloudEvents::CloudEventsError] if the request appears to be a
222
+ # CloudEvent but decoding failed.
223
+ #
224
+ def decode_rack_env env, **format_args
225
+ content_type_string = env["CONTENT_TYPE"]
226
+ content_type = ContentType.new content_type_string if content_type_string
227
+ content = read_with_charset env["rack.input"], content_type&.charset
228
+ env["rack.input"].rewind rescue nil
229
+ decode_binary_content(content, content_type, env, legacy_data_decode: true) ||
230
+ decode_structured_content(content, content_type, false, **format_args)
169
231
  end
170
232
 
171
233
  ##
172
- # Encode a single event to content data in the given format.
234
+ # Encode a single event in structured content mode in the given format.
173
235
  #
174
- # The result is a two-element array where the first element is a headers
175
- # list (as defined in the Rack specification) and the second is a string
176
- # containing the HTTP body content. The headers list will contain only
177
- # one header, a `Content-Type` whose value is of the form
178
- # `application/cloudevents+format`.
236
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
179
237
  #
180
238
  # @param event [CloudEvents::Event] The event.
181
- # @param format [String] The format code (e.g. "json")
239
+ # @param format_name [String] The format name.
182
240
  # @param format_args [keywords] Extra args to pass to the formatter.
183
241
  # @return [Array(headers,String)]
184
242
  #
185
- def encode_structured_content event, format, **format_args
186
- handlers = @structured_formatters[format] || []
187
- handlers.reverse_each do |handler|
188
- content = handler.encode event, **format_args
189
- return [{ "Content-Type" => "application/cloudevents+#{format}" }, content] if content
243
+ def encode_structured_content event, format_name, **format_args
244
+ Array(@event_encoders[format_name]).reverse_each do |handler|
245
+ result = handler.encode_event event: event, **format_args
246
+ if result&.key?(:content) && result&.key?(:content_type)
247
+ return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
248
+ end
190
249
  end
191
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
250
+ raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
192
251
  end
193
252
 
194
253
  ##
195
- # Encode a batch of events to content data in the given format.
254
+ # Encode a batch of events in structured content mode in the given format.
196
255
  #
197
- # The result is a two-element array where the first element is a headers
198
- # list (as defined in the Rack specification) and the second is a string
199
- # containing the HTTP body content. The headers list will contain only
200
- # one header, a `Content-Type` whose value is of the form
201
- # `application/cloudevents-batch+format`.
256
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
202
257
  #
203
- # @param events [Array<CloudEvents::Event>] The batch of events.
204
- # @param format [String] The format code (e.g. "json").
258
+ # @param event_batch [Array<CloudEvents::Event>] The batch of events.
259
+ # @param format_name [String] The format name.
205
260
  # @param format_args [keywords] Extra args to pass to the formatter.
206
261
  # @return [Array(headers,String)]
207
262
  #
208
- def encode_batched_content events, format, **format_args
209
- handlers = @batched_formatters[format] || []
210
- handlers.reverse_each do |handler|
211
- content = handler.encode_batch events, **format_args
212
- return [{ "Content-Type" => "application/cloudevents-batch+#{format}" }, content] if content
263
+ def encode_batched_content event_batch, format_name, **format_args
264
+ Array(@event_encoders[format_name]).reverse_each do |handler|
265
+ result = handler.encode_event event_batch: event_batch, **format_args
266
+ if result&.key?(:content) && result&.key?(:content_type)
267
+ return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
268
+ end
213
269
  end
214
- raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
270
+ raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
215
271
  end
216
272
 
217
273
  ##
218
- # Encode an event to content and headers, in binary content mode.
274
+ # Encode an event in binary content mode.
219
275
  #
220
- # The result is a two-element array where the first element is a headers
221
- # list (as defined in the Rack specification) and the second is a string
222
- # containing the HTTP body content.
276
+ # @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
223
277
  #
224
278
  # @param event [CloudEvents::Event] The event.
279
+ # @param format_args [keywords] Extra args to pass to the formatter.
225
280
  # @return [Array(headers,String)]
226
281
  #
227
- def encode_binary_content event
282
+ def encode_binary_content event, legacy_data_encode: true, **format_args
228
283
  headers = {}
229
- body = nil
230
284
  event.to_h.each do |key, value|
231
- if key == "data"
232
- body = value
233
- elsif key == "datacontenttype"
234
- headers["Content-Type"] = value
235
- else
285
+ unless ["data", "datacontenttype"].include? key
236
286
  headers["CE-#{key}"] = percent_encode value
237
287
  end
238
288
  end
239
- if body.is_a? ::String
240
- headers["Content-Type"] ||= if body.encoding == ::Encoding.ASCII_8BIT
241
- "application/octet-stream"
242
- else
243
- "text/plain; charset=#{body.encoding.name.downcase}"
244
- end
245
- elsif body.nil?
246
- headers.delete "Content-Type"
289
+ if legacy_data_encode
290
+ body = event.data
291
+ content_type = event.data_content_type&.to_s
292
+ case body
293
+ when ::String
294
+ content_type ||= string_content_type body
295
+ when nil
296
+ content_type = nil
297
+ else
298
+ body = ::JSON.dump body
299
+ content_type ||= "application/json; charset=#{body.encoding.name.downcase}"
300
+ end
247
301
  else
248
- body = ::JSON.dump body
249
- headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
302
+ body, content_type = encode_data event.spec_version, event.data, event.data_content_type, **format_args
250
303
  end
304
+ headers["Content-Type"] = content_type.to_s if content_type
251
305
  [headers, body]
252
306
  end
253
307
 
254
308
  ##
255
309
  # Decode a percent-encoded string to a UTF-8 string.
256
310
  #
311
+ # @private
312
+ #
257
313
  # @param str [String] Incoming ascii string from an HTTP header, with one
258
314
  # cycle of percent-encoding.
259
315
  # @return [String] Resulting decoded string in UTF-8.
260
316
  #
261
317
  def percent_decode str
318
+ str = str.gsub(/"((?:[^"\\]|\\.)*)"/) { ::Regexp.last_match(1).gsub(/\\(.)/, '\1') }
262
319
  decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
263
320
  decoded_str.force_encoding ::Encoding::UTF_8
264
321
  end
@@ -268,6 +325,8 @@ module CloudEvents
268
325
  # non-printing and non-ascii characters to result in an ASCII string
269
326
  # suitable for setting as an HTTP header value.
270
327
  #
328
+ # @private
329
+ #
271
330
  # @param str [String] Incoming arbitrary string that can be represented
272
331
  # in UTF-8.
273
332
  # @return [String] Resulting encoded string in ASCII.
@@ -276,7 +335,7 @@ module CloudEvents
276
335
  arr = []
277
336
  utf_str = str.to_s.encode ::Encoding::UTF_8
278
337
  utf_str.each_byte do |byte|
279
- if byte >= 33 && byte <= 126 && byte != 37
338
+ if byte >= 33 && byte <= 126 && byte != 34 && byte != 37
280
339
  arr << byte
281
340
  else
282
341
  hi = byte / 16
@@ -288,5 +347,120 @@ module CloudEvents
288
347
  end
289
348
  arr.pack "C*"
290
349
  end
350
+
351
+ private
352
+
353
+ def add_named_formatter collection, formatter, name
354
+ return unless name
355
+ formatters = collection[name] ||= []
356
+ formatters << formatter unless formatters.include? formatter
357
+ end
358
+
359
+ ##
360
+ # Decode a single event from the given request body and content type in
361
+ # structured mode.
362
+ #
363
+ def decode_structured_content content, content_type, allow_opaque, **format_args
364
+ @event_decoders.reverse_each do |decoder|
365
+ result = decoder.decode_event content: content, content_type: content_type, **format_args
366
+ event = result[:event] || result[:event_batch] if result
367
+ return event if event
368
+ end
369
+ if content_type&.media_type == "application" &&
370
+ ["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
371
+ return Event::Opaque.new content, content_type if allow_opaque
372
+ raise UnsupportedFormatError, "Unknown cloudevents content type: #{content_type}"
373
+ end
374
+ nil
375
+ end
376
+
377
+ ##
378
+ # Decode an event from the given Rack environment in binary content mode.
379
+ #
380
+ # TODO: legacy_data_decode is deprecated and can be removed when
381
+ # decode_rack_env is removed.
382
+ #
383
+ def decode_binary_content content, content_type, env, legacy_data_decode: false
384
+ spec_version = env["HTTP_CE_SPECVERSION"]
385
+ return nil unless spec_version
386
+ unless spec_version =~ /^0\.3|1(\.|$)/
387
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
388
+ end
389
+ if legacy_data_decode
390
+ data = content
391
+ else
392
+ data, content_type = decode_data spec_version, content, content_type
393
+ end
394
+ attributes = { "spec_version" => spec_version, "data" => data }
395
+ attributes["data_content_type"] = content_type if content_type
396
+ omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
397
+ env.each do |key, value|
398
+ match = /^HTTP_CE_(\w+)$/.match key
399
+ next unless match
400
+ attr_name = match[1].downcase
401
+ attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
402
+ end
403
+ Event.create spec_version: spec_version, attributes: attributes
404
+ end
405
+
406
+ def decode_data spec_version, content, content_type, **format_args
407
+ @data_decoders.reverse_each do |handler|
408
+ result = handler.decode_data spec_version: spec_version,
409
+ content: content,
410
+ content_type: content_type,
411
+ **format_args
412
+ if result&.key?(:data) && result&.key?(:content_type)
413
+ return [result[:data], result[:content_type]]
414
+ end
415
+ end
416
+ raise "Should not get here"
417
+ end
418
+
419
+ def encode_data spec_version, data_obj, content_type, **format_args
420
+ @data_encoders.reverse_each do |handler|
421
+ result = handler.encode_data spec_version: spec_version,
422
+ data: data_obj,
423
+ content_type: content_type,
424
+ **format_args
425
+ if result&.key?(:content) && result&.key?(:content_type)
426
+ return [result[:content], result[:content_type]]
427
+ end
428
+ end
429
+ raise "Should not get here"
430
+ end
431
+
432
+ def read_with_charset io, charset
433
+ return nil if io.nil?
434
+ str = io.read
435
+ if charset
436
+ begin
437
+ str.force_encoding charset
438
+ rescue ::ArgumentError
439
+ # Use binary for now if the charset is unrecognized
440
+ str.force_encoding ::Encoding::ASCII_8BIT
441
+ end
442
+ end
443
+ str
444
+ end
445
+
446
+ # @private
447
+ module DefaultDataFormat
448
+ # @private
449
+ def self.decode_data content: nil, content_type: nil, **_extra_kwargs
450
+ { data: content, content_type: content_type }
451
+ end
452
+
453
+ # @private
454
+ def self.encode_data data: nil, content_type: nil, **_extra_kwargs
455
+ content = data.to_s
456
+ content_type ||=
457
+ if content.encoding == ::Encoding::ASCII_8BIT
458
+ "application/octet-stream"
459
+ else
460
+ "text/plain; charset=#{content.encoding.name.downcase}"
461
+ end
462
+ { content: content, content_type: content_type }
463
+ end
464
+ end
291
465
  end
292
466
  end