cloud_events 0.3.1 → 0.7.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.
@@ -12,56 +12,159 @@ module CloudEvents
12
12
  # https://github.com/cloudevents/spec/blob/v1.0/json-format.md.
13
13
  #
14
14
  class JsonFormat
15
+ # @private
16
+ UNSPECIFIED = ::Object.new.freeze
17
+
15
18
  ##
16
- # Decode an event from the given input JSON string.
19
+ # Decode an event or batch from the given input JSON string.
20
+ # See {CloudEvents::Format#decode_event} for a general description.
17
21
  #
18
- # @param json [String] A JSON-formatted string
19
- # @return [CloudEvents::Event]
22
+ # Expects `:content` and `:content_type` arguments, and will decline the
23
+ # request unless both are provided.
24
+ #
25
+ # If decoding succeeded, returns a hash with _one of_ the following keys:
26
+ #
27
+ # * `:event` ({CloudEvents::Event}) A single event decoded from the input.
28
+ # * `:event_batch` (Array of {CloudEvents::Event}) A batch of events
29
+ # decoded from the input.
20
30
  #
21
- def decode json, **_other_kwargs
22
- structure = ::JSON.parse json
23
- decode_hash_structure structure
31
+ # @param content [String] Serialized content to decode.
32
+ # @param content_type [CloudEvents::ContentType] The input content type.
33
+ # @param data_decoder [#decode_data] Optional data field decoder, used for
34
+ # non-JSON content types.
35
+ # @return [Hash] if accepting the request.
36
+ # @return [nil] if declining the request.
37
+ # @raise [CloudEvents::FormatSyntaxError] if the JSON could not be parsed
38
+ # @raise [CloudEvents::SpecVersionError] if an unsupported specversion is
39
+ # found.
40
+ #
41
+ def decode_event content: nil, content_type: nil, data_decoder: nil, **_other_kwargs
42
+ return nil unless content && content_type&.media_type == "application" && content_type&.subtype_format == "json"
43
+ case content_type.subtype_base
44
+ when "cloudevents"
45
+ event = decode_hash_structure ::JSON.parse(content), charset: charset_of(content), data_decoder: data_decoder
46
+ { event: event }
47
+ when "cloudevents-batch"
48
+ charset = charset_of content
49
+ batch = Array(::JSON.parse(content)).map do |structure|
50
+ decode_hash_structure structure, charset: charset, data_decoder: data_decoder
51
+ end
52
+ { event_batch: batch }
53
+ end
54
+ rescue ::JSON::JSONError
55
+ raise FormatSyntaxError, "JSON syntax error"
24
56
  end
25
57
 
26
58
  ##
27
- # Encode an event to a JSON string.
59
+ # Encode an event or batch to a JSON string. This formatter should be able
60
+ # to handle any event.
61
+ # See {CloudEvents::Format#decode_event} for a general description.
28
62
  #
29
- # @param event [CloudEvents::Event] An input event.
63
+ # Expects _either_ the `:event` _or_ the `:event_batch` argument, but not
64
+ # both, and will decline the request unless exactly one is provided.
65
+ #
66
+ # If encoding succeeded, returns a hash with the following keys:
67
+ #
68
+ # * `:content` (String) The serialized form of the event or batch.
69
+ # * `:content_type` ({CloudEvents::ContentType}) The content type for the
70
+ # output.
71
+ #
72
+ # @param event [CloudEvents::Event] An event to encode.
73
+ # @param event_batch [Array<CloudEvents::Event>] An event batch to encode.
74
+ # @param data_encoder [#encode_data] Optional data field encoder, used for
75
+ # non-JSON content types.
30
76
  # @param sort [boolean] Whether to sort keys of the JSON output.
31
- # @return [String] The JSON representation.
77
+ # @return [Hash] if accepting the request.
78
+ # @return [nil] if declining the request.
79
+ # @raise [CloudEvents::FormatSyntaxError] if the JSON could not be parsed
32
80
  #
