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