functions_framework 0.3.1 → 0.4.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.
@@ -24,7 +24,9 @@ module FunctionsFramework
24
24
  # It supports binary (i.e. header-based) HTTP content, as well as structured
25
25
  # (body-based) content that can delegate to formatters such as JSON.
26
26
  #
27
- # See https://github.com/cloudevents/spec/blob/master/http-protocol-binding.md
27
+ # Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format.
28
+ # See https://github.com/cloudevents/spec/blob/v0.3/http-transport-binding.md
29
+ # and https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md.
28
30
  #
29
31
  class HttpBinding
30
32
  ##
@@ -101,7 +103,7 @@ module FunctionsFramework
101
103
  content_type = ContentType.new content_type_header if content_type_header
102
104
  input = env["rack.input"]
103
105
  if input && content_type&.media_type == "application"
104
- case content_type.subtype_prefix
106
+ case content_type.subtype_base
105
107
  when "cloudevents"
106
108
  input.set_encoding content_type.charset if content_type.charset
107
109
  return decode_structured_content input.read, content_type.subtype_format, **format_args
@@ -177,7 +179,7 @@ module FunctionsFramework
177
179
  match = /^HTTP_CE_(\w+)$/.match key
178
180
  next unless match
179
181
  attr_name = match[1].downcase
180
- attributes[attr_name] = value unless omit_names.include? attr_name
182
+ attributes[attr_name] = percent_decode value unless omit_names.include? attr_name
181
183
  end
182
184
  Event.create spec_version: spec_version, attributes: attributes
183
185
  end
@@ -248,7 +250,7 @@ module FunctionsFramework
248
250
  elsif key == "datacontenttype"
249
251
  headers["Content-Type"] = value
250
252
  else
251
- headers["CE-#{key}"] = value
253
+ headers["CE-#{key}"] = percent_encode value
252
254
  end
253
255
  end
254
256
  if body.is_a? ::String
@@ -265,6 +267,44 @@ module FunctionsFramework
265
267
  end
266
268
  [headers, body]
267
269
  end
270
+
271
+ ##
272
+ # Decode a percent-encoded string to a UTF-8 string.
273
+ #
274
+ # @param str [String] Incoming ascii string from an HTTP header, with one
275
+ # cycle of percent-encoding.
276
+ # @return [String] Resulting decoded string in UTF-8.
277
+ #
278
+ def percent_decode str
279
+ decoded_str = str.gsub(/%[0-9a-fA-F]{2}/) { |m| [m[1..-1].to_i(16)].pack "C" }
280
+ decoded_str.force_encoding ::Encoding::UTF_8
281
+ end
282
+
283
+ ##
284
+ # Transcode an arbitrarily-encoded string to UTF-8, then percent-encode
285
+ # non-printing and non-ascii characters to result in an ASCII string
286
+ # suitable for setting as an HTTP header value.
287
+ #
288
+ # @param str [String] Incoming arbitrary string that can be represented
289
+ # in UTF-8.
290
+ # @return [String] Resulting encoded string in ASCII.
291
+ #
292
+ def percent_encode str
293
+ arr = []
294
+ utf_str = str.to_s.encode ::Encoding::UTF_8
295
+ utf_str.each_byte do |byte|
296
+ if byte >= 33 && byte <= 126 && byte != 37
297
+ arr << byte
298
+ else
299
+ hi = byte / 16
300
+ hi = hi > 9 ? 55 + hi : 48 + hi
301
+ lo = byte % 16
302
+ lo = lo > 9 ? 55 + lo : 48 + lo
303
+ arr << 37 << hi << lo
304
+ end
305
+ end
306
+ arr.pack "C*"
307
+ end
268
308
  end
269
309
  end
270
310
  end
@@ -20,7 +20,9 @@ module FunctionsFramework
20
20
  ##
21
21
  # An implementation of JSON format and JSON batch format.
22
22
  #
23
- # See https://github.com/cloudevents/spec/blob/master/json-format.md
23
+ # Supports the CloudEvents 0.3 and CloudEvents 1.0 variants of this format.
24
+ # See https://github.com/cloudevents/spec/blob/v0.3/json-format.md and
25
+ # https://github.com/cloudevents/spec/blob/v1.0/json-format.md.
24
26
  #
25
27
  class JsonFormat
26
28
  ##
@@ -78,34 +80,39 @@ module FunctionsFramework
78
80
 
79
81
  ##
80
82
  # Decode a single event from a hash data structure with keys and types
81
- # conforming to the JSON event format.
83
+ # conforming to the JSON envelope.
82
84
  #
83
85
  # @param structure [Hash] An input hash.
