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,244 @@
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 "json"
16
+
17
+ require "rack"
18
+
19
+ require "functions_framework"
20
+
21
+ module FunctionsFramework
22
+ ##
23
+ # Helpers for writing unit tests.
24
+ #
25
+ # Methods on this module can be called as module methods, or this module can
26
+ # be included in a test class.
27
+ #
28
+ # ## Example
29
+ #
30
+ # Suppose we have the following app that uses the functions framework:
31
+ #
32
+ # # app.rb
33
+ #
34
+ # require "functions_framework"
35
+ #
36
+ # FunctionsFramework.http "my-function" do |request|
37
+ # "Hello, world!"
38
+ # end
39
+ #
40
+ # The following is a test that could be run against that app:
41
+ #
42
+ # # test_app.rb
43
+ #
44
+ # require "minitest/autorun"
45
+ # require "functions_framework/testing"
46
+ #
47
+ # class MyTest < Minitest::Test
48
+ # # Make the testing methods available.
49
+ # include FunctionsFramework::Testing
50
+ #
51
+ # def test_my_function
52
+ # # Load app.rb and apply its functions within this block
53
+ # load_temporary "app.rb" do
54
+ # # Create a mock http (rack) request
55
+ # request = make_get_request "http://example.com"
56
+ #
57
+ # # Call the function and get a rack response
58
+ # response = call_http "my-function", request
59
+ #
60
+ # # Assert against the response
61
+ # assert_equal "Hello, world!", response.body.join
62
+ # end
63
+ # end
64
+ # end
65
+ #
66
+ module Testing
67
+ ##
68
+ # Load the given functions source for the duration of the given block,
69
+ # and restore the previous status afterward.
70
+ #
71
+ # @param path [String] File path to load
72
+ #
73
+ def load_temporary path
74
+ registry = ::FunctionsFramework::Registry.new
75
+ old_registry = ::FunctionsFramework.global_registry
76
+ ::FunctionsFramework.global_registry = registry
77
+ begin
78
+ ::Kernel.load path
79
+ yield
80
+ ensure
81
+ ::FunctionsFramework.global_registry = old_registry
82
+ end
83
+ end
84
+
85
+ ##
86
+ # Call the given HTTP function for testing. The underlying function must
87
+ # be of type `:http`.
88
+ #
89
+ # @param name [String] The name of the function to call
90
+ # @param request [Rack::Request] The Rack request to send
91
+ # @return [Rack::Response]
92
+ #
93
+ def call_http name, request
94
+ function = ::FunctionsFramework.global_registry[name]
95
+ case function&.type
96
+ when :http
97
+ Testing.interpret_response { function.call request }
98
+ when nil
99
+ raise "Unknown function name #{name}"
100
+ else
101
+ raise "Function #{name} is not an HTTP function"
102
+ end
103
+ end
104
+
105
+ ##
106
+ # Call the given event function for testing. The underlying function must
107
+ # be of type `:event` or `:cloud_event`.
108
+ #
109
+ # @param name [String] The name of the function to call
110
+ # @param event [FunctionsFramework::CloudEvets::Event] The event to send
111
+ # @return [nil]
112
+ #
113
+ def call_event name, event
114
+ function = ::FunctionsFramework.global_registry[name]
115
+ case function&.type
116
+ when :event, :cloud_event
117
+ function.call event
118
+ nil
119
+ when nil
120
+ raise "Unknown function name #{name}"
121
+ else
122
+ raise "Function #{name} is not a CloudEvent function"
123
+ end
124
+ end
125
+
126
+ ##
127
+ # Make a simple GET request, for passing to a function test.
128
+ #
129
+ # @param url [URI,String] The URL to get.
130
+ # @return [Rack::Request]
131
+ #
132
+ def make_get_request url, headers = []
133
+ env = Testing.build_standard_env URI(url), headers
134
+ env[::Rack::REQUEST_METHOD] = ::Rack::GET
135
+ ::Rack::Request.new env
136
+ end
137
+
138
+ ##
139
+ # Make a simple POST request, for passing to a function test.
140
+ #
141
+ # @param url [URI,String] The URL to post to.
142
+ # @param data [String] The body to post.
143
+ # @return [Rack::Request]
144
+ #
145
+ def make_post_request url, data, headers = []
146
+ env = Testing.build_standard_env URI(url), headers
147
+ env[::Rack::REQUEST_METHOD] = ::Rack::POST
148
+ env[::Rack::INPUT_STREAM] = ::StringIO.new data
149
+ ::Rack::Request.new env
150
+ end
151
+
152
+ ##
153
+ # Make a simple CloudEvent, for passing to a function test. The event data
154
+ # is required, but all other parameters are optional (i.e. a reasonable or
155
+ # random value will be generated if not provided).
156
+ #
157
+ # @param data [Object] The data
158
+ # @param id [String] Event ID (optional)
159
+ # @param source [String,URI] Event source (optional)
160
+ # @param type [String] Event type (optional)
161
+ # @param spec_version [String] Spec version (optional)
162
+ # @param data_content_type [String,FunctionsFramework::CloudEvents::ContentType]
163
+ # Content type for the data (optional)
164
+ # @param data_schema [String,URI] Data schema (optional)
165
+ # @param subject [String] Subject (optional)
166
+ # @param time [String,DateTime] Event timestamp (optional)
167
+ # @return [FunctionsFramework::CloudEvents::Event]
168
+ #
169
+ def make_cloud_event data,
170
+ id: nil, source: nil, type: nil, spec_version: nil,
171
+ data_content_type: nil, data_schema: nil, subject: nil, time: nil
172
+ id ||= "random-id-#{rand 100_000_000}"
173
+ source ||= "functions-framework-testing"
174
+ type ||= "com.example.test"
175
+ spec_version ||= "1.0"
176
+ CloudEvents::Event.new id: id, source: source, type: type, spec_version: spec_version,
177
+ data_content_type: data_content_type, data_schema: data_schema,
178
+ subject: subject, time: time, data: data
179
+ end
180
+
181
+ extend self
182
+
183
+ class << self
184
+ ## @private
185
+ def interpret_response
186
+ response =
187
+ begin
188
+ yield
189
+ rescue ::StandardError => e
190
+ e
191
+ end
192
+ case response
193
+ when ::Rack::Response
194
+ response
195
+ when ::Array
196
+ ::Rack::Response.new response[2], response[0], response[1]
197
+ when ::String
198
+ string_response response, "text/plain", 200
199
+ when ::Hash
200
+ json = ::JSON.dump response
201
+ string_response json, "application/json", 200
202
+ when ::StandardError
203
+ message = "#{response.class}: #{response.message}\n#{response.backtrace}\n"
204
+ string_response message, "text/plain", 500
205
+ else
206
+ raise "Unexpected response type: #{response.inspect}"
207
+ end
208
+ end
209
+
210
+ ## @private
211
+ def string_response string, content_type, status
212
+ headers = {
213
+ "Content-Type" => content_type,
214
+ "Content-Length" => string.bytesize
215
+ }
216
+ ::Rack::Response.new string, status, headers
217
+ end
218
+
219
+ ## @private
220
+ def build_standard_env url, headers
221
+ env = {
222
+ ::Rack::SCRIPT_NAME => "",
223
+ ::Rack::PATH_INFO => url.path,
224
+ ::Rack::QUERY_STRING => url.query,
225
+ ::Rack::SERVER_NAME => url.host,
226
+ ::Rack::SERVER_PORT => url.port,
227
+ ::Rack::RACK_URL_SCHEME => url.scheme,
228
+ ::Rack::RACK_VERSION => ::Rack::VERSION,
229
+ ::Rack::RACK_LOGGER => ::FunctionsFramework.logger,
230
+ ::Rack::RACK_INPUT => ::StringIO.new,
231
+ ::Rack::RACK_ERRORS => ::StringIO.new
232
+ }
233
+ headers.each do |header|
234
+ name, value = header.split ":"
235
+ next unless name && value
236
+ name = name.strip.upcase.tr "-", "_"
237
+ name = "HTTP_#{name}" unless ["CONTENT_TYPE", "CONTENT_LENGTH"].include? name
238
+ env[name] = value.strip
239
+ end
240
+ env
241
+ end
242
+ end
243
+ end
244
+ end
@@ -17,5 +17,5 @@ module FunctionsFramework
17
17
  # Version of the Ruby Functions Framework
