cloud_events 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 036b0516dd1cbb3d6f2c4855809facc6edb4ac994b00355056182a704a072306
4
- data.tar.gz: 4eccf559900a089b4a86ce269a30bd604dd4bcad3973ad5ffbe69735acb2a7e7
3
+ metadata.gz: e92a0db6d57da2784f958e81f3416ade3c4c6c1e5e7830571be444c84a1001a0
4
+ data.tar.gz: e48bad4cf6cf8797c6f5a16817f6951a1c3b2138778fa2e6779b58fd49f28f21
5
5
  SHA512:
6
- metadata.gz: b0d255359ad545ec1f0898f437b2b220e72cc8766de1f59e0534c71c4863fd093b410e9350048506d8b99a22235d23aef9b9c4ca0fee757242229cdd283faaf2
7
- data.tar.gz: 84a043396ad6cd11e6cd05fd7ce6bdd2993fcede29d2fc780c88419dc2bbd99d230c573b8c237f53940e033fe42449c0976e85c9842fef6dd829e798ecd4daa2
6
+ metadata.gz: 4429473d95e6150514f796a907f74a96741ab9378e2c833ed6945fbe4f5d85fe654d0bb6502960900c349721b167f90fc62994a2525a3fd372bc7ea9468c66f6
7
+ data.tar.gz: 91f22bf6c0b20357326166d896e0036dd90ae32f66b7c233a63cb3e060abac51827b2a9d560f6324830db2b5240e436d0f942c99a41bf8a1c711a48fe6af8d80
data/.yardopts CHANGED
@@ -8,4 +8,4 @@
8
8
  -
9
9
  README.md
10
10
  CHANGELOG.md
11
- LICENSE.md
11
+ LICENSE
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ### v0.8.0 / 2025-11-04
4
+
5
+ * BREAKING CHANGE: Raise AttributeError if an illegal attribute name is used
6
+ * ADDED: Require Ruby 2.7 or later
7
+ * FIXED: Improved hashing algorithm for opaque event objects
8
+ * FIXED: Removed dependency on base64 gem
9
+ * FIXED: Raise AttributeError if an illegal attribute name is used
10
+ * DOCS: Add link to the security mailing list
11
+
12
+ ### v0.7.1 / 2023-10-04
13
+
14
+ * DOCS: Governance docs per CE PR 1226
15
+
3
16
  ### v0.7.0 / 2022-01-14
4
17
 
5
18
  * HttpBinding#probable_event? returns false if the request method is GET or HEAD.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,167 @@
