functions_framework 0.3.1 → 0.4.0

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