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.
- checksums.yaml +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +201 -0
- data/README.md +218 -0
- data/bin/functions-framework +19 -0
- data/lib/functions_framework.rb +237 -0
- data/lib/functions_framework/cli.rb +113 -0
- data/lib/functions_framework/cloud_events.rb +143 -0
- data/lib/functions_framework/cloud_events/binary_content.rb +59 -0
- data/lib/functions_framework/cloud_events/content_type.rb +139 -0
- data/lib/functions_framework/cloud_events/event.rb +277 -0
- data/lib/functions_framework/cloud_events/json_structure.rb +88 -0
- data/lib/functions_framework/function.rb +75 -0
- data/lib/functions_framework/registry.rb +137 -0
- data/lib/functions_framework/server.rb +436 -0
- data/lib/functions_framework/testing.rb +244 -0
- data/lib/functions_framework/version.rb +21 -0
- metadata +187 -0
@@ -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
|