84
86
  # @return [FunctionsFramework::CloudEvents::Event]
85
87
  #
86
88
  def decode_hash_structure structure
87
- if structure.key? "data_base64"
88
- structure = structure.dup
89
- structure["data"] = ::Base64.decode64 structure.delete "data_base64"
89
+ spec_version = structure["specversion"].to_s
90
+ case spec_version
91
+ when "0.3"
92
+ decode_hash_structure_v0 structure
93
+ when /^1(\.|$)/
94
+ decode_hash_structure_v1 structure
95
+ else
96
+ raise SpecVersionError, "Unrecognized specversion: #{spec_version}"
90
97
  end
91
- Event.create spec_version: structure["specversion"], attributes: structure
92
98
  end
93
99
 
94
100
  ##
95
101
  # Encode a single event to a hash data structure with keys and types
96
- # conforming to the JSON event format.
102
+ # conforming to the JSON envelope.
97
103
  #
98
104
  # @param event [FunctionsFramework::CloudEvents::Event] An input event.
99
105
  # @return [String] The hash structure.
100
106
  #
101
107
  def encode_hash_structure event
102
- structure = event.to_h
103
- data = structure["data"]
104
- if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
105
- structure.delete "data"
106
- structure["data_base64"] = ::Base64.encode64 data
108
+ case event
109
+ when Event::V0
110
+ encode_hash_structure_v0 event
111
+ when Event::V1
112
+ encode_hash_structure_v1 event
113
+ else
114
+ raise SpecVersionError, "Unrecognized specversion: #{event.spec_version}"
107
115
  end
108
- structure
109
116
  end
110
117
 
111
118
  private
@@ -117,6 +124,50 @@ module FunctionsFramework
117
124
  end
118
125
  result
119
126
  end
127
+
128
+ def decode_hash_structure_v0 structure
129
+ data = structure["data"]
130
+ content_type = structure["datacontenttype"]
131
+ if data.is_a?(::String) && content_type.is_a?(::String)
132
+ content_type = ContentType.new content_type
133
+ if content_type.subtype == "json" || content_type.subtype_format == "json"
134
+ structure = structure.dup
135
+ structure["data"] = ::JSON.parse data rescue data
136
+ structure["datacontenttype"] = content_type
137
+ end
138
+ end
139
+ Event::V0.new attributes: structure
140
+ end
141
+
142
+ def decode_hash_structure_v1 structure
143
+ if structure.key? "data_base64"
144
+ structure = structure.dup
145
+ structure["data"] = ::Base64.decode64 structure.delete "data_base64"
146
+ end
147
+ Event::V1.new attributes: structure
148
+ end
149
+
150
+ def encode_hash_structure_v0 event
151
+ structure = event.to_h
152
+ data = event.data
153
+ content_type = event.data_content_type
154
+ if data.is_a?(::String) && !content_type.nil?
155
+ if content_type.subtype == "json" || content_type.subtype_format == "json"
156
+ structure["data"] = ::JSON.parse data rescue data
157
+ end
158
+ end
159
+ structure
160
+ end
161
+
162
+ def encode_hash_structure_v1 event
163
+ structure = event.to_h
164
+ data = structure["data"]
165
+ if data.is_a?(::String) && data.encoding == ::Encoding::ASCII_8BIT
166
+ structure.delete "data"
167
+ structure["data_base64"] = ::Base64.encode64 data
168
+ end
169
+ structure
170
+ end
120
171
  end
121
172
  end
122
173
  end
@@ -16,22 +16,58 @@ module FunctionsFramework
16
16
  ##
17
17
  # Representation of a function.
18
18
  #
19
- # A function has a name, a type, and a code definition.
19
+ # A function has a name, a type, and an implementation.
20
+ #
21
+ # The implementation in general is an object that responds to the `call`
22
+ # method. For a function of type `:http`, the `call` method takes a single
23
+ # `Rack::Request` argument and returns one of various HTTP response types.
24
+ # See {FunctionsFramework::Registry.add_http}. For a function of type
25
+ # `:cloud_event`, the `call` method takes a single
26
+ # {FunctionsFramework::CloudEvents::Event CloudEvent} argument, and does not
27
+ # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
28
+ #
29
+ # If a callable object is provided directly, its `call` method is invoked for
30
+ # every function execution. Note that this means it may be called multiple
31
+ # times concurrently in separate threads.
32
+ #
33
+ # Alternately, the implementation may be provided as a class that should be
34
+ # instantiated to produce a callable object. If a class is provided, it should
35
+ # either subclass {FunctionsFramework::Function::CallBase} or respond to the
36
+ # same constructor interface, i.e. accepting arbitrary keyword arguments. A
37
+ # separate callable object will be instantiated from this class for every
38
+ # function invocation, so each instance will be used for only one invocation.
39
+ #
40
+ # Finally, an implementation can be provided as a block. If a block is
41
+ # provided, it will be recast as a `call` method in an anonymous subclass of
42
+ # {FunctionsFramework::Function::CallBase}. Thus, providing a block is really
43
+ # just syntactic sugar for providing a class. (This means, for example, that
44
+ # the `return` keyword will work within the block because it is treated as a
45
+ # method.)
20
46
  #