33
- def encode event, sort: false, **_other_kwargs
34
- structure = encode_hash_structure event
35
- structure = sort_keys structure if sort
36
- ::JSON.dump structure
81
+ def encode_event event: nil, event_batch: nil, data_encoder: nil, sort: false, **_other_kwargs
82
+ if event && !event_batch
83
+ structure = encode_hash_structure event, data_encoder: data_encoder
84
+ structure = sort_keys structure if sort
85
+ subtype = "cloudevents"
86
+ elsif event_batch && !event
87
+ structure = event_batch.map do |elem|
88
+ structure_elem = encode_hash_structure elem, data_encoder: data_encoder
89
+ sort ? sort_keys(structure_elem) : structure_elem
90
+ end
91
+ subtype = "cloudevents-batch"
92
+ else
93
+ return nil
94
+ end
95
+ content = ::JSON.dump structure
96
+ content_type = ContentType.new "application/#{subtype}+json; charset=#{charset_of content}"
97
+ { content: content, content_type: content_type }
98
+ rescue ::JSON::JSONError
99
+ raise FormatSyntaxError, "JSON syntax error"
37
100
  end
38
101
 
39
102
  ##
40
- # Decode a batch of events from the given input string.
103
+ # Decode an event data object from a JSON formatted string.
104
+ # See {CloudEvents::Format#decode_data} for a general description.
105
+ #
106
+ # Expects `:spec_version`, `:content` and `:content_type` arguments, and
107
+ # will decline the request unless all three are provided.
108
+ #
109
+ # If decoding succeeded, returns a hash with the following keys:
110
+ #
111
+ # * `:data` (Object) The payload object to set as the `data` attribute.
112
+ # * `:content_type` ({CloudEvents::ContentType}) The content type to be set
113
+ # as the `datacontenttype` attribute.
41
114
  #
42
- # @param json [String] A JSON-formatted string
43
- # @return [Array<CloudEvents::Event>]
115
+ # @param content [String] Serialized content to decode.
116
+ # @param content_type [CloudEvents::ContentType] The input content type.
117
+ # @return [Hash] if accepting the request.
118
+ # @return [nil] if declining the request.
119
+ # @raise [CloudEvents::FormatSyntaxError] if the JSON could not be parsed.
120
+ # @raise [CloudEvents::SpecVersionError] if an unsupported specversion is
121
+ # found.
44
122
  #
45
- def decode_batch json, **_other_kwargs
46
- structure_array = Array(::JSON.parse(json))
47
- structure_array.map do |structure|
48
- decode_hash_structure structure
123
+ def decode_data spec_version: nil, content: nil, content_type: nil, **_other_kwargs
124
+ return nil unless spec_version
125
+ return nil unless content
126
+ return nil unless json_content_type? content_type
127
+ unless spec_version =~ /^0\.3|1(\.|$)/
128
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
49
129
  end
130
+ data = ::JSON.parse content
131
+ { data: data, content_type: content_type }
132
+ rescue ::JSON::JSONError
133
+ raise FormatSyntaxError, "JSON syntax error"
50
134
  end
51
135
 
52
136
  ##
53
- # Encode a batch of event to a JSON string.
137
+ # Encode an event data object to a JSON formatted string.
138
+ # See {CloudEvents::Format#encode_data} for a general description.
139
+ #
140
+ # Expects `:spec_version`, `:data` and `:content_type` arguments, and will
141
+ # decline the request unless all three are provided.
142
+ # The `:data` object can be any Ruby object that can be interpreted as
143
+ # JSON. Most Ruby objects will work, but normally it will be a JSON value
144
+ # type comprising hashes, arrays, strings, numbers, booleans, or nil.
145
+ #
146
+ # If decoding succeeded, returns a hash with the following keys:
54
147
  #
55
- # @param events [Array<CloudEvents::Event>] An array of input events.
148
+ # * `:content` (String) The serialized form of the data.
149
+ # * `:content_type` ({CloudEvents::ContentType}) The content type for the
150
+ # output.
151
+ #
152
+ # @param data [Object] A data object to encode.
153
+ # @param content_type [CloudEvents::ContentType] The input content type
56
154
  # @param sort [boolean] Whether to sort keys of the JSON output.
57
- # @return [String] The JSON representation.
155
+ # @return [Hash] if accepting the request.
156
+ # @return [nil] if declining the request.
58
157
  #
59
- def encode_batch events, sort: false, **_other_kwargs
60
- structure_array = Array(events).map do |event|
61
- structure = encode_hash_structure event
62
- sort ? sort_keys(structure) : structure
158
+ def encode_data spec_version: nil, data: UNSPECIFIED, content_type: nil, sort: false, **_other_kwargs
159
+ return nil unless spec_version
160
+ return nil if data == UNSPECIFIED
161
+ return nil unless json_content_type? content_type
162
+ unless spec_version =~ /^0\.3|1(\.|$)/
163
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
63
164
  end