18
18
  # @return [String]
19
19
  #
20
- VERSION = "0.0.0".freeze
20
+ VERSION = "0.1.0".freeze
21
21
  end
@@ -12,10 +12,226 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "logger"
16
+
17
+ require "functions_framework/cloud_events"
18
+ require "functions_framework/function"
19
+ require "functions_framework/registry"
15
20
  require "functions_framework/version"
16
21
 
17
22
  ##
18
- # The Functions Framework for Ruby
23
+ # The Functions Framework for Ruby.
24
+ #
25
+ # Functions Framework is an open source framework for writing lightweight,
26
+ # portable Ruby functions that run in a serverless environment. For general
27
+ # information about the Functions Framework, see
28
+ # https://github.com/GoogleCloudPlatform/functions-framework.
29
+ # To get started with the functions framework for Ruby, see
30
+ # https://github.com/GoogleCloudPlatform/functions-framework-ruby for basic
31
+ # examples.
32
+ #
33
+ # ## Inside the FunctionsFramework module
34
+ #
35
+ # The FunctionsFramework module includes the main entry points for the
36
+ # functions framework. Use the {FunctionsFramework.http},
37
+ # {FunctionsFramework.event}, or {FunctionsFramework.cloud_event} methods to
38
+ # define functions. To serve functions via a web service, invoke the
39
+ # `functions-framework` executable, or use the {FunctionsFramework.start} or
40
+ # {FunctionsFramework.run} methods.
41
+ #
42
+ # ## Internal modules
43
+ #
44
+ # Here is a roadmap to the internal modules in the Ruby functions framework.
45
+ #
46
+ # * {FunctionsFramework::CloudEvents} provides an implementation of the
47
+ # [CloudEvents](https://cloudevents.io) specification. In particular, if
48
+ # you define an event function, you will receive the event as a
49
+ # {FunctionsFramework::CloudEvents::Event} object.
50
+ # * {FunctionsFramework::CLI} is the implementation of the
51
+ # `functions-framework` executable. Most apps will not need to interact
52
+ # with this class directly.
53
+ # * {FunctionsFramework::Function} is the internal representation of a
54
+ # function, indicating the type of function (http or cloud event), the
55
+ # name of the function, and the block of code implementing it. Most apps
56
+ # do not need to interact with this class directly.
57
+ # * {FunctionsFramework::Registry} looks up functions by name. When you
58
+ # define a set of named functions, they are added to a registry, and when
59
+ # you start a server and specify the target function by name, it is looked
60
+ # up from the registry. Most apps do not need to interact with this class
61
+ # directly.
62
+ # * {FunctionsFramework::Server} is a web server that makes a function
63
+ # available via HTTP. It wraps the Puma web server and runs a specific
64
+ # {FunctionsFramework::Function}. Many apps can simply run the
65
+ # `functions-framework` executable to spin up a server. However, if you
66
+ # need closer control over your execution environment, you can use the
67
+ # {FunctionsFramework::Server} class to run a server. Note that, in most
68
+ # cases, it is easier to use the {FunctionsFramework.start} or
69
+ # {FunctionsFramework.run} wrapper methods rather than instantiate a
70
+ # {FunctionsFramework::Server} class directly.
71
+ # * {FunctionsFramework::Testing} provides helpers that are useful when
72
+ # writing unit tests for functions.
19
73
  #