1
+ # Contributing to CloudEvents' Ruby SDK
2
+
3
+ :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4
+
5
+ We welcome contributions from the community! Please take some time to become
6
+ acquainted with the process before submitting a pull request. There are just
7
+ a few things to keep in mind.
8
+
9
+ # Pull Requests
10
+
11
+ Typically, a pull request should relate to an existing issue. If you have
12
+ found a bug, want to add an improvement, or suggest an API change, please
13
+ create an issue before proceeding with a pull request. For very minor changes
14
+ such as typos in the documentation this isn't really necessary.
15
+
16
+ ## Pull Request Guidelines
17
+
18
+ Here you will find step by step guidance for creating, submitting and updating
19
+ a pull request in this repository. We hope it will help you have an easy time
20
+ managing your work and a positive, satisfying experience when contributing
21
+ your code. Thanks for getting involved! :rocket:
22
+
23
+ * [Getting Started](#getting-started)
24
+ * [Branches](#branches)
25
+ * [Commit Messages](#commit-messages)
26
+ * [Staying current with main](#staying-current-with-main)
27
+ * [Submitting and Updating a Pull Request](#submitting-and-updating-a-pull-request)
28
+ * [Congratulations!](#congratulations)
29
+
30
+ ## Getting Started
31
+
32
+ When creating a pull request, first fork this repository and clone it to your
33
+ local development environment. Then add this repository as the upstream.
34
+
35
+ ```console
36
+ git clone https://github.com/mygithuborg/sdk-ruby.git
37
+ cd sdk-ruby
38
+ git remote add upstream https://github.com/cloudevents/sdk-ruby.git
39
+ ```
40
+
41
+ ## Branches
42
+
43
+ The first thing you'll need to do is create a branch for your work.
44
+ If you are submitting a pull request that fixes or relates to an existing
45
+ GitHub issue, you can use the issue number in your branch name to keep things
46
+ organized.
47
+
48
+ ```console
49
+ git fetch upstream
50
+ git reset --hard upstream/main
51
+ git checkout FETCH_HEAD
52
+ git checkout -b 48-fix-http-agent-error
53
+ ```
54
+
55
+ ## Commit Messages
56
+
57
+ Please follow the
58
+ [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary).
59
+ The first line of your commit should be prefixed with a type, be a single
60
+ sentence with no period, and succinctly indicate what this commit changes.
61
+
62
+ All commit message lines should be kept to fewer than 80 characters if possible.
63
+
64
+ An example of a good commit message.
65
+
66
+ ```log
67
+ docs: remove 0.1, 0.2 spec support from README
68
+ ```
69
+
70
+ ### Signing your commits
71
+
72
+ Each commit must be signed. Use the `--signoff` flag for your commits.
73
+
74
+ ```console
75
+ git commit --signoff
76
+ ```
77
+
78
+ This will add a line to every git commit message:
79
+
80
+ Signed-off-by: Joe Smith <joe.smith@email.com>
81
+
82
+ Use your real name (sorry, no pseudonyms or anonymous contributions.)
83
+
84
+ The sign-off is a signature line at the end of your commit message. Your
85
+ signature certifies that you wrote the patch or otherwise have the right to pass
86
+ it on as open-source code. See [developercertificate.org](http://developercertificate.org/)
87
+ for the full text of the certification.
88
+
89
+ Be sure to have your `user.name` and `user.email` set in your git config.
90
+ If your git config information is set properly then viewing the `git log`
91
+ information for your commit will look something like this:
92
+
93
+ ```
94
+ Author: Joe Smith <joe.smith@email.com>
95
+ Date: Thu Feb 2 11:41:15 2018 -0800
96
+
97
+ Update README
98
+
99
+ Signed-off-by: Joe Smith <joe.smith@email.com>
100
+ ```
101
+
102
+ Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will
103
+ be rejected by the automated DCO check.
104
+
105
+ ## Staying Current with `main`
106
+
107
+ As you are working on your branch, changes may happen on `main`. Before
108
+ submitting your pull request, be sure that your branch has been updated
109
+ with the latest commits.
110
+
111
+ ```console
112
+ git fetch upstream
113
+ git rebase upstream/main
114
+ ```
115
+
116
+ This may cause conflicts if the files you are changing on your branch are
117
+ also changed on main. Error messages from `git` will indicate if conflicts
118
+ exist and what files need attention. Resolve the conflicts in each file, then
119
+ continue with the rebase with `git rebase --continue`.
120
+
121
+
122
+ If you've already pushed some changes to your `origin` fork, you'll
123
+ need to force push these changes.
124
+
125
+ ```console
126
+ git push -f origin 48-fix-http-agent-error
127
+ ```
128
+
129
+ ## Submitting and Updating Your Pull Request
130
+
131
+ Before submitting a pull request, you should make sure that all of the tests
132
+ successfully pass.
133
+
134
+ Once you have sent your pull request, `main` may continue to evolve
135
+ before your pull request has landed. If there are any commits on `main`
136
+ that conflict with your changes, you may need to update your branch with
137
+ these changes before the pull request can land. Resolve conflicts the same
138
+ way as before.
139
+
140
+ ```console
141
+ git fetch upstream
142
+ git rebase upstream/main
143
+ # fix any potential conflicts
144
+ git push -f origin 48-fix-http-agent-error
145
+ ```
146
+
147
+ This will cause the pull request to be updated with your changes, and
148
+ CI will rerun.
149
+
150
+ A maintainer may ask you to make changes to your pull request. Sometimes these
151
+ changes are minor and shouldn't appear in the commit log. For example, you may
152
+ have a typo in one of your code comments that should be fixed before merge.
153
+ You can prevent this from adding noise to the commit log with an interactive
154
+ rebase. See the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)
155
+ for details.
156
+
157
+ ```console
158
+ git commit -m "fixup: fix typo"
159
+ git rebase -i upstream/main # follow git instructions
160
+ ```
161
+
162
+ Once you have rebased your commits, you can force push to your fork as before.
163
+
164
+ ## Congratulations!
165
+
166
+ Congratulations! You've done it! We really appreciate the time and energy
167
+ you've given to the project. Thank you.
data/MAINTAINERS.md ADDED
@@ -0,0 +1,5 @@
1
+ # Maintainers
2
+
3
+ Current active maintainers of this SDK:
4
+
5
+ - [Daniel Azuma](https://github.com/dazuma)
data/README.md CHANGED
@@ -184,31 +184,6 @@ few things to keep in mind.
184
184
  * **Make sure CI passes.** Invoke `toys ci` to run the tests locally before
185
185
  opening a pull request. This will include code style checks.
186
186
 
187
- ### Releasing
188
-
189
- Releases can be performed only by users with write access to the repository.
190
-
191
- To perform a release:
192
-
193
- 1. Go to the GitHub Actions tab, and launch the "Request Release" workflow.
194
- You can leave the input field blank.
195
-
196
- 2. The workflow will analyze the commit messages since the last release, and
197
- open a pull request with a new version and a changelog entry. You can
198
- optionally edit this pull request to modify the changelog or change the
199
- version released.
200
-
201
- 3. Merge the pull request (keeping the `release: pending` label set.) Once the
202
- CI tests have run successfully, a job will run automatically to perform the
203
- release, including tagging the commit in git, building and releasing a gem,
204
- and building and pushing documentation.
205
-
206
- These tasks can also be performed manually by running the appropriate scripts
207
- locally. See `toys release request --help` and `toys release perform --help`
208
- for more information.
209
-
210
- If a release fails, you may need to delete the release tag before retrying.
211
-
212
187
  ### For more information
213
188
 
214
189
  * Library documentation: https://cloudevents.github.io/sdk-ruby
@@ -243,18 +218,13 @@ for how PR reviews and approval, and our
243
218
  [Code of Conduct](https://github.com/cloudevents/spec/blob/master/community/GOVERNANCE.md#additional-information)
244
219
  information.
245
220
 
246
- ## Licensing
247
-
248
- Copyright 2020 Google LLC and the CloudEvents Ruby SDK Contributors
249
-
250
- Licensed under the Apache License, Version 2.0 (the "License");
251
- you may not use this software except in compliance with the License.
252
- You may obtain a copy of the License at
221
+ If there is a security concern with one of the CloudEvents specifications, or
222
+ with one of the project's SDKs, please send an email to
223
+ [cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io).
253
224
 
254
- https://www.apache.org/licenses/LICENSE-2.0
225
+ ## Additional SDK Resources
255
226
 
256
- Unless required by applicable law or agreed to in writing, software
257
- distributed under the License is distributed on an "AS IS" BASIS,
258
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
259
- See the License for the specific language governing permissions and
260
- limitations under the License.
227
+ - [List of current active maintainers](MAINTAINERS.md)
228
+ - [How to contribute to the project](CONTRIBUTING.md)
229
+ - [SDK's License](LICENSE)
230
+ - [SDK's Release process](RELEASING.md)
data/RELEASING.md ADDED
@@ -0,0 +1,25 @@
1
+ # Releasing
2
+
3
+ Releases can be performed only by users with write access to the repository.
4
+
5
+ To perform a release:
6
+
7
+ 1. Go to the GitHub Actions tab, and launch the "Request Release" workflow.
8
+ You can leave the input field blank.
9
+
10
+ 2. The workflow will analyze the commit messages since the last release, and
11
+ open a pull request with a new version and a changelog entry. You can
12
+ optionally edit this pull request to modify the changelog or change the
13
+ version released.
14
+
15
+ 3. Merge the pull request (keeping the `release: pending` label set.) Once the
16
+ CI tests have run successfully, a job will run automatically to perform the
17
+ release, including tagging the commit in git, building and releasing a gem,
18
+ and building and pushing documentation.
19
+
20
+ These tasks can also be performed manually by running the appropriate scripts
21
+ locally. See `toys release request --help` and `toys release perform --help`
22
+ for more information.
23
+
24
+ If a release fails, you may need to delete the release tag before retrying.
25
+
@@ -24,7 +24,7 @@ module CloudEvents
24
24
  # @param default_charset [String] Optional. The charset to use if none is
25
25
  # specified. Defaults to `us-ascii`.
26
26
  #
27
- def initialize string, default_charset: nil
27
+ def initialize(string, default_charset: nil)
28
28
  @string = string.to_s
29
29
  @media_type = "text"
30
30
  @subtype_base = @subtype = "plain"
@@ -32,9 +32,9 @@ module CloudEvents
32
32
  @params = []
33
33
  @charset = default_charset || "us-ascii"
34
34
  @error_message = nil
35
- parse consume_comments @string.strip
35
+ parse(consume_comments(@string.strip))
36
36
  @canonical_string = "#{@media_type}/#{@subtype}" +
37
- @params.map { |k, v| "; #{k}=#{maybe_quote v}" }.join
37
+ @params.map { |k, v| "; #{k}=#{maybe_quote(v)}" }.join
38
38
  full_freeze
39
39
  end
40
40
 
@@ -102,13 +102,13 @@ module CloudEvents
102
102
  # @param key [String]
103
103
  # @return [Array<String>]
104
104
  #
105
- def param_values key
105
+ def param_values(key)
106
106
  key = key.downcase
107
107
  @params.inject([]) { |a, (k, v)| key == k ? a << v : a }
108
108
  end
109
109
 
110
110
  ## @private
111
- def == other
111
+ def ==(other)
112
112
  other.is_a?(ContentType) && canonical_string == other.canonical_string
113
113
  end
114
114
  alias eql? ==
@@ -124,16 +124,16 @@ module CloudEvents
124
124
 
125
125
  private
126
126
 
127
- def parse str
128
- @media_type, str = consume_token str, downcase: true, error_message: "Failed to parse media type"
129
- str = consume_special str, "/"
130
- @subtype, str = consume_token str, downcase: true, error_message: "Failed to parse subtype"
131
- @subtype_base, @subtype_format = @subtype.split "+", 2
127
+ def parse(str)
128
+ @media_type, str = consume_token(str, downcase: true, error_message: "Failed to parse media type")
129
+ str = consume_special(str, "/")
130
+ @subtype, str = consume_token(str, downcase: true, error_message: "Failed to parse subtype")
131
+ @subtype_base, @subtype_format = @subtype.split("+", 2)
132
132
  until str.empty?
133
- str = consume_special str, ";"
134
- name, str = consume_token str, downcase: true, error_message: "Faled to parse attribute name"
135
- str = consume_special str, "=", error_message: "Failed to find value for attribute #{name}"
136
- val, str = consume_token_or_quoted str, error_message: "Failed to parse value for attribute #{name}"
133
+ str = consume_special(str, ";")
134
+ name, str = consume_token(str, downcase: true, error_message: "Faled to parse attribute name")
135
+ str = consume_special(str, "=", error_message: "Failed to find value for attribute #{name}")
136
+ val, str = consume_token_or_quoted(str, error_message: "Failed to parse value for attribute #{name}")
137
137
  @params << [name, val]
138
138
  @charset = val if name == "charset"
139
139
  end
@@ -141,34 +141,34 @@ module CloudEvents
141
141
  @error_message = e.message
142
142
  end
143
143
 
144
- def consume_token str, downcase: false, error_message: nil
145
- match = /^([\w!#$%&'*+.\^`{|}-]+)(.*)$/.match str
146
- raise ParseError, error_message || "Expected token" unless match
144
+ def consume_token(str, downcase: false, error_message: nil)
145
+ match = /^([\w!#$%&'*+.\^`{|}-]+)(.*)$/.match(str)
146
+ raise(ParseError, error_message || "Expected token") unless match
147
147
  token = match[1]
148
148
  token.downcase! if downcase
149
- str = consume_comments match[2].strip
149
+ str = consume_comments(match[2].strip)
150
150
  [token, str]
151
151
  end
152
152
 
153
- def consume_special str, expected, error_message: nil
154
- raise ParseError, error_message || "Expected #{expected.inspect}" unless str.start_with? expected
155
- consume_comments str[1..-1].strip
153
+ def consume_special(str, expected, error_message: nil)
154
+ raise(ParseError, error_message || "Expected #{expected.inspect}") unless str.start_with?(expected)
155
+ consume_comments(str[1..].strip)
156
156
  end
157
157
 
158
- def consume_token_or_quoted str, error_message: nil
159
- return consume_token str unless str.start_with? '"'
158
+ def consume_token_or_quoted(str, error_message: nil)
159
+ return consume_token(str) unless str.start_with?('"')
160
160
  arr = []
161
161
  index = 1
162
162
  loop do
163
163
  char = str[index]
164
164
  case char
165
165
  when nil
166
- raise ParseError, error_message || "Quoted-string never finished"
166
+ raise(ParseError, error_message || "Quoted-string never finished")
167
167
  when "\""
168
168
  break
169
169
  when "\\"
170
170
  char = str[index + 1]
171
- raise ParseError, error_message || "Quoted-string never finished" unless char
171
+ raise(ParseError, error_message || "Quoted-string never finished") unless char
172
172
  arr << char
173
173
  index += 2
174
174
  else
@@ -177,34 +177,34 @@ module CloudEvents
177
177
  end
178
178
  end
179
179
  index += 1
180
- str = consume_comments str[index..-1].strip
180
+ str = consume_comments(str[index..].strip)
181
181
  [arr.join, str]
182
182
  end
183
183
 
184
- def consume_comments str
185
- return str unless str.start_with? "("
184
+ def consume_comments(str)
185
+ return str unless str.start_with?("(")
186
186
  index = 1
187
187
  loop do
188
188
  char = str[index]
189
189
  case char
190
190
  when nil
191
- raise ParseError, "Comment never finished"
191
+ raise(ParseError, "Comment never finished")
192
192
  when ")"
193
193
  break
194
194
  when "\\"
195
195
  index += 2
196
196
  when "("
197
- str = consume_comments str[index..-1]
197
+ str = consume_comments(str[index..])
198
198
  index = 0
199
199
  else
200
200
  index += 1
201
201
  end
202
202
  end
203
203
  index += 1
204
- consume_comments str[index..-1].strip
204
+ consume_comments(str[index..].strip)
205
205
  end
206
206
 
207
- def maybe_quote str
207
+ def maybe_quote(str)
208
208
  return str if /^[\w!#$%&'*+.\^`{|}-]+$/ =~ str
209
209
  str = str.gsub("\\", "\\\\\\\\").gsub("\"", "\\\\\"")
210
210
  "\"#{str}\""
@@ -9,60 +9,61 @@ module CloudEvents
9
9
  # @private
10
10
  #
11
11
  class FieldInterpreter
12
- def initialize args
13
- @args = Utils.keys_to_strings args
12
+ def initialize(args)
13
+ @args = Utils.keys_to_strings(args)
14
14
  @attributes = {}
15
15
  end
16
16
 
17
- def finish_attributes
17
+ def finish_attributes(requires_lc_start: false)
18
18
  @args.each do |key, value|
19
+ check_attribute_name(key, requires_lc_start)
19
20
  @attributes[key.freeze] = value.to_s.freeze unless value.nil?
20
21
  end
21
22
  @args = {}
22
23
  @attributes.freeze
23
24
  end
24
25
 
25
- def string keys, required: false, allow_empty: false
26
- object keys, required: required do |value|
26
+ def string(keys, required: false, allow_empty: false)
27
+ object(keys, required: required) do |value|
27
28
  case value
28
29
  when ::String
29
- raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty? && !allow_empty
30
+ raise(AttributeError, "The #{keys.first} field cannot be empty") if value.empty? && !allow_empty
30
31
  value.freeze
31
32
  [value, value]
32
33
  else
33
- raise AttributeError, "Illegal type for #{keys.first}:" \
34
- " String expected but #{value.class} found"
34
+ raise(AttributeError, "Illegal type for #{keys.first}: " \
35
+ "String expected but #{value.class} found")
35
36
  end
36
37
  end
37
38
  end
38
39
 
39
- def uri keys, required: false
40
- object keys, required: required do |value|
40
+ def uri(keys, required: false)
41
+ object(keys, required: required) do |value|
41
42
  case value
42
43
  when ::String
43
- raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
44
+ raise(AttributeError, "The #{keys.first} field cannot be empty") if value.empty?
44
45
  begin
45
46
  [Utils.deep_freeze(::URI.parse(value)), value.freeze]
46
47
  rescue ::URI::InvalidURIError => e
47
- raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
48
+ raise(AttributeError, "Illegal format for #{keys.first}: #{e.message}")
48
49
  end
49
50
  when ::URI::Generic
50
51
  [Utils.deep_freeze(value), value.to_s.freeze]
51
52
  else
52
- raise AttributeError, "Illegal type for #{keys.first}:" \
53
- " String or URI expected but #{value.class} found"
53
+ raise(AttributeError, "Illegal type for #{keys.first}: " \
54
+ "String or URI expected but #{value.class} found")
54
55
  end
55
56
  end
56
57
  end
57
58
 
58
- def rfc3339_date_time keys, required: false
59
- object keys, required: required do |value|
59
+ def rfc3339_date_time(keys, required: false)
60
+ object(keys, required: required) do |value|
60
61
  case value
61
62
  when ::String
62
63
  begin
63
64
  [Utils.deep_freeze(::DateTime.rfc3339(value)), value.freeze]
64
65
  rescue ::Date::Error => e
65
- raise AttributeError, "Illegal format for #{keys.first}: #{e.message}"
66
+ raise(AttributeError, "Illegal format for #{keys.first}: #{e.message}")
66
67
  end
67
68
  when ::DateTime
68
69
  [Utils.deep_freeze(value), value.rfc3339.freeze]
@@ -70,44 +71,44 @@ module CloudEvents
70
71
  value = value.to_datetime
71
72
  [Utils.deep_freeze(value), value.rfc3339.freeze]
72
73
  else
73
- raise AttributeError, "Illegal type for #{keys.first}:" \
74
- " String, Time, or DateTime expected but #{value.class} found"
74
+ raise(AttributeError, "Illegal type for #{keys.first}: " \
75
+ "String, Time, or DateTime expected but #{value.class} found")
75
76
  end
76
77
  end
77
78
  end
78
79
 
79
- def content_type keys, required: false
80
- object keys, required: required do |value|
80
+ def content_type(keys, required: false)
81
+ object(keys, required: required) do |value|
81
82
  case value
82
83
  when ::String
83
- raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
84
+ raise(AttributeError, "The #{keys.first} field cannot be empty") if value.empty?
84
85
  [ContentType.new(value), value.freeze]
85
86
  when ContentType
86
87
  [value, value.to_s]
87
88
  else
88
- raise AttributeError, "Illegal type for #{keys.first}:" \
89
- " String, or ContentType expected but #{value.class} found"
89
+ raise(AttributeError, "Illegal type for #{keys.first}: " \
90
+ "String, or ContentType expected but #{value.class} found")
90
91
  end
91
92
  end
92
93
  end
93
94
 
94
- def spec_version keys, accept:
95
- object keys, required: true do |value|
95
+ def spec_version(keys, accept:)
96
+ object(keys, required: true) do |value|
96
97
  case value
97
98
  when ::String
98
- raise SpecVersionError, "Unrecognized specversion: #{value}" unless accept =~ value
99
+ raise(SpecVersionError, "Unrecognized specversion: #{value}") unless accept =~ value
99
100
  value.freeze
100
101
  [value, value]
101
102
  else
102
- raise AttributeError, "Illegal type for #{keys.first}:" \
103
- " String expected but #{value.class} found"
103
+ raise(AttributeError, "Illegal type for #{keys.first}: " \
104
+ "String expected but #{value.class} found")
104
105
  end
105
106
  end
106
107
  end
107
108
 
108
- def data_object keys, required: false
109
- object keys, required: required, allow_nil: true do |value|
110
- Utils.deep_freeze value
109
+ def data_object(keys, required: false)
110
+ object(keys, required: required, allow_nil: true) do |value|
111
+ Utils.deep_freeze(value)
111
112
  [value, value]
112
113
  end
113
114
  end
@@ -116,21 +117,28 @@ module CloudEvents
116
117
 
117
118
  private
118
119
 
119
- def object keys, required: false, allow_nil: false
120
+ def object(keys, required: false, allow_nil: false)
120
121
  value = UNDEFINED
121
122
  keys.each do |key|
122
- key_present = @args.key? key
123
- val = @args.delete key
124
- value = val if allow_nil && key_present || !allow_nil && !val.nil?
123
+ key_present = @args.key?(key)
124
+ val = @args.delete(key)
125
+ value = val if (allow_nil && key_present) || (!allow_nil && !val.nil?)
125
126
  end
126
127
  if value == UNDEFINED
127
- raise AttributeError, "The #{keys.first} field is required" if required
128
+ raise(AttributeError, "The #{keys.first} field is required") if required
128
129
  return allow_nil ? UNDEFINED : nil
129
130
  end
130
- converted, raw = yield value
131
+ converted, raw = yield(value)
131
132
  @attributes[keys.first.freeze] = raw
132
133
  converted
133
134
  end
135
+
136
+ def check_attribute_name(key, requires_lc_start)
137
+ regex = requires_lc_start ? /^[a-z][a-z0-9]*$/ : /^[a-z0-9]+$/
138
+ unless regex.match?(key)
139
+ raise(AttributeError, "Illegal key: #{key.inspect} must consist only of digits and lower-case letters")
140
+ end
141
+ end
134
142
  end
135
143
  end
136
144
  end
@@ -24,7 +24,7 @@ module CloudEvents
24
24
  # or not provided, the value will be inferred from the content type
25
25
  # if possible, or otherwise set to `nil` indicating not known.
26
26
  #
27
- def initialize content, content_type, batch: nil
27
+ def initialize(content, content_type, batch: nil)
28
28
  @content = content.freeze
29
29
  @content_type = content_type
30
30
  if batch.nil? && content_type&.media_type == "application"
@@ -63,7 +63,7 @@ module CloudEvents
63
63
  end
64
64
 
65
65
  ## @private
66
- def == other
66
+ def ==(other)
67
67
  Opaque === other &&
68
68
  @content == other.content &&
69
69
  @content_type == other.content_type &&
@@ -73,7 +73,7 @@ module CloudEvents
73
73
 
74
74
  ## @private
75
75
  def hash
76
- @content.hash ^ @content_type.hash ^ @batch.hash
76
+ [@content, @content_type, @batch].hash
77
77
  end
78
78
  end
79
79
  end