21
47
  class Function
22
48
  ##
23
49
  # Create a new function definition.
24
50
  #
25
51
  # @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
52
+ # @param type [Symbol] The type of function. Valid types are `:http` and
53
+ # `:cloud_event`.
54
+ # @param callable [Class,#call] A callable object or class.
55
+ # @param block [Proc] The function code as a block.
29
56
  #
30
- def initialize name, type, &block
57
+ def initialize name, type, callable = nil, &block
31
58
  @name = name
32
59
  @type = type
33
- @execution_context_class = Class.new do
34
- define_method :call, &block
60
+ @callable = @callable_class = nil
61
+ if callable.respond_to? :call
62
+ @callable = callable
63
+ elsif callable.is_a? ::Class
64
+ @callable_class = callable
65
+ elsif block_given?
66
+ @callable_class = ::Class.new CallBase do
67
+ define_method :call, &block
68
+ end
69
+ else
70
+ raise ::ArgumentError, "No callable given for function"
35
71
  end
36
72
  end
37
73
 
@@ -46,26 +82,48 @@ module FunctionsFramework
46
82
  attr_reader :type
47
83
 
48
84
  ##
49
- # Call the function. You must pass an argument appropriate to the type
50
- # of function.
85
+ # Get a callable for performing a function invocation. This will either
86
+ # return the singleton callable object, or instantiate a new callable from
87
+ # the configured class.
51
88
  #
52
- # * A `:http` type function takes a `Rack::Request` argument, and returns
53
- # a Rack response type. See {FunctionsFramework::Registry.add_http}.
54
- # * A `:cloud_event` type function takes a
55
- # {FunctionsFramework::CloudEvents::Event} argument, and does not
56
- # return a value. See {FunctionsFramework::Registry.add_cloud_event}.
89
+ # @param logger [::Logger] The logger for use by function executions. This
90
+ # may or may not be used by the callable.
91
+ # @return [#call]
57
92
  #
58
- # @param argument [Rack::Request,FunctionsFramework::CloudEvents::Event]
59
- # @return [Object]
93
+ def new_call logger: nil
94
+ return @callable unless @callable.nil?
95
+ logger ||= FunctionsFramework.logger
96
+ @callable_class.new logger: logger, function_name: name, function_type: type
97
+ end
98
+
99
+ ##
100
+ # A base class for a callable object that provides calling context.
60
101
  #
61
- def call argument
62
- execution_context = @execution_context_class.new
63
- case type
64
- when :event
65
- execution_context.call argument.data, argument
66
- else
67
- execution_context.call argument
102
+ # An object of this class is `self` while a function block is running.
103
+ #
104
+ class CallBase
105
+ ##
106
+ # Create a callable object with the given context.
107
+ #
108
+ # @param context [keywords] A set of context arguments. See {#context} for
109
+ # a list of keys that will generally be passed in. However,
110
+ # implementations should be prepared to accept any abritrary keys.
111
+ #
112
+ def initialize **context
113
+ @context = context
68
114
  end
115
+
116
+ ##
117
+ # A keyed hash of context information. Common context keys include:
118
+ #
119
+ # * **:logger** (`Logger`) A logger for use by this function call.
120
+ # * **:function_name** (`String`) The name of the running function.
121
+ # * **:function_type** (`Symbol`) The type of the running function,
122
+ # either `:http` or `:cloud_event`.
123
+ #
124
+ # @return [Hash]
125
+ #
126
+ attr_reader :context
69
127
  end
70
128
  end
71
129
  end
@@ -29,7 +29,7 @@ module FunctionsFramework
29
29
  #
30
30
  def decode_rack_env env
31
31
  content_type = CloudEvents::ContentType.new env["CONTENT_TYPE"]
32
- return nil unless content_type.media_type == "application" && content_type.subtype_prefix == "json"
32
+ return nil unless content_type.media_type == "application" && content_type.subtype_base == "json"
33
33
  input = read_input_json env["rack.input"], content_type.charset
34
34
  return nil unless input