20
74
  module FunctionsFramework
75
+ @global_registry = Registry.new
76
+ @logger = ::Logger.new ::STDERR
77
+ @logger.level = ::Logger::INFO
78
+
79
+ ##
80
+ # The default target function name. If you define a function without
81
+ # specifying a name, or run the framework without giving a target, this name
82
+ # is used.
83
+ #
84
+ # @return [String]
85
+ #
86
+ DEFAULT_TARGET = "function".freeze
87
+
88
+ ##
89
+ # The default source file path. The CLI loads functions from this file if no
90
+ # source file is given explicitly.
91
+ #
92
+ # @return [String]
93
+ #
94
+ DEFAULT_SOURCE = "./app.rb".freeze
95
+
96
+ class << self
97
+ ##
98
+ # The "global" registry that holds events defined by the
99
+ # {FunctionsFramework} class methods.
100
+ #
101
+ # @return [FunctionsFramework::Registry]
102
+ #
103
+ attr_accessor :global_registry
104
+
105
+ ##
106
+ # A "global" logger that is used by the framework's web server, and can
107
+ # also be used by functions.
108
+ #
109
+ # @return [Logger]
110
+ #
111
+ attr_accessor :logger
112
+
113
+ ##
114
+ # Define a function that response to HTTP requests.
115
+ #
116
+ # You must provide a name for the function, and a block that implemets the
117
+ # function. The block should take a single `Rack::Request` argument. It
118
+ # should return one of the following:
119
+ # * A standard 3-element Rack response array. See
120
+ # https://github.com/rack/rack/blob/master/SPEC
121
+ # * A `Rack::Response` object.
122
+ # * A simple String that will be sent as the response body.
123
+ # * A Hash object that will be encoded as JSON and sent as the response
124
+ # body.
125
+ #
126
+ # ## Example
127
+ #
128
+ # FunctionsFramework.http "my-function" do |request|
129
+ # "I received a request for #{request.url}"
130
+ # end
131
+ #
132
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
133
+ # @param block [Proc] The function code as a proc.
134
+ # @return [self]
135
+ #
136
+ def http name = DEFAULT_TARGET, &block
137
+ global_registry.add_http name, &block
138
+ self
139
+ end
140
+
141
+ ##
142
+ # Define a function that responds to CloudEvents.
143
+ #
144
+ # You must provide a name for the function, and a block that implemets the
145
+ # function. The block should take two arguments: the event _data_ and the
146
+ # event _context_. Any return value is ignored.
147
+ #
148
+ # The event data argument will be one of the following types:
149
+ # * A `String` (with encoding `ASCII-8BIT`) if the data is in the form of
150
+ # binary data. You may choose to perform additional interpretation of
151
+ # the binary data using information in the content type provided by the
152
+ # context argument.
153
+ # * Any data type that can be represented in JSON (i.e. `String`,
154
+ # `Integer`, `Array`, `Hash`, `true`, `false`, or `nil`) if the event
155
+ # came with a JSON payload. The content type may also be set in the
156
+ # context if the data is a String.
157
+ #
158
+ # The context argument will be of type {FunctionsFramework::CloudEvents::Event},
159
+ # and will contain CloudEvents context attributes such as `id` and `type`.
160
+ #
161
+ # See also {FunctionsFramework.cloud_event} which defines a function that
162
+ # takes a single argument of type {FunctionsFramework::CloudEvents::Event}.
163
+ #
164
+ # ## Example
165
+ #
166
+ # FunctionsFramework.event "my-function" do |data, context|
167
+ # FunctionsFramework.logger.info "Event data: #{data.inspect}"
168
+ # end
169
+ #
170
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
171
+ # @param block [Proc] The function code as a proc.
172
+ # @return [self]
173
+ #
174
+ def event name = DEFAULT_TARGET, &block
175
+ global_registry.add_event name, &block
176
+ self
177
+ end
178
+
179
+ ##
180
+ # Define a function that responds to CloudEvents.
181
+ #
182
+ # You must provide a name for the function, and a block that implemets the
183
+ # function. The block should take _one_ argument: the event object of type
184
+ # {FunctionsFramework::CloudEvents::Event}. Any return value is ignored.
185
+ #
186
+ # See also {FunctionsFramework.event} which creates a function that takes
187
+ # data and context as separate arguments.
188
+ #
189
+ # ## Example
190
+ #
191
+ # FunctionsFramework.cloud_event "my-function" do |event|
192
+ # FunctionsFramework.logger.info "Event data: #{event.data.inspect}"
193
+ # end
194
+ #
195
+ # @param name [String] The function name. Defaults to {DEFAULT_TARGET}.
196
+ # @param block [Proc] The function code as a proc.
197
+ # @return [self]
198
+ #
199
+ def cloud_event name = DEFAULT_TARGET, &block
200
+ global_registry.add_cloud_event name, &block
201
+ self
202
+ end
203
+
204
+ ##
205
+ # Start the functions framework server in the background. The server will
206
+ # look up the given target function name in the global registry.
207
+ #
208
+ # @param target [String] The name of the function to run
209
+ # @yield [FunctionsFramework::Server::Config] A config object that can be
210
+ # manipulated to configure the server.
211
+ # @return [FunctionsFramework::Server]
212
+ #
213
+ def start target, &block
214
+ require "functions_framework/server"
215
+ function = global_registry[target]
216
+ raise ::ArgumentError, "Undefined function: #{target.inspect}" if function.nil?
217
+ server = Server.new function, &block
218
+ server.respond_to_signals
219
+ server.start
220
+ end
221
+
222
+ ##
223
+ # Run the functions framework server and block until it stops. The server
224
+ # will look up the given target function name in the global registry.
225
+ #
226
+ # @param target [String] The name of the function to run
227
+ # @yield [FunctionsFramework::Server::Config] A config object that can be
228
+ # manipulated to configure the server.
229
+ # @return [self]
230
+ #
231
+ def run target, &block
232
+ server = start target, &block
233
+ server.wait_until_stopped
234
+ self
235
+ end
236
+ end
21
237
  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.0.0
