functions_framework 0.0.0 → 0.1.0

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,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
@@ -0,0 +1,88 @@
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 "base64"
16
+ require "json"
17
+
18
+ module FunctionsFramework
19
+ module CloudEvents
20
+ ##
21
+ # A content handler for the JSON structure and JSON batch format.
22
+ # See https://github.com/cloudevents/spec/blob/master/json-format.md
23
+ #
24
+ module JsonStructure
25
+ class << self
26
+ ##
27
+ # Decode an event from the given input string
28
+ #
29
+ # @param input [IO] An IO-like object providing a JSON-formatted string
30
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType]
31
+ # the content type
32
+ # @return [FunctionsFramework::CloudEvents::Event]
33
+ #
34
+ def decode_structured_content input, content_type
35
+ input = input.read if input.respond_to? :read
36
+ charset = content_type.charset
37
+ input = input.encode charset if charset
38
+ structure = ::JSON.parse input
39
+ decode_hash_structure structure
40
+ end
41
+
42
+ ##
43
+ # Decode a batch of events from the given input string
44
+ #
45
+ # @param input [IO] An IO-like object providing a JSON-formatted string
46
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType]
47
+ # the content type
48
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
49
+ #
50
+ def decode_batched_content input, content_type
51
+ input = input.read if input.respond_to? :read
52
+ charset = content_type.charset
53
+ input = input.encode charset if charset
54
+ structure_array = Array(::JSON.parse(input))
55
+ structure_array.map { |structure| decode_hash_structure structure }
56
+ end
57
+
58
+ ##
59
+ # Decode a single event from a hash data structure with keys and types
60
+ # conforming to the JSON event format
61
+ #
62
+ # @param structure [Hash] Input hash
63
+ # @return [FunctionsFramework::CloudEvents::Event]
64
+ #
65
+ def decode_hash_structure structure
66
+ data =
67
+ if structure.key? "data_base64"
68
+ ::Base64.decode64 structure["data_base64"]
69
+ else
70
+ structure["data"]
71
+ end
72
+ spec_version = structure["specversion"]
73
+ raise "Unrecognized specversion: #{spec_version}" unless spec_version == "1.0"
74
+ Event.new \
75
+ id: structure["id"],
76
+ source: structure["source"],
77
+ type: structure["type"],
78
+ spec_version: spec_version,
79
+ data: data,
80
+ data_content_type: structure["datacontenttype"],
81
+ data_schema: structure["dataschema"],
82
+ subject: structure["subject"],
83
+ time: structure["time"]
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,143 @@
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 "functions_framework/cloud_events/binary_content"
16
+ require "functions_framework/cloud_events/content_type"
17
+ require "functions_framework/cloud_events/event"
18
+
19
+ module FunctionsFramework
20
+ ##
21
+ # CloudEvents implementation.
22
+ #
23
+ # This is a Ruby implementation of the [CloudEvents](https://cloudevents.io)
24
+ # [1.0 specification](https://github.com/cloudevents/spec/blob/master/spec.md).
25
+ # It provides for unmarshaling of events from Rack environment data from
26
+ # binary (i.e. header-based) format, as well as structured (body-based) and
27
+ # batch formats. A standard JSON structure parser is included. It is also
28
+ # possible to register handlers for other formats.
29
+ #
30
+ # TODO: Unmarshaling of events is implemented, but marshaling is not.
31
+ #
32
+ module CloudEvents
33
+ @structured_formats = {}
34
+ @batched_formats = {}
35
+
36
+ class << self
37
+ ##
38
+ # Register a handler for the given structured format.
39
+ # The handler object must respond to the method
40
+ # `#decode_structured_content`. See
41
+ # {FunctionsFramework::CloudEvents::JsonStructure} for an example.
42
+ #
43
+ # @param format [String] The subtype format that should be handled by
44
+ # this handler
45
+ # @param handler [#decode_structured_content] The handler object
46
+ # @return [self]
47
+ #
48
+ def register_structured_format format, handler
49
+ handlers = @structured_formats[format.to_s.strip.downcase] ||= []
50
+ handlers << handler unless handlers.include? handler
51
+ self
52
+ end
53
+
54
+ ##
55
+ # Register a handler for the given batched format.
56
+ # The handler object must respond to the method
57
+ # `#decode_batched_content`. See
58
+ # {FunctionsFramework::CloudEvents::JsonStructure} for an example.
59
+ #
60
+ # @param format [String] The subtype format that should be handled by
61
+ # this handler
62
+ # @param handler [#decode_batched_content] The handler object
63
+ # @return [self]
64
+ #
65
+ def register_batched_format format, handler
66
+ handlers = @batched_formats[format.to_s.strip.downcase] ||= []
67
+ handlers << handler unless handlers.include? handler
68
+ self
69
+ end
70
+
71
+ ##
72
+ # Decode an event from the given Rack environment hash. Following the
73
+ # CloudEvents spec, this chooses a handler based on the Content-Type of
74
+ # the request.
75
+ #
76
+ # @param env [Hash] The Rack environment
77
+ # @return [FunctionsFramework::CloudEvents::Event] if the request
78
+ # includes a single structured or binary event
79
+ # @return [Array<FunctionsFramework::CloudEvents::Event>] if the request
80
+ # includes a batch of structured events
81
+ #
82
+ def decode_rack_env env
83
+ content_type_header = env["CONTENT_TYPE"]
84
+ raise "Missing content-type header" unless content_type_header
85
+ content_type = ContentType.new content_type_header
86
+ if content_type.media_type == "application"
87
+ case content_type.subtype_prefix
88
+ when "cloudevents"
89
+ return decode_structured_content env["rack.input"], content_type
90
+ when "cloudevents-batch"
91
+ return decode_batched_content env["rack.input"], content_type
92
+ end
93
+ end
94
+ BinaryContent.decode_rack_env env, content_type
95
+ end
96
+
97
+ ##
98
+ # Decode a single event from the given content data. This should be
99
+ # passed the request body, if the Content-Type is of the form
100
+ # `application/cloudevents+format`.
101
+ #
102
+ # @param input [IO] An IO-like object providing the content
103
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType] the
104
+ # content type
105
+ # @return [FunctionsFramework::CloudEvents::Event]
106
+ #
107
+ def decode_structured_content input, content_type
108
+ handlers = @structured_formats[content_type.subtype_format] || []
109
+ handlers.reverse_each do |handler|
110
+ event = handler.decode_structured_content input, content_type
111
+ return event if event
112
+ end
113
+ raise "Unknown cloudevents format: #{content_type.subtype_format.inspect}"
114
+ end
115
+
116
+ ##
117
+ # Decode a batch of events from the given content data. This should be
118
+ # passed the request body, if the Content-Type is of the form
119
+ # `application/cloudevents-batch+format`.
120
+ #
121
+ # @param input [IO] An IO-like object providing the content
122
+ # @param content_type [FunctionsFramework::CloudEvents::ContentType] the
123
+ # content type
124
+ # @return [Array<FunctionsFramework::CloudEvents::Event>]
125
+ #
126
+ def decode_batched_content input, content_type
127
+ handlers = @batched_formats[content_type.subtype_format] || []
128
+ handlers.reverse_each do |handler|
129
+ events = handler.decode_batched_content input, content_type
130
+ return events if events
131
+ end
132
+ raise "Unknown cloudevents batch format: #{content_type.subtype_format.inspect}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ require "functions_framework/cloud_events/json_structure"
139
+
140
+ FunctionsFramework::CloudEvents.register_structured_format \
141
+ "json", FunctionsFramework::CloudEvents::JsonStructure
142
+ FunctionsFramework::CloudEvents.register_batched_format \
143
+ "json", FunctionsFramework::CloudEvents::JsonStructure
@@ -0,0 +1,75 @@
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
+ ##
17
+ # Representation of a function.
18
+ #
19
+ # A function has a name, a type, and a code definition.
20
+ #
21
+ class Function
22
+ ##
23
+ # Create a new function definition.
24
+ #
25
+ # @param name [String] The function name
26
+ # @param type [Symbol] The type of function. Valid types are
27
+ # `:http`, `:event`, and `:cloud_event`.
28
+ # @param block [Proc] The function code as a proc
29
+ #
30
+ def initialize name, type, &block
31
+ @name = name
32
+ @type = type
33
+ @block = block
34
+ end
35
+
36
+ ##
37
+ # @return [String] The function name
38
+ #
39
+ attr_reader :name
40
+
41
+ ##
42
+ # @return [Symbol] The function type
43
+ #
44
+ attr_reader :type
45
+
46
+ ##
47
+ # @return [Proc] The function code as a proc
48
+ #
49
+ attr_reader :block
50
+
51
+ ##
52
+ # Call the function. You must pass an argument appropriate to the type
53
+ # of function.
54
+ #
55
+ # * A `:http` type function takes a `Rack::Request` argument, and returns
56
+ # a Rack response type. See {FunctionsFramework::Registry.add_http}.
57
+ # * A `:event` or `:cloud_event` type function takes a
58
+ # {FunctionsFramework::CloudEvents::Event} argument, and does not
59
+ # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
60
+ # Note that for an `:event` type function, the passed event argument is
61
+ # split into two arguments when passed to the underlying block.
62
+ #
63
+ # @param argument [Rack::Request,FunctionsFramework::CloudEvents::Event]
64
+ # @return [Object]
65
+ #
66
+ def call argument
67
+ case type
68
+ when :event
69
+ block.call argument.data, argument
70
+ else
71
+ block.call argument
72
+ end
73
+ end
74
+ end
75
+ end