35
35
  raw_context = input["context"] || input
@@ -75,21 +75,6 @@ module FunctionsFramework
75
75
  self
76
76
  end
77
77
 
78
- ##
79
- # This is an obsolete interface that defines an event function taking two
80
- # arguments (data and context) rather than one.
81
- #
82
- # @deprecated Use {Registry#add_cloud_event} instead.
83
- #
84
- def add_event name, &block
85
- name = name.to_s
86
- synchronize do
87
- raise ::ArgumentError, "Function already defined: #{name}" if @functions.key? name
88
- @functions[name] = Function.new name, :event, &block
89
- end
90
- self
91
- end
92
-
93
78
  ##
94
79
  # Add a CloudEvent function to the registry.
95
80
  #
@@ -47,7 +47,7 @@ module FunctionsFramework
47
47
  case function.type
48
48
  when :http
49
49
  HttpApp.new function, @config
50
- when :event, :cloud_event
50
+ when :cloud_event
51
51
  EventApp.new function, @config
52
52
  else
53
53
  raise "Unrecognized function type: #{function.type}"
@@ -384,10 +384,11 @@ module FunctionsFramework
384
384
  return notfound_response if excluded_path? env
385
385
  response =
386
386
  begin
387
- logger = env["rack.logger"] = @config.logger
387
+ logger = env["rack.logger"] ||= @config.logger
388
388
  request = ::Rack::Request.new env
389
389
  logger.info "FunctionsFramework: Handling HTTP #{request.request_method} request"
390
- @function.call request
390
+ calling_context = @function.new_call logger: logger
391
+ calling_context.call request
391
392
  rescue ::StandardError => e
392
393
  e
393
394
  end
@@ -406,7 +407,7 @@ module FunctionsFramework
406
407
 
407
408
  def call env
408
409
  return notfound_response if excluded_path? env
409
- logger = env["rack.logger"] = @config.logger
410
+ logger = env["rack.logger"] ||= @config.logger
410
411
  event = decode_event env
411
412
  response =
412
413
  case event
@@ -434,7 +435,8 @@ module FunctionsFramework
434
435
 
435
436
  def handle_cloud_event event, logger
436
437
  logger.info "FunctionsFramework: Handling CloudEvent"
437
- @function.call event
438
+ calling_context = @function.new_call logger: logger
439
+ calling_context.call event
438
440
  "ok"
439
441
  rescue ::StandardError => e
440
442
  e
@@ -87,7 +87,7 @@ module FunctionsFramework
87
87
  function = ::FunctionsFramework.global_registry[name]
88
88
  case function&.type
89
89
  when :http
90
- Testing.interpret_response { function.call request }
90
+ Testing.interpret_response { function.new_call.call request }
91
91
  when nil
92
92
  raise "Unknown function name #{name}"
93
93
  else
@@ -97,7 +97,7 @@ module FunctionsFramework
97
97
 
98
98
  ##
99
99
  # Call the given event function for testing. The underlying function must
100
- # be of type `:event` or `:cloud_event`.
100
+ # be of type :cloud_event`.
101
101
  #
102
102
  # @param name [String] The name of the function to call
103
103
  # @param event [FunctionsFramework::CloudEvets::Event] The event to send
@@ -106,8 +106,8 @@ module FunctionsFramework
106
106
  def call_event name, event
107
107
  function = ::FunctionsFramework.global_registry[name]
108
108
  case function&.type
109
- when :event, :cloud_event
110
- function.call event
109
+ when :cloud_event
110
+ function.new_call.call event
111
111
  nil
112
112
  when nil
113
113
  raise "Unknown function name #{name}"
@@ -17,5 +17,5 @@ module FunctionsFramework
17
17
  # Version of the Ruby Functions Framework
18
18
  # @return [String]
19
19
  #
20
- VERSION = "0.3.1".freeze
20
+ VERSION = "0.4.0".freeze
21
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: functions_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-27 00:00:00.000000000 Z
11
+ date: 2020-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: puma
@@ -162,6 +162,8 @@ files:
162
162
  - lib/functions_framework/cloud_events/content_type.rb
163
163
  - lib/functions_framework/cloud_events/errors.rb
164
164
  - lib/functions_framework/cloud_events/event.rb
165
+ - lib/functions_framework/cloud_events/event/field_interpreter.rb
166
+ - lib/functions_framework/cloud_events/event/v0.rb
165
167
  - lib/functions_framework/cloud_events/event/v1.rb
166
168
  - lib/functions_framework/cloud_events/http_binding.rb
167
169
  - lib/functions_framework/cloud_events/json_format.rb