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