cloud_events 0.1.0

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.
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "uri"
5
+
6
+ module CloudEvents
7
+ module Event
8
+ ##
9
+ # A CloudEvents V0 data type.
10
+ #
11
+ # This object represents a complete CloudEvent, including the event data
12
+ # and context attributes. It supports the standard required and optional
13
+ # attributes defined in CloudEvents V0.3, and arbitrary extension
14
+ # attributes. All attribute values can be obtained (in their string form)
15
+ # via the {Event::V0#[]} method. Additionally, standard attributes have
16
+ # their own accessor methods that may return typed objects (such as
17
+ # `DateTime` for the `time` attribute).
18
+ #
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
+ #
23
+ # See https://github.com/cloudevents/spec/blob/v0.3/spec.md for
24
+ # descriptions of the standard attributes.
25
+ #
26
+ class V0
27
+ include Event
28
+
29
+ ##
30
+ # Create a new cloud event object with the given data and attributes.
31
+ #
32
+ # Event attributes may be presented as keyword arguments, or as a Hash
33
+ # passed in via the `attributes` argument (but not both).
34
+ #
35
+ # The following standard attributes are supported and exposed as
36
+ # attribute methods on the object.
37
+ #
38
+ # * **:spec_version** (or **:specversion**) [`String`] - _required_ -
39
+ # The CloudEvents spec version (i.e. the `specversion` field.)
40
+ # * **:id** [`String`] - _required_ - The event `id` field.
41
+ # * **:source** [`String`, `URI`] - _required_ - The event `source`
42
+ # field.
43
+ # * **:type** [`String`] - _required_ - The event `type` field.
44
+ # * **:data** [`Object`] - _optional_ - The data associated with the
45
+ # event (i.e. the `data` field.)
46
+ # * **:data_content_encoding** (or **:datacontentencoding**)
47
+ # [`String`] - _optional_ - The content-encoding for the data (i.e.
48
+ # the `datacontentencoding` field.)
49
+ # * **:data_content_type** (or **:datacontenttype**) [`String`,
50
+ # {ContentType}] - _optional_ - The content-type for the data, if
51
+ # the data is a string (i.e. the event `datacontenttype` field.)
52
+ # * **:schema_url** (or **:schemaurl**) [`String`, `URI`] -
53
+ # _optional_ - The event `schemaurl` field.
54
+ # * **:subject** [`String`] - _optional_ - The event `subject` field.
55
+ # * **:time** [`String`, `DateTime`, `Time`] - _optional_ - The
56
+ # event `time` field.
57
+ #
58
+ # Any additional attributes are assumed to be extension attributes.
59
+ # They are not available as separate methods, but can be accessed via
60
+ # the {Event::V1#[]} operator.
61
+ #
62
+ # @param attributes [Hash] The data and attributes, as a hash.
63
+ # @param args [keywords] The data and attributes, as keyword arguments.
64
+ #
65
+ def initialize attributes: nil, **args
66
+ interpreter = FieldInterpreter.new attributes || args
67
+ @spec_version = interpreter.spec_version ["specversion", "spec_version"], accept: /^0\.3$/
68
+ @id = interpreter.string ["id"], required: true
69
+ @source = interpreter.uri ["source"], required: true
70
+ @type = interpreter.string ["type"], required: true
71
+ @data = interpreter.object ["data"], allow_nil: true
72
+ @data_content_encoding = interpreter.string ["datacontentencoding", "data_content_encoding"]
73
+ @data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
74
+ @schema_url = interpreter.uri ["schemaurl", "schema_url"]
75
+ @subject = interpreter.string ["subject"]
76
+ @time = interpreter.rfc3339_date_time ["time"]
77
+ @attributes = interpreter.finish_attributes
78
+ end
79
+
80
+ ##
81
+ # Create and return a copy of this event with the given changes. See
82
+ # the constructor for the parameters that can be passed. In general,
83
+ # you can pass a new value for any attribute, or pass `nil` to remove
84
+ # an optional attribute.
85
+ #
86
+ # @param changes [keywords] See {#initialize} for a list of arguments.
87
+ # @return [FunctionFramework::CloudEvents::Event]
88
+ #
89
+ def with **changes
90
+ attributes = @attributes.merge changes
91
+ V0.new attributes: attributes
92
+ end
93
+
94
+ ##
95
+ # Return the value of the given named attribute. Both standard and
96
+ # extension attributes are supported.
97
+ #
98
+ # Attribute names must be given as defined in the standard CloudEvents
99
+ # specification. For example `specversion` rather than `spec_version`.
100
+ #
101
+ # Results are given in their "raw" form, generally a string. This may
102
+ # be different from the Ruby object returned from corresponding
103
+ # attribute methods. For example:
104
+ #
105
+ # event["time"] # => String rfc3339 representation
106
+ # event.time # => DateTime object
107
+ #
108
+ # @param key [String,Symbol] The attribute name.
109
+ # @return [String,nil]
110
+ #
111
+ def [] key
112
+ @attributes[key.to_s]
113
+ end
114
+
115
+ ##
116
+ # Return a hash representation of this event.
117
+ #
118
+ # @return [Hash]
119
+ #
120
+ def to_h
121
+ @attributes.dup
122
+ end
123
+
124
+ ##
125
+ # The `id` field. Required.
126
+ #
127
+ # @return [String]
128
+ #
129
+ attr_reader :id
130
+
131
+ ##
132
+ # The `source` field as a `URI` object. Required.
133
+ #
134
+ # @return [URI]
135
+ #
136
+ attr_reader :source
137
+
138
+ ##
139
+ # The `type` field. Required.
140
+ #
141
+ # @return [String]
142
+ #
143
+ attr_reader :type
144
+
145
+ ##
146
+ # The `specversion` field. Required.
147
+ #
148
+ # @return [String]
149
+ #
150
+ attr_reader :spec_version
151
+ alias specversion spec_version
152
+
153
+ ##
154
+ # The event-specific data, or `nil` if there is no data.
155
+ #
156
+ # Data may be one of the following types:
157
+ # * Binary data, represented by a `String` using the `ASCII-8BIT`
158
+ # encoding.
159
+ # * A string in some other encoding such as `UTF-8` or `US-ASCII`.
160
+ # * Any JSON data type, such as a Boolean, Integer, Array, Hash, or
161
+ # `nil`.
162
+ #
163
+ # @return [Object]
164
+ #
165
+ attr_reader :data
166
+
167
+ ##
168
+ # The optional `datacontentencoding` field as a `String` object, or
169
+ # `nil` if the field is absent.
170
+ #
171
+ # @return [String,nil]
172
+ #
173
+ attr_reader :data_content_encoding
174
+ alias datacontentencoding data_content_encoding
175
+
176
+ ##
177
+ # The optional `datacontenttype` field as a {CloudEvents::ContentType}
178
+ # object, or `nil` if the field is absent.
179
+ #
180
+ # @return [CloudEvents::ContentType,nil]
181
+ #
182
+ attr_reader :data_content_type
183
+ alias datacontenttype data_content_type
184
+
185
+ ##
186
+ # The optional `schemaurl` field as a `URI` object, or `nil` if the
187
+ # field is absent.
188
+ #
189
+ # @return [URI,nil]
190
+ #
191
+ attr_reader :schema_url
192
+ alias schemaurl schema_url
193
+
194
+ ##
195
+ # The optional `subject` field, or `nil` if the field is absent.
196
+ #
197
+ # @return [String,nil]
198
+ #
199
+ attr_reader :subject
200
+
201
+ ##
202
+ # The optional `time` field as a `DateTime` object, or `nil` if the
203
+ # field is absent.
204
+ #
205
+ # @return [DateTime,nil]
206
+ #
207
+ attr_reader :time
208
+
209
+ ## @private
210
+ def == other
211
+ other.is_a?(V1) && @attributes == other.instance_variable_get(:@attributes)
212
+ end
213
+ alias eql? ==
214
+
215
+ ## @private
216
+ def hash
217
+ @hash ||= @attributes.hash
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "uri"
5
+
6
+ module CloudEvents
7
+ module Event
8
+ ##
9
+ # A CloudEvents V1 data type.
10
+ #
11
+ # This object represents a complete CloudEvent, including the event data
12
+ # and context attributes. It supports the standard required and optional
13
+ # attributes defined in CloudEvents V1.0, and arbitrary extension
14
+ # attributes. All attribute values can be obtained (in their string form)
15
+ # via the {Event::V1#[]} method. Additionally, standard attributes have
16
+ # their own accessor methods that may return typed objects (such as
17
+ # `DateTime` for the `time` attribute).
18
+ #
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
+ #
23
+ # See https://github.com/cloudevents/spec/blob/v1.0/spec.md for
24
+ # descriptions of the standard attributes.
25
+ #
26
+ class V1
27
+ include Event
28
+
29
+ ##
30
+ # Create a new cloud event object with the given data and attributes.
31
+ #
32
+ # Event attributes may be presented as keyword arguments, or as a Hash
33
+ # passed in via the `attributes` argument (but not both).
34
+ #
35
+ # The following standard attributes are supported and exposed as
36
+ # attribute methods on the object.
37
+ #
38
+ # * **:spec_version** (or **:specversion**) [`String`] - _required_ -
39
+ # The CloudEvents spec version (i.e. the `specversion` field.)
40
+ # * **:id** [`String`] - _required_ - The event `id` field.
41
+ # * **:source** [`String`, `URI`] - _required_ - The event `source`
42
+ # field.
43
+ # * **:type** [`String`] - _required_ - The event `type` field.
44
+ # * **:data** [`Object`] - _optional_ - The data associated with the
45
+ # event (i.e. the `data` field.)
46
+ # * **:data_content_type** (or **:datacontenttype**) [`String`,
47
+ # {ContentType}] - _optional_ - The content-type for the data, if
48
+ # the data is a string (i.e. the event `datacontenttype` field.)
49
+ # * **:data_schema** (or **:dataschema**) [`String`, `URI`] -
50
+ # _optional_ - The event `dataschema` field.
51
+ # * **:subject** [`String`] - _optional_ - The event `subject` field.
52
+ # * **:time** [`String`, `DateTime`, `Time`] - _optional_ - The
53
+ # event `time` field.
54
+ #
55
+ # Any additional attributes are assumed to be extension attributes.
56
+ # They are not available as separate methods, but can be accessed via
57
+ # the {Event::V1#[]} operator.
58
+ #
59
+ # @param attributes [Hash] The data and attributes, as a hash.
60
+ # @param args [keywords] The data and attributes, as keyword arguments.
61
+ #
62
+ def initialize attributes: nil, **args
63
+ interpreter = FieldInterpreter.new attributes || args
64
+ @spec_version = interpreter.spec_version ["specversion", "spec_version"], accept: /^1(\.|$)/
65
+ @id = interpreter.string ["id"], required: true
66
+ @source = interpreter.uri ["source"], required: true
67
+ @type = interpreter.string ["type"], required: true
68
+ @data = interpreter.object ["data"], allow_nil: true
69
+ @data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
70
+ @data_schema = interpreter.uri ["dataschema", "data_schema"]
71
+ @subject = interpreter.string ["subject"]
72
+ @time = interpreter.rfc3339_date_time ["time"]
73
+ @attributes = interpreter.finish_attributes
74
+ end
75
+
76
+ ##
77
+ # Create and return a copy of this event with the given changes. See
78
+ # the constructor for the parameters that can be passed. In general,
79
+ # you can pass a new value for any attribute, or pass `nil` to remove
80
+ # an optional attribute.
81
+ #
82
+ # @param changes [keywords] See {#initialize} for a list of arguments.
83
+ # @return [FunctionFramework::CloudEvents::Event]
84
+ #
85
+ def with **changes
86
+ attributes = @attributes.merge changes
87
+ V1.new attributes: attributes
88
+ end
89
+
90
+ ##
91
+ # Return the value of the given named attribute. Both standard and
92
+ # extension attributes are supported.
93
+ #
94
+ # Attribute names must be given as defined in the standard CloudEvents
95
+ # specification. For example `specversion` rather than `spec_version`.
96
+ #
97
+ # Results are given in their "raw" form, generally a string. This may
98
+ # be different from the Ruby object returned from corresponding
99
+ # attribute methods. For example:
100
+ #
101
+ # event["time"] # => String rfc3339 representation
102
+ # event.time # => DateTime object
103
+ #
104
+ # @param key [String,Symbol] The attribute name.
105
+ # @return [String,nil]
106
+ #
107
+ def [] key
108
+ @attributes[key.to_s]
109
+ end
110
+
111
+ ##
112
+ # Return a hash representation of this event.
113
+ #
114
+ # @return [Hash]
115
+ #
116
+ def to_h
117
+ @attributes.dup
118
+ end
119
+
120
+ ##
121
+ # The `id` field. Required.
122
+ #
123
+ # @return [String]
124
+ #
125
+ attr_reader :id
126
+
127
+ ##
128
+ # The `source` field as a `URI` object. Required.
129
+ #
130
+ # @return [URI]
131
+ #
132
+ attr_reader :source
133
+
134
+ ##
135
+ # The `type` field. Required.
136
+ #
137
+ # @return [String]
138
+ #
139
+ attr_reader :type
140
+
141
+ ##
142
+ # The `specversion` field. Required.
143
+ #
144
+ # @return [String]
145
+ #
146
+ attr_reader :spec_version
147
+ alias specversion spec_version
148
+
149
+ ##
150
+ # The event-specific data, or `nil` if there is no data.
151
+ #
152
+ # Data may be one of the following types:
153
+ # * Binary data, represented by a `String` using the `ASCII-8BIT`
154
+ # encoding.
155
+ # * A string in some other encoding such as `UTF-8` or `US-ASCII`.
156
+ # * Any JSON data type, such as a Boolean, Integer, Array, Hash, or
157
+ # `nil`.
158
+ #
159
+ # @return [Object]
160
+ #
161
+ attr_reader :data
162
+
163
+ ##
164
+ # The optional `datacontenttype` field as a {CloudEvents::ContentType}
165
+ # object, or `nil` if the field is absent.
166
+ #
167
+ # @return [CloudEvents::ContentType,nil]
168
+ #
169
+ attr_reader :data_content_type
170
+ alias datacontenttype data_content_type
171
+
172
+ ##
173
+ # The optional `dataschema` field as a `URI` object, or `nil` if the
174
+ # field is absent.
175
+ #
176
+ # @return [URI,nil]
177
+ #
178
+ attr_reader :data_schema
179
+ alias dataschema data_schema
180
+
181
+ ##
182
+ # The optional `subject` field, or `nil` if the field is absent.
183
+ #
184
+ # @return [String,nil]
185
+ #
186
+ attr_reader :subject
187
+
188
+ ##
189
+ # The optional `time` field as a `DateTime` object, or `nil` if the
190
+ # field is absent.
191
+ #
192
+ # @return [DateTime,nil]
193
+ #
194
+ attr_reader :time
195
+
196
+ ## @private
197
+ def == other
198
+ other.is_a?(V1) && @attributes == other.instance_variable_get(:@attributes)
199
+ end
200
+ alias eql? ==
201
+
202
+ ## @private
203
+ def hash
204
+ @hash ||= @attributes.hash
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudEvents
4
+ ##
5
+ # HTTP binding for CloudEvents.
6
+ #
7
+ # This class implements HTTP binding, including unmarshalling of events from
8
+ # Rack environment data, and marshalling of events to Rack environment data.
9
+ # It supports binary (i.e. header-based) HTTP content, as well as structured
10
+ # (body-based) content that can delegate to formatters such as JSON.
11
+ #
12
+ # Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format.
13
+ # See https://github.com/cloudevents/spec/blob/v0.3/http-transport-binding.md
14
+ # and https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md.
15
+ #
16
+ class HttpBinding
17
+ ##
18
+ # Returns a default binding, with JSON supported.
19
+ #
20
+ def self.default
21
+ @default ||= begin
22
+ 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
26
+ http_binding
27
+ end
28
+ end
29
+
30
+ ##
31
+ # Create an empty HTTP binding.
32
+ #
33
+ def initialize
34
+ @structured_formatters = {}
35
+ @batched_formatters = {}
36
+ end
37
+
38
+ ##
39
+ # Register a formatter for the given type.
40
+ #
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.
47
+ # @return [self]
48
+ #
49
+ def register_structured_formatter type, formatter
50
+ formatters = @structured_formatters[type.to_s.strip.downcase] ||= []
51
+ formatters << formatter unless formatters.include? formatter
52
+ self
53
+ end
54
+
55
+ ##
56
+ # Register a batch formatter for the given type.
57
+ #
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.
64
+ # @return [self]
65
+ #
66
+ def register_batched_formatter type, formatter
67
+ formatters = @batched_formatters[type.to_s.strip.downcase] ||= []
68
+ formatters << formatter unless formatters.include? formatter
69
+ self
70
+ end
71
+
72
+ ##
73
+ # Decode an event from the given Rack environment hash. Following the
74
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
75
+ # the request.
76
+ #
77
+ # @param env [Hash] The Rack environment.
78
+ # @param format_args [keywords] Extra args to pass to the formatter.
79
+ # @return [CloudEvents::Event] if the request includes a single structured
80
+ # or binary event.
81
+ # @return [Array<CloudEvents::Event>] if the request includes a batch of
82
+ # structured events.
83
+ # @return [nil] if the request was not recognized as a CloudEvent.
84
+ #
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
98
+ end
99
+ decode_binary_content env, content_type
100
+ end
101
+
102
+ ##
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`.
106
+ #
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]
111
+ #
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`.
125
+ #
126
+ # @param input [String] The string content.
127
+ # @param format [String] The format code (e.g. "json").
128
+ # @param format_args [keywords] Extra args to pass to the formatter.
129
+ # @return [Array<CloudEvents::Event>]
130
+ #
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
136
+ end
137
+ raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
138
+ end
139
+
140
+ ##
141
+ # Decode an event from the given Rack environment in binary content mode.
142
+ #
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
149
+ #
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
169
+ end
170
+
171
+ ##
172
+ # Encode a single event to content data in the given format.
173
+ #
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`.
179
+ #
180
+ # @param event [CloudEvents::Event] The event.
181
+ # @param format [String] The format code (e.g. "json")
182
+ # @param format_args [keywords] Extra args to pass to the formatter.
183
+ # @return [Array(headers,String)]
184
+ #
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
190
+ end
191
+ raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
192
+ end
193
+
194
+ ##
195
+ # Encode a batch of events to content data in the given format.
196
+ #
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`.
202
+ #
203
+ # @param events [Array<CloudEvents::Event>] The batch of events.
204
+ # @param format [String] The format code (e.g. "json").
205
+ # @param format_args [keywords] Extra args to pass to the formatter.
206
+ # @return [Array(headers,String)]
207
+ #
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
213
+ end
214
+ raise HttpContentError, "Unknown cloudevents format: #{format.inspect}"
215
+ end
216
+
217
+ ##
218
+ # Encode an event to content and headers, in binary content mode.
219
+ #
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.
223
+ #
224
+ # @param event [CloudEvents::Event] The event.
225
+ # @return [Array(headers,String)]
226
+ #
227
+ def encode_binary_content event
228
+ headers = {}
229
+ body = nil
230
+ 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
236
+ headers["CE-#{key}"] = percent_encode value
237
+ end
238
+ 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"
247
+ else
248
+ body = ::JSON.dump body
249
+ headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
250
+ end
251
+ [headers, body]
252
+ end
253
+
254
+ ##
255
+ # Decode a percent-encoded string to a UTF-8 string.
256
+ #
257
+ # @param str [String] Incoming ascii string from an HTTP header, with one
258
+ # cycle of percent-encoding.
259
+ # @return [String] Resulting decoded string in UTF-8.
260
+ #
261
+ def percent_decode str
262
+ decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
263
+ decoded_str.force_encoding ::Encoding::UTF_8
264
+ end
265
+
266
+ ##
267
+ # Transcode an arbitrarily-encoded string to UTF-8, then percent-encode
268
+ # non-printing and non-ascii characters to result in an ASCII string
269
+ # suitable for setting as an HTTP header value.
270
+ #
271
+ # @param str [String] Incoming arbitrary string that can be represented
272
+ # in UTF-8.
273
+ # @return [String] Resulting encoded string in ASCII.
274
+ #
275
+ def percent_encode str
276
+ arr = []
277
+ utf_str = str.to_s.encode ::Encoding::UTF_8
278
+ utf_str.each_byte do |byte|
279
+ if byte >= 33 && byte <= 126 && byte != 37
280
+ arr << byte
281
+ else
282
+ hi = byte / 16
283
+ hi = hi > 9 ? 55 + hi : 48 + hi
284
+ lo = byte % 16
285
+ lo = lo > 9 ? 55 + lo : 48 + lo
286
+ arr << 37 << hi << lo
287
+ end
288
+ end
289
+ arr.pack "C*"
290
+ end
291
+ end
292
+ end