cloud_events 0.1.2 → 0.5.1
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/LICENSE.md +189 -190
- data/README.md +26 -33
- data/lib/cloud_events.rb +1 -0
- data/lib/cloud_events/content_type.rb +22 -5
- data/lib/cloud_events/errors.rb +37 -5
- data/lib/cloud_events/event.rb +6 -1
- data/lib/cloud_events/event/field_interpreter.rb +25 -27
- data/lib/cloud_events/event/opaque.rb +80 -0
- data/lib/cloud_events/event/utils.rb +51 -0
- data/lib/cloud_events/event/v0.rb +22 -9
- data/lib/cloud_events/event/v1.rb +21 -8
- data/lib/cloud_events/format.rb +202 -0
- data/lib/cloud_events/http_binding.rb +319 -145
- data/lib/cloud_events/json_format.rb +162 -52
- data/lib/cloud_events/version.rb +1 -1
- metadata +9 -6
@@ -3,6 +3,9 @@
|
|
3
3
|
require "date"
|
4
4
|
require "uri"
|
5
5
|
|
6
|
+
require "cloud_events/event/field_interpreter"
|
7
|
+
require "cloud_events/event/utils"
|
8
|
+
|
6
9
|
module CloudEvents
|
7
10
|
module Event
|
8
11
|
##
|
@@ -16,9 +19,10 @@ module CloudEvents
|
|
16
19
|
# their own accessor methods that may return typed objects (such as
|
17
20
|
# `DateTime` for the `time` attribute).
|
18
21
|
#
|
19
|
-
# This object is immutable. The data and
|
20
|
-
# retrieved but not modified. To obtain an event
|
21
|
-
# the {#with} method to create a copy with the
|
22
|
+
# This object is immutable, and Ractor-shareable on Ruby 3. The data and
|
23
|
+
# attribute values can be retrieved but not modified. To obtain an event
|
24
|
+
# with modifications, use the {#with} method to create a copy with the
|
25
|
+
# desired changes.
|
22
26
|
#
|
23
27
|
# See https://github.com/cloudevents/spec/blob/v1.0/spec.md for
|
24
28
|
# descriptions of the standard attributes.
|
@@ -42,7 +46,7 @@ module CloudEvents
|
|
42
46
|
# field.
|
43
47
|
# * **:type** [`String`] - _required_ - The event `type` field.
|
44
48
|
# * **:data** [`Object`] - _optional_ - The data associated with the
|
45
|
-
# event (i.e. the `data` field.
|
49
|
+
# event (i.e. the `data` field).
|
46
50
|
# * **:data_content_type** (or **:datacontenttype**) [`String`,
|
47
51
|
# {ContentType}] - _optional_ - The content-type for the data, if
|
48
52
|
# the data is a string (i.e. the event `datacontenttype` field.)
|
@@ -56,6 +60,11 @@ module CloudEvents
|
|
56
60
|
# They are not available as separate methods, but can be accessed via
|
57
61
|
# the {Event::V1#[]} operator.
|
58
62
|
#
|
63
|
+
# Note that attribute objects passed in may get deep-frozen if they are
|
64
|
+
# used in the final event object. This is particularly important for the
|
65
|
+
# `:data` field, for example if you pass a structured hash. If this is an
|
66
|
+
# issue, make a deep copy of objects before passing to this constructor.
|
67
|
+
#
|
59
68
|
# @param attributes [Hash] The data and attributes, as a hash.
|
60
69
|
# @param args [keywords] The data and attributes, as keyword arguments.
|
61
70
|
#
|
@@ -65,12 +74,13 @@ module CloudEvents
|
|
65
74
|
@id = interpreter.string ["id"], required: true
|
66
75
|
@source = interpreter.uri ["source"], required: true
|
67
76
|
@type = interpreter.string ["type"], required: true
|
68
|
-
@data = interpreter.
|
77
|
+
@data = interpreter.data_object ["data"]
|
69
78
|
@data_content_type = interpreter.content_type ["datacontenttype", "data_content_type"]
|
70
79
|
@data_schema = interpreter.uri ["dataschema", "data_schema"]
|
71
80
|
@subject = interpreter.string ["subject"]
|
72
81
|
@time = interpreter.rfc3339_date_time ["time"]
|
73
82
|
@attributes = interpreter.finish_attributes
|
83
|
+
freeze
|
74
84
|
end
|
75
85
|
|
76
86
|
##
|
@@ -101,6 +111,8 @@ module CloudEvents
|
|
101
111
|
# event["time"] # => String rfc3339 representation
|
102
112
|
# event.time # => DateTime object
|
103
113
|
#
|
114
|
+
# Results are also always frozen and cannot be modified in place.
|
115
|
+
#
|
104
116
|
# @param key [String,Symbol] The attribute name.
|
105
117
|
# @return [String,nil]
|
106
118
|
#
|
@@ -109,12 +121,13 @@ module CloudEvents
|
|
109
121
|
end
|
110
122
|
|
111
123
|
##
|
112
|
-
# Return a hash representation of this event.
|
124
|
+
# Return a hash representation of this event. The returned hash is an
|
125
|
+
# unfrozen deep copy. Modifications do not affect the original event.
|
113
126
|
#
|
114
127
|
# @return [Hash]
|
115
128
|
#
|
116
129
|
def to_h
|
117
|
-
@attributes
|
130
|
+
Utils.deep_dup @attributes
|
118
131
|
end
|
119
132
|
|
120
133
|
##
|
@@ -201,7 +214,7 @@ module CloudEvents
|
|
201
214
|
|
202
215
|
## @private
|
203
216
|
def hash
|
204
|
-
@
|
217
|
+
@attributes.hash
|
205
218
|
end
|
206
219
|
end
|
207
220
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module CloudEvents
|
7
|
+
##
|
8
|
+
# This module documents the method signatures that may be implemented by
|
9
|
+
# formatters.
|
10
|
+
#
|
11
|
+
# A formatter is an object that implenets "structured" event encoding and
|
12
|
+
# decoding strategies for a particular format (such as JSON). In general,
|
13
|
+
# this includes four operations:
|
14
|
+
#
|
15
|
+
# * Decoding an entire event or batch of events from a input source.
|
16
|
+
# This is implemented by the {Format#decode_event} method.
|
17
|
+
# * Encoding an entire event or batch of events to an output sink.
|
18
|
+
# This is implemented by the {Format#encode_event} method.
|
19
|
+
# * Decoding an event payload (i.e. the `data` attribute) Ruby object from a
|
20
|
+
# serialized representation.
|
21
|
+
# This is implemented by the {Format#decode_data} method.
|
22
|
+
# * Encoding an event payload (i.e. the `data` attribute) Ruby object to a
|
23
|
+
# serialized representation.
|
24
|
+
# This is implemented by the {Format#encode_data} method.
|
25
|
+
#
|
26
|
+
# Each method takes a set of keyword arguments, and returns either a `Hash`
|
27
|
+
# or `nil`. A Hash indicates that the formatter understands the request and
|
28
|
+
# is returning its response. A return value of `nil` means the formatter does
|
29
|
+
# not understand the request and is declining to perform the operation. In
|
30
|
+
# such a case, it is possible that a different formatter should handle it.
|
31
|
+
#
|
32
|
+
# Both the keyword arguments recognized and the returned hash members may
|
33
|
+
# vary from formatter to formatter; similarly, the keyword arguments provided
|
34
|
+
# and the resturned hash members recognized may also vary for different
|
35
|
+
# callers. This interface will define a set of common argument and result key
|
36
|
+
# names, but both callers and formatters must gracefully handle the case of
|
37
|
+
# missing or extra information. For example, if a formatter expects a certain
|
38
|
+
# argument but does not receive it, it can assume the caller does not have
|
39
|
+
# the required information, and it may respond by returning `nil` to decline
|
40
|
+
# the request. Similarly, if a caller expects a response key but does not
|
41
|
+
# receive it, it can assume the formatter does not provide it, and it may
|
42
|
+
# respond by trying a different formatter.
|
43
|
+
#
|
44
|
+
# Additionally, any particular formatter need not implement all methods. For
|
45
|
+
# example, an event formatter would generally implement {Format#decode_event}
|
46
|
+
# and {Format#encode_event}, but might not implement {Format#decode_data} or
|
47
|
+
# {Format#encode_data}.
|
48
|
+
#
|
49
|
+
# Finally, this module itself is present primarily for documentation, and
|
50
|
+
# need not be directly included by formatter implementations.
|
51
|
+
#
|
52
|
+
module Format
|
53
|
+
##
|
54
|
+
# Decode an event or batch from the given serialized input. This is
|
55
|
+
# typically called by a protocol binding to deserialize event data from an
|
56
|
+
# input stream.
|
57
|
+
#
|
58
|
+
# Common arguments include:
|
59
|
+
#
|
60
|
+
# * `:content` (String) Serialized content to decode. For example, it could
|
61
|
+
# be from an HTTP request body.
|
62
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type. For
|
63
|
+
# example, it could be from the `Content-Type` header of an HTTP request.
|
64
|
+
#
|
65
|
+
# The formatter must first determine whether it is able to interpret the
|
66
|
+
# given input. Typically, this is done by inspecting the `content_type`.
|
67
|
+
# If the formatter determines that it is unable to interpret the input, it
|
68
|
+
# should return `nil`. Otherwise, if the formatter determines it can decode
|
69
|
+
# the input, it should return a `Hash`. Common hash keys include:
|
70
|
+
#
|
71
|
+
# * `:event` ({CloudEvents::Event}) A single event decoded from the input.
|
72
|
+
# * `:event_batch` (Array of {CloudEvents::Event}) A batch of events
|
73
|
+
# decoded from the input.
|
74
|
+
#
|
75
|
+
# The formatter may also raise a {CloudEvents::CloudEventsError} subclass
|
76
|
+
# if it understood the request but determines that the input source is
|
77
|
+
# malformed.
|
78
|
+
#
|
79
|
+
# @param _kwargs [keywords] Arguments
|
80
|
+
# @return [Hash] if accepting the request and returning a result
|
81
|
+
# @return [nil] if declining the request.
|
82
|
+
#
|
83
|
+
def decode_event **_kwargs
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Encode an event or batch to a string. This is typically called by a
|
89
|
+
# protocol binding to serialize event data to an output stream.
|
90
|
+
#
|
91
|
+
# Common arguments include:
|
92
|
+
#
|
93
|
+
# * `:event` ({CloudEvents::Event}) A single event to encode.
|
94
|
+
# * `:event_batch` (Array of {CloudEvents::Event}) A batch of events to
|
95
|
+
# encode.
|
96
|
+
#
|
97
|
+
# The formatter must first determine whether it is able to interpret the
|
98
|
+
# given input. Typically, most formatters should be able to handle any
|
99
|
+
# event or event batch, but a specialized formatter that can handle only
|
100
|
+
# certain kinds of events may return `nil` to decline unwanted inputs.
|
101
|
+
# Otherwise, if the formatter determines it can encode the input, it should
|
102
|
+
# return a `Hash`. common hash keys include:
|
103
|
+
#
|
104
|
+
# * `:content` (String) The serialized form of the event. This might, for
|
105
|
+
# example, be written to an HTTP request body. Care should be taken to
|
106
|
+
# set the string's encoding properly. In particular, to output binary
|
107
|
+
# data, the encoding should probably be set to `ASCII_8BIT`.
|
108
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type for the
|
109
|
+
# output. This might, for example, be written to the `Content-Type`
|
110
|
+
# header of an HTTP request.
|
111
|
+
#
|
112
|
+
# The formatter may also raise a {CloudEvents::CloudEventsError} subclass
|
113
|
+
# if it understood the request but determines that the input source is
|
114
|
+
# malformed.
|
115
|
+
#
|
116
|
+
# @param _kwargs [keywords] Arguments
|
117
|
+
# @return [Hash] if accepting the request and returning a result
|
118
|
+
# @return [nil] if declining the request.
|
119
|
+
#
|
120
|
+
def encode_event **_kwargs
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Decode an event data object from string format. This is typically called
|
126
|
+
# by a protocol binding to deserialize the payload (i.e. `data` attribute)
|
127
|
+
# of an event as part of "binary content mode" decoding.
|
128
|
+
#
|
129
|
+
# Common arguments include:
|
130
|
+
#
|
131
|
+
# * `:spec_version` (String) The `specversion` of the event.
|
132
|
+
# * `:content` (String) Serialized payload to decode. For example, it could
|
133
|
+
# be from an HTTP request body.
|
134
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type. For
|
135
|
+
# example, it could be from the `Content-Type` header of an HTTP request.
|
136
|
+
#
|
137
|
+
# The formatter must first determine whether it is able to interpret the
|
138
|
+
# given input. Typically, this is done by inspecting the `content_type`.
|
139
|
+
# If the formatter determines that it is unable to interpret the input, it
|
140
|
+
# should return `nil`. Otherwise, if the formatter determines it can decode
|
141
|
+
# the input, it should return a `Hash`. Common hash keys include:
|
142
|
+
#
|
143
|
+
# * `:data` (Object) The payload object to be set as the `data` attribute
|
144
|
+
# in a {CloudEvents::Event} object.
|
145
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type to be set
|
146
|
+
# as the `datacontenttype` attribute in a {CloudEvents::Event} object.
|
147
|
+
# In many cases, this may simply be copied from the `:content_type`
|
148
|
+
# argument, but a formatter could modify it to provide corrections or
|
149
|
+
# additional information.
|
150
|
+
#
|
151
|
+
# The formatter may also raise a {CloudEvents::CloudEventsError} subclass
|
152
|
+
# if it understood the request but determines that the input source is
|
153
|
+
# malformed.
|
154
|
+
#
|
155
|
+
# @param _kwargs [keywords] Arguments
|
156
|
+
# @return [Hash] if accepting the request and returning a result
|
157
|
+
# @return [nil] if declining the request.
|
158
|
+
#
|
159
|
+
def decode_data **_kwargs
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
##
|
164
|
+
# Encode an event data object to string format. This is typically called by
|
165
|
+
# a protocol binding to serialize the payload (i.e. `data` attribute and
|
166
|
+
# corresponding `datacontenttype` attribute) of an event as part of "binary
|
167
|
+
# content mode" encoding.
|
168
|
+
#
|
169
|
+
# Common arguments include:
|
170
|
+
#
|
171
|
+
# * `:spec_version` (String) The `specversion` of the event.
|
172
|
+
# * `:data` (Object) The payload object from an event's `data` attribute.
|
173
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type from an
|
174
|
+
# event's `datacontenttype` attribute.
|
175
|
+
#
|
176
|
+
# The formatter must first determine whether it is able to interpret the
|
177
|
+
# given input. Typically, this is done by inspecting the `content_type`.
|
178
|
+
# If the formatter determines that it is unable to interpret the input, it
|
179
|
+
# should return `nil`. Otherwise, if the formatter determines it can decode
|
180
|
+
# the input, it should return a `Hash`. Common hash keys include:
|
181
|
+
#
|
182
|
+
# * `:content` (String) The serialized form of the data. This might, for
|
183
|
+
# example, be written to an HTTP request body. Care should be taken to
|
184
|
+
# set the string's encoding properly. In particular, to output binary
|
185
|
+
# data, the encoding should probably be set to `ASCII_8BIT`.
|
186
|
+
# * `:content_type` ({CloudEvents::ContentType}) The content type for the
|
187
|
+
# output. This might, for example, be written to the `Content-Type`
|
188
|
+
# header of an HTTP request.
|
189
|
+
#
|
190
|
+
# The formatter may also raise a {CloudEvents::CloudEventsError} subclass
|
191
|
+
# if it understood the request but determines that the input source is
|
192
|
+
# malformed.
|
193
|
+
#
|
194
|
+
# @param _kwargs [keywords] Arguments
|
195
|
+
# @return [Hash] if accepting the request and returning a result
|
196
|
+
# @return [nil] if declining the request.
|
197
|
+
#
|
198
|
+
def encode_data **_kwargs
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -15,14 +15,21 @@ module CloudEvents
|
|
15
15
|
#
|
16
16
|
class HttpBinding
|
17
17
|
##
|
18
|
-
#
|
18
|
+
# The name of the JSON decoder/encoder
|
19
|
+
# @return [String]
|
20
|
+
#
|
21
|
+
JSON_FORMAT = "json"
|
22
|
+
|
23
|
+
##
|
24
|
+
# Returns a default HTTP binding, including support for JSON format.
|
25
|
+
#
|
26
|
+
# @return [HttpBinding]
|
19
27
|
#
|
20
28
|
def self.default
|
21
29
|
@default ||= begin
|
22
30
|
http_binding = new
|
23
|
-
|
24
|
-
http_binding.
|
25
|
-
http_binding.register_batched_formatter "json", json_format
|
31
|
+
http_binding.register_formatter JsonFormat.new, JSON_FORMAT
|
32
|
+
http_binding.default_encoder_name = JSON_FORMAT
|
26
33
|
http_binding
|
27
34
|
end
|
28
35
|
end
|
@@ -31,234 +38,284 @@ module CloudEvents
|
|
31
38
|
# Create an empty HTTP binding.
|
32
39
|
#
|
33
40
|
def initialize
|
34
|
-
@
|
35
|
-
@
|
41
|
+
@event_decoders = []
|
42
|
+
@event_encoders = {}
|
43
|
+
@data_decoders = [DefaultDataFormat]
|
44
|
+
@data_encoders = [DefaultDataFormat]
|
45
|
+
@default_encoder_name = nil
|
36
46
|
end
|
37
47
|
|
38
48
|
##
|
39
|
-
# Register a formatter for
|
49
|
+
# Register a formatter for all operations it supports, based on which
|
50
|
+
# methods are implemented by the formatter object. See
|
51
|
+
# {CloudEvents::Format} for a list of possible methods.
|
40
52
|
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# this formatter.
|
46
|
-
# @param formatter [Object] The formatter object.
|
53
|
+
# @param formatter [Object] The formatter
|
54
|
+
# @param name [String] The encoder name under which this formatter will
|
55
|
+
# register its encode operations. Optional. If not specified, any event
|
56
|
+
# encoder will _not_ be registered.
|
47
57
|
# @return [self]
|
48
58
|
#
|
49
|
-
def
|
50
|
-
|
51
|
-
|
59
|
+
def register_formatter formatter, name = nil
|
60
|
+
name = name.to_s.strip.downcase if name
|
61
|
+
decode_event = formatter.respond_to? :decode_event
|
62
|
+
encode_event = name if formatter.respond_to? :encode_event
|
63
|
+
decode_data = formatter.respond_to? :decode_data
|
64
|
+
encode_data = formatter.respond_to? :encode_data
|
65
|
+
register_formatter_methods formatter,
|
66
|
+
decode_event: decode_event,
|
67
|
+
encode_event: encode_event,
|
68
|
+
decode_data: decode_data,
|
69
|
+
encode_data: encode_data
|
52
70
|
self
|
53
71
|
end
|
54
72
|
|
55
73
|
##
|
56
|
-
#
|
74
|
+
# Registers the given formatter for the given operations. Some arguments
|
75
|
+
# are activated by passing `true`, whereas those that rely on a format name
|
76
|
+
# are activated by passing in a name string.
|
57
77
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
# @param
|
62
|
-
#
|
63
|
-
# @param
|
78
|
+
# @param formatter [Object] The formatter
|
79
|
+
# @param decode_event [boolean] If true, register the formatter's
|
80
|
+
# {CloudEvents::Format#decode_event} method.
|
81
|
+
# @param encode_event [String] If set to a string, use the formatter's
|
82
|
+
# {CloudEvents::Format#encode_event} method when that name is requested.
|
83
|
+
# @param decode_data [boolean] If true, register the formatter's
|
84
|
+
# {CloudEvents::Format#decode_data} method.
|
85
|
+
# @param encode_data [boolean] If true, register the formatter's
|
86
|
+
# {CloudEvents::Format#encode_data} method.
|
64
87
|
# @return [self]
|
65
88
|
#
|
66
|
-
def
|
67
|
-
|
68
|
-
|
89
|
+
def register_formatter_methods formatter,
|
90
|
+
decode_event: false,
|
91
|
+
encode_event: nil,
|
92
|
+
decode_data: false,
|
93
|
+
encode_data: false
|
94
|
+
@event_decoders << formatter if decode_event
|
95
|
+
if encode_event
|
96
|
+
formatters = @event_encoders[encode_event] ||= []
|
97
|
+
formatters << formatter unless formatters.include? formatter
|
98
|
+
end
|
99
|
+
@data_decoders << formatter if decode_data
|
100
|
+
@data_encoders << formatter if encode_data
|
69
101
|
self
|
70
102
|
end
|
71
103
|
|
104
|
+
##
|
105
|
+
# The name of the encoder to use if none is specified
|
106
|
+
# @return [String,nil]
|
107
|
+
#
|
108
|
+
attr_accessor :default_encoder_name
|
109
|
+
|
110
|
+
##
|
111
|
+
# Analyze a Rack environment hash and determine whether it is _probably_
|
112
|
+
# a CloudEvent. This is done by examining headers only, and does not read
|
113
|
+
# or parse the request body. The result is a best guess: false negatives or
|
114
|
+
# false positives are possible for edge cases, but the logic should
|
115
|
+
# generally detect canonically-formatted events.
|
116
|
+
#
|
117
|
+
# @param env [Hash] The Rack environment.
|
118
|
+
# @return [boolean] Whether the request is likely a CloudEvent.
|
119
|
+
#
|
120
|
+
def probable_event? env
|
121
|
+
return true if env["HTTP_CE_SPECVERSION"]
|
122
|
+
content_type = ContentType.new env["CONTENT_TYPE"].to_s
|
123
|
+
content_type.media_type == "application" &&
|
124
|
+
["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
|
125
|
+
end
|
126
|
+
|
72
127
|
##
|
73
128
|
# Decode an event from the given Rack environment hash. Following the
|
74
129
|
# CloudEvents spec, this chooses a handler based on the Content-Type of
|
75
130
|
# the request.
|
76
131
|
#
|
132
|
+
# Note that this method will read the body (i.e. `rack.input`) stream.
|
133
|
+
# If you need to access the body after calling this method, you will need
|
134
|
+
# to rewind the stream. To determine whether the request is a CloudEvent
|
135
|
+
# without reading the body, use {#probable_event?}.
|
136
|
+
#
|
77
137
|
# @param env [Hash] The Rack environment.
|
138
|
+
# @param allow_opaque [boolean] If true, returns opaque event objects if
|
139
|
+
# the input is not in a recognized format. If false, raises
|
140
|
+
# {CloudEvents::UnsupportedFormatError} in that case. Default is false.
|
78
141
|
# @param format_args [keywords] Extra args to pass to the formatter.
|
79
142
|
# @return [CloudEvents::Event] if the request includes a single structured
|
80
143
|
# or binary event.
|
81
144
|
# @return [Array<CloudEvents::Event>] if the request includes a batch of
|
82
145
|
# structured events.
|
83
|
-
# @
|
146
|
+
# @raise [CloudEvents::CloudEventsError] if an event could not be decoded
|
147
|
+
# from the request.
|
84
148
|
#
|
85
|
-
def
|
86
|
-
|
87
|
-
content_type = ContentType.new
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
when "cloudevents-batch"
|
95
|
-
input.set_encoding content_type.charset if content_type.charset
|
96
|
-
return decode_batched_content input.read, content_type.subtype_format, **format_args
|
97
|
-
end
|
149
|
+
def decode_event env, allow_opaque: false, **format_args
|
150
|
+
content_type_string = env["CONTENT_TYPE"]
|
151
|
+
content_type = ContentType.new content_type_string if content_type_string
|
152
|
+
content = read_with_charset env["rack.input"], content_type&.charset
|
153
|
+
result = decode_binary_content(content, content_type, env) ||
|
154
|
+
decode_structured_content(content, content_type, allow_opaque, **format_args)
|
155
|
+
if result.nil?
|
156
|
+
content_type_string = content_type_string ? content_type_string.inspect : "not present"
|
157
|
+
raise NotCloudEventError, "Content-Type is #{content_type_string}, and CE-SpecVersion is not present"
|
98
158
|
end
|
99
|
-
|
159
|
+
result
|
100
160
|
end
|
101
161
|
|
102
162
|
##
|
103
|
-
#
|
104
|
-
# passed the request body, if the Content-Type is of the form
|
105
|
-
# `application/cloudevents+format`.
|
163
|
+
# Encode an event or batch of events into HTTP headers and body.
|
106
164
|
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# @param format_args [keywords] Extra args to pass to the formatter.
|
110
|
-
# @return [CloudEvents::Event]
|
165
|
+
# You may provide an event, an array of events, or an opaque event. You may
|
166
|
+
# also specify what content mode and format to use.
|
111
167
|
#
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
end
|
120
|
-
|
121
|
-
##
|
122
|
-
# Decode a batch of events from the given content data. This should be
|
123
|
-
# passed the request body, if the Content-Type is of the form
|
124
|
-
# `application/cloudevents-batch+format`.
|
168
|
+
# The result is a two-element array where the first element is a headers
|
169
|
+
# list (as defined in the Rack specification) and the second is a string
|
170
|
+
# containing the HTTP body content. When using structured content mode, the
|
171
|
+
# headers list will contain only a `Content-Type` header and the body will
|
172
|
+
# contain the serialized event. When using binary mode, the header list
|
173
|
+
# will contain the serialized event attributes and the body will contain
|
174
|
+
# the serialized event data.
|
125
175
|
#
|
126
|
-
# @param
|
127
|
-
#
|
176
|
+
# @param event [CloudEvents::Event,Array<CloudEvents::Event>,CloudEvents::Event::Opaque]
|
177
|
+
# The event, batch, or opaque event.
|
178
|
+
# @param structured_format [boolean,String] If given, the data will be
|
179
|
+
# encoded in structured content mode. You can pass a string to select
|
180
|
+
# a format name, or pass `true` to use the default format. If set to
|
181
|
+
# `false` (the default), the data will be encoded in binary mode.
|
128
182
|
# @param format_args [keywords] Extra args to pass to the formatter.
|
129
|
-
# @return [Array
|
183
|
+
# @return [Array(headers,String)]
|
130
184
|
#
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
185
|
+
def encode_event event, structured_format: false, **format_args
|
186
|
+
if event.is_a? Event::Opaque
|
187
|
+
[{ "Content-Type" => event.content_type.to_s }, event.content]
|
188
|
+
elsif !structured_format
|
189
|
+
if event.is_a? ::Array
|
190
|
+
raise ::ArgumentError, "Encoding a batch requires structured_format"
|
191
|
+
end
|
192
|
+
encode_binary_content event, legacy_data_encode: false, **format_args
|
193
|
+
else
|
194
|
+
structured_format = default_encoder_name if structured_format == true
|
195
|
+
raise ::ArgumentError, "Format name not specified, and no default is set" unless structured_format
|
196
|
+
case event
|
197
|
+
when ::Array
|
198
|
+
encode_batched_content event, structured_format, **format_args
|
199
|
+
when Event
|
200
|
+
encode_structured_content event, structured_format, **format_args
|
201
|
+
else
|
202
|
+
raise ::ArgumentError, "Unknown event type: #{event.class}"
|
203
|
+
end
|
136
204
|
end
|
137
|
-
raise HttpContentError, "Unknown cloudevents batch format: #{format.inspect}"
|
138
205
|
end
|
139
206
|
|
140
207
|
##
|
141
|
-
# Decode an event from the given Rack environment
|
208
|
+
# Decode an event from the given Rack environment hash. Following the
|
209
|
+
# CloudEvents spec, this chooses a handler based on the Content-Type of
|
210
|
+
# the request.
|
142
211
|
#
|
143
|
-
# @
|
144
|
-
# @param content_type [CloudEvents::ContentType] the content type from the
|
145
|
-
# Rack environment.
|
146
|
-
# @return [CloudEvents::Event] if a CloudEvent could be decoded from the
|
147
|
-
# Rack environment.
|
148
|
-
# @return [nil] if the Rack environment does not indicate a CloudEvent
|
212
|
+
# @deprecated Will be removed in version 1.0. Use {#decode_event} instead.
|
149
213
|
#
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
end
|
168
|
-
Event.create spec_version: spec_version, attributes: attributes
|
214
|
+
# @param env [Hash] The Rack environment.
|
215
|
+
# @param format_args [keywords] Extra args to pass to the formatter.
|
216
|
+
# @return [CloudEvents::Event] if the request includes a single structured
|
217
|
+
# or binary event.
|
218
|
+
# @return [Array<CloudEvents::Event>] if the request includes a batch of
|
219
|
+
# structured events.
|
220
|
+
# @return [nil] if the request does not appear to be a CloudEvent.
|
221
|
+
# @raise [CloudEvents::CloudEventsError] if the request appears to be a
|
222
|
+
# CloudEvent but decoding failed.
|
223
|
+
#
|
224
|
+
def decode_rack_env env, **format_args
|
225
|
+
content_type_string = env["CONTENT_TYPE"]
|
226
|
+
content_type = ContentType.new content_type_string if content_type_string
|
227
|
+
content = read_with_charset env["rack.input"], content_type&.charset
|
228
|
+
env["rack.input"].rewind rescue nil
|
229
|
+
decode_binary_content(content, content_type, env, legacy_data_decode: true) ||
|
230
|
+
decode_structured_content(content, content_type, false, **format_args)
|
169
231
|
end
|
170
232
|
|
171
233
|
##
|
172
|
-
# Encode a single event
|
234
|
+
# Encode a single event in structured content mode in the given format.
|
173
235
|
#
|
174
|
-
#
|
175
|
-
# list (as defined in the Rack specification) and the second is a string
|
176
|
-
# containing the HTTP body content. The headers list will contain only
|
177
|
-
# one header, a `Content-Type` whose value is of the form
|
178
|
-
# `application/cloudevents+format`.
|
236
|
+
# @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
|
179
237
|
#
|
180
238
|
# @param event [CloudEvents::Event] The event.
|
181
|
-
# @param
|
239
|
+
# @param format_name [String] The format name.
|
182
240
|
# @param format_args [keywords] Extra args to pass to the formatter.
|
183
241
|
# @return [Array(headers,String)]
|
184
242
|
#
|
185
|
-
def encode_structured_content event,
|
186
|
-
|
187
|
-
|
188
|
-
content
|
189
|
-
|
243
|
+
def encode_structured_content event, format_name, **format_args
|
244
|
+
Array(@event_encoders[format_name]).reverse_each do |handler|
|
245
|
+
result = handler.encode_event event: event, **format_args
|
246
|
+
if result&.key?(:content) && result&.key?(:content_type)
|
247
|
+
return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
|
248
|
+
end
|
190
249
|
end
|
191
|
-
raise
|
250
|
+
raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
|
192
251
|
end
|
193
252
|
|
194
253
|
##
|
195
|
-
# Encode a batch of events
|
254
|
+
# Encode a batch of events in structured content mode in the given format.
|
196
255
|
#
|
197
|
-
#
|
198
|
-
# list (as defined in the Rack specification) and the second is a string
|
199
|
-
# containing the HTTP body content. The headers list will contain only
|
200
|
-
# one header, a `Content-Type` whose value is of the form
|
201
|
-
# `application/cloudevents-batch+format`.
|
256
|
+
# @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
|
202
257
|
#
|
203
|
-
# @param
|
204
|
-
# @param
|
258
|
+
# @param event_batch [Array<CloudEvents::Event>] The batch of events.
|
259
|
+
# @param format_name [String] The format name.
|
205
260
|
# @param format_args [keywords] Extra args to pass to the formatter.
|
206
261
|
# @return [Array(headers,String)]
|
207
262
|
#
|
208
|
-
def encode_batched_content
|
209
|
-
|
210
|
-
|
211
|
-
content
|
212
|
-
|
263
|
+
def encode_batched_content event_batch, format_name, **format_args
|
264
|
+
Array(@event_encoders[format_name]).reverse_each do |handler|
|
265
|
+
result = handler.encode_event event_batch: event_batch, **format_args
|
266
|
+
if result&.key?(:content) && result&.key?(:content_type)
|
267
|
+
return [{ "Content-Type" => result[:content_type].to_s }, result[:content]]
|
268
|
+
end
|
213
269
|
end
|
214
|
-
raise
|
270
|
+
raise ::ArgumentError, "Unknown format name: #{format_name.inspect}"
|
215
271
|
end
|
216
272
|
|
217
273
|
##
|
218
|
-
# Encode an event
|
274
|
+
# Encode an event in binary content mode.
|
219
275
|
#
|
220
|
-
#
|
221
|
-
# list (as defined in the Rack specification) and the second is a string
|
222
|
-
# containing the HTTP body content.
|
276
|
+
# @deprecated Will be removed in version 1.0. Use {#encode_event} instead.
|
223
277
|
#
|
224
278
|
# @param event [CloudEvents::Event] The event.
|
279
|
+
# @param format_args [keywords] Extra args to pass to the formatter.
|
225
280
|
# @return [Array(headers,String)]
|
226
281
|
#
|
227
|
-
def encode_binary_content event
|
282
|
+
def encode_binary_content event, legacy_data_encode: true, **format_args
|
228
283
|
headers = {}
|
229
|
-
body = nil
|
230
284
|
event.to_h.each do |key, value|
|
231
|
-
|
232
|
-
body = value
|
233
|
-
elsif key == "datacontenttype"
|
234
|
-
headers["Content-Type"] = value
|
235
|
-
else
|
285
|
+
unless ["data", "datacontenttype"].include? key
|
236
286
|
headers["CE-#{key}"] = percent_encode value
|
237
287
|
end
|
238
288
|
end
|
239
|
-
if
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
289
|
+
if legacy_data_encode
|
290
|
+
body = event.data
|
291
|
+
content_type = event.data_content_type&.to_s
|
292
|
+
case body
|
293
|
+
when ::String
|
294
|
+
content_type ||= string_content_type body
|
295
|
+
when nil
|
296
|
+
content_type = nil
|
297
|
+
else
|
298
|
+
body = ::JSON.dump body
|
299
|
+
content_type ||= "application/json; charset=#{body.encoding.name.downcase}"
|
300
|
+
end
|
247
301
|
else
|
248
|
-
body =
|
249
|
-
headers["Content-Type"] ||= "application/json; charset=#{body.encoding.name.downcase}"
|
302
|
+
body, content_type = encode_data event.spec_version, event.data, event.data_content_type, **format_args
|
250
303
|
end
|
304
|
+
headers["Content-Type"] = content_type.to_s if content_type
|
251
305
|
[headers, body]
|
252
306
|
end
|
253
307
|
|
254
308
|
##
|
255
309
|
# Decode a percent-encoded string to a UTF-8 string.
|
256
310
|
#
|
311
|
+
# @private
|
312
|
+
#
|
257
313
|
# @param str [String] Incoming ascii string from an HTTP header, with one
|
258
314
|
# cycle of percent-encoding.
|
259
315
|
# @return [String] Resulting decoded string in UTF-8.
|
260
316
|
#
|
261
317
|
def percent_decode str
|
318
|
+
str = str.gsub(/"((?:[^"\\]|\\.)*)"/) { ::Regexp.last_match(1).gsub(/\\(.)/, '\1') }
|
262
319
|
decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
|
263
320
|
decoded_str.force_encoding ::Encoding::UTF_8
|
264
321
|
end
|
@@ -268,6 +325,8 @@ module CloudEvents
|
|
268
325
|
# non-printing and non-ascii characters to result in an ASCII string
|
269
326
|
# suitable for setting as an HTTP header value.
|
270
327
|
#
|
328
|
+
# @private
|
329
|
+
#
|
271
330
|
# @param str [String] Incoming arbitrary string that can be represented
|
272
331
|
# in UTF-8.
|
273
332
|
# @return [String] Resulting encoded string in ASCII.
|
@@ -276,7 +335,7 @@ module CloudEvents
|
|
276
335
|
arr = []
|
277
336
|
utf_str = str.to_s.encode ::Encoding::UTF_8
|
278
337
|
utf_str.each_byte do |byte|
|
279
|
-
if byte >= 33 && byte <= 126 && byte != 37
|
338
|
+
if byte >= 33 && byte <= 126 && byte != 34 && byte != 37
|
280
339
|
arr << byte
|
281
340
|
else
|
282
341
|
hi = byte / 16
|
@@ -288,5 +347,120 @@ module CloudEvents
|
|
288
347
|
end
|
289
348
|
arr.pack "C*"
|
290
349
|
end
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def add_named_formatter collection, formatter, name
|
354
|
+
return unless name
|
355
|
+
formatters = collection[name] ||= []
|
356
|
+
formatters << formatter unless formatters.include? formatter
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
# Decode a single event from the given request body and content type in
|
361
|
+
# structured mode.
|
362
|
+
#
|
363
|
+
def decode_structured_content content, content_type, allow_opaque, **format_args
|
364
|
+
@event_decoders.reverse_each do |decoder|
|
365
|
+
result = decoder.decode_event content: content, content_type: content_type, **format_args
|
366
|
+
event = result[:event] || result[:event_batch] if result
|
367
|
+
return event if event
|
368
|
+
end
|
369
|
+
if content_type&.media_type == "application" &&
|
370
|
+
["cloudevents", "cloudevents-batch"].include?(content_type.subtype_base)
|
371
|
+
return Event::Opaque.new content, content_type if allow_opaque
|
372
|
+
raise UnsupportedFormatError, "Unknown cloudevents content type: #{content_type}"
|
373
|
+
end
|
374
|
+
nil
|
375
|
+
end
|
376
|
+
|
377
|
+
##
|
378
|
+
# Decode an event from the given Rack environment in binary content mode.
|
379
|
+
#
|
380
|
+
# TODO: legacy_data_decode is deprecated and can be removed when
|
381
|
+
# decode_rack_env is removed.
|
382
|
+
#
|
383
|
+
def decode_binary_content content, content_type, env, legacy_data_decode: false
|
384
|
+
spec_version = env["HTTP_CE_SPECVERSION"]
|
385
|
+
return nil unless spec_version
|
386
|
+
unless spec_version =~ /^0\.3|1(\.|$)/
|
387
|
+
raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
|
388
|
+
end
|
389
|
+
if legacy_data_decode
|
390
|
+
data = content
|
391
|
+
else
|
392
|
+
data, content_type = decode_data spec_version, content, content_type
|
393
|
+
end
|
394
|
+
attributes = { "spec_version" => spec_version, "data" => data }
|
395
|
+
attributes["data_content_type"] = content_type if content_type
|
396
|
+
omit_names = ["specversion", "spec_version", "data", "datacontenttype", "data_content_type"]
|
397
|
+
env.each do |key, value|
|
398
|
+
match = /^HTTP_CE_(\w+)$/.match key
|
399
|
+
next unless match
|
400
|
+
attr_name = match[1].downcase
|
401
|
+
attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
|
402
|
+
end
|
403
|
+
Event.create spec_version: spec_version, attributes: attributes
|
404
|
+
end
|
405
|
+
|
406
|
+
def decode_data spec_version, content, content_type, **format_args
|
407
|
+
@data_decoders.reverse_each do |handler|
|
408
|
+
result = handler.decode_data spec_version: spec_version,
|
409
|
+
content: content,
|
410
|
+
content_type: content_type,
|
411
|
+
**format_args
|
412
|
+
if result&.key?(:data) && result&.key?(:content_type)
|
413
|
+
return [result[:data], result[:content_type]]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
raise "Should not get here"
|
417
|
+
end
|
418
|
+
|
419
|
+
def encode_data spec_version, data_obj, content_type, **format_args
|
420
|
+
@data_encoders.reverse_each do |handler|
|
421
|
+
result = handler.encode_data spec_version: spec_version,
|
422
|
+
data: data_obj,
|
423
|
+
content_type: content_type,
|
424
|
+
**format_args
|
425
|
+
if result&.key?(:content) && result&.key?(:content_type)
|
426
|
+
return [result[:content], result[:content_type]]
|
427
|
+
end
|
428
|
+
end
|
429
|
+
raise "Should not get here"
|
430
|
+
end
|
431
|
+
|
432
|
+
def read_with_charset io, charset
|
433
|
+
return nil if io.nil?
|
434
|
+
str = io.read
|
435
|
+
if charset
|
436
|
+
begin
|
437
|
+
str.force_encoding charset
|
438
|
+
rescue ::ArgumentError
|
439
|
+
# Use binary for now if the charset is unrecognized
|
440
|
+
str.force_encoding ::Encoding::ASCII_8BIT
|
441
|
+
end
|
442
|
+
end
|
443
|
+
str
|
444
|
+
end
|
445
|
+
|
446
|
+
# @private
|
447
|
+
module DefaultDataFormat
|
448
|
+
# @private
|
449
|
+
def self.decode_data content: nil, content_type: nil, **_extra_kwargs
|
450
|
+
{ data: content, content_type: content_type }
|
451
|
+
end
|
452
|
+
|
453
|
+
# @private
|
454
|
+
def self.encode_data data: nil, content_type: nil, **_extra_kwargs
|
455
|
+
content = data.to_s
|
456
|
+
content_type ||=
|
457
|
+
if content.encoding == ::Encoding::ASCII_8BIT
|
458
|
+
"application/octet-stream"
|
459
|
+
else
|
460
|
+
"text/plain; charset=#{content.encoding.name.downcase}"
|
461
|
+
end
|
462
|
+
{ content: content, content_type: content_type }
|
463
|
+
end
|
464
|
+
end
|
291
465
|
end
|
292
466
|
end
|