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.
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