functions_framework 0.1.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.
@@ -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