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,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).