cloud_events 0.1.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.
- checksums.yaml +7 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +203 -0
- data/README.md +250 -0
- data/lib/cloud_events.rb +31 -0
- data/lib/cloud_events/content_type.rb +208 -0
- data/lib/cloud_events/errors.rb +28 -0
- data/lib/cloud_events/event.rb +69 -0
- data/lib/cloud_events/event/field_interpreter.rb +136 -0
- data/lib/cloud_events/event/v0.rb +221 -0
- data/lib/cloud_events/event/v1.rb +208 -0
- data/lib/cloud_events/http_binding.rb +292 -0
- data/lib/cloud_events/json_format.rb +158 -0
- data/lib/cloud_events/version.rb +9 -0
- metadata +63 -0
data/lib/cloud_events.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cloud_events/content_type"
|
4
|
+
require "cloud_events/errors"
|
5
|
+
require "cloud_events/event"
|
6
|
+
require "cloud_events/http_binding"
|
7
|
+
require "cloud_events/json_format"
|
8
|
+
|
9
|
+
##
|
10
|
+
# CloudEvents implementation.
|
11
|
+
#
|
12
|
+
# This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
|
13
|
+
# specification. It supports both
|
14
|
+
# [CloudEvents 0.3](https://github.com/cloudevents/spec/blob/v0.3/spec.md) and
|
15
|
+
# [CloudEvents 1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
|
16
|
+
#
|
17
|
+
module CloudEvents
|
18
|
+
# @private
|
19
|
+
SUPPORTED_SPEC_VERSIONS = ["0.3", "1.0"].freeze
|
20
|
+
|
21
|
+
class << self
|
22
|
+
##
|
23
|
+
# The spec versions supported by this implementation.
|
24
|
+
#
|
25
|
+
# @return [Array<String>]
|
26
|
+
#
|
27
|
+
def supported_spec_versions
|
28
|
+
SUPPORTED_SPEC_VERSIONS
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CloudEvents
|
4
|
+
##
|
5
|
+
# A parsed content-type header.
|
6
|
+
#
|
7
|
+
# This object represents the information contained in a Content-Type,
|
8
|
+
# obtained by parsing the header according to RFC 2045.
|
9
|
+
#
|
10
|
+
# Case-insensitive fields, such as media_type and subtype, are normalized
|
11
|
+
# to lower case.
|
12
|
+
#
|
13
|
+
# If parsing fails, this class will try to get as much information as it
|
14
|
+
# can, and fill the rest with defaults as recommended in RFC 2045 sec 5.2.
|
15
|
+
# In case of a parsing error, the {#error_message} field will be set.
|
16
|
+
#
|
17
|
+
class ContentType
|
18
|
+
##
|
19
|
+
# Parse the given header value.
|
20
|
+
#
|
21
|
+
# @param string [String] Content-Type header value in RFC 2045 format
|
22
|
+
#
|
23
|
+
def initialize string
|
24
|
+
@string = string
|
25
|
+
@media_type = "text"
|
26
|
+
@subtype_base = @subtype = "plain"
|
27
|
+
@subtype_format = nil
|
28
|
+
@params = []
|
29
|
+
@charset = "us-ascii"
|
30
|
+
@error_message = nil
|
31
|
+
parse consume_comments string.strip
|
32
|
+
@canonical_string = "#{@media_type}/#{@subtype}" +
|
33
|
+
@params.map { |k, v| "; #{k}=#{maybe_quote v}" }.join
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# The original header content string.
|
38
|
+
# @return [String]
|
39
|
+
#
|
40
|
+
attr_reader :string
|
41
|
+
alias to_s string
|
42
|
+
|
43
|
+
##
|
44
|
+
# A "canonical" header content string with spacing and capitalization
|
45
|
+
# normalized.
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
attr_reader :canonical_string
|
49
|
+
|
50
|
+
##
|
51
|
+
# The media type.
|
52
|
+
# @return [String]
|
53
|
+
#
|
54
|
+
attr_reader :media_type
|
55
|
+
|
56
|
+
##
|
57
|
+
# The entire content subtype (which could include an extension delimited
|
58
|
+
# by a plus sign).
|
59
|
+
# @return [String]
|
60
|
+
#
|
61
|
+
attr_reader :subtype
|
62
|
+
|
63
|
+
##
|
64
|
+
# The portion of the content subtype before any plus sign.
|
65
|
+
# @return [String]
|
66
|
+
#
|
67
|
+
attr_reader :subtype_base
|
68
|
+
|
69
|
+
##
|
70
|
+
# The portion of the content subtype after any plus sign, or nil if there
|
71
|
+
# is no plus sign in the subtype.
|
72
|
+
# @return [String,nil]
|
73
|
+
#
|
74
|
+
attr_reader :subtype_format
|
75
|
+
|
76
|
+
##
|
77
|
+
# An array of parameters, each element as a two-element array of the
|
78
|
+
# parameter name and value.
|
79
|
+
# @return [Array<Array(String,String)>]
|
80
|
+
#
|
81
|
+
attr_reader :params
|
82
|
+
|
83
|
+
##
|
84
|
+
# The charset, defaulting to "us-ascii" if none is explicitly set.
|
85
|
+
# @return [String]
|
86
|
+
#
|
87
|
+
attr_reader :charset
|
88
|
+
|
89
|
+
##
|
90
|
+
# The error message when parsing, or `nil` if there was no error message.
|
91
|
+
# @return [String,nil]
|
92
|
+
#
|
93
|
+
attr_reader :error_message
|
94
|
+
|
95
|
+
##
|
96
|
+
# An array of values for the given parameter name
|
97
|
+
# @param key [String]
|
98
|
+
# @return [Array<String>]
|
99
|
+
#
|
100
|
+
def param_values key
|
101
|
+
key = key.downcase
|
102
|
+
@params.inject([]) { |a, (k, v)| key == k ? a << v : a }
|
103
|
+
end
|
104
|
+
|
105
|
+
## @private
|
106
|
+
def == other
|
107
|
+
other.is_a?(ContentType) && canonical_string == other.canonical_string
|
108
|
+
end
|
109
|
+
alias eql? ==
|
110
|
+
|
111
|
+
## @private
|
112
|
+
def hash
|
113
|
+
canonical_string.hash
|
114
|
+
end
|
115
|
+
|
116
|
+
## @private
|
117
|
+
class ParseError < ::StandardError
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def parse str
|
123
|
+
@media_type, str = consume_token str, downcase: true, error_message: "Failed to parse media type"
|
124
|
+
str = consume_special str, "/"
|
125
|
+
@subtype, str = consume_token str, downcase: true, error_message: "Failed to parse subtype"
|
126
|
+
@subtype_base, @subtype_format = @subtype.split "+", 2
|
127
|
+
until str.empty?
|
128
|
+
str = consume_special str, ";"
|
129
|
+
name, str = consume_token str, downcase: true, error_message: "Faled to parse attribute name"
|
130
|
+
str = consume_special str, "=", error_message: "Failed to find value for attribute #{name}"
|
131
|
+
val, str = consume_token_or_quoted str, error_message: "Failed to parse value for attribute #{name}"
|
132
|
+
@params << [name, val]
|
133
|
+
@charset = val if name == "charset"
|
134
|
+
end
|
135
|
+
rescue ParseError => e
|
136
|
+
@error_message = e.message
|
137
|
+
end
|
138
|
+
|
139
|
+
def consume_token str, downcase: false, error_message: nil
|
140
|
+
match = /^([\w!#\$%&'\*\+\.\^`\{\|\}-]+)(.*)$/.match str
|
141
|
+
raise ParseError, error_message || "Expected token" unless match
|
142
|
+
token = match[1]
|
143
|
+
token.downcase! if downcase
|
144
|
+
str = consume_comments match[2].strip
|
145
|
+
[token, str]
|
146
|
+
end
|
147
|
+
|
148
|
+
def consume_special str, expected, error_message: nil
|
149
|
+
raise ParseError, error_message || "Expected #{expected.inspect}" unless str.start_with? expected
|
150
|
+
consume_comments str[1..-1].strip
|
151
|
+
end
|
152
|
+
|
153
|
+
def consume_token_or_quoted str, error_message: nil
|
154
|
+
return consume_token str unless str.start_with? '"'
|
155
|
+
arr = []
|
156
|
+
index = 1
|
157
|
+
loop do
|
158
|
+
char = str[index]
|
159
|
+
case char
|
160
|
+
when nil
|
161
|
+
raise ParseError, error_message || "Quoted-string never finished"
|
162
|
+
when "\""
|
163
|
+
break
|
164
|
+
when "\\"
|
165
|
+
char = str[index + 1]
|
166
|
+
raise ParseError, error_message || "Quoted-string never finished" unless char
|
167
|
+
arr << char
|
168
|
+
index += 2
|
169
|
+
else
|
170
|
+
arr << char
|
171
|
+
index += 1
|
172
|
+
end
|
173
|
+
end
|
174
|
+
index += 1
|
175
|
+
str = consume_comments str[index..-1].strip
|
176
|
+
[arr.join, str]
|
177
|
+
end
|
178
|
+
|
179
|
+
def consume_comments str
|
180
|
+
return str unless str.start_with? "("
|
181
|
+
index = 1
|
182
|
+
loop do
|
183
|
+
char = str[index]
|
184
|
+
case char
|
185
|
+
when nil
|
186
|
+
raise ParseError, "Comment never finished"
|
187
|
+
when ")"
|
188
|
+
break
|
189
|
+
when "\\"
|
190
|
+
index += 2
|
191
|
+
when "("
|
192
|
+
str = consume_comments str[index..-1]
|
193
|
+
index = 0
|
194
|
+
else
|
195
|
+
index += 1
|
196
|
+
end
|
197
|
+
end
|
198
|
+
index += 1
|
199
|
+
consume_comments str[index..-1].strip
|
200
|
+
end
|
201
|
+
|
202
|
+
def maybe_quote str
|
203
|
+
return str if /^[\w!#\$%&'\*\+\.\^`\{\|\}-]+$/ =~ str
|
204
|
+
str = str.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
|
205
|
+
"\"#{str}\""
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CloudEvents
|
4
|
+
##
|
5
|
+
# Base class for all CloudEvents errors.
|
6
|
+
#
|
7
|
+
class CloudEventsError < ::StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# Errors indicating unsupported or incorrectly formatted HTTP content or
|
12
|
+
# headers.
|
13
|
+
#
|
14
|
+
class HttpContentError < CloudEventsError
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Errors indicating an unsupported or incorrect spec version.
|
19
|
+
#
|
20
|
+
class SpecVersionError < CloudEventsError
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Errors related to CloudEvent attributes.
|
25
|
+
#
|
26
|
+
class AttributeError < CloudEventsError
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
require "cloud_events/event/field_interpreter"
|
7
|
+
require "cloud_events/event/v0"
|
8
|
+
require "cloud_events/event/v1"
|
9
|
+
|
10
|
+
module CloudEvents
|
11
|
+
##
|
12
|
+
# An Event object represents a complete CloudEvent, including both data and
|
13
|
+
# context attributes. The following are true of all event objects:
|
14
|
+
#
|
15
|
+
# * Event classes are defined within this module. For example, events
|
16
|
+
# conforming to the CloudEvents 1.0 specification are of type
|
17
|
+
# {CloudEvents::Event::V1}.
|
18
|
+
# * All event classes include this module, so you can use
|
19
|
+
# `is_a? CloudEvents::Event` to test whether an object is an event.
|
20
|
+
# * All event objects are immutable. Data and atribute values can be
|
21
|
+
# retrieved but not modified. To "modify" an event, make a copy with
|
22
|
+
# the desired changes. Generally, event classes will provide a helper
|
23
|
+
# method for this purpose.
|
24
|
+
# * All event objects have a `spec_version` method that returns the
|
25
|
+
# version of the CloudEvents spec implemented by that event. (Other
|
26
|
+
# methods may be different, depending on the spec version.)
|
27
|
+
#
|
28
|
+
# To create an event, you may either:
|
29
|
+
#
|
30
|
+
# * Construct an instance of the event class directly, for example by
|
31
|
+
# calling {CloudEvents::Event::V1.new} and passing a set of attributes.
|
32
|
+
# * Call {CloudEvents::Event.create} and pass a spec version and a set of
|
33
|
+
# attributes. This will choose the appropriate event class based on the
|
34
|
+
# version.
|
35
|
+
# * Decode an event from another representation. For example, use
|
36
|
+
# {CloudEvents::JsonFormat} to decode an event from JSON, or use
|
37
|
+
# {CloudEvents::HttpBinding} to decode an event from an HTTP request.
|
38
|
+
#
|
39
|
+
# See https://github.com/cloudevents/spec for more information about
|
40
|
+
# CloudEvents. The documentation for the individual event classes
|
41
|
+
# {CloudEvents::Event::V0} and {CloudEvents::Event::V1} also include links to
|
42
|
+
# their respective specifications.
|
43
|
+
#
|
44
|
+
module Event
|
45
|
+
class << self
|
46
|
+
##
|
47
|
+
# Create a new cloud event object with the given version. Generally,
|
48
|
+
# you must also pass additional keyword arguments providing the event's
|
49
|
+
# data and attributes. For example, if you pass `1.0` as the
|
50
|
+
# `spec_version`, the remaining keyword arguments will be passed
|
51
|
+
# through to the {CloudEvents::Event::V1} constructor.
|
52
|
+
#
|
53
|
+
# @param spec_version [String] The required `specversion` field.
|
54
|
+
# @param kwargs [keywords] Additional parameters for the event.
|
55
|
+
#
|
56
|
+
def create spec_version:, **kwargs
|
57
|
+
case spec_version
|
58
|
+
when "0.3"
|
59
|
+
V0.new spec_version: spec_version, **kwargs
|
60
|
+
when /^1(\.|$)/
|
61
|
+
V1.new spec_version: spec_version, **kwargs
|
62
|
+
else
|
63
|
+
raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
alias new create
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CloudEvents
|
4
|
+
module Event
|
5
|
+
##
|
6
|
+
# A helper that extracts and interprets event fields from an input hash.
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
#
|
10
|
+
class FieldInterpreter
|
11
|
+
def initialize args
|
12
|
+
@args = keys_to_strings args
|
13
|
+
@attributes = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def finish_attributes
|
17
|
+
@attributes.merge! @args
|
18
|
+
@args = {}
|
19
|
+
@attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
def string keys, required: false
|
23
|
+
object keys, required: required do |value|
|
24
|
+
case value
|
25
|
+
when ::String
|
26
|
+
raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
|
27
|
+
[value, value]
|
28
|
+
else
|
29
|
+
raise AttributeError, "Illegal type for #{keys.first}:" \
|
30
|
+
" String expected but #{value.class} found"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def uri keys, required: false
|
36
|
+
object keys, required: required do |value|
|
37
|
+
case value
|
38
|
+
when ::String
|
39
|
+
raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
|
40
|
+
begin
|
41
|
+
[::URI.parse(value), value]
|
42
|
+
rescue ::URI::InvalidURIError => e
|
43
|
+
raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
|
44
|
+
end
|
45
|
+
when ::URI::Generic
|
46
|
+
[value, value.to_s]
|
47
|
+
else
|
48
|
+
raise AttributeError, "Illegal type for #{keys.first}:" \
|
49
|
+
" String or URI expected but #{value.class} found"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def rfc3339_date_time keys, required: false
|
55
|
+
object keys, required: required do |value|
|
56
|
+
case value
|
57
|
+
when ::String
|
58
|
+
begin
|
59
|
+
[::DateTime.rfc3339(value), value]
|
60
|
+
rescue ::Date::Error => e
|
61
|
+
raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
|
62
|
+
end
|
63
|
+
when ::DateTime
|
64
|
+
[value, value.rfc3339]
|
65
|
+
when ::Time
|
66
|
+
value = value.to_datetime
|
67
|
+
[value, value.rfc3339]
|
68
|
+
else
|
69
|
+
raise AttributeError, "Illegal type for #{keys.first}:" \
|
70
|
+
" String, Time, or DateTime expected but #{value.class} found"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def content_type keys, required: false
|
76
|
+
object keys, required: required do |value|
|
77
|
+
case value
|
78
|
+
when ::String
|
79
|
+
raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
|
80
|
+
[ContentType.new(value), value]
|
81
|
+
when ContentType
|
82
|
+
[value, value.to_s]
|
83
|
+
else
|
84
|
+
raise AttributeError, "Illegal type for #{keys.first}:" \
|
85
|
+
" String, or ContentType expected but #{value.class} found"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def spec_version keys, accept:
|
91
|
+
object keys, required: true do |value|
|
92
|
+
case value
|
93
|
+
when ::String
|
94
|
+
raise SpecVersionError, "Unrecognized specversion: #{value}" unless accept =~ value
|
95
|
+
[value, value]
|
96
|
+
else
|
97
|
+
raise AttributeError, "Illegal type for #{keys.first}:" \
|
98
|
+
" String expected but #{value.class} found"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
UNDEFINED = ::Object.new
|
104
|
+
|
105
|
+
def object keys, required: false, allow_nil: false
|
106
|
+
value = UNDEFINED
|
107
|
+
keys.each do |key|
|
108
|
+
key_present = @args.key? key
|
109
|
+
val = @args.delete key
|
110
|
+
value = val if allow_nil && key_present || !allow_nil && !val.nil?
|
111
|
+
end
|
112
|
+
if value == UNDEFINED
|
113
|
+
raise AttributeError, "The #{keys.first} field is required" if required
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
if block_given?
|
117
|
+
converted, raw = yield value
|
118
|
+
else
|
119
|
+
converted = raw = value
|
120
|
+
end
|
121
|
+
@attributes[keys.first] = raw
|
122
|
+
converted
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def keys_to_strings hash
|
128
|
+
result = {}
|
129
|
+
hash.each do |key, val|
|
130
|
+
result[key.to_s] = val
|
131
|
+
end
|
132
|
+
result
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|