64
- ::JSON.dump structure_array
165
+ data = sort_keys data if sort
166
+ content = ::JSON.dump data
167
+ { content: content, content_type: content_type }
65
168
  end
66
169
 
67
170
  ##
@@ -69,15 +172,19 @@ module CloudEvents
69
172
  # conforming to the JSON envelope.
70
173
  #
71
174
  # @param structure [Hash] An input hash.
175
+ # @param charset [String] The charset of the original encoded JSON document
176
+ # if known. Used to provide default content types.
177
+ # @param data_decoder [#decode_data] Optional data field decoder, used for
178
+ # non-JSON content types.
72
179
  # @return [CloudEvents::Event]
73
180
  #
74
- def decode_hash_structure structure
181
+ def decode_hash_structure structure, charset: nil, data_decoder: nil
75
182
  spec_version = structure["specversion"].to_s
76
183
  case spec_version
77
184
  when "0.3"
78
- decode_hash_structure_v0 structure
185
+ decode_hash_structure_v0 structure, charset
79
186
  when /^1(\.|$)/
80
- decode_hash_structure_v1 structure
187
+ decode_hash_structure_v1 structure, charset, spec_version, data_decoder
81
188
  else
82
189
  raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
83
190
  end
@@ -88,70 +195,134 @@ module CloudEvents
88
195
  # conforming to the JSON envelope.
89
196
  #
90
197
  # @param event [CloudEvents::Event] An input event.
198
+ # @param data_encoder [#encode_data] Optional data field encoder, used for
199
+ # non-JSON content types.
91
200
  # @return [String] The hash structure.
92
201
  #
93
- def encode_hash_structure event
202
+ def encode_hash_structure event, data_encoder: nil
94
203
  case event
95
204
  when Event::V0
96
205
  encode_hash_structure_v0 event
97
206
  when Event::V1
98
- encode_hash_structure_v1 event
207
+ encode_hash_structure_v1 event, data_encoder
99
208
  else
100
- raise SpecVersionError, "Unrecognized specversion: #{event.spec_version}"
209
+ raise SpecVersionError, "Unrecognized event type: #{event.class}"
101
210
  end
102
211
  end
103
212
 
104
213
  private
105
214
 
106
- def sort_keys hash
215
+ def json_content_type? content_type
216
+ content_type&.subtype_base == "json" || content_type&.subtype_format == "json"
217
+ end
218
+
219
+ def sort_keys obj
220
+ return obj unless obj.is_a? ::Hash
107
221
  result = {}
108
- hash.keys.sort.each do |key|
109
- result[key] = hash[key]
222
+ obj.keys.sort.each do |key|
223
+ result[key] = sort_keys obj[key]
110
224
  end
111
225
  result
112
226
  end
113
227
 
114
- def decode_hash_structure_v0 structure
115
- data = structure["data"]
116
- content_type = structure["datacontenttype"]
117
- if data.is_a?(::String) && content_type.is_a?(::String)
118
- content_type = ContentType.new content_type
119
- if content_type.subtype == "json" || content_type.subtype_format == "json"
120
- structure = structure.dup
121
- structure["data"] = ::JSON.parse data rescue data
122
- structure["datacontenttype"] = content_type
123
- end
228
+ def charset_of str
229
+ encoding = str.encoding
230
+ if encoding == ::Encoding::ASCII_8BIT
231
+ "binary"
232
+ else
233
+ encoding.name.downcase
234
+ end
235
+ end
236
+
237
+ def decode_hash_structure_v0 structure, charset
238
+ unless structure.key? "datacontenttype"
239
+ structure = structure.dup
240
+ content_type = "application/json"
241
+ content_type = "#{content_type}; charset=#{charset}" if charset
242
+ structure["datacontenttype"] = content_type
124
243
  end
125
244
  Event::V0.new attributes: structure
126
245
  end
127
246
 
128
- def decode_hash_structure_v1 structure
247
+ def decode_hash_structure_v1 structure, charset, spec_version, data_decoder
248
+ unless structure.key?("data") || structure.key?("data_base64")
249
+ return Event::V1.new set_attributes: structure
250
+ end
251
+ structure = structure.dup
252
+ content, content_type = retrieve_content_from_data_fields structure, charset
253
+ populate_data_fields_from_content structure, content, content_type, spec_version, data_decoder
254
+ Event::V1.new set_attributes: structure
255
+ end
256
+
257
+ def retrieve_content_from_data_fields structure, charset
129
258
  if structure.key? "data_base64"