4
+ version: 0.1.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-01-12 00:00:00.000000000 Z
11
+ date: 2020-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: puma
@@ -139,16 +139,27 @@ dependencies:
139
139
  description: The Functions Framework implementation for Ruby.
140
140
  email:
141
141
  - dazuma@google.com
142
- executables: []
142
+ executables:
143
+ - functions-framework
143
144
  extensions: []
144
145
  extra_rdoc_files: []
145
146
  files:
146
147
  - ".yardopts"
147
148
  - CHANGELOG.md
148
- - CONTRIBUTING.md
149
149
  - LICENSE
150
150
  - README.md
151
+ - bin/functions-framework
151
152
  - lib/functions_framework.rb
153
+ - lib/functions_framework/cli.rb
154
+ - lib/functions_framework/cloud_events.rb
155
+ - lib/functions_framework/cloud_events/binary_content.rb
156
+ - lib/functions_framework/cloud_events/content_type.rb
157
+ - lib/functions_framework/cloud_events/event.rb
158
+ - lib/functions_framework/cloud_events/json_structure.rb
159
+ - lib/functions_framework/function.rb
160
+ - lib/functions_framework/registry.rb
161
+ - lib/functions_framework/server.rb
162
+ - lib/functions_framework/testing.rb
152
163
  - lib/functions_framework/version.rb
