phaedra 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +132 -22
- data/example/Gemfile +4 -0
- data/example/api/simple.rb +3 -3
- data/example/config.ru +1 -1
- data/example/phaedra/initializers.rb +11 -0
- data/lib/phaedra.rb +1 -4
- data/lib/phaedra/base.rb +7 -3
- data/lib/phaedra/concerns/callbacks_actionable.rb +6 -6
- data/lib/phaedra/initializers.rb +69 -0
- data/lib/phaedra/rack_app.rb +4 -1
- data/lib/phaedra/rack_middleware.rb +2 -0
- data/lib/phaedra/rack_middleware/not_found.rb +21 -0
- data/lib/phaedra/rack_middleware/static.rb +26 -0
- data/lib/phaedra/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed786e8ee4e64448d25817144069d17839cbae1416b6d55daec427f336183101
|
4
|
+
data.tar.gz: 8d436c4e1560ca6acda2a03c5c68a86c6d82909a78cef5c4b0052d132fa9438c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54debc72ff247af26e5414dd4684ac1fe543e4390a7ad96ed8f784ab8f8c32febb4b1b95076afb3aa62c6c72de6d9f7f6cacfaca9b9a39628fe2ba71cbe46de5
|
7
|
+
data.tar.gz: 7b1489588052128fd22902674ad41c3d28fb620ca1bdeefbf5d8131651aaa240478e6ee52d4d1f2f0758c64edb13f71edb02ceece474994eb3d18fdccf546fcf
|
data/README.md
CHANGED
@@ -53,7 +53,7 @@ Your function can support `get`, `post`, `put`, `patch`, and `delete` methods wh
|
|
53
53
|
|
54
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.
|
55
55
|
|
56
|
-
|
56
|
+
### Callbacks
|
57
57
|
|
58
58
|
Functions can define `action` callbacks:
|
59
59
|
|
@@ -77,30 +77,109 @@ end
|
|
77
77
|
|
78
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.
|
79
79
|
|
80
|
-
|
80
|
+
### Shared Code You Only Want to Run Once
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
In your OpenFaaS project's function folder (e.g., `testphaedra`), simply define a file `handler.rb` which will load Phaedra's default Rack app:
|
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.
|
85
83
|
|
86
84
|
```ruby
|
87
|
-
#
|
85
|
+
# api/run-it-once.rb
|
88
86
|
|
89
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
|
+
```
|
90
96
|
|
91
|
-
|
92
|
-
|
93
|
-
status, headers, body = Phaedra::RackApp.new({
|
94
|
-
"root_dir" => File.join(Dir.pwd, "function")
|
95
|
-
}).call(env)
|
97
|
+
```ruby
|
98
|
+
# lib/shared_code.rb
|
96
99
|
|
97
|
-
|
98
|
-
|
100
|
+
module SharedCode
|
101
|
+
def self.run_once
|
102
|
+
@one_time ||= Time.now
|
99
103
|
end
|
100
104
|
end
|
101
105
|
```
|
102
106
|
|
103
|
-
|
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 . .
|
152
|
+
|
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:
|
104
183
|
|
105
184
|
```yaml
|
106
185
|
# testphaedra.yml
|
@@ -111,24 +190,24 @@ provider:
|
|
111
190
|
gateway: http://127.0.0.1:8080
|
112
191
|
functions:
|
113
192
|
testphaedra:
|
114
|
-
lang:
|
193
|
+
lang: dockerfile
|
115
194
|
handler: ./testphaedra
|
116
195
|
image: yourdockerusername/testphaedra:latest
|
117
196
|
```
|
118
197
|
|
198
|
+
(Replace `yourdockerusername` with your [Docker Hub](https://hub.docker.com) username.)
|
199
|
+
|
119
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:
|
120
201
|
|
121
202
|
```sh
|
122
203
|
curl http://127.0.0.1:8080/function/testphaedra/api/run-me
|
123
|
-
|
124
|
-
# output of the Ruby function
|
125
204
|
```
|
126
205
|
|
127
|
-
In case you're wondering: yes, with Phaedra you can write multiple Ruby functions accessible via different URL paths
|
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.
|
128
207
|
|
129
|
-
|
208
|
+
### Rack
|
130
209
|
|
131
|
-
Booting Phaedra up as Rack app is very simple. All you need is a `config.ru` file alongside your `api` folder:
|
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:
|
132
211
|
|
133
212
|
```ruby
|
134
213
|
require "phaedra"
|
@@ -136,9 +215,38 @@ require "phaedra"
|
|
136
215
|
run Phaedra::RackApp.new
|
137
216
|
```
|
138
217
|
|
139
|
-
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:
|
221
|
+
|
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:
|
140
230
|
|
141
|
-
|
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
|
142
250
|
|
143
251
|
Integrating Phaedra into a WEBrick server is pretty straightforward. Given a `server` object, it can be accomplished thusly:
|
144
252
|
|
@@ -172,6 +280,8 @@ load File.join(Dir.pwd, "api", "func.rb")
|
|
172
280
|
@server.mount '/path', Handler
|
173
281
|
```
|
174
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
|
+
|
175
285
|
----
|
176
286
|
|
177
287
|
## Development
|
data/example/Gemfile
ADDED
data/example/api/simple.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
require_relative "../phaedra/initializers"
|
2
2
|
|
3
3
|
class PhaedraFunction < Phaedra::Base
|
4
4
|
def get(params)
|
5
|
-
response["Content-Type"] = "text/html"
|
6
|
-
"<p>This is Interesting.
|
5
|
+
response["Content-Type"] = "text/html; charset=utf-8"
|
6
|
+
"<p>This is Interesting. 😁 #{Phaedra.the_time}</p>"
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
data/example/config.ru
CHANGED
data/lib/phaedra.rb
CHANGED
data/lib/phaedra/base.rb
CHANGED
@@ -7,6 +7,10 @@ module Phaedra
|
|
7
7
|
class Base
|
8
8
|
include CallbacksActionable
|
9
9
|
|
10
|
+
before_action do
|
11
|
+
Initializers.run
|
12
|
+
end
|
13
|
+
|
10
14
|
# Used by WEBrick
|
11
15
|
def self.get_instance(server, *options)
|
12
16
|
self.new(server, *options)
|
@@ -112,7 +116,7 @@ module Phaedra
|
|
112
116
|
|
113
117
|
def set_initial_status
|
114
118
|
@res.status = 200
|
115
|
-
@res["Content-Type"] = "application/json"
|
119
|
+
@res["Content-Type"] = "application/json; charset=utf-8"
|
116
120
|
end
|
117
121
|
|
118
122
|
def call_method_action(params)
|
@@ -122,8 +126,8 @@ module Phaedra
|
|
122
126
|
|
123
127
|
def complete_response
|
124
128
|
if @res.body.is_a?(String) && !@res["Content-Type"].start_with?("text/")
|
125
|
-
@res["Content-Type"] = "text/plain"
|
126
|
-
elsif @res["Content-Type"]
|
129
|
+
@res["Content-Type"] = "text/plain; charset=utf-8"
|
130
|
+
elsif @res["Content-Type"].start_with? "application/json"
|
127
131
|
@res.body = @res.body.to_json
|
128
132
|
end
|
129
133
|
end
|
@@ -11,14 +11,14 @@ module Phaedra
|
|
11
11
|
end
|
12
12
|
|
13
13
|
module ClassMethods
|
14
|
-
def before_action(*args)
|
15
|
-
set_callback :action, :before, *args
|
14
|
+
def before_action(*args, &block)
|
15
|
+
set_callback :action, :before, *args, &block
|
16
16
|
end
|
17
|
-
def after_action(*args)
|
18
|
-
set_callback :action, :after, *args
|
17
|
+
def after_action(*args, &block)
|
18
|
+
set_callback :action, :after, *args, &block
|
19
19
|
end
|
20
|
-
def around_action(*args)
|
21
|
-
set_callback :action, :around, *args
|
20
|
+
def around_action(*args, &block)
|
21
|
+
set_callback :action, :around, *args, &block
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Phaedra
|
2
|
+
module Initializers
|
3
|
+
Registration = Struct.new(
|
4
|
+
:origin,
|
5
|
+
:priority,
|
6
|
+
:block,
|
7
|
+
keyword_init: true
|
8
|
+
) do
|
9
|
+
def to_s
|
10
|
+
"#{owner}:#{priority} for #{block}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
DEFAULT_PRIORITY = 20
|
15
|
+
|
16
|
+
PRIORITY_MAP = {
|
17
|
+
low: 10,
|
18
|
+
normal: 20,
|
19
|
+
high: 30,
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# initial empty hooks
|
23
|
+
@registry = []
|
24
|
+
|
25
|
+
NotAvailable = Class.new(RuntimeError)
|
26
|
+
Uncallable = Class.new(RuntimeError)
|
27
|
+
|
28
|
+
# Ensure the priority is a Fixnum
|
29
|
+
def self.priority_value(priority)
|
30
|
+
return priority if priority.is_a?(Integer)
|
31
|
+
|
32
|
+
PRIORITY_MAP[priority] || DEFAULT_PRIORITY
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.register(origin, priority: DEFAULT_PRIORITY, &block)
|
36
|
+
raise Uncallable, "Initializers must respond to :call" unless block.respond_to? :call
|
37
|
+
|
38
|
+
@registry << Registration.new(
|
39
|
+
origin: origin,
|
40
|
+
priority: priority_value(priority),
|
41
|
+
block: block
|
42
|
+
)
|
43
|
+
|
44
|
+
block
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.remove(origin)
|
48
|
+
@registry.delete_if { |item| item.origin == origin }
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.run(force: false)
|
52
|
+
if !@initializers_ran || force
|
53
|
+
prioritized_initializers.each do |initializer|
|
54
|
+
initializer.block.call
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@initializers_ran = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.prioritized_initializers
|
62
|
+
# sort initializers according to priority and load order
|
63
|
+
grouped_initializers = @registry.group_by(&:priority)
|
64
|
+
grouped_initializers.keys.sort.reverse.map do |priority|
|
65
|
+
grouped_initializers[priority]
|
66
|
+
end.flatten
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/phaedra/rack_app.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "phaedra"
|
3
|
+
|
1
4
|
module Phaedra
|
2
5
|
class Request < Rack::Request
|
3
6
|
def query
|
@@ -11,7 +14,7 @@ module Phaedra
|
|
11
14
|
end
|
12
15
|
|
13
16
|
def body
|
14
|
-
@request_body ||= get_header(Rack::RACK_INPUT).read
|
17
|
+
@request_body ||= "" + get_header(Rack::RACK_INPUT).read
|
15
18
|
end
|
16
19
|
end
|
17
20
|
|
@@ -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/version.rb
CHANGED
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.5.0
|
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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -80,13 +80,19 @@ files:
|
|
80
80
|
- LICENSE.txt
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
|
+
- example/Gemfile
|
83
84
|
- example/api/simple.rb
|
84
85
|
- example/api/the-time.rb
|
85
86
|
- example/config.ru
|
87
|
+
- example/phaedra/initializers.rb
|
86
88
|
- lib/phaedra.rb
|
87
89
|
- lib/phaedra/base.rb
|
88
90
|
- lib/phaedra/concerns/callbacks_actionable.rb
|
91
|
+
- lib/phaedra/initializers.rb
|
89
92
|
- lib/phaedra/rack_app.rb
|
93
|
+
- lib/phaedra/rack_middleware.rb
|
94
|
+
- lib/phaedra/rack_middleware/not_found.rb
|
95
|
+
- lib/phaedra/rack_middleware/static.rb
|
90
96
|
- lib/phaedra/version.rb
|
91
97
|
- phaedra.gemspec
|
92
98
|
homepage: https://github.com/whitefusionhq/phaedra
|