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.
data/lib/cloud_events.rb CHANGED
@@ -3,6 +3,7 @@
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"
8
9
 
@@ -14,23 +14,28 @@ module CloudEvents
14
14
  # can, and fill the rest with defaults as recommended in RFC 2045 sec 5.2.
15
15
  # In case of a parsing error, the {#error_message} field will be set.
16
16
  #
17
+ # This object is immutable, and Ractor-shareable on Ruby 3.
18
+ #
17
19
  class ContentType
18
20
  ##
19
21
  # Parse the given header value.
20
22
  #
21
23
  # @param string [String] Content-Type header value in RFC 2045 format
24
+ # @param default_charset [String] Optional. The charset to use if none is
25
+ # specified. Defaults to `us-ascii`.
22
26
  #
23
- def initialize string
24
- @string = string
27
+ def initialize string, default_charset: nil
28
+ @string = string.to_s
25
29
  @media_type = "text"
26
30
  @subtype_base = @subtype = "plain"
27
31
  @subtype_format = nil
28
32
  @params = []
29
- @charset = "us-ascii"
33
+ @charset = default_charset || "us-ascii"
30
34
  @error_message = nil
31
35
  parse consume_comments string.strip
32
36
  @canonical_string = "#{@media_type}/#{@subtype}" +
33
37
  @params.map { |k, v| "; #{k}=#{maybe_quote v}" }.join
38
+ full_freeze
34
39
  end
35
40
 
36
41
  ##
@@ -137,7 +142,7 @@ module CloudEvents
137
142
  end
138
143
 
139
144
  def consume_token str, downcase: false, error_message: nil
