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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/LICENSE.md +189 -190
- data/README.md +26 -33
- data/lib/cloud_events.rb +1 -0
- data/lib/cloud_events/content_type.rb +22 -5
- data/lib/cloud_events/errors.rb +37 -5
- data/lib/cloud_events/event.rb +6 -1
- data/lib/cloud_events/event/field_interpreter.rb +25 -27
- data/lib/cloud_events/event/opaque.rb +80 -0
- data/lib/cloud_events/event/utils.rb +51 -0
- data/lib/cloud_events/event/v0.rb +22 -9
- data/lib/cloud_events/event/v1.rb +21 -8
- data/lib/cloud_events/format.rb +202 -0
- data/lib/cloud_events/http_binding.rb +319 -145
- data/lib/cloud_events/json_format.rb +162 -52
- data/lib/cloud_events/version.rb +1 -1
- metadata +9 -6
data/lib/cloud_events.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/cloud_events/errors.rb
CHANGED
@@ -8,21 +8,53 @@ module CloudEvents
|
|
8
8
|
end
|
9
9
|
|
10
10
|
##
|
11
|
-
#
|
12
|
-
#
|
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
|
17
|
+
class NotCloudEventError < CloudEventsError
|
15
18
|
end
|
16
19
|
|
17
20
|
##
|
18
|
-
#
|
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
|
-
#
|
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
|
data/lib/cloud_events/event.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "date"
|
4
4
|
require "uri"
|
5
5
|
|
6
|
-
require "cloud_events/event/
|
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
|
-
|
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
|
-
|
119
|
-
|
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
|
20
|
-
# retrieved but not modified. To obtain an event
|
21
|
-
# the {#with} method to create a copy with the
|
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.
|
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
|
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?(
|
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
|
-
@
|
230
|
+
@attributes.hash
|
218
231
|
end
|
219
232
|
end
|
220
233
|
end
|