153
164
  homepage: https://github.com/GoogleCloudPlatform/functions-framework-ruby
154
165
  licenses:
data/CONTRIBUTING.md DELETED
@@ -1,32 +0,0 @@
1
- # Contributing
2
-
3
- Want to contribute? Great! First, read this page (including the small print at the end).
4
-
5
- ### Before you contribute
6
-
7
- Before we can use your code, you must sign the
8
- [Google Individual Contributor License Agreement]
9
- (https://cla.developers.google.com/about/google-individual)
10
- (CLA), which you can do online. The CLA is necessary mainly because you own the
11
- copyright to your changes, even after your contribution becomes part of our
12
- codebase, so we need your permission to use and distribute your code. We also
13
- need to be sure of various other things—for instance that you'll tell us if you
14
- know that your code infringes on other people's patents. You don't have to sign
15
- the CLA until after you've submitted your code for review and a member has
16
- approved it, but you must do it before we can put your code into our codebase.
17
- Before you start working on a larger contribution, you should get in touch with
18
- us first through the issue tracker with your idea so that we can help out and
19
- possibly guide you. Coordinating up front makes it much easier to avoid
20
- frustration later on.
21
-
22
- ### Code reviews
23
-
24
- All submissions, including submissions by project members, require review. We
25
- use Github pull requests for this purpose.
26
-
27
- ### The small print
28
-
29
- Contributions made by corporations are covered by a different agreement than
30
- the one above, the
31
- [Software Grant and Corporate Contributor License Agreement]
32
- (https://cla.developers.google.com/about/google-corporate).