130
- structure = structure.dup
131
- structure["data"] = ::Base64.decode64 structure.delete "data_base64"
259
+ content = ::Base64.decode64 structure.delete "data_base64"
260
+ content_type = structure["datacontenttype"] || "application/octet-stream"
261
+ else
262
+ content = structure["data"]
263
+ content_type = structure["datacontenttype"]
264
+ content_type ||= charset ? "application/json; charset=#{charset}" : "application/json"
132
265
  end
133
- Event::V1.new attributes: structure
266
+ [content, ContentType.new(content_type)]
267
+ end
268
+
269
+ def populate_data_fields_from_content structure, content, content_type, spec_version, data_decoder
270
+ if json_content_type? content_type
271
+ structure["data_encoded"] = ::JSON.dump content
272
+ structure["data"] = content
273
+ else
274
+ structure["data_encoded"] = content = content.to_s
275
+ result = data_decoder&.decode_data spec_version: spec_version, content: content, content_type: content_type
276
+ if result
277
+ structure["data"] = result[:data]
278
+ content_type = result[:content_type]
279
+ else
280
+ structure.delete "data"
281
+ end
282
+ end
283
+ structure["datacontenttype"] = content_type
134
284
  end
135
285
 
136
286
  def encode_hash_structure_v0 event
137
287
  structure = event.to_h
138
- data = event.data
288
+ structure["datacontenttype"] ||= "application/json"
289
+ structure
290
+ end
291
+
292
+ def encode_hash_structure_v1 event, data_encoder
293
+ structure = event.to_h
294
+ return structure unless structure.key?("data") || structure.key?("data_encoded")
139
295
  content_type = event.data_content_type
140
- if data.is_a?(::String) && !content_type.nil? &&
141
- (content_type.subtype == "json" || content_type.subtype_format == "json")
142
- structure["data"] = ::JSON.parse data rescue data
296
+ if content_type.nil? || json_content_type?(content_type)
297
+ encode_data_fields_for_json_content structure, event
298
+ else
299
+ encode_data_fields_for_other_content structure, event, data_encoder
143
300
  end
144
301
  structure
145
302
  end
146
303
 
147
- def encode_hash_structure_v1 event
148
- structure = event.to_h
149
- data = structure["data"]
150
- if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
304
+ def encode_data_fields_for_json_content structure, event
305
+ structure["data"] = ::JSON.parse event.data unless event.data_decoded?
306
+ structure.delete "data_encoded"
307
+ structure["datacontenttype"] ||= "application/json"
308
+ end
309
+
310
+ def encode_data_fields_for_other_content structure, event, data_encoder
311
+ data_encoded = structure.delete "data_encoded"
312
+ unless data_encoded
313
+ result = data_encoder&.encode_data spec_version: event.spec_version,
314
+ data: event.data,
315
+ content_type: event.data_content_type
316
+ raise UnsupportedFormatError, "Unable to encode data of media type #{event.data_content_type}" unless result
317
+ data_encoded = result[:content]
318
+ structure["datacontenttype"] = result[:content_type].to_s
319
+ end
320
+ if data_encoded.encoding == ::Encoding::ASCII_8BIT
321
+ structure["data_base64"] = ::Base64.encode64 data_encoded
151
322
  structure.delete "data"
152
- structure["data_base64"] = ::Base64.encode64 data
323
+ else
324
+ structure["data"] = data_encoded
153
325
  end
154
- structure
155
326
  end
156
327
  end
