functions_framework 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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