cloud_events 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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