157
328
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+
6
+ module CloudEvents
7
+ ##
8
+ # An data encoder/decoder for text content types. This handles any media type
9
+ # of the form `text/*` or `application/octet-stream`, and passes strings
10
+ # through as-is.
11
+ #
12
+ class TextFormat
13
+ # @private
14
+ UNSPECIFIED = ::Object.new.freeze
15
+
16
+ ##
17
+ # Trivially decode an event data string using text format.
18
+ # See {CloudEvents::Format#decode_data} for a general description.
19
+ #
20
+ # Expects `:content` and `:content_type` arguments, and will decline the
21
+ # request unless all three are provided.
22
+ #
23
+ # If decoding succeeded, returns a hash with the following keys:
24
+ #
25
+ # * `:data` (Object) The payload object to set as the `data` attribute.
26
+ # * `:content_type` ({CloudEvents::ContentType}) The content type to be set
27
+ # as the `datacontenttype` attribute.
28
+ #
29
+ # @param content [String] Serialized content to decode.
30
+ # @param content_type [CloudEvents::ContentType] The input content type.
31
+ # @return [Hash] if accepting the request.
32
+ # @return [nil] if declining the request.
33
+ #
34
+ def decode_data content: nil, content_type: nil, **_other_kwargs
35
+ return nil unless content
36
+ return nil unless text_content_type? content_type
37
+ { data: content.to_s, content_type: content_type }
38
+ end
39
+
40
+ ##
41
+ # Trivially an event data object using text format.
42
+ # See {CloudEvents::Format#encode_data} for a general description.
43
+ #
44
+ # Expects `:data` and `:content_type` arguments, and will decline the
45
+ # request unless all three are provided.
46
+ # The `:data` object will be converted to a string if it is not already a
47
+ # string.
48
+ #
49
+ # If decoding succeeded, returns a hash with the following keys:
50
+ #
51
+ # * `:content` (String) The serialized form of the data.
52
+ # * `:content_type` ({CloudEvents::ContentType}) The content type for the
53
+ # output.
54
+ #
55
+ # @param data [Object] A data object to encode.
56
+ # @param content_type [CloudEvents::ContentType] The input content type
57
+ # @return [Hash] if accepting the request.
58
+ # @return [nil] if declining the request.
59
+ #
60
+ def encode_data data: UNSPECIFIED, content_type: nil, **_other_kwargs
61
+ return nil if data == UNSPECIFIED
62
+ return nil unless text_content_type? content_type
63
+ { content: data.to_s, content_type: content_type }
64
+ end
65
+
66
+ private
67
+
68
+ def text_content_type? content_type
69
+ content_type&.media_type == "text" ||
70
+ content_type&.media_type == "application" && content_type&.subtype == "octet-stream"
71
+ end
72
+ end
73
+ end
@@ -5,5 +5,5 @@ module CloudEvents
5
5
  # Version of the Ruby CloudEvents SDK
6
6
  # @return [String]
7
7
  #
8
- VERSION = "0.3.1"
8
+ VERSION = "0.7.0"
9
9
  end
data/lib/cloud_events.rb CHANGED
@@ -3,8 +3,10 @@
3
3
  require "cloud_events/content_type"
4
4
  require "cloud_events/errors"
5
5
  require "cloud_events/event"
6
+ require "cloud_events/format"
6
7
  require "cloud_events/http_binding"
7
8
  require "cloud_events/json_format"
9
+ require "cloud_events/text_format"
8
10
 
9
11
  ##
10
12
  # CloudEvents implementation.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloud_events
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-25 00:00:00.000000000 Z
11
+ date: 2022-01-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The official Ruby implementation of the CloudEvents Specification. Provides
14
14
  data types for events, and HTTP/JSON bindings for marshalling and unmarshalling
@@ -28,20 +28,23 @@ files:
28
28
  - lib/cloud_events/errors.rb
29
29
  - lib/cloud_events/event.rb
30
30
  - lib/cloud_events/event/field_interpreter.rb
31
+ - lib/cloud_events/event/opaque.rb
31
32
  - lib/cloud_events/event/utils.rb
32
33
  - lib/cloud_events/event/v0.rb
33
34
  - lib/cloud_events/event/v1.rb
35
+ - lib/cloud_events/format.rb
34
36
  - lib/cloud_events/http_binding.rb
35
37
  - lib/cloud_events/json_format.rb
38
+ - lib/cloud_events/text_format.rb
36
39
  - lib/cloud_events/version.rb
37
40
  homepage: https://github.com/cloudevents/sdk-ruby
38
41
  licenses:
39
42
  - Apache-2.0
40
43
  metadata:
41
- changelog_uri: https://cloudevents.github.io/sdk-ruby/v0.3.1/file.CHANGELOG.html
44
+ changelog_uri: https://cloudevents.github.io/sdk-ruby/v0.7.0/file.CHANGELOG.html
42
45
  source_code_uri: https://github.com/cloudevents/sdk-ruby
43
46
  bug_tracker_uri: https://github.com/cloudevents/sdk-ruby/issues
44
- documentation_uri: https://cloudevents.github.io/sdk-ruby/v0.3.1
47
+ documentation_uri: https://cloudevents.github.io/sdk-ruby/v0.7.0
45
48
  post_install_message:
46
49
  rdoc_options: []
47
50
  require_paths: