phaedra 0.2.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +172 -15
- data/example/Gemfile +4 -0
- data/example/api/simple.rb +10 -0
- data/example/api/the-time.rb +0 -1
- data/lib/phaedra/base.rb +6 -2
- data/lib/phaedra/middleware.rb +2 -0
- data/lib/phaedra/middleware/not_found.rb +21 -0
- data/lib/phaedra/middleware/static.rb +26 -0
- data/lib/phaedra/rack_app.rb +4 -4
- data/lib/phaedra/version.rb +1 -1
- data/phaedra.gemspec +1 -1
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47c1b79ae715e83f3dd098fb9d0d29c5d89299f96c01d084f1cb44573f216022
|
4
|
+
data.tar.gz: e716ca8ef384fdeadd773485042099d7bf7840b007e636588ce6fb28e40ab4b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf41193d15f6bc3678ebccd9239e7185479b3bd4029f593887202ca3e88b02529702688f64f2b373c90e54fed766b264a9f12c2a8a39e4c90039d0d708d9b89a
|
7
|
+
data.tar.gz: 23686c007d52d61763ac393ced71d6485dbf3b3c45cc153828950f6e9c1e8018934834a0dfb90a7908d3357334c3542b34e74629d9774663607f86b21080fb94
|
data/README.md
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# Phaedra: Serverless Ruby Functions
|
2
2
|
|
3
|
-
|
3
|
+
Phaedra is a web microframework for writing serverless Ruby functions. They are isolated pieces of logic which respond to HTTP requests (GET, POST, etc.) and typically get mounted at a particular URL path. They can be tested locally and deployed to a supported serverless hosting platform or to any [Rack-compatible web server](https://github.com/rack/rack).
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
Serverless compatibility is presently focused on [Vercel](https://vercel.com), but there are likely additional platforms we'll be adding support for in the future (OpenFaaS, Fission, etc.).
|
5
|
+
Serverless compatibility is presently focused on [Vercel](https://vercel.com) and [OpenFaaS](https://openfaas.com), but there are likely additional platforms we'll be adding support for in the future.
|
8
6
|
|
9
7
|
## Installation
|
10
8
|
|
@@ -32,7 +30,7 @@ Functions are single Ruby files which respond to a URL path (aka `/api/path/to/f
|
|
32
30
|
|
33
31
|
Functions are written as subclasses of `Phaedra::Base` using the name `PhaedraFunction`. The `params` argument is a Hash containing the parsed contents of the incoming query string, form data, or body JSON. The response object returned by your function is typically a Hash which will be transformed into JSON output automatically, but it can also be plain text.
|
34
32
|
|
35
|
-
Some platforms require the function class name to be `Handler`, so you can put that at the bottom of your file for full compatibility.
|
33
|
+
Some platforms such as Vercel require the function class name to be `Handler`, so you can put that at the bottom of your file for full compatibility.
|
36
34
|
|
37
35
|
Here's a basic example:
|
38
36
|
|
@@ -55,7 +53,7 @@ Your function can support `get`, `post`, `put`, `patch`, and `delete` methods wh
|
|
55
53
|
|
56
54
|
Each method is provided access to `request` and `response` objects. If your function was directly instantiated by WEBrick, those will be `WEBrick::HTTPRequest` and `WEBrick::HTTPResponse` respectively. If your function was instantiated by Rack, those will be `Phaedra::Request` (a thin wrapper around `Rack::Request`) and `Rack::Response` respectively.
|
57
55
|
|
58
|
-
|
56
|
+
### Callbacks
|
59
57
|
|
60
58
|
Functions can define `action` callbacks:
|
61
59
|
|
@@ -79,9 +77,137 @@ end
|
|
79
77
|
|
80
78
|
You can modify the `request` object in a `before_action` callback to perform setup tasks before the actions are executed, or you can modify `response` in a `after_action` to further process the response.
|
81
79
|
|
82
|
-
|
80
|
+
### Shared Code You Only Want to Run Once
|
81
|
+
|
82
|
+
You can use `require_relative` to load and execute shared Ruby code from another folder, say `lib`. This is particularly useful when setting up a database connection or performing expensive operations you only want to do once, rather than for every request.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
# api/run-it-once.rb
|
86
|
+
|
87
|
+
require "phaedra"
|
88
|
+
require_relative "../lib/shared_code"
|
89
|
+
|
90
|
+
class PhaedraFunction < Phaedra::Base
|
91
|
+
def get(params)
|
92
|
+
"Run it once! #{SharedCode.run_once} / #{Time.now}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
# lib/shared_code.rb
|
99
|
+
|
100
|
+
module SharedCode
|
101
|
+
def self.run_once
|
102
|
+
@one_time ||= Time.now
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Now each time you invoke the function at `/api/run-it-once`, the first timestamp will never change until the next redeployment.
|
108
|
+
|
109
|
+
**NOTE:** When running in a Rack-based configuration (see below), Ruby's `load` method is invoked for every request to any Phaedra function. This means Ruby has to parse and compile the code in your function each time. For small functions this happens extremely quickly, but if you find yourself writing a large function and seeing some performance slowdowns, consider extracting most of the function code to additional Ruby files and use the `require_relative` technique as mentioned above. The Ruby code in those required files will only be compiled once and all classes/modules/etc. will be saved in memory until the next redeployment.
|
110
|
+
|
111
|
+
## Deployment
|
112
|
+
|
113
|
+
### Vercel
|
114
|
+
|
115
|
+
All you have to do is create a static site repo ([Bridgetown](https://www.bridgetownrb.com), Jekyll, Middleman, etc.) with an `api` folder and Vercel will automatically set up the serverless functions every time there's a new branch or production deployment. As mentioned above, you'll need to ensure you add `Handler = PhaedraFunction` to the bottom of each Ruby function.
|
116
|
+
|
117
|
+
### OpenFaaS
|
118
|
+
|
119
|
+
We recommend using OpenFaaS' dockerfile template so you can define your own `Dockerfile` to book Rack + Phaedra using the Puma web server. This also allows you to customize the Docker image configuration to install and configure other tools as necessary.
|
120
|
+
|
121
|
+
First, make sure you've pulled down the template:
|
122
|
+
|
123
|
+
```sh
|
124
|
+
faas-cli template store pull dockerfile
|
125
|
+
```
|
126
|
+
|
127
|
+
Then add a `Dockerfile` to your OpenFaaS project's function folder (e.g., `testphaedra`):
|
128
|
+
|
129
|
+
```dockerfile
|
130
|
+
# testphaedra/Dockerfile
|
131
|
+
|
132
|
+
FROM openfaas/of-watchdog:0.7.7 as watchdog
|
133
|
+
|
134
|
+
FROM ruby:2.6.6-slim-stretch
|
135
|
+
|
136
|
+
COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
|
137
|
+
RUN chmod +x /usr/bin/fwatchdog
|
138
|
+
|
139
|
+
ARG ADDITIONAL_PACKAGE
|
140
|
+
RUN apt-get update \
|
141
|
+
&& apt-get install -qy --no-install-recommends build-essential ${ADDITIONAL_PACKAGE}
|
142
|
+
|
143
|
+
WORKDIR /home/app
|
144
|
+
|
145
|
+
# Use cache layer for Gemfile
|
146
|
+
COPY Gemfile .
|
147
|
+
RUN bundle install
|
148
|
+
RUN gem install puma -N
|
149
|
+
|
150
|
+
# Copy over the rest
|
151
|
+
COPY . .
|
83
152
|
|
84
|
-
|
153
|
+
# Create a non-root user
|
154
|
+
RUN addgroup --system app \
|
155
|
+
&& adduser --system --ingroup app app
|
156
|
+
RUN chown app:app -R /home/app
|
157
|
+
USER app
|
158
|
+
|
159
|
+
# Run Puma as the server process
|
160
|
+
ENV fprocess="puma -p 5000"
|
161
|
+
|
162
|
+
EXPOSE 8080
|
163
|
+
|
164
|
+
HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1
|
165
|
+
|
166
|
+
ENV upstream_url="http://127.0.0.1:5000"
|
167
|
+
ENV mode="http"
|
168
|
+
|
169
|
+
CMD ["fwatchdog"]
|
170
|
+
```
|
171
|
+
|
172
|
+
Next add the `config.ru` file to boot Rack:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# testphaedra/config.ru
|
176
|
+
|
177
|
+
require "phaedra"
|
178
|
+
|
179
|
+
run Phaedra::RackApp.new
|
180
|
+
```
|
181
|
+
|
182
|
+
Finally, add a YAML file that lives alongside your function folder:
|
183
|
+
|
184
|
+
```yaml
|
185
|
+
# testphaedra.yml
|
186
|
+
|
187
|
+
version: 1.0
|
188
|
+
provider:
|
189
|
+
name: openfaas
|
190
|
+
gateway: http://127.0.0.1:8080
|
191
|
+
functions:
|
192
|
+
testphaedra:
|
193
|
+
lang: dockerfile
|
194
|
+
handler: ./testphaedra
|
195
|
+
image: yourdockerusername/testphaedra:latest
|
196
|
+
```
|
197
|
+
|
198
|
+
(Replace `yourdockerusername` with your [Docker Hub](https://hub.docker.com) username.)
|
199
|
+
|
200
|
+
Now run `faas-cli up -f testphaedra.yml` to build and deploy the function. Given the Ruby function `testphaedra/api/run-me.rb`, you'd call it like so:
|
201
|
+
|
202
|
+
```sh
|
203
|
+
curl http://127.0.0.1:8080/function/testphaedra/api/run-me
|
204
|
+
```
|
205
|
+
|
206
|
+
In case you're wondering: yes, with Phaedra you can write multiple Ruby functions which will be accessible via different URL paths—all handled by a single OpenFaaS function. Of course it's possible set up multiple Phaedra projects and deploy them as separate OpenFaaS functions if you wish.
|
207
|
+
|
208
|
+
### Rack
|
209
|
+
|
210
|
+
Booting Phaedra up as Rack app is very simple. All you need to do is add a `config.ru` file alongside your `api` folder:
|
85
211
|
|
86
212
|
```ruby
|
87
213
|
require "phaedra"
|
@@ -89,9 +215,38 @@ require "phaedra"
|
|
89
215
|
run Phaedra::RackApp.new
|
90
216
|
```
|
91
217
|
|
92
|
-
Then run `rackup` in the terminal.
|
218
|
+
Then run `rackup` in the terminal, or use another Rack-compatible server like Puma or Passenger.
|
219
|
+
|
220
|
+
The settings (and their defaults) you can pass to the `new` method are as follows:
|
93
221
|
|
94
|
-
|
222
|
+
```ruby
|
223
|
+
{
|
224
|
+
"root_dir" => Dir.pwd,
|
225
|
+
"serverless_api_dir" => "api"
|
226
|
+
}
|
227
|
+
```
|
228
|
+
|
229
|
+
Wondering if you can deploy a static site with an `api` folder via Nginx + Passenger? Yes, you can! Just configure your `my_site.conf` file like so:
|
230
|
+
|
231
|
+
```nginxconf
|
232
|
+
server {
|
233
|
+
listen 80;
|
234
|
+
server_name www.domain.com;
|
235
|
+
|
236
|
+
# Tell Nginx and Passenger where your site destination folder is
|
237
|
+
root /home/me/my_site/output;
|
238
|
+
|
239
|
+
# Turn on Passenger
|
240
|
+
location /api {
|
241
|
+
passenger_enabled on;
|
242
|
+
passenger_ruby /usr/local/rvm/gems/ruby-2.6.6@mysite/wrappers/ruby;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
```
|
246
|
+
|
247
|
+
Change the `server_name`, `root`, and `passenger_ruby` paths to your particular setup and you'll be good to go. (If you run into any errors, double-check there's a `config.ru` in the parent folder of your site destination folder.)
|
248
|
+
|
249
|
+
### WEBrick
|
95
250
|
|
96
251
|
Integrating Phaedra into a WEBrick server is pretty straightforward. Given a `server` object, it can be accomplished thusly:
|
97
252
|
|
@@ -105,13 +260,13 @@ server.mount_proc "/#{base_api_folder}" do |req, res|
|
|
105
260
|
ruby_path = File.join(full_api_path, api_folder, "#{endpoint}.rb")
|
106
261
|
|
107
262
|
if File.exist?(ruby_path)
|
108
|
-
|
109
|
-
|
263
|
+
if Object.constants.include?(:PhaedraFunction)
|
264
|
+
Object.send(:remove_const, :PhaedraFunction)
|
265
|
+
end
|
110
266
|
load ruby_path
|
111
|
-
$VERBOSE = original_verbosity
|
112
267
|
|
113
|
-
|
114
|
-
|
268
|
+
func = PhaedraFunction.new
|
269
|
+
func.service(req, res)
|
115
270
|
else
|
116
271
|
raise HTTPStatus::NotFound, "`#{req.path}' not found."
|
117
272
|
end
|
@@ -125,6 +280,8 @@ load File.join(Dir.pwd, "api", "func.rb")
|
|
125
280
|
@server.mount '/path', Handler
|
126
281
|
```
|
127
282
|
|
283
|
+
This method precludes any automatic routing by Phaedra, so it's discouraged unless you are using WEBrick within a larger setup that utilizes its own routing method. (Interestingly enough, that's how Vercel works under the hood.)
|
284
|
+
|
128
285
|
----
|
129
286
|
|
130
287
|
## Development
|
data/example/Gemfile
ADDED
data/example/api/the-time.rb
CHANGED
data/lib/phaedra/base.rb
CHANGED
@@ -112,7 +112,7 @@ module Phaedra
|
|
112
112
|
|
113
113
|
def set_initial_status
|
114
114
|
@res.status = 200
|
115
|
-
@res["Content-Type"] =
|
115
|
+
@res["Content-Type"] = "application/json"
|
116
116
|
end
|
117
117
|
|
118
118
|
def call_method_action(params)
|
@@ -121,7 +121,11 @@ module Phaedra
|
|
121
121
|
end
|
122
122
|
|
123
123
|
def complete_response
|
124
|
-
|
124
|
+
if @res.body.is_a?(String) && !@res["Content-Type"].start_with?("text/")
|
125
|
+
@res["Content-Type"] = "text/plain; charset=utf-8"
|
126
|
+
elsif @res["Content-Type"] == "application/json"
|
127
|
+
@res.body = @res.body.to_json
|
128
|
+
end
|
125
129
|
end
|
126
130
|
|
127
131
|
def error_condition
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Phaedra
|
2
|
+
module Middleware
|
3
|
+
class NotFound
|
4
|
+
def initialize(app, path, content_type = 'text/html; charset=utf-8')
|
5
|
+
@app = app
|
6
|
+
@content = File.read(path)
|
7
|
+
@length = @content.bytesize.to_s
|
8
|
+
@content_type = content_type
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
response = @app.call(env)
|
13
|
+
if response[0] == 404
|
14
|
+
[404, {'Content-Type' => @content_type, 'Content-Length' => @length}, [@content]]
|
15
|
+
else
|
16
|
+
response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Phaedra
|
2
|
+
module Middleware
|
3
|
+
# Based on Rack::TryStatic middleware
|
4
|
+
# https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/try_static.rb
|
5
|
+
|
6
|
+
class Static
|
7
|
+
def initialize(app, options)
|
8
|
+
@app = app
|
9
|
+
@try = ["", ".html", "index.html", "/index.html", *options[:try]]
|
10
|
+
@static = Rack::Static.new(
|
11
|
+
lambda { |_| [404, {}, []] },
|
12
|
+
options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
orig_path = env['PATH_INFO']
|
17
|
+
found = nil
|
18
|
+
@try.each do |path|
|
19
|
+
resp = @static.call(env.merge!({'PATH_INFO' => orig_path + path}))
|
20
|
+
break if !(403..405).include?(resp[0]) && found = resp
|
21
|
+
end
|
22
|
+
found or @app.call(env.merge!('PATH_INFO' => orig_path))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/phaedra/rack_app.rb
CHANGED
@@ -11,7 +11,7 @@ module Phaedra
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def body
|
14
|
-
@request_body ||= get_header(Rack::RACK_INPUT).read
|
14
|
+
@request_body ||= "" + get_header(Rack::RACK_INPUT).read
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -33,10 +33,10 @@ module Phaedra
|
|
33
33
|
endpoint = File.basename(req.path)
|
34
34
|
ruby_path = File.join(full_api_path, api_folder, "#{endpoint}.rb")
|
35
35
|
if File.exist?(ruby_path)
|
36
|
-
|
37
|
-
|
36
|
+
if Object.constants.include?(:PhaedraFunction)
|
37
|
+
Object.send(:remove_const, :PhaedraFunction)
|
38
|
+
end
|
38
39
|
load ruby_path
|
39
|
-
$VERBOSE = original_verbosity
|
40
40
|
|
41
41
|
func = PhaedraFunction.new
|
42
42
|
func.service(req, res)
|
data/lib/phaedra/version.rb
CHANGED
data/phaedra.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.authors = ["Jared White"]
|
7
7
|
spec.email = ["jared@whitefusion.io"]
|
8
8
|
|
9
|
-
spec.summary = %q{Write serverless Ruby functions via a REST microframework compatible with Rack or WEBrick.}
|
9
|
+
spec.summary = %q{Write serverless Ruby functions via a REST-like microframework compatible with Rack or WEBrick.}
|
10
10
|
spec.description = spec.summary
|
11
11
|
spec.homepage = "https://github.com/whitefusionhq/phaedra"
|
12
12
|
spec.license = "MIT"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: phaedra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jared White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '12.0'
|
69
|
-
description: Write serverless Ruby functions via a REST microframework compatible
|
69
|
+
description: Write serverless Ruby functions via a REST-like microframework compatible
|
70
70
|
with Rack or WEBrick.
|
71
71
|
email:
|
72
72
|
- jared@whitefusion.io
|
@@ -80,11 +80,16 @@ files:
|
|
80
80
|
- LICENSE.txt
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
|
+
- example/Gemfile
|
84
|
+
- example/api/simple.rb
|
83
85
|
- example/api/the-time.rb
|
84
86
|
- example/config.ru
|
85
87
|
- lib/phaedra.rb
|
86
88
|
- lib/phaedra/base.rb
|
87
89
|
- lib/phaedra/concerns/callbacks_actionable.rb
|
90
|
+
- lib/phaedra/middleware.rb
|
91
|
+
- lib/phaedra/middleware/not_found.rb
|
92
|
+
- lib/phaedra/middleware/static.rb
|
88
93
|
- lib/phaedra/rack_app.rb
|
89
94
|
- lib/phaedra/version.rb
|
90
95
|
- phaedra.gemspec
|
@@ -107,9 +112,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
112
|
- !ruby/object:Gem::Version
|
108
113
|
version: '0'
|
109
114
|
requirements: []
|
110
|
-
rubygems_version: 3.0.
|
115
|
+
rubygems_version: 3.0.6
|
111
116
|
signing_key:
|
112
117
|
specification_version: 4
|
113
|
-
summary: Write serverless Ruby functions via a REST microframework compatible
|
114
|
-
Rack or WEBrick.
|
118
|
+
summary: Write serverless Ruby functions via a REST-like microframework compatible
|
119
|
+
with Rack or WEBrick.
|
115
120
|
test_files: []
|