140
- match = /^([\w!#\$%&'\*\+\.\^`\{\|\}-]+)(.*)$/.match str
145
+ match = /^([\w!#$%&'*+.\^`{|}-]+)(.*)$/.match str
141
146
  raise ParseError, error_message || "Expected token" unless match
142
147
  token = match[1]
143
148
  token.downcase! if downcase
@@ -200,9 +205,21 @@ module CloudEvents
200
205
  end
201
206
 
202
207
  def maybe_quote str
203
- return str if /^[\w!#\$%&'\*\+\.\^`\{\|\}-]+$/ =~ str
208
+ return str if /^[\w!#$%&'*+.\^`{|}-]+$/ =~ str
204
209
  str = str.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
205
210
  "\"#{str}\""
206
211
  end
212
+
213
+ def full_freeze
214
+ instance_variables.each do |iv|
215
+ instance_variable_get(iv).freeze
216
+ end
217
+ @params.each do |name_val|
218
+ name_val[0].freeze
219
+ name_val[1].freeze
220
+ name_val.freeze
221
+ end
222
+ freeze
223
+ end
207
224
  end
208
225
  end
@@ -8,21 +8,53 @@ module CloudEvents
8
8
  end
9
9
 
10
10
  ##
11
- # Errors indicating unsupported or incorrectly formatted HTTP content or
12
- # headers.
11
+ # An error raised when a protocol binding was asked to decode a CloudEvent
12
+ # from input data, but does not believe that the data was intended to be a
13
+ # CloudEvent. For example, the HttpBinding might raise this exception if
14
+ # given a request that has neither the requisite headers for binary content
15
+ # mode, nor an appropriate content-type for structured content mode.
13
16
  #
14
- class HttpContentError < CloudEventsError
17
+ class NotCloudEventError < CloudEventsError
15
18
  end
16
19
 
17
20
  ##
18
- # Errors indicating an unsupported or incorrect spec version.
21
+ # An error raised when a protocol binding was asked to decode a CloudEvent
22
+ # from input data, and the data appears to be a CloudEvent, but was encoded
23
+ # in a format that is not supported. Some protocol bindings can be configured
24
+ # to return a {CloudEvents::Event::Opaque} object instead of raising this
25
+ # error.
26
+ #
27
+ class UnsupportedFormatError < CloudEventsError
28
+ end
29
+
30
+ ##
31
+ # An error raised when a protocol binding was asked to decode a CloudEvent
32
+ # from input data, and the data appears to be intended as a CloudEvent, but
33
+ # has unrecoverable format or syntax errors. This error _may_ have a `cause`
34
+ # such as a `JSON::ParserError` with additional information.
35
+ #
36
+ class FormatSyntaxError < CloudEventsError
37
+ end
38
+
39
+ ##
40
+ # An error raised when a `specversion` is set to a value not recognized or
41
+ # supported by the SDK.
19
42
  #
20
43
  class SpecVersionError < CloudEventsError
21
44
  end
22
45
 
23
46
  ##
24
- # Errors related to CloudEvent attributes.
47
+ # An error raised when a malformed CloudEvents attribute is encountered,
48
+ # often because a required attribute is missing, or a value does not match
49
+ # the attribute's type specification.
25
50
  #
26
51
  class AttributeError < CloudEventsError
27
52
  end
53
+
54
+ ##
55
+ # Alias of UnsupportedFormatError, for backward compatibility.
56
+ #
57
+ # @deprecated Will be removed in version 1.0. Use {UnsupportedFormatError}.
58
+ #
59
+ HttpContentError = UnsupportedFormatError
28
60
  end
@@ -3,7 +3,7 @@
3
3
  require "date"
4
4
  require "uri"
5
5
 
6
- require "cloud_events/event/field_interpreter"
6
+ require "cloud_events/event/opaque"
7
7
  require "cloud_events/event/v0"
8
8
  require "cloud_events/event/v1"
9
9
 
@@ -50,6 +50,11 @@ module CloudEvents
50
50
  # `spec_version`, the remaining keyword arguments will be passed
51
51
  # through to the {CloudEvents::Event::V1} constructor.
52
52
  #
53
+ # Note that argument objects passed in may get deep-frozen if they are
54
+ # used in the final event object. This is particularly important for the
55
+ # `:data` field, for example if you pass a structured hash. If this is an
56
+ # issue, make a deep copy of objects before passing them to this method.
57
+ #
53
58
  # @param spec_version [String] The required `specversion` field.
54
59
  # @param kwargs [keywords] Additional parameters for the event.
55
60
  #
@@ -1,24 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cloud_events/event/utils"
4
+
3
5
  module CloudEvents
4
6
  module Event
5
7
  ##
6
8
  # A helper that extracts and interprets event fields from an input hash.
7
- #
8
9
  # @private
9
10
  #
10
11
  class FieldInterpreter
11
12
  def initialize args
12
- @args = keys_to_strings args
13
+ @args = Utils.keys_to_strings args
13
14
  @attributes = {}
14
15
  end
15
16
 
16
17
  def finish_attributes
17
18
  @args.each do |key, value|
18
- @attributes[key] = value.to_s unless value.nil?
19
+ @attributes[key.freeze] = value.to_s.freeze unless value.nil?
19
20
  end
20
21
  @args = {}
21
- @attributes
22
+ @attributes.freeze
22
23
  end
23
24
 
24
25
  def string keys, required: false
@@ -26,6 +27,7 @@ module CloudEvents
26
27
  case value
27
28
  when ::String
28
29
  raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
30
+ value.freeze
29
31
  [value, value]
30
32
  else
31
33
  raise AttributeError, "Illegal type for #{keys.first}:" \
@@ -40,12 +42,12 @@ module CloudEvents
40
42
  when ::String
41
43
  raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
42
44
  begin
43
- [::URI.parse(value), value]
45
+ [Utils.deep_freeze(::URI.parse(value)), value.freeze]
44
46
  rescue ::URI::InvalidURIError => e
45
47
  raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
46
48
  end
47
49
  when ::URI::Generic
48
- [value, value.to_s]
50
+ [Utils.deep_freeze(value), value.to_s.freeze]
49
51
  else
50
52
  raise AttributeError, "Illegal type for #{keys.first}:" \
51
53
  " String or URI expected but #{value.class} found"
@@ -58,15 +60,15 @@ module CloudEvents
58
60
  case value
59
61
  when ::String
60
62
  begin
61
- [::DateTime.rfc3339(value), value]
63
+ [Utils.deep_freeze(::DateTime.rfc3339(value)), value.freeze]
62
64
  rescue ::Date::Error => e
63
65
  raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
64
66
  end
65
67
  when ::DateTime
66
- [value, value.rfc3339]
68
+ [Utils.deep_freeze(value), value.rfc3339.freeze]
67
69
  when ::Time
68
70
  value = value.to_datetime
69
- [value, value.rfc3339]
71
+ [Utils.deep_freeze(value), value.rfc3339.freeze]
70
72
  else
71
73
  raise AttributeError, "Illegal type for #{keys.first}:" \
72
74
  " String, Time, or DateTime expected but #{value.class} found"
@@ -79,7 +81,7 @@ module CloudEvents
79
81
  case value
80
82
  when ::String
81
83
  raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
82
- [ContentType.new(value), value]
84
+ [ContentType.new(value), value.freeze]
83
85
  when ContentType
84
86
  [value, value.to_s]
85
87
  else
@@ -94,6 +96,7 @@ module CloudEvents
94
96
  case value
95
97
  when ::String
96
98
  raise SpecVersionError, "Unrecognized specversion: #{value}" unless accept =~ value
99
+ value.freeze
97
100
  [value, value]
98
101
  else
99
102
  raise AttributeError, "Illegal type for #{keys.first}:" \
@@ -102,7 +105,16 @@ module CloudEvents
102
105
  end
103
106
  end
104
107
 
105
- UNDEFINED = ::Object.new
108
+ def data_object keys, required: false
109
+ object keys, required: required, allow_nil: true do |value|
110
+ Utils.deep_freeze value
111
+ [value, value]
112
+ end
113
+ end
114
+
115
+ UNDEFINED = ::Object.new.freeze
116
+
117
+ private
106
118
 
107
119
  def object keys, required: false, allow_nil: false
108
120
  value = UNDEFINED
@@ -115,24 +127,10 @@ module CloudEvents
115
127
  raise AttributeError, "The #{keys.first} field is required" if required
116
128
  return nil
117
129
  end
118
- if block_given?
119
- converted, raw = yield value
120
- else
121
- converted = raw = value
122
- end
123
- @attributes[keys.first] = raw
130
+ converted, raw = yield value
131
+ @attributes[keys.first.freeze] = raw
124
132
  converted
125
133
  end
126
-
127
- private
128
-
129
- def keys_to_strings hash
130
- result = {}
131
- hash.each do |key, val|
132
- result[key.to_s] = val
133
- end
134
- result
135
- end
136
134
  end
137
135
  end
138
136
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudEvents
4
+ module Event
5
+ ##
6
+ # This object represents opaque event data that arrived in structured
7
+ # content mode but was not in a recognized format. It may represent a
8
+ # single event or a batch of events.
9
+ #
10
+ # The event data is retained in a form that can be reserialized (in a
11
+ # structured cotent mode in the same format) but cannot otherwise be
12
+ # inspected.
13
+ #
14
+ # This object is immutable, and Ractor-shareable on Ruby 3.
15
+ #
16
+ class Opaque
17
+ ##
18
+ # Create an opaque object wrapping the given content and a content type.
19
+ #
20
+ # @param content [String] The opaque serialized event data.
21
+ # @param content_type [CloudEvents::ContentType,nil] The content type,
22
+ # or `nil` if there is no content type.
23
+ # @param batch [boolean] Whether this represents a batch. If set to `nil`
24
+ # or not provided, the value will be inferred from the content type
25
+ # if possible, or otherwise set to `nil` indicating not known.
26
+ #
27
+ def initialize content, content_type, batch: nil
28
+ @content = content.freeze
29
+ @content_type = content_type
30
+ if batch.nil? && content_type&.media_type == "application"
31
+ case content_type.subtype_base
32
+ when "cloudevents"
33
+ batch = false
34
+ when "cloudevents-batch"
35
+ batch = true
36
+ end
37
+ end
38
+ @batch = batch
39
+ freeze
40
+ end
41
+
42
+ ##
43
+ # The opaque serialized event data
44
+ #
45
+ # @return [String]
46
+ #
47
+ attr_reader :content
48
+
49
+ ##
50
+ # The content type, or `nil` if there is no content type.
51
+ #
52
+ # @return [CloudEvents::ContentType,nil]
53
+ #
54
+ attr_reader :content_type
55
+
56
+ ##
57
+ # Whether this represents a batch, or `nil` if not known.
58
+ #
59
+ # @return [boolean,nil]
60
+ #
61
+ def batch?
62
+ @batch
63
+ end
64
+
65
+ ## @private
66
+ def == other
67
+ Opaque === other &&
68
+ @content == other.content &&
69
+ @content_type == other.content_type &&
70
+ @batch == other.batch?
71
+ end
72
+ alias eql? ==
73
+
74
+ ## @private
75
+ def hash
76
+ @content.hash ^ @content_type.hash ^ @batch.hash
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudEvents
4
+ module Event
5
+ ##
6
+ # A variety of helper methods.
7
+ # @private
8
+ #
9
+ module Utils
10
+ class << self
11
+ def deep_freeze obj
12
+ case obj
13
+ when ::Hash
14
+ obj.each do |key, val|
15
+ deep_freeze key
16
+ deep_freeze val
17
+ end
18
+ when ::Array
19
+ obj.each do |val|
20
+ deep_freeze val
21
+ end
22
+ else
23
+ obj.instance_variables.each do |iv|
24
+ deep_freeze obj.instance_variable_get iv
25
+ end
26
+ end
27
+ obj.freeze
28
+ end
29
+
30
+ def deep_dup obj
31
+ case obj
32
+ when ::Hash
33
+ obj.each_with_object({}) { |(key, val), hash| hash[deep_dup key] = deep_dup val }
34
+ when ::Array
35
+ obj.map { |val| deep_dup val }
36
+ else
37
+ obj.dup
38
+ end
39
+ end
40
+
41
+ def keys_to_strings hash
42
+ result = {}
43
+ hash.each do |key, val|
44
+ result[key.to_s] = val
45
+ end
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -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/v0.3/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_encoding** (or **:datacontentencoding**)
47
51
  # [`String`] - _optional_ - The content-encoding for the data (i.e.
48
52
  # the `datacontentencoding` field.)
@@ -59,6 +63,11 @@ module CloudEvents
59
63
  # They are not available as separate methods, but can be accessed via
60
64
  # the {Event::V1#[]} operator.
61
65
  #
66
+ # Note that attribute objects passed in may get deep-frozen if they are
67
+ # used in the final event object. This is particularly important for the
68
+ # `:data` field, for example if you pass a structured hash. If this is an
69
+ # issue, make a deep copy of objects before passing to this constructor.
70
+ #
62
71
  # @param attributes [Hash] The data and attributes, as a hash.
63
72
  # @param args [keywords] The data and attributes, as keyword arguments.
64
73
  #
@@ -68,13 +77,14 @@ module CloudEvents
68
77
  @id = interpreter.string ["id"], required: true
69
78
  @source = interpreter.uri ["source"], required: true
70
79
  @type = interpreter.string ["type"], required: true
71
- @data = interpreter.object ["data"], allow_nil: true
80
+ @data = interpreter.data_object ["data"]
72
81
  @data_content_encoding = interpreter.string ["datacontentencoding", "data_content_encoding"]
73
82
  @data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
74
83
  @schema_url = interpreter.uri ["schemaurl", "schema_url"]
75
84
  @subject = interpreter.string ["subject"]
76
85
  @time = interpreter.rfc3339_date_time ["time"]
77
86
  @attributes = interpreter.finish_attributes
87
+ freeze
78
88
  end
79
89
 
80
90
  ##
@@ -105,6 +115,8 @@ module CloudEvents
105
115
  # event["time"] # => String rfc3339 representation
106
116
  # event.time # => DateTime object
107
117
  #
118
+ # Results are also always frozen and cannot be modified in place.
119
+ #
108
120
  # @param key [String,Symbol] The attribute name.
109
121
  # @return [String,nil]
110
122
  #
@@ -113,12 +125,13 @@ module CloudEvents
113
125
  end
114
126
 
115
127
  ##
116
- # Return a hash representation of this event.
128
+ # Return a hash representation of this event. The returned hash is an
129
+ # unfrozen deep copy. Modifications do not affect the original event.
117
130
  #
118
131
  # @return [Hash]
119
132
  #
120
133
  def to_h
121
- @attributes.dup
134
+ Utils.deep_dup @attributes
122
135
  end
123
136
 
124
137
  ##
@@ -208,13 +221,13 @@ module CloudEvents
208
221
 
209
222
  ## @private
210
223
  def == other
211
- other.is_a?(V1) && @attributes == other.instance_variable_get(:@attributes)
224
+ other.is_a?(V0) && @attributes == other.instance_variable_get(:@attributes)
212
225
  end
213
226
  alias eql? ==
214
227
 
215
228
  ## @private
216
229
  def hash
217
- @hash ||= @attributes.hash
230
+ @attributes.hash
218
231
  end
219
232
  end
220
233
  end