cloud_events 0.1.2 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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