Skip to content

Commit c0a3080

Browse files
authored
feat: Provide data_encoded and additional data methods on the V1 event object (#67)
Full change list: * Added `data_encoded`, `data_decoded?` and `data?` methods to CloudEvents::Event::V1, added `:data_encoded` as an input attribute, and clarified the encoding semantics of each field. * Changed `:attributes` keyword argument in event constructors to `:set_attributes`, to avoid any possible collision with a real extension attribute name. (The old argument name is deprecated and will be removed in 1.0.) * Fixed various inconsistencies in the data encoding behavior of JsonFormat and HttpBinding. * Support passing a data content encoder/decoder into JsonFormat#encode_event and JsonFormat#decode_event. * Provided TextFormat to handle media types with trivial encoding. * Provided Format::Multi to handle checking a series of encoders/decoders. Signed-off-by: Daniel Azuma <[email protected]>
1 parent 8091189 commit c0a3080

File tree

14 files changed

+1313
-682
lines changed

14 files changed

+1313
-682
lines changed

.rubocop.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Metrics/BlockLength:
88
Exclude:
99
- "test/**/test_*.rb"
1010
Metrics/ClassLength:
11-
Max: 250
11+
Max: 300
1212
Metrics/ModuleLength:
13-
Max: 250
13+
Max: 300
1414
Naming/FileName:
1515
Exclude:
1616
- "examples/*/Gemfile"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ require "cloud_events"
4747
cloud_events_http = CloudEvents::HttpBinding.default
4848

4949
post "/" do
50-
event = cloud_events_http.decode_rack_env request.env
50+
event = cloud_events_http.decode_event request.env
5151
logger.info "Received CloudEvent: #{event.to_h}"
5252
end
5353
```

lib/cloud_events.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require "cloud_events/format"
77
require "cloud_events/http_binding"
88
require "cloud_events/json_format"
9+
require "cloud_events/text_format"
910

1011
##
1112
# CloudEvents implementation.

lib/cloud_events/event/field_interpreter.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ def finish_attributes
2222
@attributes.freeze
2323
end
2424

25-
def string keys, required: false
25+
def string keys, required: false, allow_empty: false
2626
object keys, required: required do |value|
2727
case value
2828
when ::String
29-
raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty?
29+
raise AttributeError, "The #{keys.first} field cannot be empty" if value.empty? && !allow_empty
3030
value.freeze
3131
[value, value]
3232
else
@@ -125,7 +125,7 @@ def object keys, required: false, allow_nil: false
125125
end
126126
if value == UNDEFINED
127127
raise AttributeError, "The #{keys.first} field is required" if required
128-
return nil
128+
return allow_nil ? UNDEFINED : nil
129129
end
130130
converted, raw = yield value
131131
@attributes[keys.first.freeze] = raw

lib/cloud_events/event/v0.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ class V0
3434
# Create a new cloud event object with the given data and attributes.
3535
#
3636
# Event attributes may be presented as keyword arguments, or as a Hash
37-
# passed in via the `attributes` argument (but not both).
37+
# passed in via the special `:set_attributes` keyword argument (but not
38+
# both). The `:set_attributes` keyword argument is useful for passing in
39+
# attributes whose keys are strings rather than symbols, which some
40+
# versions of Ruby will not accept as keyword arguments.
3841
#
3942
# The following standard attributes are supported and exposed as
4043
# attribute methods on the object.
@@ -68,16 +71,18 @@ class V0
6871
# `:data` field, for example if you pass a structured hash. If this is an
6972
# issue, make a deep copy of objects before passing to this constructor.
7073
#
71-
# @param attributes [Hash] The data and attributes, as a hash.
74+
# @param set_attributes [Hash] The data and attributes, as a hash.
75+
# (Also available as `attributes` but this usage is deprecated.)
7276
# @param args [keywords] The data and attributes, as keyword arguments.
7377
#
74-
def initialize attributes: nil, **args
75-
interpreter = FieldInterpreter.new attributes || args
78+
def initialize set_attributes: nil, attributes: nil, **args
79+
interpreter = FieldInterpreter.new set_attributes || attributes || args
7680
@spec_version = interpreter.spec_version ["specversion", "spec_version"], accept: /^0\.3$/
7781
@id = interpreter.string ["id"], required: true
7882
@source = interpreter.uri ["source"], required: true
7983
@type = interpreter.string ["type"], required: true
8084
@data = interpreter.data_object ["data"]
85+
@data = nil if @data == FieldInterpreter::UNDEFINED
8186
@data_content_encoding = interpreter.string ["datacontentencoding", "data_content_encoding"]
8287
@data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
8388
@schema_url = interpreter.uri ["schemaurl", "schema_url"]
@@ -98,7 +103,7 @@ def initialize attributes: nil, **args
98103
#
99104
def with **changes
100105
attributes = @attributes.merge changes
101-
V0.new attributes: attributes
106+
V0.new set_attributes: attributes
102107
end
103108

104109
##

lib/cloud_events/event/v1.rb

Lines changed: 144 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,29 @@ module Event
1414
# This object represents a complete CloudEvent, including the event data
1515
# and context attributes. It supports the standard required and optional
1616
# attributes defined in CloudEvents V1.0, and arbitrary extension
17-
# attributes. All attribute values can be obtained (in their string form)
17+
# attributes.
18+
#
19+
# Values for most attributes can be obtained in their encoded string form
1820
# via the {Event::V1#[]} method. Additionally, standard attributes have
19-
# their own accessor methods that may return typed objects (such as
20-
# `DateTime` for the `time` attribute).
21+
# their own accessor methods that may return decoded Ruby objects (such as
22+
# a `DateTime` object for the `time` attribute).
23+
#
24+
# The `data` attribute is treated specially because it is subject to
25+
# arbitrary encoding governed by the `datacontenttype` attribute. Data is
26+
# expressed in two related fields: {Event::V1#data} and
27+
# {Event::V1#data_encoded}. The former, `data`, _may_ be an arbitrary Ruby
28+
# object representing the decoded form of the data (for example, a Hash for
29+
# most JSON-formatted data.) The latter, `data_encoded`, _must_, if
30+
# present, be a Ruby String object representing the encoded string or
31+
# byte array form of the data.
32+
#
33+
# When the CloudEvents Ruby SDK encodes an event for transmission, it will
34+
# use the `data_encoded` field if present. Otherwise, it will attempt to
35+
# encode the `data` field using any available encoder that recognizes the
36+
# content-type. Currently, text and JSON types are supported. If the type
37+
# is not supported, event encoding may fail. It is thus recommended that
38+
# applications provide a `data_encoded` string, if the `data` object is
39+
# nontrivially encoded.
2140
#
2241
# This object is immutable, and Ractor-shareable on Ruby 3. The data and
2342
# attribute values can be retrieved but not modified. To obtain an event
@@ -33,8 +52,13 @@ class V1
3352
##
3453
# Create a new cloud event object with the given data and attributes.
3554
#
55+
# ### Specifying event attributes
56+
#
3657
# Event attributes may be presented as keyword arguments, or as a Hash
37-
# passed in via the `attributes` argument (but not both).
58+
# passed in via the special `:set_attributes` keyword argument (but not
59+
# both). The `:set_attributes` keyword argument is useful for passing in
60+
# attributes whose keys are strings rather than symbols, which some
61+
# versions of Ruby will not accept as keyword arguments.
3862
#
3963
# The following standard attributes are supported and exposed as
4064
# attribute methods on the object.
@@ -45,11 +69,15 @@ class V1
4569
# * **:source** [`String`, `URI`] - _required_ - The event `source`
4670
# field.
4771
# * **:type** [`String`] - _required_ - The event `type` field.
48-
# * **:data** [`Object`] - _optional_ - The data associated with the
49-
# event (i.e. the `data` field).
72+
# * **:data** [`Object`] - _optional_ - The "decoded" Ruby object form
73+
# of the event `data` field, if known. (e.g. a Hash representing a
74+
# JSON document)
75+
# * **:data_encoded** [`String`] - _optional_ - The "encoded" string
76+
# form of the event `data` field, if known. This should be set along
77+
# with the `data_content_type`.
5078
# * **:data_content_type** (or **:datacontenttype**) [`String`,
51-
# {ContentType}] - _optional_ - The content-type for the data, if
52-
# the data is a string (i.e. the event `datacontenttype` field.)
79+
# {ContentType}] - _optional_ - The content-type for the encoded data
80+
# (i.e. the event `datacontenttype` field.)
5381
# * **:data_schema** (or **:dataschema**) [`String`, `URI`] -
5482
# _optional_ - The event `dataschema` field.
5583
# * **:subject** [`String`] - _optional_ - The event `subject` field.
@@ -65,16 +93,63 @@ class V1
6593
# `:data` field, for example if you pass a structured hash. If this is an
6694
# issue, make a deep copy of objects before passing to this constructor.
6795
#
68-
# @param attributes [Hash] The data and attributes, as a hash.
96+
# ### Specifying payload data
97+
#
98+
# Typically you should provide _both_ the `:data` and `:data_encoded`
99+
# fields, the former representing the decoded (Ruby object) form of the
100+
# data, and the second providing a hint to formatters and protocol
101+
# bindings for how to seralize the data. In this case, the {#data} and
102+
# {#data_encoded} methods will return the corresponding values, and
103+
# {#data_decoded?} will return true to indicate that {#data} represents
104+
# the decoded form.
105+
#
106+
# If you provide _only_ the `:data` field, omitting `:data_encoded`, then
107+
# the value is expected to represent the decoded (Ruby object) form of
108+
# the data. The {#data} method will return this decoded value, and
109+
# {#data_decoded?} will return true. The {#data_encoded} method will
110+
# return nil.
111+
# When serializing such an event, it will be up to the formatter or
112+
# protocol binding to encode the data. This means serialization _could_
113+
# fail if the formatter does not understand the data's content type.
114+
# Omitting `:data_encoded` is common if the content type is JSON related
115+
# (e.g. `application/json`) and the event is being encoded in JSON
116+
# structured format, because the data encoding is trivial. This form can
117+
# also be used when the content type is `text/*`, for which encoding is
118+
# also trivial.
119+
#
120+
# If you provide _only_ the `:data_encoded` field, omitting `:data`, then
121+
# the value is expected to represent the encoded (string) form of the
122+
# data. The {#data_encoded} method will return this value. Additionally,
123+
# the {#data} method will return the same _encoded_ value, and
124+
# {#data_decoded?} will return false.
125+
# Event objects of this form may be returned from a protocol binding when
126+
# it decodes an event with a `datacontenttype` that it does not know how
127+
# to interpret. Applications should query {#data_decoded?} to determine
128+
# whether the {#data} method returns encoded or decoded data.
129+
#
130+
# If you provide _neither_ `:data` nor `:data_encoded`, the event will
131+
# have no payload data. Both {#data} and {#data_encoded} will return nil,
132+
# and {#data_decoded?} will return false. (Additionally, {#data?} will
133+
# return false to signal the absence of any data.)
134+
#
135+
# @param set_attributes [Hash] The data and attributes, as a hash.
136+
# (Also available as `attributes` but this usage is deprecated.)
69137
# @param args [keywords] The data and attributes, as keyword arguments.
70138
#
71-
def initialize attributes: nil, **args
72-
interpreter = FieldInterpreter.new attributes || args
139+
def initialize set_attributes: nil, attributes: nil, **args
140+
interpreter = FieldInterpreter.new set_attributes || attributes || args
73141
@spec_version = interpreter.spec_version ["specversion", "spec_version"], accept: /^1(\.|$)/
74142
@id = interpreter.string ["id"], required: true
75143
@source = interpreter.uri ["source"], required: true
76144
@type = interpreter.string ["type"], required: true
145+
@data_encoded = interpreter.string ["data_encoded"], allow_empty: true
77146
@data = interpreter.data_object ["data"]
147+
if @data == FieldInterpreter::UNDEFINED
148+
@data = @data_encoded
149+
@data_decoded = false
150+
else
151+
@data_decoded = true
152+
end
78153
@data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
79154
@data_schema = interpreter.uri ["dataschema", "data_schema"]
80155
@subject = interpreter.string ["subject"]
@@ -93,8 +168,14 @@ def initialize attributes: nil, **args
93168
# @return [FunctionFramework::CloudEvents::Event]
94169
#
95170
def with **changes
96-
attributes = @attributes.merge changes
97-
V1.new attributes: attributes
171+
changes = Utils.keys_to_strings changes
172+
attributes = @attributes.dup
173+
if changes.key?("data") || changes.key?("data_encoded")
174+
attributes.delete "data"
175+
attributes.delete "data_encoded"
176+
end
177+
attributes.merge! changes
178+
V1.new set_attributes: attributes
98179
end
99180

100181
##
@@ -160,19 +241,61 @@ def to_h
160241
alias specversion spec_version
161242

162243
##
163-
# The event-specific data, or `nil` if there is no data.
244+
# The event `data` field, or `nil` if there is no data.
245+
#
246+
# This may return the data as an encoded string _or_ as a decoded Ruby
247+
# object. The {#data_decoded?} method specifies whether the `data` value
248+
# is decoded or encoded.
249+
#
250+
# In most cases, {#data} returns a decoded value, unless the event was
251+
# received from a source that could not decode the content. For example,
252+
# most protocol bindings understand how to decode JSON, so an event
253+
# received with a {#data_content_type} of `application/json` will usually
254+
# return a decoded object (usually a Hash) from {#data}.
164255
#
165-
# Data may be one of the following types:
166-
# * Binary data, represented by a `String` using the `ASCII-8BIT`
167-
# encoding.
168-
# * A string in some other encoding such as `UTF-8` or `US-ASCII`.
169-
# * Any JSON data type, such as a Boolean, Integer, Array, Hash, or
170-
# `nil`.
256+
# See also {#data_encoded} and {#data_decoded?}.
171257
#
172-
# @return [Object]
258+
# @return [Object] if containing decoded data
259+
# @return [String] if containing encoded data
260+
# @return [nil] if there is no data
173261
#
174262
attr_reader :data
175263

264+
##
265+
# The encoded string representation of the data, i.e. its raw form used
266+
# when encoding an event for transmission. This may be `nil` if there is
267+
# no data, or if the encoded form is not known.
268+
#
269+
# See also {#data}.
270+
#
271+
# @return [String,nil]
272+
#
273+
attr_reader :data_encoded
274+
275+
##
276+
# Indicates whether the {#data} field returns decoded data.
277+
#
278+
# @return [true] if {#data} returns a decoded Ruby object
279+
# @return [false] if {#data} returns an encoded string or if the event
280+
# has no data.
281+
#
282+
def data_decoded?
283+
@data_decoded
284+
end
285+
286+
##
287+
# Indicates whether the data field is present. If there is no data,
288+
# {#data} will return `nil`, and {#data_decoded?} will return false.
289+
#
290+
# Generally, if there is no data, the {#data_content_type} field should
291+
# also be absent, but this is not enforced.
292+
#
293+
# @return [boolean]
294+
#
295+
def data?
296+
!@data.nil? || @data_decoded
297+
end
298+
176299
##
177300
# The optional `datacontenttype` field as a {CloudEvents::ContentType}
178301
# object, or `nil` if the field is absent.

0 commit comments

Comments
 (0)