functions_framework 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module FunctionsFramework
16
+ module CloudEvents
17
+ ##
18
+ # A content handler for the binary mode.
19
+ # See https://github.com/cloudevents/spec/blob/master/http-protocol-binding.md
20
+ #
21
+ module BinaryContent
22
+ class << self
23
+ ##
24
+ # Decode an event from the given Rack environment
25
+ #
26
+ # @param env [Hash] Rack environment hash
27
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType]
28
+ # the content type from the Rack environment
29
+ # @return [FunctionsFramework::CloudEvents::Event]
30
+ #
31
+ def decode_rack_env env, content_type
32
+ data = env["rack.input"]&.read
33
+ spec_version = interpret_header env, "HTTP_CE_SPECVERSION"
34
+ raise "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
35
+ Event.new \
36
+ id: interpret_header(env, "HTTP_CE_ID"),
37
+ source: interpret_header(env, "HTTP_CE_SOURCE"),
38
+ type: interpret_header(env, "HTTP_CE_TYPE"),
39
+ spec_version: spec_version,
40
+ data: data,
41
+ data_content_type: content_type,
42
+ data_schema: interpret_header(env, "HTTP_CE_DATASCHEMA"),
43
+ subject: interpret_header(env, "HTTP_CE_SUBJECT"),
44
+ time: interpret_header(env, "HTTP_CE_TIME")
45
+ end
46
+
47
+ private
48
+
49
+ def interpret_header env, key
50
+ escaped_value = env[key]
51
+ return nil if escaped_value.nil?
52
+ escaped_value.gsub(/%([0-9a-fA-F]{2})/) do
53
+ [$1.to_i(16)].pack "C" # rubocop:disable Style/PerlBackrefs
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,139 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module FunctionsFramework
16
+ module CloudEvents
17
+ ##
18
+ # A parsed content-type header.
19
+ #
20
+ # This object represents the information contained in a Content-Type,
21
+ # obtained by parsing the header according to RFC 2045.
22
+ #
23
+ # Case-insensitive fields, such as media_type and subtype, are normalized
24
+ # to lower case.
25
+ #
26
+ class ContentType
27
+ ##
28
+ # Parse the given header value
29
+ #
30
+ # @param string [String] Content-Type header value in RFC 2045 format
31
+ #
32
+ def initialize string
33
+ @string = string
34
+ # TODO: This handles simple cases but is not RFC-822 compliant.
35
+ sections = string.to_s.split ";"
36
+ media_type, subtype = sections.shift.split "/"
37
+ subtype_prefix, subtype_format = subtype.split "+"
38
+ @media_type = media_type.strip.downcase
39
+ @subtype = subtype.strip.downcase
40
+ @subtype_prefix = subtype_prefix.strip.downcase
41
+ @subtype_format = subtype_format&.strip&.downcase
42
+ @params = initialize_params sections
43
+ @canonical_string = "#{@media_type}/#{@subtype}" +
44
+ @params.map { |k, v| "; #{k}=#{v}" }.join
45
+ end
46
+
47
+ ##
48
+ # The original header content string
49
+ # @return [String]
50
+ #
51
+ attr_reader :string
52
+ alias to_s string
53
+
54
+ ##
55
+ # A "canonical" header content string with spacing and capitalization
56
+ # normalized.
57
+ # @return [String]
58
+ #
59
+ attr_reader :canonical_string
60
+
61
+ ##
62
+ # The media type.
63
+ # @return [String]
64
+ #
65
+ attr_reader :media_type
66
+
67
+ ##
68
+ # The entire content subtype (which could include an extension delimited
69
+ # by a plus sign)
70
+ # @return [String]
71
+ #
72
+ attr_reader :subtype
73
+
74
+ ##
75
+ # The portion of the content subtype before any plus sign.
76
+ # @return [String]
77
+ #
78
+ attr_reader :subtype_prefix
79
+
80
+ ##
81
+ # The portion of the content subtype after any plus sign, or nil if there
82
+ # is no plus sign in the subtype.
83
+ # @return [String,nil]
84
+ #
85
+ attr_reader :subtype_format
86
+
87
+ ##
88
+ # An array of parameters, each element as a two-element array of the
89
+ # parameter name and value.
90
+ # @return [Array<Array(String,String)>]
91
+ #
92
+ attr_reader :params
93
+
94
+ ##
95
+ # An array of values for the given parameter name
96
+ # @param key [String]
97
+ # @return [Array<String>]
98
+ #
99
+ def param_values key
100
+ key = key.downcase
101
+ @params.inject([]) { |a, (k, v)| key == k ? a << v : a }
102
+ end
103
+
104
+ ##
105
+ # The first value of the "charset" parameter, or nil if there is no
106
+ # charset.
107
+ # @return [String,nil]
108
+ #
109
+ def charset
110
+ param_values("charset").first
111
+ end
112
+
113
+ ## @private
114
+ def == other
115
+ other.is_a?(ContentType) && canonical_string == other.canonical_string
116
+ end
117
+ alias eql? ==
118
+
119
+ ## @private
120
+ def hash
121
+ canonical_string.hash
122
+ end
123
+
124
+ private
125
+
126
+ def initialize_params sections
127
+ params = sections.map do |s|
128
+ k, v = s.split "="
129
+ [k.strip.downcase, v.strip]
130
+ end
131
+ params.sort! do |(k1, v1), (k2, v2)|
132
+ a = k1 <=> k2
133
+ a.zero? ? v1 <=> v2 : a
134
+ end
135
+ params
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,277 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "date"
16
+ require "uri"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ ##
21
+ # A cloud event data type.
22
+ #
23
+ # This object represents both the event data and the context attributes.
24
+ # It is immutable. The data and attribute values can be retrieved but not
25
+ # modified. To obtain an event with modifications, use the {#with} method
26
+ # to create a copy with the desired changes.
27
+ #
28
+ # See https://github.com/cloudevents/spec/blob/master/spec.md for
29
+ # descriptions of the various attributes.
30
+ #
31
+ class Event
32
+ ##
33
+ # Create a new cloud event object with the given data and attributes.
34
+ #
35
+ # @param id [String] The required `id` field
36
+ # @param source [String,URI] The required `source` field
37
+ # @param type [String] The required `type` field
38
+ # @param spec_version [String] The required `specversion` field
39
+ # @param data [String,Boolean,Integer,Array,Hash] The optional `data`
40
+ # field
41
+ # @param data_content_type [String,FunctionsFramework::CloudEvents::ContentType]
42
+ # The optional `datacontenttype` field
43
+ # @param data_schema [String,URI] The optional `dataschema` field
44
+ # @param subject [String] The optional `subject` field
45
+ # @param time [String,DateTime] The optional `time` field
46
+ #
47
+ def initialize \
48
+ id:,
49
+ source:,
50
+ type:,
51
+ spec_version:,
52
+ data: nil,
53
+ data_content_type: nil,
54
+ data_schema: nil,
55
+ subject: nil,
56
+ time: nil
57
+ @id = interpret_string "id", id, true
58
+ @source, @source_string = interpret_uri "source", source, true
59
+ @type = interpret_string "type", type, true
60
+ @spec_version = interpret_string "spec_version", spec_version, true
61
+ @data = data
62
+ @data_content_type, @data_content_type_string =
63
+ interpret_content_type "data_content_type", data_content_type
64
+ @data_schema, @data_schema_string = interpret_uri "data_schema", data_schema
65
+ @subject = interpret_string "subject", subject
66
+ @time, @time_string = interpret_date_time "time", time
67
+ end
68
+
69
+ ##
70
+ # Create and return a copy of this event with the given changes. See the
71
+ # constructor for the parameters that can be passed.
72
+ #
73
+ # @param changes [keywords] See {#initialize} for a list of arguments.
74
+ # @return [FunctionFramework::CloudEvents::Event]
75
+ #
76
+ def with **changes
77
+ params = {
78
+ id: id,
79
+ source: source,
80
+ type: type,
81
+ spec_version: spec_version,
82
+ data: data,
83
+ data_content_type: data_content_type,
84
+ data_schema: data_schema,
85
+ subject: subject,
86
+ time: time
87
+ }
88
+ params.merge! changes
89
+ Event.new(**params)
90
+ end
91
+
92
+ ##
93
+ # The `id` field
94
+ # @return [String]
95
+ #
96
+ attr_reader :id
97
+
98
+ ##
99
+ # The `source` field as a `URI` object
100
+ # @return [URI]
101
+ #
102
+ attr_reader :source
103
+
104
+ ##
105
+ # The string representation of the `source` field
106
+ # @return [String]
107
+ #
108
+ attr_reader :source_string
109
+
110
+ ##
111
+ # The `type` field
112
+ # @return [String]
113
+ #
114
+ attr_reader :type
115
+
116
+ ##
117
+ # The `specversion` field
118
+ # @return [String]
119
+ #
120
+ attr_reader :spec_version
121
+ alias specversion spec_version
122
+
123
+ ##
124
+ # The event-specific data, or `nil` if there is no data.
125
+ #
126
+ # Data may be one of the following types:
127
+ # * Binary data, represented by a `String` using `ASCII-8BIT` encoding
128
+ # * A string in some other encoding such as `UTF-8` or `US-ASCII`
129
+ # * Any JSON data type, such as String, boolean, Integer, Array, or Hash
130
+ #
131
+ # @return [Object]
132
+ #
133
+ attr_reader :data
134
+
135
+ ##
136
+ # The optional `datacontenttype` field as a
137
+ # {FunctionsFramework::CloudEvents::ContentType} object, or `nil` if the
138
+ # field is absent
139
+ #
140
+ # @return [FunctionsFramework::CloudEvents::ContentType,nil]
141
+ #
142
+ attr_reader :data_content_type
143
+ alias datacontenttype data_content_type
144
+
145
+ ##
146
+ # The string representation of the optional `datacontenttype` field, or
147
+ # `nil` if the field is absent
148
+ #
149
+ # @return [String,nil]
150
+ #
151
+ attr_reader :data_content_type_string
152
+ alias datacontenttype_string data_content_type_string
153
+
154
+ ##
155
+ # The optional `dataschema` field as a `URI` object, or `nil` if the
156
+ # field is absent
157
+ #
158
+ # @return [URI,nil]
159
+ #
160
+ attr_reader :data_schema
161
+ alias dataschema data_schema
162
+
163
+ ##
164
+ # The string representation of the optional `dataschema` field, or `nil`
165
+ # if the field is absent
166
+ #
167
+ # @return [String,nil]
168
+ #
169
+ attr_reader :data_schema_string
170
+ alias dataschema_string data_schema_string
171
+
172
+ ##
173
+ # The optional `subject` field, or `nil` if the field is absent
174
+ #
175
+ # @return [String,nil]
176
+ #
177
+ attr_reader :subject
178
+
179
+ ##
180
+ # The optional `time` field as a `DateTime` object, or `nil` if the field
181
+ # is absent
182
+ #
183
+ # @return [DateTime,nil]
184
+ #
185
+ attr_reader :time
186
+
187
+ ##
188
+ # The string representation of the optional `time` field, or `nil` if the
189
+ # field is absent
190
+ #
191
+ # @return [String,nil]
192
+ #
193
+ attr_reader :time_string
194
+
195
+ ## @private
196
+ def == other
197
+ other.is_a?(ContentType) &&
198
+ id == other.id &&
199
+ source == other.source &&
200
+ type == other.type &&
201
+ spec_version == other.spec_version &&
202
+ data_content_type == other.data_content_type &&
203
+ data_schema == other.data_schema &&
204
+ subject == other.subject &&
205
+ time == other.time &&
206
+ data == other.data
207
+ end
208
+ alias eql? ==
209
+
210
+ ## @private
211
+ def hash
212
+ @hash ||=
213
+ [id, source, type, spec_version, data_content_type, data_schema, subject, time, data].hash
214
+ end
215
+
216
+ private
217
+
218
+ def interpret_string name, input, required = false
219
+ case input
220
+ when ::String
221
+ raise ::ArgumentError, "The #{name} field cannot be empty" if input.empty?
222
+ input
223
+ when nil
224
+ raise ::ArgumentError, "The #{name} field is required" if required
225
+ nil
226
+ else
227
+ raise ::ArgumentError, "Illegal type for #{name} field: #{input.inspect}"
228
+ end
229
+ end
230
+
231
+ def interpret_uri name, input, required = false
232
+ case input
233
+ when ::String
234
+ raise ::ArgumentError, "The #{name} field cannot be empty" if input.empty?
235
+ [::URI.parse(input), input]
236
+ when ::URI::Generic
237
+ [input, input.to_s]
238
+ when nil
239
+ raise ::ArgumentError, "The #{name} field is required" if required
240
+ [nil, nil]
241
+ else
242
+ raise ::ArgumentError, "Illegal type for #{name} field: #{input.inspect}"
243
+ end
244
+ end
245
+
246
+ def interpret_date_time name, input, required = false
247
+ case input
248
+ when ::String
249
+ raise ::ArgumentError, "The #{name} field cannot be empty" if input.empty?
250
+ [::DateTime.rfc3339(input), input]
251
+ when ::DateTime
252
+ [input, input.rfc3339]
253
+ when nil
254
+ raise ::ArgumentError, "The #{name} field is required" if required
255
+ [nil, nil]
256
+ else
257
+ raise ::ArgumentError, "Illegal type for #{name} field: #{input.inspect}"
258
+ end
259
+ end
260
+
261
+ def interpret_content_type name, input, required = false
262
+ case input
263
+ when ::String
264
+ raise ::ArgumentError, "The #{name} field cannot be empty" if input.empty?
265
+ [ContentType.new(input), input]
266
+ when ContentType
267
+ [input, input.to_s]
268
+ when nil
269
+ raise ::ArgumentError, "The #{name} field is required" if required
270
+ [nil, nil]
271
+ else
272
+ raise ::ArgumentError, "Illegal type for #{name} field: #{input.inspect}"
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end